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:
@@ -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)',
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user