feat: Vorbereitung-Module auf 100% — Persistenz, Backend-Services, UCCA Frontend
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
All checks were successful
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / test-go-ai-compliance (push) Successful in 37s
CI / test-python-backend-compliance (push) Successful in 32s
CI / test-python-document-crawler (push) Successful in 22s
CI / test-python-dsms-gateway (push) Successful in 18s
Phase A: PostgreSQL State Store (sdk_states Tabelle, InMemory-Fallback) Phase B: Modules dynamisch vom Backend, Scope DB-Persistenz, Source Policy State Phase C: UCCA Frontend (3 Seiten, Wizard, RiskScoreGauge), Obligations Live-Daten Phase D: Document Import (PDF/LLM/Gap-Analyse), System Screening (SBOM/OSV.dev) Phase E: Company Profile CRUD mit Audit-Logging Phase F: Tests (Python + TypeScript), flow-data.ts DB-Tabellen aktualisiert Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -148,7 +148,7 @@ export function Sidebar({ onRoleChange }: SidebarProps) {
|
||||
<div className="h-16 flex items-center justify-between px-4 border-b border-slate-700">
|
||||
{!collapsed && (
|
||||
<Link href="/dashboard" className="font-bold text-lg">
|
||||
Admin Lehrer KI
|
||||
Compliance Admin
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
'use client'
|
||||
|
||||
import { RiskScoreGauge } from './RiskScoreGauge'
|
||||
|
||||
interface AssessmentResult {
|
||||
feasibility: string
|
||||
risk_level: string
|
||||
risk_score: number
|
||||
complexity: string
|
||||
dsfa_recommended: boolean
|
||||
art22_risk: boolean
|
||||
training_allowed: string
|
||||
summary: string
|
||||
recommendation: string
|
||||
alternative_approach?: string
|
||||
triggered_rules?: Array<{
|
||||
rule_code: string
|
||||
title: string
|
||||
severity: string
|
||||
gdpr_ref: string
|
||||
}>
|
||||
required_controls?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
effort: string
|
||||
}>
|
||||
recommended_architecture?: Array<{
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
benefit: string
|
||||
}>
|
||||
}
|
||||
|
||||
interface AssessmentResultCardProps {
|
||||
result: AssessmentResult
|
||||
}
|
||||
|
||||
const FEASIBILITY_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
YES: { bg: 'bg-green-100', text: 'text-green-700', label: 'Machbar' },
|
||||
CONDITIONAL: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Bedingt machbar' },
|
||||
NO: { bg: 'bg-red-100', text: 'text-red-700', label: 'Nicht empfohlen' },
|
||||
}
|
||||
|
||||
const SEVERITY_STYLES: Record<string, string> = {
|
||||
INFO: 'bg-blue-100 text-blue-700',
|
||||
WARN: 'bg-yellow-100 text-yellow-700',
|
||||
BLOCK: 'bg-red-100 text-red-700',
|
||||
}
|
||||
|
||||
export function AssessmentResultCard({ result }: AssessmentResultCardProps) {
|
||||
const feasibility = FEASIBILITY_STYLES[result.feasibility] || FEASIBILITY_STYLES.YES
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header with Score and Feasibility */}
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<div className="flex items-start gap-6">
|
||||
<RiskScoreGauge score={result.risk_score} riskLevel={result.risk_level} size="lg" />
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<span className={`px-3 py-1 rounded-full text-sm font-medium ${feasibility.bg} ${feasibility.text}`}>
|
||||
{feasibility.label}
|
||||
</span>
|
||||
<span className="px-3 py-1 rounded-full text-sm bg-gray-100 text-gray-700">
|
||||
Komplexitaet: {result.complexity}
|
||||
</span>
|
||||
{result.dsfa_recommended && (
|
||||
<span className="px-3 py-1 rounded-full text-sm bg-orange-100 text-orange-700">
|
||||
DSFA empfohlen
|
||||
</span>
|
||||
)}
|
||||
{result.art22_risk && (
|
||||
<span className="px-3 py-1 rounded-full text-sm bg-red-100 text-red-700">
|
||||
Art. 22 Risiko
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-gray-700">{result.summary}</p>
|
||||
<p className="text-sm text-gray-500 mt-2">{result.recommendation}</p>
|
||||
{result.alternative_approach && (
|
||||
<div className="mt-3 p-3 bg-blue-50 rounded-lg text-sm text-blue-700">
|
||||
<span className="font-medium">Alternative: </span>
|
||||
{result.alternative_approach}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Triggered Rules */}
|
||||
{result.triggered_rules && result.triggered_rules.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Ausgeloeste Regeln ({result.triggered_rules.length})
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{result.triggered_rules.map((rule) => (
|
||||
<div key={rule.rule_code} className="flex items-center gap-3 p-3 bg-gray-50 rounded-lg">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${SEVERITY_STYLES[rule.severity] || 'bg-gray-100 text-gray-700'}`}>
|
||||
{rule.severity}
|
||||
</span>
|
||||
<span className="text-xs text-gray-400 font-mono">{rule.rule_code}</span>
|
||||
<span className="text-sm font-medium text-gray-800 flex-1">{rule.title}</span>
|
||||
<span className="text-xs text-purple-600">{rule.gdpr_ref}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Required Controls */}
|
||||
{result.required_controls && result.required_controls.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Erforderliche Kontrollen ({result.required_controls.length})
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{result.required_controls.map((control) => (
|
||||
<div key={control.id} className="p-4 border border-gray-200 rounded-lg">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-medium text-gray-900 text-sm">{control.title}</span>
|
||||
<span className="px-2 py-0.5 text-xs bg-gray-100 text-gray-600 rounded">
|
||||
{control.effort}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500">{control.description}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Recommended Architecture Patterns */}
|
||||
{result.recommended_architecture && result.recommended_architecture.length > 0 && (
|
||||
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
||||
<h3 className="text-lg font-semibold text-gray-900 mb-4">
|
||||
Empfohlene Architektur-Patterns
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
{result.recommended_architecture.map((pattern) => (
|
||||
<div key={pattern.id} className="p-4 bg-purple-50 border border-purple-200 rounded-lg">
|
||||
<h4 className="font-medium text-purple-900">{pattern.title}</h4>
|
||||
<p className="text-sm text-purple-700 mt-1">{pattern.description}</p>
|
||||
<p className="text-xs text-purple-600 mt-2">Vorteil: {pattern.benefit}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
'use client'
|
||||
|
||||
interface RiskScoreGaugeProps {
|
||||
score: number // 0-100
|
||||
riskLevel: string
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
}
|
||||
|
||||
const RISK_COLORS: Record<string, string> = {
|
||||
MINIMAL: '#22c55e',
|
||||
LOW: '#84cc16',
|
||||
MEDIUM: '#eab308',
|
||||
HIGH: '#f97316',
|
||||
UNACCEPTABLE: '#ef4444',
|
||||
}
|
||||
|
||||
const RISK_LABELS: Record<string, string> = {
|
||||
MINIMAL: 'Minimal',
|
||||
LOW: 'Niedrig',
|
||||
MEDIUM: 'Mittel',
|
||||
HIGH: 'Hoch',
|
||||
UNACCEPTABLE: 'Unzulaessig',
|
||||
}
|
||||
|
||||
export function RiskScoreGauge({ score, riskLevel, size = 'md' }: RiskScoreGaugeProps) {
|
||||
const color = RISK_COLORS[riskLevel] || '#9ca3af'
|
||||
const label = RISK_LABELS[riskLevel] || riskLevel
|
||||
|
||||
const sizes = {
|
||||
sm: { w: 80, r: 30, stroke: 6, fontSize: '1rem', labelSize: '0.65rem' },
|
||||
md: { w: 120, r: 46, stroke: 8, fontSize: '1.5rem', labelSize: '0.75rem' },
|
||||
lg: { w: 160, r: 62, stroke: 10, fontSize: '2rem', labelSize: '0.875rem' },
|
||||
}
|
||||
|
||||
const s = sizes[size]
|
||||
const circumference = 2 * Math.PI * s.r
|
||||
const dashOffset = circumference - (score / 100) * circumference
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center">
|
||||
<svg width={s.w} height={s.w} viewBox={`0 0 ${s.w} ${s.w}`}>
|
||||
{/* Background circle */}
|
||||
<circle
|
||||
cx={s.w / 2}
|
||||
cy={s.w / 2}
|
||||
r={s.r}
|
||||
fill="none"
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth={s.stroke}
|
||||
/>
|
||||
{/* Score arc */}
|
||||
<circle
|
||||
cx={s.w / 2}
|
||||
cy={s.w / 2}
|
||||
r={s.r}
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={s.stroke}
|
||||
strokeDasharray={circumference}
|
||||
strokeDashoffset={dashOffset}
|
||||
strokeLinecap="round"
|
||||
transform={`rotate(-90 ${s.w / 2} ${s.w / 2})`}
|
||||
className="transition-all duration-500"
|
||||
/>
|
||||
{/* Score text */}
|
||||
<text
|
||||
x={s.w / 2}
|
||||
y={s.w / 2}
|
||||
textAnchor="middle"
|
||||
dominantBaseline="central"
|
||||
fill={color}
|
||||
style={{ fontSize: s.fontSize, fontWeight: 700 }}
|
||||
>
|
||||
{score}
|
||||
</text>
|
||||
</svg>
|
||||
<span
|
||||
className="mt-1 font-medium"
|
||||
style={{ color, fontSize: s.labelSize }}
|
||||
>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user