From 49ce41742861f0501b32b3244ffcadf0339f8bbd Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 14 Mar 2026 21:03:04 +0100 Subject: [PATCH] feat: add compliance modules 2-5 (dashboard, security templates, process manager, evidence collector) Module 2: Extended Compliance Dashboard with roadmap, module-status, next-actions, snapshots, score-history Module 3: 7 German security document templates (IT-Sicherheitskonzept, Datenschutz, Backup, Logging, Incident-Response, Zugriff, Risikomanagement) Module 4: Compliance Process Manager with CRUD, complete/skip/seed, ~50 seed tasks, 3-tab UI Module 5: Evidence Collector Extended with automated checks, control-mapping, coverage report, 4-tab UI Also includes: canonical control library enhancements (verification method, categories, dedup), control generator improvements, RAG client extensions 52 tests pass, frontend builds clean. Co-Authored-By: Claude Opus 4.6 --- .../app/api/sdk/v1/canonical/route.ts | 2 + .../app/sdk/compliance-hub/page.tsx | 938 +++++++---- .../components/ControlDetail.tsx | 56 +- .../components/ControlForm.tsx | 20 +- .../control-library/components/helpers.tsx | 16 + .../app/sdk/control-library/page.tsx | 126 +- .../app/sdk/document-generator/page.tsx | 1 + admin-compliance/app/sdk/evidence/page.tsx | 425 +++++ .../app/sdk/process-tasks/page.tsx | 1383 +++++++++++++++++ ai-compliance-sdk/cmd/server/main.go | 1 + .../internal/api/handlers/rag_handlers.go | 45 + .../api/handlers/rag_handlers_test.go | 66 + ai-compliance-sdk/internal/ucca/legal_rag.go | 136 ++ .../internal/ucca/legal_rag_test.go | 191 +++ backend-compliance/compliance/api/__init__.py | 2 + .../api/canonical_control_routes.py | 19 +- .../api/control_generator_routes.py | 103 +- .../compliance/api/dashboard_routes.py | 279 +++- .../compliance/api/evidence_check_routes.py | 1151 ++++++++++++++ .../compliance/api/legal_template_routes.py | 8 + .../compliance/api/process_task_routes.py | 1072 +++++++++++++ .../compliance/services/control_generator.py | 387 ++++- .../compliance/services/rag_client.py | 50 + backend-compliance/main.py | 6 + .../migrations/048_processing_path_expand.sql | 17 + .../migrations/049_target_audience.sql | 8 + .../migrations/050_score_snapshots.sql | 22 + .../migrations/051_security_templates.sql | 1098 +++++++++++++ .../migrations/052_process_tasks.sql | 53 + .../migrations/053_evidence_checks.sql | 62 + .../tests/test_dashboard_routes_extended.py | 209 +++ .../tests/test_evidence_check_routes.py | 374 +++++ .../tests/test_process_task_routes.py | 525 +++++++ .../tests/test_security_templates.py | 175 +++ .../sdk-modules/canonical-control-library.md | 137 +- 35 files changed, 8741 insertions(+), 422 deletions(-) create mode 100644 admin-compliance/app/sdk/process-tasks/page.tsx create mode 100644 backend-compliance/compliance/api/evidence_check_routes.py create mode 100644 backend-compliance/compliance/api/process_task_routes.py create mode 100644 backend-compliance/migrations/048_processing_path_expand.sql create mode 100644 backend-compliance/migrations/049_target_audience.sql create mode 100644 backend-compliance/migrations/050_score_snapshots.sql create mode 100644 backend-compliance/migrations/051_security_templates.sql create mode 100644 backend-compliance/migrations/052_process_tasks.sql create mode 100644 backend-compliance/migrations/053_evidence_checks.sql create mode 100644 backend-compliance/tests/test_dashboard_routes_extended.py create mode 100644 backend-compliance/tests/test_evidence_check_routes.py create mode 100644 backend-compliance/tests/test_process_task_routes.py create mode 100644 backend-compliance/tests/test_security_templates.py diff --git a/admin-compliance/app/api/sdk/v1/canonical/route.ts b/admin-compliance/app/api/sdk/v1/canonical/route.ts index 83ed5ad..1febef0 100644 --- a/admin-compliance/app/api/sdk/v1/canonical/route.ts +++ b/admin-compliance/app/api/sdk/v1/canonical/route.ts @@ -29,11 +29,13 @@ export async function GET(request: NextRequest) { const domain = searchParams.get('domain') const verificationMethod = searchParams.get('verification_method') const categoryFilter = searchParams.get('category') + const targetAudience = searchParams.get('target_audience') const params = new URLSearchParams() if (severity) params.set('severity', severity) if (domain) params.set('domain', domain) if (verificationMethod) params.set('verification_method', verificationMethod) if (categoryFilter) params.set('category', categoryFilter) + if (targetAudience) params.set('target_audience', targetAudience) const qs = params.toString() backendPath = `/api/compliance/v1/canonical/controls${qs ? `?${qs}` : ''}` break diff --git a/admin-compliance/app/sdk/compliance-hub/page.tsx b/admin-compliance/app/sdk/compliance-hub/page.tsx index c089bd8..28e571c 100644 --- a/admin-compliance/app/sdk/compliance-hub/page.tsx +++ b/admin-compliance/app/sdk/compliance-hub/page.tsx @@ -3,12 +3,11 @@ /** * Compliance Hub Page (SDK Version - Zusatzmodul) * - * Central compliance management dashboard with: - * - Compliance Score Overview - * - Quick Access to all compliance modules (SDK paths) - * - Control-Mappings with statistics - * - Audit Findings - * - Regulations overview + * Central compliance management dashboard with tabs: + * - Uebersicht: Score, Stats, Quick Access, Findings + * - Roadmap: 4-column Kanban (Quick Wins / Must Have / Should Have / Nice to Have) + * - Module: Grid with module cards + progress bars + * - Trend: Score history chart */ import { useState, useEffect } from 'react' @@ -53,6 +52,62 @@ interface FindingsData { open_minors: number } +interface RoadmapItem { + id: string + control_id: string + title: string + status: string + domain: string + owner: string | null + next_review_at: string | null + days_overdue: number + weight: number +} + +interface RoadmapData { + buckets: Record + counts: Record +} + +interface ModuleInfo { + key: string + label: string + count: number + status: string + progress: number +} + +interface ModuleStatusData { + modules: ModuleInfo[] + total: number + started: number + complete: number + overall_progress: number +} + +interface NextAction { + id: string + control_id: string + title: string + status: string + domain: string + owner: string | null + days_overdue: number + urgency_score: number + reason: string +} + +interface ScoreSnapshot { + id: string + score: number + controls_total: number + controls_pass: number + snapshot_date: string + created_at: string +} + +type TabKey = 'overview' | 'roadmap' | 'modules' | 'trend' + const DOMAIN_LABELS: Record = { gov: 'Governance', priv: 'Datenschutz', @@ -65,44 +120,67 @@ const DOMAIN_LABELS: Record = { aud: 'Audit', } +const BUCKET_LABELS: Record = { + quick_wins: { label: 'Quick Wins', color: 'text-green-700', bg: 'bg-green-50 border-green-200' }, + must_have: { label: 'Must Have', color: 'text-red-700', bg: 'bg-red-50 border-red-200' }, + should_have: { label: 'Should Have', color: 'text-yellow-700', bg: 'bg-yellow-50 border-yellow-200' }, + nice_to_have: { label: 'Nice to Have', color: 'text-slate-700', bg: 'bg-slate-50 border-slate-200' }, +} + +const MODULE_ICONS: Record = { + vvt: '๐Ÿ“‹', tom: '๐Ÿ”’', dsfa: 'โš ๏ธ', loeschfristen: '๐Ÿ—‘๏ธ', risks: '๐ŸŽฏ', + controls: 'โœ…', evidence: '๐Ÿ“Ž', obligations: '๐Ÿ“œ', incidents: '๐Ÿšจ', + vendor: '๐Ÿค', legal_templates: '๐Ÿ“„', training: '๐ŸŽ“', audit: '๐Ÿ”', + security_backlog: '๐Ÿ›ก๏ธ', quality: 'โญ', +} + export default function ComplianceHubPage() { + const [activeTab, setActiveTab] = useState('overview') const [dashboard, setDashboard] = useState(null) const [regulations, setRegulations] = useState([]) const [mappings, setMappings] = useState(null) const [findings, setFindings] = useState(null) + const [roadmap, setRoadmap] = useState(null) + const [moduleStatus, setModuleStatus] = useState(null) + const [nextActions, setNextActions] = useState([]) + const [scoreHistory, setScoreHistory] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [seeding, setSeeding] = useState(false) + const [savingSnapshot, setSavingSnapshot] = useState(false) useEffect(() => { loadData() }, []) + useEffect(() => { + if (activeTab === 'roadmap' && !roadmap) loadRoadmap() + if (activeTab === 'modules' && !moduleStatus) loadModuleStatus() + if (activeTab === 'trend' && scoreHistory.length === 0) loadScoreHistory() + }, [activeTab]) // eslint-disable-line react-hooks/exhaustive-deps + const loadData = async () => { setLoading(true) setError(null) try { - const [dashboardRes, regulationsRes, mappingsRes, findingsRes] = await Promise.all([ + const [dashboardRes, regulationsRes, mappingsRes, findingsRes, actionsRes] = await Promise.all([ fetch('/api/sdk/v1/compliance/dashboard'), fetch('/api/sdk/v1/compliance/regulations'), fetch('/api/sdk/v1/compliance/mappings'), fetch('/api/sdk/v1/isms/findings?status=open'), + fetch('/api/sdk/v1/compliance/dashboard/next-actions?limit=5'), ]) - if (dashboardRes.ok) { - setDashboard(await dashboardRes.json()) - } + if (dashboardRes.ok) setDashboard(await dashboardRes.json()) if (regulationsRes.ok) { const data = await regulationsRes.json() setRegulations(data.regulations || []) } - if (mappingsRes.ok) { - const data = await mappingsRes.json() - setMappings(data) - } - if (findingsRes.ok) { - const data = await findingsRes.json() - setFindings(data) + if (mappingsRes.ok) setMappings(await mappingsRes.json()) + if (findingsRes.ok) setFindings(await findingsRes.json()) + if (actionsRes.ok) { + const data = await actionsRes.json() + setNextActions(data.actions || []) } } catch (err) { console.error('Failed to load compliance data:', err) @@ -112,6 +190,41 @@ export default function ComplianceHubPage() { } } + const loadRoadmap = async () => { + try { + const res = await fetch('/api/sdk/v1/compliance/dashboard/roadmap') + if (res.ok) setRoadmap(await res.json()) + } catch { /* silent */ } + } + + const loadModuleStatus = async () => { + try { + const res = await fetch('/api/sdk/v1/compliance/dashboard/module-status') + if (res.ok) setModuleStatus(await res.json()) + } catch { /* silent */ } + } + + const loadScoreHistory = async () => { + try { + const res = await fetch('/api/sdk/v1/compliance/dashboard/score-history?months=12') + if (res.ok) { + const data = await res.json() + setScoreHistory(data.snapshots || []) + } + } catch { /* silent */ } + } + + const saveSnapshot = async () => { + setSavingSnapshot(true) + try { + const res = await fetch('/api/sdk/v1/compliance/dashboard/snapshot', { method: 'POST' }) + if (res.ok) { + loadScoreHistory() + } + } catch { /* silent */ } + finally { setSavingSnapshot(false) } + } + const seedDatabase = async () => { setSeeding(true) try { @@ -141,14 +254,51 @@ export default function ComplianceHubPage() { const scoreColor = score >= 80 ? 'text-green-600' : score >= 60 ? 'text-yellow-600' : 'text-red-600' const scoreBgColor = score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-yellow-500' : 'bg-red-500' + const tabs: { key: TabKey; label: string }[] = [ + { key: 'overview', label: 'Uebersicht' }, + { key: 'roadmap', label: 'Roadmap' }, + { key: 'modules', label: 'Module' }, + { key: 'trend', label: 'Trend' }, + ] + return (
- {/* Title Card (Zusatzmodul - no StepHeader) */} + {/* Title Card */}
-

Compliance Hub

-

- Zentrale Verwaltung aller Compliance-Anforderungen nach DSGVO, AI Act, BSI TR-03161 und weiteren Regulierungen. -

+
+
+

Compliance Hub

+

+ Zentrale Verwaltung aller Compliance-Anforderungen nach DSGVO, AI Act, BSI TR-03161 und weiteren Regulierungen. +

+
+ +
+
+ + {/* Tabs */} +
+
+ {tabs.map(tab => ( + + ))} +
{/* Error Banner */} @@ -183,332 +333,478 @@ export default function ComplianceHubPage() {
)} - {/* Quick Actions */} -
-

Schnellzugriff

-
- -
- - - -
-

Audit Checkliste

-

{dashboard?.total_requirements || '...'} Anforderungen

- - - -
- - - -
-

Controls

-

{dashboard?.total_controls || '...'} Massnahmen

- - - -
- - - -
-

Evidence

-

Nachweise

- - - -
- - - -
-

Risk Matrix

-

5x5 Risiken

- - - -
- - - -
-

Service Registry

-

Module

- - - -
- - - -
-

Audit Report

-

PDF Export

- -
-
- {loading ? (
) : ( <> - {/* Score and Stats Row */} -
-
-

Compliance Score

-
- {score.toFixed(0)}% + {/* ============================================================ */} + {/* TAB: Uebersicht */} + {/* ============================================================ */} + {activeTab === 'overview' && ( + <> + {/* Quick Actions */} +
+

Schnellzugriff

+
+ {[ + { href: '/sdk/audit-checklist', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01', label: 'Audit Checkliste', sub: `${dashboard?.total_requirements || '...'} Anforderungen`, color: 'purple' }, + { href: '/sdk/controls', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z', label: 'Controls', sub: `${dashboard?.total_controls || '...'} Massnahmen`, color: 'green' }, + { href: '/sdk/evidence', icon: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z', label: 'Evidence', sub: 'Nachweise', color: 'blue' }, + { href: '/sdk/risks', icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z', label: 'Risk Matrix', sub: '5x5 Risiken', color: 'red' }, + { href: '/sdk/process-tasks', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4', label: 'Prozesse', sub: 'Aufgaben', color: 'indigo' }, + { href: '/sdk/audit-report', icon: 'M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z', label: 'Audit Report', sub: 'PDF Export', color: 'orange' }, + ].map(item => ( + +
+ + + +
+

{item.label}

+

{item.sub}

+ + ))} +
-
-
-
-

- {dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden -

-
-
-
-
-

Verordnungen

-

{dashboard?.total_regulations || 0}

+ {/* Score and Stats Row */} +
+
+

Compliance Score

+
+ {score.toFixed(0)}% +
+
+
+
+

+ {dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden +

-
- - - -
-
-

{dashboard?.total_requirements || 0} Anforderungen

-
-
-
-
-

Controls

-

{dashboard?.total_controls || 0}

-
-
- - - -
+ {[ + { label: 'Verordnungen', value: dashboard?.total_regulations || 0, sub: `${dashboard?.total_requirements || 0} Anforderungen`, iconColor: 'blue', icon: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z' }, + { label: 'Controls', value: dashboard?.total_controls || 0, sub: `${dashboard?.controls_by_status?.pass || 0} bestanden`, iconColor: 'green', icon: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' }, + { label: 'Nachweise', value: dashboard?.total_evidence || 0, sub: `${dashboard?.evidence_by_status?.valid || 0} aktiv`, iconColor: 'purple', icon: 'M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z' }, + { label: 'Risiken', value: dashboard?.total_risks || 0, sub: `${(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch`, iconColor: 'red', icon: 'M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z' }, + ].map(stat => ( +
+
+
+

{stat.label}

+

{stat.value}

+
+
+ + + +
+
+

{stat.sub}

+
+ ))}
-

{dashboard?.controls_by_status?.pass || 0} bestanden

-
-
-
-
-

Nachweise

-

{dashboard?.total_evidence || 0}

+ {/* Next Actions + Findings */} +
+ {/* Next Actions */} +
+

Naechste Aktionen

+ {nextActions.length === 0 ? ( +

Keine offenen Aktionen.

+ ) : ( +
+ {nextActions.map(action => ( +
+
0 ? 'bg-red-500' : 'bg-yellow-500' + }`} /> +
+

{action.title}

+

+ {action.control_id} ยท {DOMAIN_LABELS[action.domain] || action.domain} + {action.days_overdue > 0 && {action.days_overdue}d ueberfaellig} +

+
+ + {action.status} + +
+ ))} +
+ )}
-
- - - -
-
-

{dashboard?.evidence_by_status?.valid || 0} aktiv

-
-
-
-
-

Risiken

-

{dashboard?.total_risks || 0}

-
-
- - - -
-
-

- {(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch -

-
-
- - {/* Control-Mappings & Findings Row */} -
-
-
-

Control-Mappings

- - Alle anzeigen โ†’ - -
-
-
-

{mappings?.total || 0}

-

Mappings gesamt

-
-
-

Nach Verordnung

-
- {mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => ( - - {reg}: {count} + {/* Audit Findings */} +
+
+

Audit Findings

+ + Audit Checkliste โ†’ + +
+
+
+
+
+ Hauptabweichungen +
+

{findings?.open_majors || 0}

+

offen (blockiert Zertifizierung)

+
+
+
+
+ Nebenabweichungen +
+

{findings?.open_minors || 0}

+

offen (erfordert CAPA)

+
+
+
+ + Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI) + + {(findings?.open_majors || 0) === 0 ? ( + + Zertifizierung moeglich + + ) : ( + + Zertifizierung blockiert - ))} - {!mappings?.by_regulation && ( - Keine Mappings vorhanden )}
-

- Automatisch generierte Verknuepfungen zwischen {dashboard?.total_controls || 0} Controls - und {dashboard?.total_requirements || 0} Anforderungen aus {dashboard?.total_regulations || 0} Verordnungen. -

-
+ {/* Control-Mappings & Domain Chart */} +
+
+
+

Control-Mappings

+ + Alle anzeigen โ†’ + +
+
+
+

{mappings?.total || 0}

+

Mappings gesamt

+
+
+

Nach Verordnung

+
+ {mappings?.by_regulation && Object.entries(mappings.by_regulation).slice(0, 5).map(([reg, count]) => ( + + {reg}: {count} + + ))} + {!mappings?.by_regulation && ( + Keine Mappings vorhanden + )} +
+
+
+
+ +
+

Controls nach Domain

+
+ {Object.entries(dashboard?.controls_by_domain || {}).slice(0, 6).map(([domain, stats]) => { + const total = stats.total || 0 + const pass = stats.pass || 0 + const partial = stats.partial || 0 + const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0 + + return ( +
+ + {DOMAIN_LABELS[domain] || domain} + +
+
+
+
+ {passPercent.toFixed(0)}% +
+ ) + })} +
+
+
+ + {/* Regulations Table */} +
+
+

Verordnungen & Standards ({regulations.length})

+ +
+
+ + + + + + + + + + + {regulations.slice(0, 15).map((reg) => ( + + + + + + + ))} + +
CodeNameTypAnforderungen
+ {reg.code} + +

{reg.name}

+
+ + {reg.regulation_type === 'eu_regulation' ? 'EU-VO' : + reg.regulation_type === 'eu_directive' ? 'EU-RL' : + reg.regulation_type === 'bsi_standard' ? 'BSI' : + reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type} + + + {reg.requirement_count} +
+
+
+ + )} + + {/* ============================================================ */} + {/* TAB: Roadmap */} + {/* ============================================================ */} + {activeTab === 'roadmap' && ( +
+ {!roadmap ? ( +
+
+
+ ) : ( +
+ {(['quick_wins', 'must_have', 'should_have', 'nice_to_have'] as const).map(bucketKey => { + const meta = BUCKET_LABELS[bucketKey] + const items = roadmap.buckets[bucketKey] || [] + + return ( +
+
+

{meta.label}

+ + {items.length} + +
+
+ {items.length === 0 ? ( +

Keine Eintraege

+ ) : ( + items.map(item => ( +
+

{item.title}

+
+ {item.control_id} + ยท + {DOMAIN_LABELS[item.domain] || item.domain} +
+ {item.days_overdue > 0 && ( +

{item.days_overdue}d ueberfaellig

+ )} + {item.owner && ( +

{item.owner}

+ )} +
+ )) + )} +
+
+ ) + })} +
+ )} +
+ )} + + {/* ============================================================ */} + {/* TAB: Module */} + {/* ============================================================ */} + {activeTab === 'modules' && ( +
+ {!moduleStatus ? ( +
+
+
+ ) : ( + <> + {/* Summary */} +
+
+

Gesamt-Fortschritt

+

{moduleStatus.overall_progress.toFixed(0)}%

+
+
+

Module gestartet

+

{moduleStatus.started}/{moduleStatus.total}

+
+
+

Module abgeschlossen

+

{moduleStatus.complete}/{moduleStatus.total}

+
+
+ + {/* Module Grid */} +
+ {moduleStatus.modules.map(mod => ( +
+
+ {MODULE_ICONS[mod.key] || '๐Ÿ“ฆ'} +
+

{mod.label}

+

{mod.count} Eintraege

+
+ + {mod.status === 'complete' ? 'Fertig' : + mod.status === 'in_progress' ? 'In Arbeit' : 'Offen'} + +
+
+
+
+
+ ))} +
+ + )} +
+ )} + + {/* ============================================================ */} + {/* TAB: Trend */} + {/* ============================================================ */} + {activeTab === 'trend' && (
-
-

Audit Findings

- - Audit Checkliste โ†’ - +
+

Score-Verlauf

+
-
-
-
-
- Hauptabweichungen -
-

{findings?.open_majors || 0}

-

offen (blockiert Zertifizierung)

-
-
-
-
- Nebenabweichungen -
-

{findings?.open_minors || 0}

-

offen (erfordert CAPA)

-
-
-
- - Gesamt: {findings?.total || 0} Findings ({findings?.major_count || 0} Major, {findings?.minor_count || 0} Minor, {findings?.ofi_count || 0} OFI) - - {(findings?.open_majors || 0) === 0 ? ( - - Zertifizierung moeglich - - ) : ( - - Zertifizierung blockiert - - )} -
-
-
- {/* Domain Chart */} -
-

Controls nach Domain

-
- {Object.entries(dashboard?.controls_by_domain || {}).map(([domain, stats]) => { - const total = stats.total || 0 - const pass = stats.pass || 0 - const partial = stats.partial || 0 - const passPercent = total > 0 ? ((pass + partial * 0.5) / total) * 100 : 0 - - return ( -
-
- - {DOMAIN_LABELS[domain] || domain.toUpperCase()} - - - {pass}/{total} ({passPercent.toFixed(0)}%) - -
-
-
-
+ {scoreHistory.length === 0 ? ( +
+

Noch keine Score-Snapshots vorhanden.

+

Klicken Sie auf "Aktuellen Score speichern", um den ersten Datenpunkt zu erstellen.

+
+ ) : ( + <> + {/* Simple SVG Line Chart */} +
+ + {/* Grid lines */} + {[0, 25, 50, 75, 100].map(pct => ( + + ))} + {/* Score line */} + { + const x = scoreHistory.length === 1 ? 400 : (i / (scoreHistory.length - 1)) * 780 + 10 + const y = 200 - (s.score / 100) * 200 + return `${x},${y}` + }).join(' ')} + /> + {/* Points */} + {scoreHistory.map((s, i) => { + const x = scoreHistory.length === 1 ? 400 : (i / (scoreHistory.length - 1)) * 780 + 10 + const y = 200 - (s.score / 100) * 200 + return ( + + ) + })} + + {/* Y-axis labels */} +
+ 100% + 75% + 50% + 25% + 0%
- ) - })} -
-
- {/* Regulations Table */} -
-
-

Verordnungen & Standards ({regulations.length})

- + {/* Snapshot Table */} +
+ + + + + + + + + + + {scoreHistory.slice().reverse().map(snap => ( + + + + + + + ))} + +
DatumScoreControlsBestanden
{new Date(snap.snapshot_date).toLocaleDateString('de-DE')} + = 80 ? 'text-green-600' : snap.score >= 60 ? 'text-yellow-600' : 'text-red-600' + }`}> + {typeof snap.score === 'number' ? snap.score.toFixed(1) : snap.score}% + + {snap.controls_total}{snap.controls_pass}
+
+ + )}
-
- - - - - - - - - - - {regulations.slice(0, 15).map((reg) => ( - - - - - - - ))} - -
CodeNameTypAnforderungen
- {reg.code} - -

{reg.name}

-
- - {reg.regulation_type === 'eu_regulation' ? 'EU-VO' : - reg.regulation_type === 'eu_directive' ? 'EU-RL' : - reg.regulation_type === 'bsi_standard' ? 'BSI' : - reg.regulation_type === 'de_law' ? 'DE' : reg.regulation_type} - - - {reg.requirement_count} -
-
-
+ )} )}
diff --git a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx index 2f8518b..58d0b51 100644 --- a/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx +++ b/admin-compliance/app/sdk/control-library/components/ControlDetail.tsx @@ -8,7 +8,7 @@ import { } from 'lucide-react' import { CanonicalControl, EFFORT_LABELS, BACKEND_URL, - SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, + SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, VERIFICATION_METHODS, CATEGORY_OPTIONS, } from './helpers' @@ -124,6 +124,7 @@ export function ControlDetail({ +

{ctrl.title}

@@ -163,22 +164,46 @@ export function ControlDetail({

{ctrl.rationale}

- {/* Source Info (Rule 1 + 2) */} + {/* Gesetzliche Grundlage (Rule 1 + 2) */} {ctrl.source_citation && (
-
+
-

Quellenangabe

+

Gesetzliche Grundlage

+ {ctrl.license_rule === 1 && ( + Direkte gesetzliche Pflicht + )} + {ctrl.license_rule === 2 && ( + Standard mit Zitationspflicht + )}
-
- {Object.entries(ctrl.source_citation).map(([k, v]) => ( -

{k}: {v}

- ))} +
+
+ {ctrl.source_citation.source && ( +

{ctrl.source_citation.source}

+ )} + {ctrl.source_citation.license && ( +

Lizenz: {ctrl.source_citation.license}

+ )} + {ctrl.source_citation.license_notice && ( +

{ctrl.source_citation.license_notice}

+ )} +
+ {ctrl.source_citation.url && ( + + Quelle + + )}
{ctrl.source_original_text && (
Originaltext anzeigen -

+

{ctrl.source_original_text}

@@ -186,6 +211,19 @@ export function ControlDetail({
)} + {/* Impliziter Gesetzesbezug (Rule 3 โ€” kein Originaltext, aber ggf. Gesetzesbezug ueber Anchors) */} + {!ctrl.source_citation && ctrl.open_anchors.length > 0 && ( +
+
+ +

+ Dieser Control setzt implizit gesetzliche Anforderungen um (z.B. DSGVO Art. 32, NIS2 Art. 21). + Die konkreten Massnahmen leiten sich aus den Open-Source-Referenzen unten ab. +

+
+
+ )} + {/* Scope */} {(ctrl.scope.platforms?.length || ctrl.scope.components?.length || ctrl.scope.data_classes?.length) ? (
diff --git a/admin-compliance/app/sdk/control-library/components/ControlForm.tsx b/admin-compliance/app/sdk/control-library/components/ControlForm.tsx index 95218c5..4e8a664 100644 --- a/admin-compliance/app/sdk/control-library/components/ControlForm.tsx +++ b/admin-compliance/app/sdk/control-library/components/ControlForm.tsx @@ -2,7 +2,7 @@ import { useState } from 'react' import { BookOpen, Trash2, Save, X } from 'lucide-react' -import { EMPTY_CONTROL, VERIFICATION_METHODS, CATEGORY_OPTIONS } from './helpers' +import { EMPTY_CONTROL, VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS } from './helpers' export function ControlForm({ initial, @@ -268,8 +268,8 @@ export function ControlForm({
- {/* Verification Method & Category */} -
+ {/* Verification Method, Category & Target Audience */} +
setForm({ ...form, target_audience: e.target.value || null })} + className="w-full px-3 py-2 text-sm border border-gray-300 rounded-lg" + > + + {Object.entries(TARGET_AUDIENCE_OPTIONS).map(([k, v]) => ( + + ))} + +

Fuer wen ist dieses Control relevant?

+
) diff --git a/admin-compliance/app/sdk/control-library/components/helpers.tsx b/admin-compliance/app/sdk/control-library/components/helpers.tsx index bf765d0..c0d9d37 100644 --- a/admin-compliance/app/sdk/control-library/components/helpers.tsx +++ b/admin-compliance/app/sdk/control-library/components/helpers.tsx @@ -44,6 +44,7 @@ export interface CanonicalControl { customer_visible?: boolean verification_method: string | null category: string | null + target_audience: string | null generation_metadata?: Record | null created_at: string updated_at: string @@ -96,6 +97,7 @@ export const EMPTY_CONTROL = { tags: [] as string[], verification_method: null as string | null, category: null as string | null, + target_audience: null as string | null, } export const DOMAIN_OPTIONS = [ @@ -138,6 +140,13 @@ export const CATEGORY_OPTIONS = [ { value: 'identity', label: 'Identitaetsmanagement' }, ] +export const TARGET_AUDIENCE_OPTIONS: Record = { + enterprise: { bg: 'bg-cyan-100 text-cyan-700', label: 'Unternehmen' }, + authority: { bg: 'bg-rose-100 text-rose-700', label: 'Behoerden' }, + provider: { bg: 'bg-violet-100 text-violet-700', label: 'Anbieter' }, + all: { bg: 'bg-gray-100 text-gray-700', label: 'Alle' }, +} + export const COLLECTION_OPTIONS = [ { value: 'bp_compliance_ce', label: 'CE (OWASP, ENISA, BSI)' }, { value: 'bp_compliance_gesetze', label: 'Gesetze (EU, DE, BSI)' }, @@ -213,6 +222,13 @@ export function CategoryBadge({ category }: { category: string | null }) { ) } +export function TargetAudienceBadge({ audience }: { audience: string | null }) { + if (!audience) return null + const config = TARGET_AUDIENCE_OPTIONS[audience] + if (!config) return null + return {config.label} +} + export function getDomain(controlId: string): string { return controlId.split('-')[0] || '' } diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 2cdd51e..dc1b7d4 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -2,13 +2,14 @@ import { useState, useEffect, useMemo, useCallback } from 'react' import { - Shield, Search, ChevronRight, Filter, Lock, + Shield, Search, ChevronRight, ChevronLeft, Filter, Lock, BookOpen, Plus, Zap, BarChart3, ListChecks, + ChevronsLeft, ChevronsRight, } from 'lucide-react' import { CanonicalControl, Framework, BACKEND_URL, EMPTY_CONTROL, - SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, - getDomain, VERIFICATION_METHODS, CATEGORY_OPTIONS, + SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, + getDomain, VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS, } from './components/helpers' import { ControlForm } from './components/ControlForm' import { ControlDetail } from './components/ControlDetail' @@ -32,6 +33,7 @@ export default function ControlLibraryPage() { const [stateFilter, setStateFilter] = useState('') const [verificationFilter, setVerificationFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') + const [audienceFilter, setAudienceFilter] = useState('') // CRUD state const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list') @@ -42,6 +44,10 @@ export default function ControlLibraryPage() { const [processedStats, setProcessedStats] = useState>>([]) const [showStats, setShowStats] = useState(false) + // Pagination + const [currentPage, setCurrentPage] = useState(1) + const PAGE_SIZE = 50 + // Review mode const [reviewMode, setReviewMode] = useState(false) const [reviewIndex, setReviewIndex] = useState(0) @@ -80,6 +86,7 @@ export default function ControlLibraryPage() { if (stateFilter && c.release_state !== stateFilter) return false if (verificationFilter && c.verification_method !== verificationFilter) return false if (categoryFilter && c.category !== categoryFilter) return false + if (audienceFilter && c.target_audience !== audienceFilter) return false if (searchQuery) { const q = searchQuery.toLowerCase() return ( @@ -91,7 +98,17 @@ export default function ControlLibraryPage() { } return true }) - }, [controls, severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, searchQuery]) + }, [controls, severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, searchQuery]) + + // Reset page when filters change + useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, searchQuery]) + + // Pagination + const totalPages = Math.max(1, Math.ceil(filteredControls.length / PAGE_SIZE)) + const paginatedControls = useMemo(() => { + const start = (currentPage - 1) * PAGE_SIZE + return filteredControls.slice(start, start + PAGE_SIZE) + }, [filteredControls, currentPage]) // Review queue items const reviewItems = useMemo(() => { @@ -413,6 +430,16 @@ export default function ControlLibraryPage() { ))} +
{/* Processing Stats */} @@ -443,10 +470,19 @@ export default function ControlLibraryPage() { /> )} + {/* Pagination Header */} +
+ + {filteredControls.length} Controls gefunden + {filteredControls.length !== controls.length && ` (von ${controls.length} gesamt)`} + + Seite {currentPage} von {totalPages} +
+ {/* Control List */}
- {filteredControls.map(ctrl => ( + {paginatedControls.map(ctrl => ( + + + {/* Page numbers */} + {Array.from({ length: totalPages }, (_, i) => i + 1) + .filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2) + .reduce<(number | 'dots')[]>((acc, p, i, arr) => { + if (i > 0 && p - (arr[i - 1] as number) > 1) acc.push('dots') + acc.push(p) + return acc + }, []) + .map((p, i) => + p === 'dots' ? ( + ... + ) : ( + + ) + ) + } + + + +
+ )}
) diff --git a/admin-compliance/app/sdk/document-generator/page.tsx b/admin-compliance/app/sdk/document-generator/page.tsx index 51a39d5..dfae88d 100644 --- a/admin-compliance/app/sdk/document-generator/page.tsx +++ b/admin-compliance/app/sdk/document-generator/page.tsx @@ -44,6 +44,7 @@ const CATEGORIES: { key: string; label: string; types: string[] | null }[] = [ { key: 'cloud', label: 'Cloud', types: ['cloud_service_agreement'] }, { key: 'misc', label: 'Weitere', types: ['community_guidelines', 'copyright_policy', 'data_usage_clause'] }, { key: 'dsfa', label: 'DSFA', types: ['dsfa'] }, + { key: 'security', label: 'Sicherheitskonzepte', types: ['it_security_concept', 'data_protection_concept', 'backup_recovery_concept', 'logging_concept', 'incident_response_plan', 'access_control_concept', 'risk_management_concept'] }, ] // ============================================================================= diff --git a/admin-compliance/app/sdk/evidence/page.tsx b/admin-compliance/app/sdk/evidence/page.tsx index a66844f..a5bb1a9 100644 --- a/admin-compliance/app/sdk/evidence/page.tsx +++ b/admin-compliance/app/sdk/evidence/page.tsx @@ -303,8 +303,76 @@ function LoadingSkeleton() { // MAIN PAGE // ============================================================================= +// ============================================================================= +// EVIDENCE CHECK TYPES +// ============================================================================= + +interface EvidenceCheck { + id: string + check_code: string + title: string + description: string | null + check_type: string + target_url: string | null + frequency: string + is_active: boolean + last_run_at: string | null + next_run_at: string | null +} + +interface CheckResult { + id: string + check_id: string + run_status: string + summary: string | null + findings_count: number + critical_findings: number + duration_ms: number + run_at: string +} + +interface EvidenceMapping { + id: string + evidence_id: string + control_code: string + mapping_type: string + verified_at: string | null + verified_by: string | null + notes: string | null +} + +interface CoverageReport { + total_controls: number + controls_with_evidence: number + controls_without_evidence: number + coverage_percent: number +} + +type EvidenceTabKey = 'evidence' | 'checks' | 'mapping' | 'report' + +const CHECK_TYPE_LABELS: Record = { + tls_scan: { label: 'TLS-Scan', color: 'bg-blue-100 text-blue-700' }, + header_check: { label: 'Header-Check', color: 'bg-green-100 text-green-700' }, + certificate_check: { label: 'Zertifikat', color: 'bg-yellow-100 text-yellow-700' }, + dns_check: { label: 'DNS-Check', color: 'bg-purple-100 text-purple-700' }, + api_scan: { label: 'API-Scan', color: 'bg-indigo-100 text-indigo-700' }, + config_scan: { label: 'Config-Scan', color: 'bg-orange-100 text-orange-700' }, + port_scan: { label: 'Port-Scan', color: 'bg-red-100 text-red-700' }, +} + +const RUN_STATUS_LABELS: Record = { + running: { label: 'Laeuft...', color: 'bg-blue-100 text-blue-700' }, + passed: { label: 'Bestanden', color: 'bg-green-100 text-green-700' }, + failed: { label: 'Fehlgeschlagen', color: 'bg-red-100 text-red-700' }, + warning: { label: 'Warnung', color: 'bg-yellow-100 text-yellow-700' }, + error: { label: 'Fehler', color: 'bg-red-100 text-red-700' }, +} + +const CHECK_API = '/api/sdk/v1/compliance/evidence-checks' + export default function EvidencePage() { const { state, dispatch } = useSDK() + const [activeTab, setActiveTab] = useState('evidence') const [filter, setFilter] = useState('all') const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -314,6 +382,17 @@ export default function EvidencePage() { const [pageSize] = useState(20) const [total, setTotal] = useState(0) + // Evidence Checks state + const [checks, setChecks] = useState([]) + const [checksLoading, setChecksLoading] = useState(false) + const [runningCheckId, setRunningCheckId] = useState(null) + const [checkResults, setCheckResults] = useState>({}) + + // Mappings state + const [mappings, setMappings] = useState([]) + const [coverageReport, setCoverageReport] = useState(null) + const [seedingChecks, setSeedingChecks] = useState(false) + // Fetch evidence from backend on mount and when page changes useEffect(() => { const fetchEvidence = async () => { @@ -511,8 +590,86 @@ export default function EvidencePage() { } } + // Load checks when tab changes + const loadChecks = async () => { + setChecksLoading(true) + try { + const res = await fetch(`${CHECK_API}?limit=50`) + if (res.ok) { + const data = await res.json() + setChecks(data.checks || []) + } + } catch { /* silent */ } + finally { setChecksLoading(false) } + } + + const runCheck = async (checkId: string) => { + setRunningCheckId(checkId) + try { + const res = await fetch(`${CHECK_API}/${checkId}/run`, { method: 'POST' }) + if (res.ok) { + const result = await res.json() + setCheckResults(prev => ({ + ...prev, + [checkId]: [result, ...(prev[checkId] || [])].slice(0, 5), + })) + loadChecks() // refresh last_run_at + } + } catch { /* silent */ } + finally { setRunningCheckId(null) } + } + + const loadCheckResults = async (checkId: string) => { + try { + const res = await fetch(`${CHECK_API}/${checkId}/results?limit=5`) + if (res.ok) { + const data = await res.json() + setCheckResults(prev => ({ ...prev, [checkId]: data.results || [] })) + } + } catch { /* silent */ } + } + + const seedChecks = async () => { + setSeedingChecks(true) + try { + await fetch(`${CHECK_API}/seed`, { method: 'POST' }) + loadChecks() + } catch { /* silent */ } + finally { setSeedingChecks(false) } + } + + const loadMappings = async () => { + try { + const res = await fetch(`${CHECK_API}/mappings`) + if (res.ok) { + const data = await res.json() + setMappings(data.mappings || []) + } + } catch { /* silent */ } + } + + const loadCoverageReport = async () => { + try { + const res = await fetch(`${CHECK_API}/mappings/report`) + if (res.ok) setCoverageReport(await res.json()) + } catch { /* silent */ } + } + + useEffect(() => { + if (activeTab === 'checks' && checks.length === 0) loadChecks() + if (activeTab === 'mapping') { loadMappings(); loadCoverageReport() } + if (activeTab === 'report') loadCoverageReport() + }, [activeTab]) // eslint-disable-line react-hooks/exhaustive-deps + const stepInfo = STEP_EXPLANATIONS['evidence'] + const evidenceTabs: { key: EvidenceTabKey; label: string }[] = [ + { key: 'evidence', label: 'Nachweise' }, + { key: 'checks', label: 'Automatische Checks' }, + { key: 'mapping', label: 'Control-Mapping' }, + { key: 'report', label: 'Report' }, + ] + return (
{/* Hidden file input */} @@ -556,6 +713,25 @@ export default function EvidencePage() { + {/* Tab Navigation */} +
+
+ {evidenceTabs.map(tab => ( + + ))} +
+
+ {/* Error Banner */} {error && (
@@ -564,6 +740,11 @@ export default function EvidencePage() {
)} + {/* ============================================================ */} + {/* TAB: Nachweise (existing content) */} + {/* ============================================================ */} + {activeTab === 'evidence' && <> + {/* Controls Alert */} {state.controls.length === 0 && !loading && (
@@ -681,6 +862,250 @@ export default function EvidencePage() {

Passen Sie den Filter an oder laden Sie neue Nachweise hoch.

)} + + } + + {/* ============================================================ */} + {/* TAB: Automatische Checks */} + {/* ============================================================ */} + {activeTab === 'checks' && ( + <> + {/* Seed if empty */} + {!checksLoading && checks.length === 0 && ( +
+
+

Keine automatischen Checks vorhanden

+

Laden Sie ca. 15 Standard-Checks (TLS, Header, Zertifikate, etc.).

+
+ +
+ )} + + {checksLoading ? ( +
+
+
+ ) : ( +
+ {checks.map(check => { + const typeMeta = CHECK_TYPE_LABELS[check.check_type] || { label: check.check_type, color: 'bg-gray-100 text-gray-700' } + const results = checkResults[check.id] || [] + const lastResult = results[0] + const isRunning = runningCheckId === check.id + + return ( +
+
+
+
+

{check.title}

+ {typeMeta.label} + {!check.is_active && ( + Deaktiviert + )} +
+ {check.description &&

{check.description}

} +
+ Code: {check.check_code} + {check.target_url && Ziel: {check.target_url}} + Frequenz: {check.frequency} + {check.last_run_at && Letzter Lauf: {new Date(check.last_run_at).toLocaleDateString('de-DE')}} +
+
+
+ {lastResult && ( + + {RUN_STATUS_LABELS[lastResult.run_status]?.label || lastResult.run_status} + + )} + + +
+
+ + {/* Results */} + {results.length > 0 && ( +
+

Letzte Ergebnisse

+
+ {results.slice(0, 3).map(r => ( +
+ + {RUN_STATUS_LABELS[r.run_status]?.label || r.run_status} + + {new Date(r.run_at).toLocaleString('de-DE')} + {r.duration_ms}ms + {r.findings_count > 0 && ( + {r.findings_count} Findings ({r.critical_findings} krit.) + )} + {r.summary && {r.summary}} +
+ ))} +
+
+ )} +
+ ) + })} +
+ )} + + )} + + {/* ============================================================ */} + {/* TAB: Control-Mapping */} + {/* ============================================================ */} + {activeTab === 'mapping' && ( + <> + {coverageReport && ( +
+
+

Gesamt Controls

+

{coverageReport.total_controls}

+
+
+

Mit Nachweis

+

{coverageReport.controls_with_evidence}

+
+
+

Ohne Nachweis

+

{coverageReport.controls_without_evidence}

+
+
+

Abdeckung

+

{coverageReport.coverage_percent.toFixed(0)}%

+
+
+ )} + +
+
+

Evidence-Control-Verknuepfungen ({mappings.length})

+
+ {mappings.length === 0 ? ( +
+

Noch keine Verknuepfungen erstellt.

+

Fuehren Sie automatische Checks aus, um Nachweise automatisch mit Controls zu verknuepfen.

+
+ ) : ( + + + + + + + + + + + {mappings.map(m => ( + + + + + + + ))} + +
ControlEvidenceTypVerifiziert
{m.control_code}{m.evidence_id.slice(0, 8)}... + {m.mapping_type} + + {m.verified_at ? `${new Date(m.verified_at).toLocaleDateString('de-DE')} von ${m.verified_by || 'โ€”'}` : 'Ausstehend'} +
+ )} +
+ + )} + + {/* ============================================================ */} + {/* TAB: Report */} + {/* ============================================================ */} + {activeTab === 'report' && ( +
+

Evidence Coverage Report

+ + {!coverageReport ? ( +
+
+
+ ) : ( + <> + {/* Coverage Bar */} +
+
+ Gesamt-Abdeckung + = 80 ? 'text-green-600' : + coverageReport.coverage_percent >= 50 ? 'text-yellow-600' : 'text-red-600' + }`}> + {coverageReport.coverage_percent.toFixed(1)}% + +
+
+
= 80 ? 'bg-green-500' : + coverageReport.coverage_percent >= 50 ? 'bg-yellow-500' : 'bg-red-500' + }`} + style={{ width: `${coverageReport.coverage_percent}%` }} + /> +
+
+ + {/* Summary */} +
+
+

{coverageReport.total_controls}

+

Controls gesamt

+
+
+

{coverageReport.controls_with_evidence}

+

Mit Nachweis belegt

+
+
+

{coverageReport.controls_without_evidence}

+

Ohne Nachweis

+
+
+ + {/* Check Summary */} +
+

Automatische Checks

+
+ {checks.length} Check-Definitionen + {checks.filter(c => c.is_active).length} aktiv + {checks.filter(c => c.last_run_at).length} mindestens 1x ausgefuehrt +
+
+ + {/* Evidence Summary */} +
+

Nachweise

+
+ {displayEvidence.length} Nachweise gesamt + {validCount} gueltig + {expiredCount} abgelaufen + {pendingCount} ausstehend +
+
+ + )} +
+ )}
) } diff --git a/admin-compliance/app/sdk/process-tasks/page.tsx b/admin-compliance/app/sdk/process-tasks/page.tsx new file mode 100644 index 0000000..32d2bf4 --- /dev/null +++ b/admin-compliance/app/sdk/process-tasks/page.tsx @@ -0,0 +1,1383 @@ +'use client' + +import React, { useState, useEffect, useCallback } from 'react' +import { useSDK } from '@/lib/sdk' + +// ============================================================================= +// Types +// ============================================================================= + +interface ProcessTask { + id: string + tenant_id: string + project_id: string | null + task_code: string + title: string + description: string | null + category: string + priority: string + frequency: string + assigned_to: string | null + responsible_team: string | null + linked_control_ids: string[] + linked_module: string | null + last_completed_at: string | null + next_due_date: string | null + due_reminder_days: number + status: string + completion_date: string | null + completion_result: string | null + completion_evidence_id: string | null + follow_up_actions: string[] + is_seed: boolean + notes: string | null + tags: string[] + created_at: string + updated_at: string +} + +interface TaskStats { + total: number + by_status: Record + by_category: Record + overdue_count: number + due_7_days: number + due_14_days: number + due_30_days: number +} + +interface TaskFormData { + task_code: string + title: string + description: string + category: string + priority: string + frequency: string + assigned_to: string + responsible_team: string + linked_module: string + next_due_date: string + due_reminder_days: number + notes: string +} + +interface CompleteFormData { + completed_by: string + result: string + notes: string +} + +interface HistoryEntry { + id: string + task_id: string + completed_by: string | null + completed_at: string + result: string | null + evidence_id: string | null + notes: string | null + status: string +} + +const EMPTY_FORM: TaskFormData = { + task_code: '', + title: '', + description: '', + category: 'dsgvo', + priority: 'medium', + frequency: 'yearly', + assigned_to: '', + responsible_team: '', + linked_module: '', + next_due_date: '', + due_reminder_days: 14, + notes: '', +} + +const EMPTY_COMPLETE: CompleteFormData = { + completed_by: '', + result: '', + notes: '', +} + +const API = '/api/sdk/v1/compliance/process-tasks' + +// ============================================================================= +// Constants +// ============================================================================= + +const CATEGORY_LABELS: Record = { + dsgvo: 'DSGVO', + nis2: 'NIS2', + bsi: 'BSI', + iso27001: 'ISO 27001', + ai_act: 'AI Act', + internal: 'Intern', +} + +const CATEGORY_COLORS: Record = { + dsgvo: 'bg-blue-100 text-blue-700', + nis2: 'bg-purple-100 text-purple-700', + bsi: 'bg-green-100 text-green-700', + iso27001: 'bg-indigo-100 text-indigo-700', + ai_act: 'bg-orange-100 text-orange-700', + internal: 'bg-gray-100 text-gray-600', +} + +const PRIORITY_LABELS: Record = { + critical: 'Kritisch', + high: 'Hoch', + medium: 'Mittel', + low: 'Niedrig', +} + +const PRIORITY_COLORS: Record = { + critical: 'bg-red-100 text-red-700', + high: 'bg-orange-100 text-orange-700', + medium: 'bg-yellow-100 text-yellow-700', + low: 'bg-green-100 text-green-700', +} + +const STATUS_LABELS: Record = { + pending: 'Ausstehend', + in_progress: 'In Bearbeitung', + completed: 'Erledigt', + overdue: 'Ueberfaellig', + skipped: 'Uebersprungen', +} + +const STATUS_COLORS: Record = { + pending: 'bg-gray-100 text-gray-600', + in_progress: 'bg-blue-100 text-blue-700', + completed: 'bg-green-100 text-green-700', + overdue: 'bg-red-100 text-red-700', + skipped: 'bg-yellow-100 text-yellow-700', +} + +const STATUS_ICONS: Record = { + pending: '\u25CB', + in_progress: '\u25D4', + completed: '\u2714', + overdue: '\u26A0', + skipped: '\u2192', +} + +const FREQUENCY_LABELS: Record = { + weekly: 'Woechentlich', + monthly: 'Monatlich', + quarterly: 'Quartalsweise', + semi_annual: 'Halbjaehrlich', + yearly: 'Jaehrlich', + once: 'Einmalig', +} + +// ============================================================================= +// Helpers +// ============================================================================= + +function formatDate(d: string | null): string { + if (!d) return '\u2014' + const dt = new Date(d) + return dt.toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric' }) +} + +function daysUntil(d: string | null): number | null { + if (!d) return null + return Math.ceil((new Date(d).getTime() - Date.now()) / 86400000) +} + +function dueLabel(d: string | null): string { + const days = daysUntil(d) + if (days === null) return '\u2014' + if (days < 0) return `${Math.abs(days)} Tage ueberfaellig` + if (days === 0) return 'Heute faellig' + if (days === 1) return 'Morgen faellig' + return `In ${days} Tagen` +} + +function dueLabelColor(d: string | null): string { + const days = daysUntil(d) + if (days === null) return 'text-gray-400' + if (days < 0) return 'text-red-600 font-semibold' + if (days <= 7) return 'text-orange-600' + if (days <= 30) return 'text-yellow-600' + return 'text-green-600' +} + +// ============================================================================= +// Toast +// ============================================================================= + +function Toast({ message, onClose }: { message: string; onClose: () => void }) { + useEffect(() => { + const t = setTimeout(onClose, 3000) + return () => clearTimeout(t) + }, [onClose]) + + return ( +
+ {message} +
+ ) +} + +// ============================================================================= +// Complete Modal +// ============================================================================= + +function CompleteModal({ + task, + onClose, + onComplete, +}: { + task: ProcessTask + onClose: () => void + onComplete: (data: CompleteFormData) => Promise +}) { + const [form, setForm] = useState({ ...EMPTY_COMPLETE }) + const [saving, setSaving] = useState(false) + + const handleSave = async () => { + setSaving(true) + try { + await onComplete(form) + onClose() + } catch { + setSaving(false) + } + } + + return ( +
e.target === e.currentTarget && onClose()}> +
+
+

Aufgabe erledigen

+ +
+
+

{task.title}

+
+ + setForm(prev => ({ ...prev, completed_by: e.target.value }))} + className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500" + placeholder="Name / Rolle" + /> +
+
+ +