feat: Regulatory News Dashboard — proaktive Compliance-Alerts
Some checks failed
Build + Deploy / build-backend-compliance (push) Successful in 2m43s
Build + Deploy / build-admin-compliance (push) Successful in 1m46s
Build + Deploy / build-ai-sdk (push) Successful in 47s
Build + Deploy / build-developer-portal (push) Successful in 1m0s
Build + Deploy / build-tts (push) Successful in 1m14s
Build + Deploy / build-document-crawler (push) Successful in 37s
Build + Deploy / build-dsms-gateway (push) Successful in 20s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 19s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m35s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 42s
CI / test-python-document-crawler (push) Successful in 24s
CI / test-python-dsms-gateway (push) Successful in 27s
CI / validate-canonical-controls (push) Successful in 23s
Build + Deploy / trigger-orca (push) Failing after 2h32m34s

Zeigt anstehende regulatorische Fristen im Dashboard an, abgeleitet
aus den bestehenden Obligation v2 JSON-Dateien. Keine neue DB-Tabelle.

Erster News-Eintrag: Widerrufsbutton-Pflicht ab 19.06.2026
(EU-RL 2023/2673, §356a BGB) — eigener Text, keine externe Quelle.

Features:
- Go Service: scannt Obligations nach Fristen, berechnet Urgency
- API: GET /sdk/v1/regulatory-news mit Countdown + Farbcodierung
- Dashboard: RegulatoryNewsFeed Sektion mit Countdown-Badges
- Vorlage: news-Feld in v2 JSON fuer zukuenftige regulatorische Updates
- 11 Tests (Sortierung, Urgency, Deadline-Parsing, Real-File-Test)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-25 17:43:19 +02:00
parent 55a2cd4a3d
commit 717c31547a
11 changed files with 552 additions and 1 deletions

View File

@@ -0,0 +1,26 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_URL = process.env.SDK_URL || 'http://ai-compliance-sdk:8090'
const DEFAULT_TENANT_ID = process.env.DEFAULT_TENANT_ID || '9282a473-5c95-4b3a-bf78-0ecc0ec71d3e'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const qs = searchParams.toString()
const url = `${SDK_URL}/sdk/v1/regulatory-news${qs ? `?${qs}` : ''}`
const response = await fetch(url, {
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': request.headers.get('X-Tenant-ID') || DEFAULT_TENANT_ID,
},
})
if (!response.ok) {
return NextResponse.json({ error: 'SDK error' }, { status: response.status })
}
return NextResponse.json(await response.json())
} catch {
return NextResponse.json({ error: 'Connection failed' }, { status: 503 })
}
}

View File

@@ -4,6 +4,7 @@ import React from 'react'
import Link from 'next/link'
import { useSDK, SDK_PACKAGES, getStepsForPackage } from '@/lib/sdk'
import { ProjectSelector } from '@/components/sdk/ProjectSelector/ProjectSelector'
import { RegulatoryNewsFeed } from '@/components/sdk/regulatory-news/RegulatoryNewsFeed'
import type { SDKPackageId } from '@/lib/sdk/types'
// =============================================================================
@@ -331,6 +332,9 @@ export default function SDKDashboard() {
</div>
)}
{/* Regulatory News */}
<RegulatoryNewsFeed businessModel={state.companyProfile?.businessModel as string} />
{/* 5 Packages */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">Compliance-Pakete</h2>

View File

@@ -0,0 +1,60 @@
'use client'
import Link from 'next/link'
export interface RegulatoryNewsItemData {
id: string
headline: string
summary: string
legal_reference: string
deadline: string
days_remaining: number
urgency: 'critical' | 'high' | 'medium' | 'low'
affected: string
action_required: string
action_link: string
regulation: string
sanctions?: string
}
const URGENCY_STYLES = {
critical: { badge: 'bg-red-100 text-red-700 border-red-200', border: 'border-l-red-500', icon: '🔴' },
high: { badge: 'bg-orange-100 text-orange-700 border-orange-200', border: 'border-l-orange-400', icon: '🟠' },
medium: { badge: 'bg-yellow-100 text-yellow-700 border-yellow-200', border: 'border-l-yellow-400', icon: '🟡' },
low: { badge: 'bg-gray-100 text-gray-600 border-gray-200', border: 'border-l-gray-300', icon: '🔵' },
}
export function RegulatoryNewsCard({ item }: { item: RegulatoryNewsItemData }) {
const style = URGENCY_STYLES[item.urgency] || URGENCY_STYLES.low
return (
<div className={`bg-white border border-gray-200 border-l-4 ${style.border} rounded-lg p-4`}>
<div className="flex items-start justify-between gap-3">
<div className="flex-1 min-w-0">
<h4 className="text-sm font-semibold text-gray-900">{item.headline}</h4>
<p className="text-xs text-gray-600 mt-1">{item.summary}</p>
<p className="text-xs text-gray-400 mt-1 italic">{item.legal_reference}</p>
{item.sanctions && (
<p className="text-xs text-red-600 mt-1">Sanktionen: {item.sanctions}</p>
)}
</div>
<div className="flex flex-col items-end gap-1 shrink-0">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium border ${style.badge}`}>
{style.icon} {item.days_remaining} Tage
</span>
<span className="text-xs text-gray-400">
{new Date(item.deadline).toLocaleDateString('de-DE')}
</span>
</div>
</div>
<div className="flex items-center justify-between mt-3 pt-2 border-t border-gray-100">
<span className="text-xs text-gray-400">{item.affected}</span>
{item.action_link && (
<Link href={item.action_link} className="text-xs text-blue-600 hover:underline font-medium">
Massnahme ergreifen
</Link>
)}
</div>
</div>
)
}

View File

@@ -0,0 +1,54 @@
'use client'
import React, { useState, useEffect } from 'react'
import { RegulatoryNewsCard, RegulatoryNewsItemData } from './RegulatoryNewsCard'
interface RegulatoryNewsFeedProps {
businessModel?: string
maxItems?: number
}
export function RegulatoryNewsFeed({ businessModel, maxItems = 3 }: RegulatoryNewsFeedProps) {
const [items, setItems] = useState<RegulatoryNewsItemData[]>([])
const [loading, setLoading] = useState(true)
const [showAll, setShowAll] = useState(false)
useEffect(() => {
const params = new URLSearchParams({ limit: '10', horizon_days: '365' })
if (businessModel) params.set('business_model', businessModel)
fetch(`/api/sdk/v1/regulatory-news?${params}`)
.then(r => r.ok ? r.json() : { items: [] })
.then(data => setItems(data.items || []))
.catch(() => setItems([]))
.finally(() => setLoading(false))
}, [businessModel])
if (loading) return null
if (items.length === 0) return null
const visible = showAll ? items : items.slice(0, maxItems)
return (
<div className="space-y-3">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900 flex items-center gap-2">
<span>📢</span> Regulatorische Neuigkeiten
</h2>
{items.length > maxItems && (
<button
onClick={() => setShowAll(!showAll)}
className="text-sm text-blue-600 hover:underline"
>
{showAll ? 'Weniger' : `Alle ${items.length} anzeigen`}
</button>
)}
</div>
<div className="space-y-2">
{visible.map(item => (
<RegulatoryNewsCard key={item.id} item={item} />
))}
</div>
</div>
)
}