feat(ucca): Pflichtendatenbank v2 (325 Obligations), Trigger-Engine, TOM-Control-Mapping
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 32s
CI / test-python-backend-compliance (push) Successful in 29s
CI / test-python-document-crawler (push) Successful in 20s
CI / test-python-dsms-gateway (push) Successful in 18s

- 9 Regulation-JSON-Dateien (DSGVO 80, AI Act 60, NIS2 40, BDSG 30, TTDSG 20, DSA 35, Data Act 25, EU-Maschinen 15, DORA 20)
- Condition-Tree-Engine fuer automatische Pflichtenselektion (all_of/any_of, 80+ Field-Paths)
- Generischer JSONRegulationModule-Loader mit YAML-Fallback
- Bidirektionales TOM-Control-Mapping (291 Obligation→Control, 92 Control→Obligation)
- Gap-Analyse-Engine (Compliance-%, Priority Actions, Domain Breakdown)
- ScopeDecision→UnifiedFacts Bridge fuer Auto-Profiling
- 4 neue API-Endpoints (assess-from-scope, tom-controls, gap-analysis, reverse-lookup)
- Frontend: Auto-Profiling Button, Regulation-Filter Chips, TOM-Panel, Gap-Analyse-View
- 18 Unit Tests (Condition Engine, v2 Loader, TOM Mapper)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-05 14:51:44 +01:00
parent 2540a2189a
commit 38e278ee3c
32 changed files with 22870 additions and 41 deletions

View File

