feat: Fortschritts-Tracker + Verifikation-Endpoints + Tech-File Erweiterung

- Übersicht: Completeness Gates durch Projektfortschritts-Tracker ersetzt
  (6 CE-Prozessschritte mit Status + Naechster-Schritt Empfehlung)
- Verifikation: GET/POST/DELETE /verifications Endpoints + Alias-Handler
- Tech-File: Anhang IV Struktur-Erweiterung
- Maßnahmen: Expandable Details vorbereitet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-08 01:02:41 +02:00
parent c4532049d8
commit 89af88ef7d
8 changed files with 184 additions and 280 deletions
@@ -48,6 +48,7 @@ export default function MitigationsPage() {
const [mitPages, setMitPages] = useState<Record<string, number>>({ design: 1, protection: 1, information: 1 })
const [selected, setSelected] = useState<Set<string>>(new Set())
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
const [expandedMeasure, setExpandedMeasure] = useState<string | null>(null)
function toggleSection(type: string) {
setExpanded((prev) => ({ ...prev, [type]: !prev[type] }))
@@ -203,31 +204,45 @@ export default function MitigationsPage() {
<div>Status</div>
</div>
{/* Rows — paginated */}
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => (
<div key={m.id}
className={`grid grid-cols-[24px_2fr_1fr_80px] gap-2 px-4 py-2 border-t border-gray-50 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors ${selected.has(m.id) ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
<div className="pt-0.5">
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
className="accent-purple-600" />
</div>
<div className="min-w-0">
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
{m.description && <div className="text-xs text-gray-400 mt-0.5">{m.description}</div>}
{(() => {
const refs = measureNorms[(m.title || '').toLowerCase()]
return refs?.length > 0 ? (
<div className="text-[9px] text-blue-500 mt-0.5">Normen: {refs.join(', ')}</div>
) : null
})()}
</div>
<div className="text-xs text-gray-500">
{(m.linked_hazard_names || []).join(', ') || '-'}
</div>
<div>
<StatusBadge status={m.status} />
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => {
const isDetailOpen = expandedMeasure === m.id
const catMatch = (m.description || '').match(/Kategorie\s+(\S+)/)
const category = catMatch?.[1]
const refs = measureNorms[(m.title || '').toLowerCase()]
return (
<div key={m.id}>
<div onClick={() => setExpandedMeasure(isDetailOpen ? null : m.id)}
className={`grid grid-cols-[24px_2fr_1fr_80px] gap-2 px-4 py-2 border-t border-gray-50 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors cursor-pointer ${selected.has(m.id) ? 'bg-purple-50 dark:bg-purple-900/10' : ''}`}>
<div className="pt-0.5" onClick={(e) => e.stopPropagation()}>
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
className="accent-purple-600" />
</div>
<div className="min-w-0 flex items-start gap-1">
<svg className={`w-3 h-3 mt-1 shrink-0 text-gray-400 transition-transform ${isDetailOpen ? 'rotate-90' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
<div>
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
{!isDetailOpen && category && <div className="text-[10px] text-gray-400 mt-0.5">Kategorie: {category}</div>}
</div>
</div>
<div className="text-xs text-gray-500">
{(m.linked_hazard_names || []).join(', ') || '-'}
</div>
<div>
<StatusBadge status={m.status} />
</div>
</div>
{isDetailOpen && (
<div className="px-12 py-3 bg-gray-50 dark:bg-gray-750 border-t border-gray-100 dark:border-gray-700 text-xs space-y-1">
{m.description && <p className="text-gray-600 dark:text-gray-300">{m.description}</p>}
{category && <p className="text-purple-600">Diese Massnahme gilt fuer alle Gefaehrdungen der Kategorie <strong>{category}</strong>.</p>}
{refs?.length > 0 && <p className="text-blue-500">Normen: {refs.join(', ')}</p>}
</div>
)}
</div>
))}
)
})}
{items.length > (mitPages[type] || 1) * 50 && (
<button onClick={() => setMitPages(prev => ({ ...prev, [type]: (prev[type] || 1) + 1 }))}
className="w-full py-2 text-xs text-purple-600 hover:bg-purple-50 border-t border-gray-100 transition-colors">
@@ -15,7 +15,7 @@ interface ProjectOverview {
completeness_pct: number
created_at: string
updated_at: string
gates: Gate[]
metadata?: { limits_form?: Record<string, unknown> }
risk_summary?: {
critical?: number
high?: number
@@ -28,14 +28,6 @@ interface ProjectOverview {
mitigation_count: number
}
interface Gate {
id: string
name: string
description: string
passed: boolean | null
required: boolean
}
const QUICK_ACTIONS = [
{ href: '/components', label: 'Komponenten verwalten', icon: 'cube', description: 'SW/FW/AI/HMI Baum bearbeiten' },
{ href: '/classification', label: 'Klassifikation pruefen', icon: 'tag', description: 'AI Act, MVO, CRA, NIS2' },
@@ -47,33 +39,6 @@ const QUICK_ACTIONS = [
{ href: '/monitoring', label: 'Monitoring', icon: 'activity', description: 'Post-Market Ueberwachung' },
]
function GateIndicator({ gate }: { gate: Gate }) {
const color = gate.passed === true
? 'bg-green-500'
: gate.passed === false
? 'bg-red-500'
: 'bg-gray-300'
const textColor = gate.passed === true
? 'text-green-700'
: gate.passed === false
? 'text-red-700'
: 'text-gray-500'
return (
<div className="flex items-center gap-3 py-2">
<div className={`w-3 h-3 rounded-full ${color} flex-shrink-0`} />
<div className="flex-1 min-w-0">
<div className={`text-sm font-medium ${textColor}`}>{gate.name}</div>
<div className="text-xs text-gray-400">{gate.description}</div>
</div>
{gate.required && (
<span className="text-xs text-gray-400 flex-shrink-0">Pflicht</span>
)}
</div>
)
}
function RiskGauge({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
const pct = max > 0 ? Math.round((value / max) * 100) : 0
return (
@@ -150,12 +115,15 @@ export default function ProjectOverviewPage() {
mitCount = live.total_mitigations || 0
}
// Calculate dynamic completeness percentage
// Calculate dynamic completeness percentage from CE process steps
const compCount = json.components?.length || 0
const gates = (json.completeness_gates || json.gates || [])
const gatesPassed = gates.filter((g: Record<string, unknown>) => g.passed === true).length
const gatesTotal = gates.length || 1
const completeness = Math.round((gatesPassed / gatesTotal) * 100)
const limitsForm = json.metadata?.limits_form || {}
const hasLimits = Object.keys(limitsForm).length > 0
const hasComponents = compCount > 0
const hasHazards = hazCount > 0
const hasMitigations = mitCount > 0
const stepsComplete = [hasLimits, hasComponents, hasHazards, hasMitigations].filter(Boolean).length
const completeness = Math.round((stepsComplete / 6) * 100)
setProject({
...json,
@@ -163,6 +131,7 @@ export default function ProjectOverviewPage() {
component_count: compCount,
hazard_count: hazCount,
mitigation_count: mitCount,
metadata: json.metadata,
risk_summary: {
critical: rs.critical || 0,
high: rs.high || 0,
@@ -170,13 +139,6 @@ export default function ProjectOverviewPage() {
low: rs.low || 0,
total: rs.total || hazCount,
},
gates: gates.map((g: Record<string, unknown>) => ({
id: g.id,
name: g.name || g.label || '',
description: g.description || g.details || '',
passed: g.passed,
required: g.required,
})),
})
} catch (err) {
console.error('Failed to fetch project:', err)
@@ -327,15 +289,44 @@ export default function ProjectOverviewPage() {
</div>
</div>
{/* Completeness Gates */}
{/* Progress Tracker */}
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">Completeness Gates</h2>
<div className="space-y-1">
{project.gates && project.gates.length > 0 ? (
project.gates.map((gate) => <GateIndicator key={gate.id} gate={gate} />)
) : (
<p className="text-sm text-gray-400">Keine Gates definiert</p>
)}
<div className="flex items-center justify-between mb-3">
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Projektfortschritt</h2>
<span className="text-sm font-bold text-purple-600">{project.completeness_pct}%</span>
</div>
<div className="w-full bg-gray-200 rounded-full h-2.5 mb-4">
<div className="bg-purple-500 h-2.5 rounded-full transition-all" style={{ width: `${project.completeness_pct}%` }} />
</div>
<div className="space-y-2">
{(() => {
const hasLimits = Object.keys(project.metadata?.limits_form || {}).length > 0
const steps = [
{ done: hasLimits, label: 'Grenzen definiert', detail: hasLimits ? 'Felder ausgefuellt' : 'ausstehend' },
{ done: project.component_count > 0, label: 'Komponenten erfasst', detail: `${project.component_count} Komponenten` },
{ done: project.hazard_count > 0, label: 'Gefaehrdungen identifiziert', detail: `${project.hazard_count} bewertet` },
{ done: project.mitigation_count > 0, label: 'Massnahmen zugeordnet', detail: `${project.mitigation_count} Massnahmen` },
{ done: false, label: 'Verifikation', detail: 'ausstehend' },
{ done: false, label: 'CE-Akte', detail: 'ausstehend' },
]
const firstPending = steps.find((s) => !s.done)
return (
<>
{steps.map((step) => (
<div key={step.label} className="flex items-center gap-2">
<span className={`text-sm flex-shrink-0 ${step.done ? 'text-green-600' : 'text-gray-400'}`}>
{step.done ? '\u2713' : '\u25CB'}
</span>
<span className={`text-sm flex-1 ${step.done ? 'text-gray-900 dark:text-white' : 'text-gray-400'}`}>{step.label}</span>
<span className="text-xs text-gray-400">{step.detail}</span>
</div>
))}
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500">
Naechster Schritt: {firstPending?.label || 'Alle Schritte abgeschlossen'}
</div>
</>
)
})()}
</div>
</div>
</div>
@@ -19,21 +19,51 @@ interface TechFileSection {
}
const SECTION_TYPES: Record<string, { icon: string; description: string }> = {
risk_assessment_report: {
icon: '📊',
description: 'Zusammenfassung der Risikobeurteilung mit allen bewerteten Gefaehrdungen',
// Annex IV mandatory sections (EU Machinery Regulation 2023/1230)
general_description: {
icon: '🏭',
description: 'Anhang IV.1 — Allgemeine Beschreibung der Maschine mit bestimmungsgemaesser Verwendung',
},
hazard_log: {
icon: '⚠️',
description: 'Vollstaendiges Gefaehrdungsprotokoll mit S/E/P-Bewertungen',
design_specifications: {
icon: '📐',
description: 'Anhang IV.2 — Gesamtplan, Schaltplaene und Systemarchitektur',
},
component_list: {
icon: '🔧',
description: 'Verzeichnis aller sicherheitsrelevanten Komponenten',
description: 'Anhang IV.3 — Detailplaene und Verzeichnis aller sicherheitsrelevanten Komponenten',
},
classification_report: {
risk_assessment_report: {
icon: '📊',
description: 'Anhang IV.4 — Risikobeurteilung nach ISO 12100 mit allen bewerteten Gefaehrdungen',
},
standards_applied: {
icon: '📏',
description: 'Anhang IV.5 — Angewandte harmonisierte Normen und deren Vermutungswirkung',
},
test_reports: {
icon: '🧪',
description: 'Anhang IV.6 — Pruefberichte und Verifikationsergebnisse',
},
instructions_for_use: {
icon: '📖',
description: 'Anhang IV.7 — Betriebsanleitung mit Sicherheitshinweisen',
},
declaration_of_conformity: {
icon: '📜',
description: 'Anhang IV.8 — EU-Konformitaetserklaerung',
},
assembly_declaration: {
icon: '🔩',
description: 'Anhang IV.9 — Einbauerklaerung fuer unvollstaendige Maschinen',
},
// Supplementary CE-Akte sections
hazard_log_combined: {
icon: '⚠️',
description: 'Vollstaendiges Gefaehrdungsprotokoll (Hazard Log) mit S/E/P-Bewertungen',
},
essential_requirements: {
icon: '📋',
description: 'Regulatorische Klassifikation (AI Act, MVO, CRA, NIS2)',
description: 'Grundlegende Anforderungen (EHSR) nach MVO Anhang III',
},
mitigation_report: {
icon: '🛡️',
@@ -47,17 +77,30 @@ const SECTION_TYPES: Record<string, { icon: string; description: string }> = {
icon: '📎',
description: 'Index aller Nachweisdokumente mit Verknuepfungen',
},
declaration_of_conformity: {
icon: '📜',
description: 'EU-Konformitaetserklaerung',
},
instructions_for_use: {
icon: '📖',
description: 'Sicherheitshinweise fuer Betriebsanleitung',
classification_report: {
icon: '🏷️',
description: 'Regulatorische Klassifikation (AI Act, MVO, CRA, NIS2)',
},
monitoring_plan: {
icon: '📡',
description: 'Post-Market Surveillance Plan',
description: 'Post-Market Surveillance und Ueberwachungsplan',
},
// AI-specific sections (when AI components present)
ai_intended_purpose: {
icon: '🎯',
description: 'Bestimmungsgemaesser Zweck des KI-Systems (AI Act Art. 13)',
},
ai_model_description: {
icon: '🧠',
description: 'KI-Modellbeschreibung, Trainingsdaten und Architektur',
},
ai_risk_management: {
icon: '⚙️',
description: 'KI-Risikomanagementsystem (AI Act Art. 9)',
},
ai_human_oversight: {
icon: '👁️',
description: 'Menschliche Aufsicht und Kontrollmassnahmen (AI Act Art. 14)',
},
}