[split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
98
website/app/admin/compliance/_components/ArchitekturTab.tsx
Normal file
98
website/app/admin/compliance/_components/ArchitekturTab.tsx
Normal file
@@ -0,0 +1,98 @@
|
||||
export default function ArchitekturTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900 mb-4">Systemarchitektur</h2>
|
||||
<p className="text-slate-600 mb-6">
|
||||
Das Compliance & Audit Framework ist modular aufgebaut und integriert sich nahtlos in die bestehende Breakpilot-Infrastruktur.
|
||||
</p>
|
||||
|
||||
{/* Architecture Diagram */}
|
||||
<div className="bg-slate-50 rounded-lg p-6 mb-6">
|
||||
<pre className="text-sm text-slate-700 font-mono whitespace-pre overflow-x-auto">{`
|
||||
┌─────────────────────────────────────────────────────────────────────────┐
|
||||
│ COMPLIANCE FRAMEWORK │
|
||||
├─────────────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ Next.js │ │ FastAPI │ │ PostgreSQL │ │
|
||||
│ │ Frontend │───▶│ Backend │───▶│ Database │ │
|
||||
│ │ (Port 3000)│ │ (Port 8000)│ │ (Port 5432)│ │
|
||||
│ └─────────────┘ └─────────────┘ └─────────────┘ │
|
||||
│ │ │ │ │
|
||||
│ │ │ │ │
|
||||
│ ┌─────▼─────┐ ┌─────▼─────┐ ┌─────▼─────┐ │
|
||||
│ │ Admin UI │ │ Compliance │ │ 7 Tables │ │
|
||||
│ │ /admin/ │ │ Module │ │ compliance_│ │
|
||||
│ │compliance/│ │ /backend/ │ │ regulations│ │
|
||||
│ └───────────┘ │compliance/ │ │ _controls │ │
|
||||
│ └───────────┘ │ _evidence │ │
|
||||
│ │ _risks │ │
|
||||
│ │ ... │ │
|
||||
│ └───────────┘ │
|
||||
└─────────────────────────────────────────────────────────────────────────┘
|
||||
`}</pre>
|
||||
</div>
|
||||
|
||||
{/* Component Details */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="font-semibold text-slate-900 mb-2">Frontend (Next.js)</h3>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- Dashboard mit Compliance Score</li>
|
||||
<li>- Control Catalogue mit Filtern</li>
|
||||
<li>- Evidence Upload & Management</li>
|
||||
<li>- Risk Matrix Visualisierung</li>
|
||||
<li>- Audit Export Wizard</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="font-semibold text-slate-900 mb-2">Backend (FastAPI)</h3>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- REST API Endpoints</li>
|
||||
<li>- Repository Pattern</li>
|
||||
<li>- Pydantic Schemas</li>
|
||||
<li>- Seeder Service</li>
|
||||
<li>- Export Generator</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-4">
|
||||
<h3 className="font-semibold text-slate-900 mb-2">Datenbank (PostgreSQL)</h3>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- compliance_regulations</li>
|
||||
<li>- compliance_requirements</li>
|
||||
<li>- compliance_controls</li>
|
||||
<li>- compliance_control_mappings</li>
|
||||
<li>- compliance_evidence</li>
|
||||
<li>- compliance_risks</li>
|
||||
<li>- compliance_audit_exports</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Data Flow */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Datenfluss</h3>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ num: 1, bg: 'bg-blue-50', numBg: 'bg-blue-500', title: 'Regulations & Requirements', desc: 'EU-Verordnungen, BSI-Standards werden als Seed-Daten geladen' },
|
||||
{ num: 2, bg: 'bg-green-50', numBg: 'bg-green-500', title: 'Controls & Mappings', desc: 'Technische Controls werden Requirements zugeordnet' },
|
||||
{ num: 3, bg: 'bg-purple-50', numBg: 'bg-purple-500', title: 'Evidence Collection', desc: 'Nachweise werden manuell oder automatisiert erfasst' },
|
||||
{ num: 4, bg: 'bg-orange-50', numBg: 'bg-orange-500', title: 'Audit Export', desc: 'ZIP-Pakete fuer externe Pruefer generieren' },
|
||||
].map((step) => (
|
||||
<div key={step.num} className={`flex items-center gap-4 p-4 ${step.bg} rounded-lg`}>
|
||||
<div className={`w-8 h-8 ${step.numBg} text-white rounded-full flex items-center justify-center font-bold`}>{step.num}</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{step.title}</p>
|
||||
<p className="text-sm text-slate-600">{step.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
81
website/app/admin/compliance/_components/AuditTab.tsx
Normal file
81
website/app/admin/compliance/_components/AuditTab.tsx
Normal file
@@ -0,0 +1,81 @@
|
||||
import Link from 'next/link'
|
||||
|
||||
export default function AuditTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Audit Export</h3>
|
||||
<Link
|
||||
href="/admin/compliance/export"
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 text-sm"
|
||||
>
|
||||
Export Wizard oeffnen
|
||||
</Link>
|
||||
</div>
|
||||
<p className="text-slate-600 mb-6">
|
||||
Erstellen Sie ZIP-Pakete mit allen relevanten Compliance-Daten fuer externe Pruefer.
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Vollstaendiger Export</h4>
|
||||
<p className="text-sm text-slate-600">Alle Daten inkl. Regulations, Controls, Evidence, Risks</p>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Nur Controls</h4>
|
||||
<p className="text-sm text-slate-600">Control Catalogue mit Mappings</p>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Nur Evidence</h4>
|
||||
<p className="text-sm text-slate-600">Evidence-Dateien und Metadaten</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Format */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Export Format</h3>
|
||||
<div className="bg-slate-50 rounded-lg p-4 font-mono text-sm">
|
||||
<pre className="whitespace-pre overflow-x-auto">{`
|
||||
audit_export_2026-01-16/
|
||||
├── index.html # Navigations-Uebersicht
|
||||
├── summary.json # Maschinenlesbare Zusammenfassung
|
||||
├── regulations/
|
||||
│ ├── gdpr.json
|
||||
│ ├── aiact.json
|
||||
│ └── ...
|
||||
├── controls/
|
||||
│ ├── control_catalogue.json
|
||||
│ └── control_catalogue.xlsx
|
||||
├── evidence/
|
||||
│ ├── scan_reports/
|
||||
│ ├── policies/
|
||||
│ └── configs/
|
||||
├── risks/
|
||||
│ └── risk_register.json
|
||||
└── README.md # Erklaerung fuer Pruefer
|
||||
`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Audit Quick Links */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<Link
|
||||
href="/admin/compliance/controls"
|
||||
className="bg-white rounded-xl shadow-sm border p-6 hover:border-primary-500 transition-colors"
|
||||
>
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Control Reviews</h4>
|
||||
<p className="text-sm text-slate-600">Controls ueberpruefen und Status aktualisieren</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/admin/compliance/evidence"
|
||||
className="bg-white rounded-xl shadow-sm border p-6 hover:border-primary-500 transition-colors"
|
||||
>
|
||||
<h4 className="font-semibold text-slate-900 mb-2">Evidence Management</h4>
|
||||
<p className="text-sm text-slate-600">Nachweise hochladen und verwalten</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
import { DOMAIN_LABELS } from '../types'
|
||||
|
||||
export default function DokumentationTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Quick Start Guide */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Quick Start Guide</h3>
|
||||
<div className="space-y-4">
|
||||
{[
|
||||
{ num: 1, title: 'Datenbank initialisieren', desc: 'Klicken Sie auf "Datenbank initialisieren" im Dashboard, um die Seed-Daten zu laden.' },
|
||||
{ num: 2, title: 'Controls reviewen', desc: 'Gehen Sie zum Control Catalogue und bewerten Sie den Status jedes Controls.' },
|
||||
{ num: 3, title: 'Evidence hochladen', desc: 'Laden Sie Nachweise (Scan-Reports, Policies, Screenshots) fuer Ihre Controls hoch.' },
|
||||
{ num: 4, title: 'Risiken bewerten', desc: 'Dokumentieren Sie identifizierte Risiken in der Risk Matrix.' },
|
||||
{ num: 5, title: 'Audit Export', desc: 'Generieren Sie ein ZIP-Paket fuer externe Pruefer.' },
|
||||
].map((step) => (
|
||||
<div key={step.num} className="flex gap-4">
|
||||
<div className="w-8 h-8 bg-primary-100 text-primary-600 rounded-full flex items-center justify-center font-bold flex-shrink-0">{step.num}</div>
|
||||
<div>
|
||||
<p className="font-medium text-slate-900">{step.title}</p>
|
||||
<p className="text-sm text-slate-600">{step.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Regulatory Framework */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Abgedeckte Verordnungen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-900 mb-2">EU-Verordnungen & Richtlinien</h4>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- DSGVO (Datenschutz-Grundverordnung)</li>
|
||||
<li>- AI Act (KI-Verordnung)</li>
|
||||
<li>- CRA (Cyber Resilience Act)</li>
|
||||
<li>- NIS2 (Netzwerk- und Informationssicherheit)</li>
|
||||
<li>- DSA (Digital Services Act)</li>
|
||||
<li>- Data Act (Datenverordnung)</li>
|
||||
<li>- DGA (Data Governance Act)</li>
|
||||
<li>- ePrivacy-Richtlinie</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-900 mb-2">Deutsche Standards</h4>
|
||||
<ul className="text-sm text-slate-600 space-y-1">
|
||||
<li>- BSI-TR-03161-1 (Mobile Anwendungen Teil 1)</li>
|
||||
<li>- BSI-TR-03161-2 (Mobile Anwendungen Teil 2)</li>
|
||||
<li>- BSI-TR-03161-3 (Mobile Anwendungen Teil 3)</li>
|
||||
<li>- TDDDG (Telekommunikation-Digitale-Dienste-Datenschutz)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Control Domains */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Control Domains</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
|
||||
{Object.entries(DOMAIN_LABELS).map(([key, label]) => (
|
||||
<div key={key} className="border rounded-lg p-4">
|
||||
<span className="font-mono text-xs text-primary-600 bg-primary-50 px-2 py-0.5 rounded">{key}</span>
|
||||
<p className="font-medium text-slate-900 mt-2">{label}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* External Links */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Externe Ressourcen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{[
|
||||
{ href: 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng', label: 'DSGVO - EUR-Lex' },
|
||||
{ href: 'https://eur-lex.europa.eu/eli/reg/2024/1689/oj/eng', label: 'AI Act - EUR-Lex' },
|
||||
{ href: 'https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng', label: 'CRA - EUR-Lex' },
|
||||
{ href: 'https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Standards-und-Zertifizierung/Technische-Richtlinien/TR-nach-Thema-sortiert/tr03161/tr-03161.html', label: 'BSI-TR-03161 - BSI' },
|
||||
].map((link) => (
|
||||
<a
|
||||
key={link.href}
|
||||
href={link.href}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-2 p-3 border rounded-lg hover:bg-slate-50"
|
||||
>
|
||||
<svg className="w-5 h-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
<span className="text-sm text-slate-700">{link.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
303
website/app/admin/compliance/_components/ExecutiveTab.tsx
Normal file
303
website/app/admin/compliance/_components/ExecutiveTab.tsx
Normal file
@@ -0,0 +1,303 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { ExecutiveDashboardData } from '../types'
|
||||
|
||||
const ComplianceTrendChart = dynamic(
|
||||
() => import('@/components/compliance/charts/ComplianceTrendChart'),
|
||||
{ ssr: false, loading: () => <div className="h-48 bg-slate-100 animate-pulse rounded" /> }
|
||||
)
|
||||
|
||||
interface ExecutiveTabProps {
|
||||
loading: boolean
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export default function ExecutiveTab({ loading, onRefresh }: ExecutiveTabProps) {
|
||||
const [executiveData, setExecutiveData] = useState<ExecutiveDashboardData | null>(null)
|
||||
const [execLoading, setExecLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
useEffect(() => {
|
||||
loadExecutiveData()
|
||||
}, [])
|
||||
|
||||
const loadExecutiveData = async () => {
|
||||
setExecLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/dashboard/executive`)
|
||||
if (res.ok) {
|
||||
setExecutiveData(await res.json())
|
||||
} else {
|
||||
setError('Executive Dashboard konnte nicht geladen werden')
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load executive dashboard:', err)
|
||||
setError('Verbindung zum Backend fehlgeschlagen')
|
||||
} finally {
|
||||
setExecLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (execLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !executiveData) {
|
||||
return (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-6 text-center">
|
||||
<p className="text-red-700">{error || 'Keine Daten verfuegbar'}</p>
|
||||
<button
|
||||
onClick={loadExecutiveData}
|
||||
className="mt-4 px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
|
||||
>
|
||||
Erneut versuchen
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const { traffic_light_status, overall_score, score_trend, score_change, top_risks, upcoming_deadlines, team_workload } = executiveData
|
||||
|
||||
const trafficLightColors = {
|
||||
green: { bg: 'bg-green-500', ring: 'ring-green-200', text: 'text-green-700', label: 'Gut' },
|
||||
yellow: { bg: 'bg-yellow-500', ring: 'ring-yellow-200', text: 'text-yellow-700', label: 'Achtung' },
|
||||
red: { bg: 'bg-red-500', ring: 'ring-red-200', text: 'text-red-700', label: 'Kritisch' },
|
||||
}
|
||||
|
||||
const tlConfig = trafficLightColors[traffic_light_status]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header Row: Traffic Light + Key Metrics */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
<TrafficLightCard
|
||||
tlConfig={tlConfig}
|
||||
overall_score={overall_score}
|
||||
score_change={score_change}
|
||||
/>
|
||||
<MetricCard
|
||||
label="Verordnungen"
|
||||
value={executiveData.total_regulations}
|
||||
detail={`${executiveData.total_requirements} Anforderungen`}
|
||||
iconBg="bg-blue-100"
|
||||
iconColor="text-blue-600"
|
||||
iconPath="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"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Massnahmen"
|
||||
value={executiveData.total_controls}
|
||||
detail="Technische Controls"
|
||||
iconBg="bg-green-100"
|
||||
iconColor="text-green-600"
|
||||
iconPath="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Offene Risiken"
|
||||
value={executiveData.open_risks}
|
||||
detail="Unmitigiert"
|
||||
iconBg="bg-red-100"
|
||||
iconColor="text-red-600"
|
||||
iconPath="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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Charts Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<TrendChartCard score_trend={score_trend} onRefresh={loadExecutiveData} />
|
||||
<TopRisksCard top_risks={top_risks} />
|
||||
</div>
|
||||
|
||||
{/* Bottom Row: Deadlines + Workload */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<DeadlinesCard upcoming_deadlines={upcoming_deadlines} />
|
||||
<WorkloadCard team_workload={team_workload} />
|
||||
</div>
|
||||
|
||||
{/* Last Updated */}
|
||||
<div className="text-right text-sm text-slate-400">
|
||||
Zuletzt aktualisiert: {new Date(executiveData.last_updated).toLocaleString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sub-components
|
||||
// ============================================================================
|
||||
|
||||
function TrafficLightCard({ tlConfig, overall_score, score_change }: {
|
||||
tlConfig: { bg: string; ring: string; text: string; label: string }
|
||||
overall_score: number
|
||||
score_change: number | null
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6 flex flex-col items-center justify-center">
|
||||
<div
|
||||
className={`w-28 h-28 rounded-full flex items-center justify-center ${tlConfig.bg} ring-8 ${tlConfig.ring} shadow-lg mb-4`}
|
||||
>
|
||||
<span className="text-4xl font-bold text-white">{overall_score.toFixed(0)}%</span>
|
||||
</div>
|
||||
<p className={`text-lg font-semibold ${tlConfig.text}`}>{tlConfig.label}</p>
|
||||
<p className="text-sm text-slate-500 mt-1">Erfuellungsgrad</p>
|
||||
{score_change !== null && (
|
||||
<p className={`text-sm mt-2 ${score_change >= 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{score_change >= 0 ? '\u2191' : '\u2193'} {Math.abs(score_change).toFixed(1)}% zum Vormonat
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function MetricCard({ label, value, detail, iconBg, iconColor, iconPath }: {
|
||||
label: string
|
||||
value: number
|
||||
detail: string
|
||||
iconBg: string
|
||||
iconColor: string
|
||||
iconPath: string
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<p className="text-sm text-slate-500">{label}</p>
|
||||
<span className={`w-8 h-8 ${iconBg} rounded-lg flex items-center justify-center`}>
|
||||
<svg className={`w-4 h-4 ${iconColor}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={iconPath} />
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-3xl font-bold text-slate-900">{value}</p>
|
||||
<p className="text-sm text-slate-500 mt-1">{detail}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TrendChartCard({ score_trend, onRefresh }: {
|
||||
score_trend: { date: string; score: number; label: string }[]
|
||||
onRefresh: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Compliance-Trend (12 Monate)</h3>
|
||||
<button onClick={onRefresh} className="text-sm text-primary-600 hover:text-primary-700">
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div className="h-48">
|
||||
<ComplianceTrendChart data={score_trend} lang="de" height={180} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TopRisksCard({ top_risks }: { top_risks: ExecutiveDashboardData['top_risks'] }) {
|
||||
const riskColors: Record<string, string> = {
|
||||
critical: 'bg-red-100 text-red-700 border-red-200',
|
||||
high: 'bg-orange-100 text-orange-700 border-orange-200',
|
||||
medium: 'bg-yellow-100 text-yellow-700 border-yellow-200',
|
||||
low: 'bg-green-100 text-green-700 border-green-200',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Top 5 Risiken</h3>
|
||||
{top_risks.length === 0 ? (
|
||||
<p className="text-slate-500 text-center py-8">Keine offenen Risiken</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{top_risks.map((risk) => (
|
||||
<div key={risk.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded border ${riskColors[risk.risk_level] || riskColors.medium}`}>
|
||||
{risk.risk_level.toUpperCase()}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="font-medium text-slate-900 truncate">{risk.title}</p>
|
||||
<p className="text-xs text-slate-500">{risk.owner || 'Kein Owner'}</p>
|
||||
</div>
|
||||
<span className="text-xs text-slate-400">{risk.risk_id}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DeadlinesCard({ upcoming_deadlines }: { upcoming_deadlines: ExecutiveDashboardData['upcoming_deadlines'] }) {
|
||||
const statusColors: Record<string, string> = {
|
||||
overdue: 'bg-red-100 text-red-700',
|
||||
at_risk: 'bg-yellow-100 text-yellow-700',
|
||||
on_track: 'bg-green-100 text-green-700',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Naechste Fristen</h3>
|
||||
{upcoming_deadlines.length === 0 ? (
|
||||
<p className="text-slate-500 text-center py-8">Keine anstehenden Fristen</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{upcoming_deadlines.slice(0, 5).map((deadline) => (
|
||||
<div key={deadline.id} className="flex items-center gap-3 p-3 bg-slate-50 rounded-lg">
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded ${statusColors[deadline.status] || statusColors.on_track}`}>
|
||||
{deadline.days_remaining < 0
|
||||
? `${Math.abs(deadline.days_remaining)}d ueberfaellig`
|
||||
: deadline.days_remaining === 0
|
||||
? 'Heute'
|
||||
: `${deadline.days_remaining}d`}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<p className="text-sm font-medium text-slate-900 truncate">{deadline.title}</p>
|
||||
<p className="text-xs text-slate-500">{new Date(deadline.deadline).toLocaleDateString('de-DE')}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function WorkloadCard({ team_workload }: { team_workload: ExecutiveDashboardData['team_workload'] }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Team-Auslastung</h3>
|
||||
{team_workload.length === 0 ? (
|
||||
<p className="text-slate-500 text-center py-8">Keine Daten verfuegbar</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{team_workload.slice(0, 5).map((member) => (
|
||||
<div key={member.name}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="font-medium text-slate-700">{member.name}</span>
|
||||
<span className="text-slate-500">
|
||||
{member.completed_tasks}/{member.total_tasks} ({member.completion_rate.toFixed(0)}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-200 rounded-full overflow-hidden flex">
|
||||
<div
|
||||
className="bg-green-500 h-full"
|
||||
style={{ width: `${(member.completed_tasks / member.total_tasks) * 100}%` }}
|
||||
/>
|
||||
<div
|
||||
className="bg-yellow-500 h-full"
|
||||
style={{ width: `${(member.in_progress_tasks / member.total_tasks) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
105
website/app/admin/compliance/_components/RoadmapTab.tsx
Normal file
105
website/app/admin/compliance/_components/RoadmapTab.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import { BACKLOG_ITEMS } from '../types'
|
||||
|
||||
export default function RoadmapTab() {
|
||||
const completedCount = BACKLOG_ITEMS.filter(i => i.status === 'completed').length
|
||||
const inProgressCount = BACKLOG_ITEMS.filter(i => i.status === 'in_progress').length
|
||||
const plannedCount = BACKLOG_ITEMS.filter(i => i.status === 'planned').length
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Progress Overview */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
||||
<p className="text-sm text-green-600 font-medium">Abgeschlossen</p>
|
||||
<p className="text-3xl font-bold text-green-700">{completedCount}</p>
|
||||
</div>
|
||||
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-6">
|
||||
<p className="text-sm text-yellow-600 font-medium">In Bearbeitung</p>
|
||||
<p className="text-3xl font-bold text-yellow-700">{inProgressCount}</p>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-xl p-6">
|
||||
<p className="text-sm text-slate-600 font-medium">Geplant</p>
|
||||
<p className="text-3xl font-bold text-slate-700">{plannedCount}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Implemented Features */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Implementierte Features (v2.0 - Stand: 2026-01-17)</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{[
|
||||
'558 Requirements aus 19 Regulations extrahiert',
|
||||
'44 Controls in 9 Domains mit Auto-Mapping',
|
||||
'474 Control-Mappings automatisch generiert',
|
||||
'30 Service-Module in Registry kartiert',
|
||||
'AI-Interpretation fuer alle Requirements',
|
||||
'EU-Lex Scraper fuer Live-Regulation-Fetch',
|
||||
'BSI-TR-03161 PDF Parser (alle 3 Teile)',
|
||||
'Evidence Management mit File Upload',
|
||||
'Risk Matrix (5x5 Likelihood x Impact)',
|
||||
'Audit Export Wizard (ZIP Generator)',
|
||||
'Compliance Score Berechnung',
|
||||
'Dashboard mit Echtzeit-Statistiken',
|
||||
].map((feature, idx) => (
|
||||
<div key={idx} className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-green-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="text-slate-700">{feature}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Backlog */}
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className="p-4 border-b">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Backlog</h3>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Titel</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Kategorie</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Prioritaet</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{BACKLOG_ITEMS.map((item) => (
|
||||
<tr key={item.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-medium text-slate-900">{item.title}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="text-sm text-slate-600">{item.category}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
item.priority === 'high' ? 'bg-red-100 text-red-700' :
|
||||
item.priority === 'medium' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{item.priority}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
item.status === 'completed' ? 'bg-green-100 text-green-700' :
|
||||
item.status === 'in_progress' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{item.status === 'completed' ? 'Fertig' :
|
||||
item.status === 'in_progress' ? 'In Arbeit' : 'Geplant'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
88
website/app/admin/compliance/_components/TechnischTab.tsx
Normal file
88
website/app/admin/compliance/_components/TechnischTab.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
export default function TechnischTab() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* API Endpoints */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">API Endpoints</h3>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{ method: 'GET', path: '/api/v1/compliance/regulations', desc: 'Liste aller Verordnungen' },
|
||||
{ method: 'GET', path: '/api/v1/compliance/controls', desc: 'Control Catalogue' },
|
||||
{ method: 'PUT', path: '/api/v1/compliance/controls/{id}/review', desc: 'Control Review' },
|
||||
{ method: 'GET', path: '/api/v1/compliance/evidence', desc: 'Evidence Liste' },
|
||||
{ method: 'POST', path: '/api/v1/compliance/evidence/upload', desc: 'Evidence Upload' },
|
||||
{ method: 'GET', path: '/api/v1/compliance/risks', desc: 'Risk Register' },
|
||||
{ method: 'GET', path: '/api/v1/compliance/risks/matrix', desc: 'Risk Matrix' },
|
||||
{ method: 'GET', path: '/api/v1/compliance/dashboard', desc: 'Dashboard Stats' },
|
||||
{ method: 'POST', path: '/api/v1/compliance/export', desc: 'Audit Export erstellen' },
|
||||
{ method: 'POST', path: '/api/v1/compliance/seed', desc: 'Datenbank seeden' },
|
||||
].map((ep, idx) => (
|
||||
<div key={idx} className="flex items-center gap-4 p-3 bg-slate-50 rounded-lg font-mono text-sm">
|
||||
<span className={`px-2 py-1 rounded text-xs font-bold ${
|
||||
ep.method === 'GET' ? 'bg-green-100 text-green-700' :
|
||||
ep.method === 'POST' ? 'bg-blue-100 text-blue-700' :
|
||||
'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
{ep.method}
|
||||
</span>
|
||||
<span className="text-slate-700 flex-1">{ep.path}</span>
|
||||
<span className="text-slate-500 text-xs">{ep.desc}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Database Schema */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Datenmodell</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{[
|
||||
{ table: 'compliance_regulations', fields: 'id, code, name, regulation_type, source_url, effective_date' },
|
||||
{ table: 'compliance_requirements', fields: 'id, regulation_id, article, title, description, is_applicable' },
|
||||
{ table: 'compliance_controls', fields: 'id, control_id, domain, title, status, is_automated, owner' },
|
||||
{ table: 'compliance_control_mappings', fields: 'id, requirement_id, control_id, coverage_level' },
|
||||
{ table: 'compliance_evidence', fields: 'id, control_id, evidence_type, title, artifact_path, status' },
|
||||
{ table: 'compliance_risks', fields: 'id, risk_id, title, likelihood, impact, inherent_risk, status' },
|
||||
{ table: 'compliance_audit_exports', fields: 'id, export_type, status, file_path, file_hash' },
|
||||
].map((t, idx) => (
|
||||
<div key={idx} className="border rounded-lg p-4">
|
||||
<h4 className="font-mono font-semibold text-primary-600 mb-2">{t.table}</h4>
|
||||
<p className="text-xs text-slate-500 font-mono">{t.fields}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enums */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Enums</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-semibold text-slate-900 mb-2">ControlDomainEnum</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['gov', 'priv', 'iam', 'crypto', 'sdlc', 'ops', 'ai', 'cra', 'aud'].map((d) => (
|
||||
<span key={d} className="px-2 py-0.5 bg-slate-100 text-slate-700 text-xs rounded">{d}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-semibold text-slate-900 mb-2">ControlStatusEnum</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['pass', 'partial', 'fail', 'planned', 'n/a'].map((s) => (
|
||||
<span key={s} className="px-2 py-0.5 bg-slate-100 text-slate-700 text-xs rounded">{s}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border rounded-lg p-4">
|
||||
<h4 className="font-semibold text-slate-900 mb-2">RiskLevelEnum</h4>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['low', 'medium', 'high', 'critical'].map((l) => (
|
||||
<span key={l} className="px-2 py-0.5 bg-slate-100 text-slate-700 text-xs rounded">{l}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
293
website/app/admin/compliance/_components/UebersichtTab.tsx
Normal file
293
website/app/admin/compliance/_components/UebersichtTab.tsx
Normal file
@@ -0,0 +1,293 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { DashboardData, Regulation, AIStatus, DOMAIN_LABELS } from '../types'
|
||||
|
||||
interface UebersichtTabProps {
|
||||
dashboard: DashboardData | null
|
||||
regulations: Regulation[]
|
||||
aiStatus: AIStatus | null
|
||||
loading: boolean
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export default function UebersichtTab({
|
||||
dashboard,
|
||||
regulations,
|
||||
aiStatus,
|
||||
loading,
|
||||
onRefresh,
|
||||
}: UebersichtTabProps) {
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const score = dashboard?.compliance_score || 0
|
||||
const scoreColor = score >= 80 ? 'text-green-600' : score >= 60 ? 'text-yellow-600' : 'text-red-600'
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* AI Status Banner */}
|
||||
<AIStatusBanner aiStatus={aiStatus} />
|
||||
|
||||
{/* Score and Stats Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
<ScoreCard score={score} scoreColor={scoreColor} dashboard={dashboard} />
|
||||
<StatCard
|
||||
label="Verordnungen"
|
||||
value={dashboard?.total_regulations || 0}
|
||||
detail={`${dashboard?.total_requirements || 0} Anforderungen`}
|
||||
iconBg="bg-blue-100"
|
||||
iconColor="text-blue-600"
|
||||
iconPath="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"
|
||||
/>
|
||||
<StatCard
|
||||
label="Controls"
|
||||
value={dashboard?.total_controls || 0}
|
||||
detail={`${dashboard?.controls_by_status?.pass || 0} bestanden`}
|
||||
iconBg="bg-green-100"
|
||||
iconColor="text-green-600"
|
||||
iconPath="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
<StatCard
|
||||
label="Nachweise"
|
||||
value={dashboard?.total_evidence || 0}
|
||||
detail={`${dashboard?.evidence_by_status?.valid || 0} aktiv`}
|
||||
iconBg="bg-purple-100"
|
||||
iconColor="text-purple-600"
|
||||
iconPath="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"
|
||||
/>
|
||||
<StatCard
|
||||
label="Risiken"
|
||||
value={dashboard?.total_risks || 0}
|
||||
detail={`${(dashboard?.risks_by_level?.high || 0) + (dashboard?.risks_by_level?.critical || 0)} kritisch`}
|
||||
iconBg="bg-red-100"
|
||||
iconColor="text-red-600"
|
||||
iconPath="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"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Domain Chart and Quick Actions */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<DomainChart dashboard={dashboard} />
|
||||
<QuickActions />
|
||||
</div>
|
||||
|
||||
{/* Regulations Table */}
|
||||
<RegulationsTable regulations={regulations} onRefresh={onRefresh} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Sub-components
|
||||
// ============================================================================
|
||||
|
||||
function AIStatusBanner({ aiStatus }: { aiStatus: AIStatus | null }) {
|
||||
if (!aiStatus) return null
|
||||
|
||||
return (
|
||||
<div className={`rounded-lg p-4 flex items-center justify-between ${
|
||||
aiStatus.is_available && !aiStatus.is_mock
|
||||
? 'bg-gradient-to-r from-purple-50 to-pink-50 border border-purple-200'
|
||||
: aiStatus.is_mock
|
||||
? 'bg-yellow-50 border border-yellow-200'
|
||||
: 'bg-red-50 border border-red-200'
|
||||
}`}>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-2xl">🤖</span>
|
||||
<div>
|
||||
<div className="font-medium text-slate-900">
|
||||
AI-Compliance-Assistent {aiStatus.is_available ? 'aktiv' : 'nicht verfuegbar'}
|
||||
</div>
|
||||
<div className="text-sm text-slate-600">
|
||||
{aiStatus.is_mock ? (
|
||||
<span className="text-yellow-700">Mock-Modus (kein API-Key konfiguriert)</span>
|
||||
) : (
|
||||
<>Provider: <span className="font-mono">{aiStatus.provider}</span> | Modell: <span className="font-mono">{aiStatus.model}</span></>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`px-3 py-1 rounded-full text-sm font-medium ${
|
||||
aiStatus.is_available && !aiStatus.is_mock
|
||||
? 'bg-green-100 text-green-700'
|
||||
: aiStatus.is_mock
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{aiStatus.is_available && !aiStatus.is_mock ? 'Online' : aiStatus.is_mock ? 'Mock' : 'Offline'}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ScoreCard({ score, scoreColor, dashboard }: { score: number; scoreColor: string; dashboard: DashboardData | null }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-sm font-medium text-slate-500 mb-4">Compliance Score</h3>
|
||||
<div className={`text-5xl font-bold ${scoreColor}`}>
|
||||
{score.toFixed(0)}%
|
||||
</div>
|
||||
<div className="mt-4 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all duration-500 ${score >= 80 ? 'bg-green-500' : score >= 60 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${score}%` }}
|
||||
/>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-500">
|
||||
{dashboard?.controls_by_status?.pass || 0} von {dashboard?.total_controls || 0} Controls bestanden
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StatCard({ label, value, detail, iconBg, iconColor, iconPath }: {
|
||||
label: string
|
||||
value: number
|
||||
detail: string
|
||||
iconBg: string
|
||||
iconColor: string
|
||||
iconPath: string
|
||||
}) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<p className="text-sm text-slate-500">{label}</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{value}</p>
|
||||
</div>
|
||||
<div className={`w-10 h-10 ${iconBg} rounded-lg flex items-center justify-center`}>
|
||||
<svg className={`w-5 h-5 ${iconColor}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={iconPath} />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-slate-500">{detail}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function DomainChart({ dashboard }: { dashboard: DashboardData | null }) {
|
||||
return (
|
||||
<div className="lg:col-span-2 bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Controls nach Domain</h3>
|
||||
<div className="space-y-4">
|
||||
{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 (
|
||||
<div key={domain}>
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="font-medium text-slate-700">
|
||||
{DOMAIN_LABELS[domain] || domain.toUpperCase()}
|
||||
</span>
|
||||
<span className="text-slate-500">
|
||||
{pass}/{total} ({passPercent.toFixed(0)}%)
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-slate-200 rounded-full overflow-hidden flex">
|
||||
<div className="bg-green-500 h-full" style={{ width: `${(pass / total) * 100}%` }} />
|
||||
<div className="bg-yellow-500 h-full" style={{ width: `${(partial / total) * 100}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function QuickActions() {
|
||||
const actions = [
|
||||
{ href: '/admin/compliance/controls', label: 'Controls', color: 'primary', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-primary-600', iconPath: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
{ href: '/admin/compliance/evidence', label: 'Evidence', color: 'purple', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-purple-600', iconPath: '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' },
|
||||
{ href: '/admin/compliance/risks', label: 'Risiken', color: 'red', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-red-600', iconPath: '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' },
|
||||
{ href: '/admin/compliance/scraper', label: 'Scraper', color: 'orange', hoverBorder: 'hover:border-orange-500', hoverBg: 'hover:bg-orange-50', iconColor: 'text-orange-600', iconPath: 'M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15' },
|
||||
{ href: '/admin/compliance/export', label: 'Export', color: 'green', hoverBorder: 'hover:border-primary-500', hoverBg: 'hover:bg-primary-50', iconColor: 'text-green-600', iconPath: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4' },
|
||||
{ href: '/admin/compliance/audit-workspace', label: 'Audit Workspace', color: 'blue', hoverBorder: 'hover:border-blue-500', hoverBg: 'hover:bg-blue-50', iconColor: 'text-blue-600', iconPath: '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' },
|
||||
{ href: '/admin/compliance/modules', label: 'Service Module Registry', color: 'pink', hoverBorder: 'hover:border-pink-500', hoverBg: 'hover:bg-pink-50', iconColor: 'text-pink-600', iconPath: 'M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schnellaktionen</h3>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
{actions.map((action) => (
|
||||
<Link
|
||||
key={action.href}
|
||||
href={action.href}
|
||||
className={`p-4 rounded-lg border border-slate-200 ${action.hoverBorder} ${action.hoverBg} transition-colors`}
|
||||
>
|
||||
<div className={`${action.iconColor} mb-2`}>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={action.iconPath} />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="font-medium text-slate-900 text-sm">{action.label}</p>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RegulationsTable({ regulations, onRefresh }: { regulations: Regulation[]; onRefresh: () => void }) {
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className="p-4 border-b flex items-center justify-between">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Verordnungen & Standards</h3>
|
||||
<button onClick={onRefresh} className="text-sm text-primary-600 hover:text-primary-700">
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Code</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Name</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Typ</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Anforderungen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{regulations.slice(0, 10).map((reg) => (
|
||||
<tr key={reg.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono font-medium text-primary-600">{reg.code}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="font-medium text-slate-900">{reg.name}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
reg.regulation_type === 'eu_regulation' ? 'bg-blue-100 text-blue-700' :
|
||||
reg.regulation_type === 'eu_directive' ? 'bg-purple-100 text-purple-700' :
|
||||
reg.regulation_type === 'bsi_standard' ? 'bg-green-100 text-green-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{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}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className="font-medium">{reg.requirement_count}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
191
website/app/admin/compliance/types.ts
Normal file
191
website/app/admin/compliance/types.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* Types and constants for the Compliance & Audit Framework Dashboard
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Data Types
|
||||
// ============================================================================
|
||||
|
||||
export interface DashboardData {
|
||||
compliance_score: number
|
||||
total_regulations: number
|
||||
total_requirements: number
|
||||
total_controls: number
|
||||
controls_by_status: Record<string, number>
|
||||
controls_by_domain: Record<string, Record<string, number>>
|
||||
total_evidence: number
|
||||
evidence_by_status: Record<string, number>
|
||||
total_risks: number
|
||||
risks_by_level: Record<string, number>
|
||||
}
|
||||
|
||||
export interface AIStatus {
|
||||
provider: string
|
||||
model: string
|
||||
is_available: boolean
|
||||
is_mock: boolean
|
||||
}
|
||||
|
||||
export interface Regulation {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
full_name: string
|
||||
regulation_type: string
|
||||
effective_date: string | null
|
||||
description: string
|
||||
requirement_count: number
|
||||
}
|
||||
|
||||
export interface ExecutiveDashboardData {
|
||||
traffic_light_status: 'green' | 'yellow' | 'red'
|
||||
overall_score: number
|
||||
score_trend: { date: string; score: number; label: string }[]
|
||||
previous_score: number | null
|
||||
score_change: number | null
|
||||
total_regulations: number
|
||||
total_requirements: number
|
||||
total_controls: number
|
||||
open_risks: number
|
||||
top_risks: {
|
||||
id: string
|
||||
risk_id: string
|
||||
title: string
|
||||
risk_level: string
|
||||
owner: string | null
|
||||
status: string
|
||||
category: string
|
||||
impact: number
|
||||
likelihood: number
|
||||
}[]
|
||||
upcoming_deadlines: {
|
||||
id: string
|
||||
title: string
|
||||
deadline: string
|
||||
days_remaining: number
|
||||
type: string
|
||||
status: string
|
||||
owner: string | null
|
||||
}[]
|
||||
team_workload: {
|
||||
name: string
|
||||
pending_tasks: number
|
||||
in_progress_tasks: number
|
||||
completed_tasks: number
|
||||
total_tasks: number
|
||||
completion_rate: number
|
||||
}[]
|
||||
last_updated: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tab Definitions
|
||||
// ============================================================================
|
||||
|
||||
export type TabId = 'executive' | 'uebersicht' | 'architektur' | 'roadmap' | 'technisch' | 'audit' | 'dokumentation'
|
||||
|
||||
export const tabs: { id: TabId; name: string; icon: JSX.Element }[] = [
|
||||
{
|
||||
id: 'executive',
|
||||
name: 'Executive',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" 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>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'uebersicht',
|
||||
name: 'Uebersicht',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'architektur',
|
||||
name: 'Architektur',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'roadmap',
|
||||
name: 'Roadmap',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="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" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'technisch',
|
||||
name: 'Technisch',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'audit',
|
||||
name: 'Audit',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'dokumentation',
|
||||
name: 'Dokumentation',
|
||||
icon: (
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
export const DOMAIN_LABELS: Record<string, string> = {
|
||||
gov: 'Governance',
|
||||
priv: 'Datenschutz',
|
||||
iam: 'Identity & Access',
|
||||
crypto: 'Kryptografie',
|
||||
sdlc: 'Secure Dev',
|
||||
ops: 'Operations',
|
||||
ai: 'KI-spezifisch',
|
||||
cra: 'Supply Chain',
|
||||
aud: 'Audit',
|
||||
}
|
||||
|
||||
export const STATUS_COLORS: Record<string, string> = {
|
||||
pass: 'bg-green-500',
|
||||
partial: 'bg-yellow-500',
|
||||
fail: 'bg-red-500',
|
||||
planned: 'bg-slate-400',
|
||||
'n/a': 'bg-slate-300',
|
||||
}
|
||||
|
||||
export const BACKLOG_ITEMS = [
|
||||
{ id: 1, title: 'EU-Lex Live-Fetch (19 Regulations)', priority: 'high', status: 'completed', category: 'Integration' },
|
||||
{ id: 2, title: 'BSI-TR-03161 PDF Parser (3 PDFs)', priority: 'high', status: 'completed', category: 'Data Import' },
|
||||
{ id: 3, title: 'AI-Interpretation fuer Requirements', priority: 'high', status: 'completed', category: 'AI' },
|
||||
{ id: 4, title: 'Auto-Mapping Controls zu Requirements (474)', priority: 'high', status: 'completed', category: 'Automation' },
|
||||
{ id: 5, title: 'Service-Modul-Registry (30 Module)', priority: 'high', status: 'completed', category: 'Architecture' },
|
||||
{ id: 6, title: 'Audit Trail fuer alle Aenderungen', priority: 'high', status: 'completed', category: 'Audit' },
|
||||
{ id: 7, title: 'Automatische Evidence-Sammlung aus CI/CD', priority: 'high', status: 'planned', category: 'Automation' },
|
||||
{ id: 8, title: 'Control-Review Workflow mit Benachrichtigungen', priority: 'medium', status: 'planned', category: 'Workflow' },
|
||||
{ id: 9, title: 'Risk Treatment Plan Tracking', priority: 'medium', status: 'planned', category: 'Risk Management' },
|
||||
{ id: 10, title: 'Compliance Score Trend-Analyse', priority: 'low', status: 'planned', category: 'Analytics' },
|
||||
{ id: 11, title: 'SBOM Integration fuer CRA Compliance', priority: 'medium', status: 'planned', category: 'Integration' },
|
||||
{ id: 12, title: 'Multi-Mandanten Compliance Trennung', priority: 'medium', status: 'planned', category: 'Architecture' },
|
||||
{ id: 13, title: 'Compliance Report PDF Generator', priority: 'medium', status: 'planned', category: 'Export' },
|
||||
]
|
||||
Reference in New Issue
Block a user