@@ -0,0 +1,34 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
async function proxyRequest(request: NextRequest, method: string) {
try {
const pathSegments = request.nextUrl.pathname.replace('/api/sdk/v1/ucca/obligations/', '')
const targetUrl = `${SDK_BASE_URL}/sdk/v1/ucca/obligations/${pathSegments}${request.nextUrl.search}`
const headers: Record<string, string> = { 'Content-Type': 'application/json' }
const tenantId = request.headers.get('X-Tenant-ID')
if (tenantId) headers['X-Tenant-ID'] = tenantId
const fetchOptions: RequestInit = { method, headers }
if (method === 'POST' || method === 'PUT' || method === 'PATCH') {
fetchOptions.body = await request.text()
}
const response = await fetch(targetUrl, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json({ error: 'SDK backend error', details: errorText }, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('UCCA obligations proxy error:', error)
return NextResponse.json({ error: 'Failed to connect to SDK backend' }, { status: 503 })
}
}
export async function GET(request: NextRequest) { return proxyRequest(request, 'GET') }
export async function POST(request: NextRequest) { return proxyRequest(request, 'POST') }

View File

@@ -2,6 +2,8 @@
import React, { useState, useEffect, useCallback } from 'react'
import { StepHeader, STEP_EXPLANATIONS } from '@/components/sdk/StepHeader'
import TOMControlPanel from '@/components/sdk/obligations/TOMControlPanel'
import GapAnalysisView from '@/components/sdk/obligations/GapAnalysisView'
// =============================================================================
// Types
@@ -178,7 +180,7 @@ function ObligationModal({
onChange={e => update('source', e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
{['DSGVO', 'AI Act', 'NIS2', 'BDSG', 'TTDSG', 'Sonstig'].map(s => (
{['DSGVO', 'AI Act', 'NIS2', 'BDSG', 'TTDSG', 'DSA', 'Data Act', 'DORA', 'EU-Maschinen', 'Sonstig'].map(s => (
<option key={s}>{s}</option>
))}
</select>
@@ -426,6 +428,7 @@ function ObligationCard({
onDetails: () => void
}) {
const [saving, setSaving] = useState(false)
const [showTOM, setShowTOM] = useState(false)
const daysUntil = obligation.deadline
? Math.ceil((new Date(obligation.deadline).getTime() - Date.now()) / 86400000)
@@ -480,6 +483,13 @@ function ObligationCard({
)}
</div>
<div className="flex items-center gap-2" onClick={e => e.stopPropagation()}>
<button
onClick={(e) => { e.stopPropagation(); setShowTOM(v => !v) }}
className="px-2 py-1 text-purple-600 hover:bg-purple-50 rounded-lg transition-colors"
title="TOM Controls anzeigen"
>
TOM
</button>
<button
onClick={onDetails}
className="text-purple-600 hover:text-purple-700 font-medium"
@@ -497,6 +507,12 @@ function ObligationCard({
)}
</div>
</div>
{showTOM && (
<div className="mt-3" onClick={e => e.stopPropagation()}>
<TOMControlPanel obligationId={obligation.id} onClose={() => setShowTOM(false)} />
</div>
)}
</div>
)
}
@@ -505,16 +521,42 @@ function ObligationCard({
// Main Page
// =============================================================================
function mapPriority(p: string): 'critical' | 'high' | 'medium' | 'low' {
const map: Record<string, 'critical' | 'high' | 'medium' | 'low'> = {
kritisch: 'critical', hoch: 'high', mittel: 'medium', niedrig: 'low',
critical: 'critical', high: 'high', medium: 'medium', low: 'low',
}
return map[p?.toLowerCase()] || 'medium'
}
const REGULATION_CHIPS = [
{ key: 'all', label: 'Alle' },
{ key: 'DSGVO', label: 'DSGVO' },
{ key: 'AI Act', label: 'AI Act' },
{ key: 'NIS2', label: 'NIS2' },
{ key: 'BDSG', label: 'BDSG' },
{ key: 'TTDSG', label: 'TTDSG' },
{ key: 'DSA', label: 'DSA' },
{ key: 'Data Act', label: 'Data Act' },
{ key: 'DORA', label: 'DORA' },
{ key: 'EU-Maschinen', label: 'EU-Maschinen' },
]
const UCCA_API = '/api/sdk/v1/ucca/obligations'
export default function ObligationsPage() {
const [obligations, setObligations] = useState<Obligation[]>([])
const [stats, setStats] = useState<ObligationStats | null>(null)
const [filter, setFilter] = useState('all')
const [regulationFilter, setRegulationFilter] = useState('all')
const [searchQuery, setSearchQuery] = useState('')
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [showModal, setShowModal] = useState(false)
const [editObligation, setEditObligation] = useState<Obligation | null>(null)
const [detailObligation, setDetailObligation] = useState<Obligation | null>(null)
const [showGapAnalysis, setShowGapAnalysis] = useState(false)
const [profiling, setProfiling] = useState(false)
const loadData = useCallback(async () => {
setLoading(true)
@@ -621,10 +663,68 @@ export default function ObligationsPage() {
fetch(`${API}/stats`).then(r => r.json()).then(setStats).catch(() => {})
}
const handleAutoProfiling = async () => {
setProfiling(true)
setError(null)
try {
const res = await fetch(`${UCCA_API}/assess-from-scope`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
employee_count: 50,
industry: 'technology',
processes_personal_data: true,
is_controller: true,
uses_processors: true,
processes_special_categories: false,
cross_border_transfer: true,
uses_ai: true,
determined_level: 'L2',
}),
})
if (!res.ok) throw new Error(`HTTP ${res.status}`)
const data = await res.json()
if (data.obligations?.length > 0) {
// Merge auto-profiled obligations into the view
const autoObls: Obligation[] = data.obligations.map((o: Record<string, unknown>) => ({
id: o.id as string,
title: o.title as string,
description: (o.description as string) || '',
source: (o.regulation_id as string || '').toUpperCase(),
source_article: '',
deadline: null,
status: 'pending' as const,
priority: mapPriority(o.priority as string),
responsible: (o.responsible as string) || '',
linked_systems: [],
rule_code: 'auto-profiled',
}))
setObligations(prev => {
const existingIds = new Set(prev.map(p => p.id))
const newOnes = autoObls.filter(a => !existingIds.has(a.id))
return [...prev, ...newOnes]
})
}
} catch (e) {
setError(e instanceof Error ? e.message : 'Auto-Profiling fehlgeschlagen')
} finally {
setProfiling(false)
}
}
const stepInfo = STEP_EXPLANATIONS['obligations']
const filteredObligations = obligations.filter(o => {
if (filter === 'ai') return o.source.toLowerCase().includes('ai')
// Status/priority filter
if (filter === 'ai') {
if (!o.source.toLowerCase().includes('ai')) return false
}
// Regulation filter
if (regulationFilter !== 'all') {
const src = o.source?.toLowerCase() || ''
const key = regulationFilter.toLowerCase()
if (!src.includes(key)) return false
}
return true
})
@@ -679,15 +779,38 @@ export default function ObligationsPage() {
explanation={stepInfo?.explanation || ''}
tips={stepInfo?.tips || []}
>
<button
onClick={() => setShowModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Pflicht hinzufuegen
</button>
<div className="flex items-center gap-2">
<button
onClick={handleAutoProfiling}
disabled={profiling}
className="flex items-center gap-2 px-4 py-2 bg-white border border-purple-300 text-purple-700 rounded-lg hover:bg-purple-50 transition-colors text-sm disabled:opacity-50"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
{profiling ? 'Profiling...' : 'Auto-Profiling'}
</button>
<button
onClick={() => setShowGapAnalysis(v => !v)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg transition-colors text-sm ${
showGapAnalysis ? 'bg-purple-100 text-purple-700' : 'bg-white border border-gray-300 text-gray-700 hover:bg-gray-50'
}`}
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Gap-Analyse
</button>
<button
onClick={() => setShowModal(true)}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Pflicht hinzufuegen
</button>
</div>
</StepHeader>
{/* Error */}
@@ -725,7 +848,28 @@ export default function ObligationsPage() {
</div>
)}
{/* Search + Filter */}
{/* Gap Analysis View */}
{showGapAnalysis && (
<GapAnalysisView />
)}
{/* Regulation Filter Chips */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-gray-500 mr-1">Regulation:</span>
{REGULATION_CHIPS.map(r => (
<button
key={r.key}
onClick={() => setRegulationFilter(r.key)}
className={`px-3 py-1 text-xs rounded-full transition-colors ${
regulationFilter === r.key ? 'bg-purple-600 text-white' : 'bg-purple-50 text-purple-700 hover:bg-purple-100'
}`}
>
{r.label}
</button>
))}
</div>
{/* Search + Status Filter */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3">
<input
type="text"
@@ -735,7 +879,7 @@ export default function ObligationsPage() {
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
/>
<div className="flex items-center gap-2 flex-wrap">
{['all', 'overdue', 'pending', 'in-progress', 'completed', 'critical', 'ai'].map(f => (
{['all', 'overdue', 'pending', 'in-progress', 'completed', 'critical'].map(f => (
<button
key={f}
onClick={() => setFilter(f)}
@@ -743,7 +887,7 @@ export default function ObligationsPage() {
filter === f ? 'bg-purple-600 text-white' : 'bg-gray-100 text-gray-600 hover:bg-gray-200'
}`}
>
{f === 'all' ? 'Alle' : f === 'in-progress' ? 'In Bearbeitung' : f === 'overdue' ? 'Ueberfaellig' : f === 'pending' ? 'Ausstehend' : f === 'completed' ? 'Abgeschlossen' : f === 'critical' ? 'Kritisch' : 'AI Act'}
{f === 'all' ? 'Alle' : f === 'in-progress' ? 'In Bearbeitung' : f === 'overdue' ? 'Ueberfaellig' : f === 'pending' ? 'Ausstehend' : f === 'completed' ? 'Abgeschlossen' : 'Kritisch'}
</button>
))}
</div>