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 [mitPages, setMitPages] = useState<Record<string, number>>({ design: 1, protection: 1, information: 1 })
|
||||||
const [selected, setSelected] = useState<Set<string>>(new Set())
|
const [selected, setSelected] = useState<Set<string>>(new Set())
|
||||||
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
|
const [batchAction, setBatchAction] = useState<'verify' | 'delete' | null>(null)
|
||||||
|
const [expandedMeasure, setExpandedMeasure] = useState<string | null>(null)
|
||||||
|
|
||||||
function toggleSection(type: string) {
|
function toggleSection(type: string) {
|
||||||
setExpanded((prev) => ({ ...prev, [type]: !prev[type] }))
|
setExpanded((prev) => ({ ...prev, [type]: !prev[type] }))
|
||||||
@@ -203,31 +204,45 @@ export default function MitigationsPage() {
|
|||||||
<div>Status</div>
|
<div>Status</div>
|
||||||
</div>
|
</div>
|
||||||
{/* Rows — paginated */}
|
{/* Rows — paginated */}
|
||||||
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => (
|
{items.slice(0, (mitPages[type] || 1) * 50).map((m) => {
|
||||||
<div key={m.id}
|
const isDetailOpen = expandedMeasure === 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' : ''}`}>
|
const catMatch = (m.description || '').match(/Kategorie\s+(\S+)/)
|
||||||
<div className="pt-0.5">
|
const category = catMatch?.[1]
|
||||||
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
|
const refs = measureNorms[(m.title || '').toLowerCase()]
|
||||||
className="accent-purple-600" />
|
return (
|
||||||
</div>
|
<div key={m.id}>
|
||||||
<div className="min-w-0">
|
<div onClick={() => setExpandedMeasure(isDetailOpen ? null : m.id)}
|
||||||
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
|
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' : ''}`}>
|
||||||
{m.description && <div className="text-xs text-gray-400 mt-0.5">{m.description}</div>}
|
<div className="pt-0.5" onClick={(e) => e.stopPropagation()}>
|
||||||
{(() => {
|
<input type="checkbox" checked={selected.has(m.id)} onChange={() => toggleSelect(m.id)}
|
||||||
const refs = measureNorms[(m.title || '').toLowerCase()]
|
className="accent-purple-600" />
|
||||||
return refs?.length > 0 ? (
|
</div>
|
||||||
<div className="text-[9px] text-blue-500 mt-0.5">Normen: {refs.join(', ')}</div>
|
<div className="min-w-0 flex items-start gap-1">
|
||||||
) : null
|
<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" />
|
||||||
</div>
|
</svg>
|
||||||
<div className="text-xs text-gray-500">
|
<div>
|
||||||
{(m.linked_hazard_names || []).join(', ') || '-'}
|
<div className="text-sm text-gray-900 dark:text-white">{m.title || ''}</div>
|
||||||
</div>
|
{!isDetailOpen && category && <div className="text-[10px] text-gray-400 mt-0.5">Kategorie: {category}</div>}
|
||||||
<div>
|
</div>
|
||||||
<StatusBadge status={m.status} />
|
</div>
|
||||||
|
<div className="text-xs text-gray-500">
|
||||||
|
{(m.linked_hazard_names || []).join(', ') || '-'}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<StatusBadge status={m.status} />
|
||||||
|
</div>
|
||||||
</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>
|
</div>
|
||||||
))}
|
)
|
||||||
|
})}
|
||||||
{items.length > (mitPages[type] || 1) * 50 && (
|
{items.length > (mitPages[type] || 1) * 50 && (
|
||||||
<button onClick={() => setMitPages(prev => ({ ...prev, [type]: (prev[type] || 1) + 1 }))}
|
<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">
|
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
|
completeness_pct: number
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
gates: Gate[]
|
metadata?: { limits_form?: Record<string, unknown> }
|
||||||
risk_summary?: {
|
risk_summary?: {
|
||||||
critical?: number
|
critical?: number
|
||||||
high?: number
|
high?: number
|
||||||
@@ -28,14 +28,6 @@ interface ProjectOverview {
|
|||||||
mitigation_count: number
|
mitigation_count: number
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Gate {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
description: string
|
|
||||||
passed: boolean | null
|
|
||||||
required: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
const QUICK_ACTIONS = [
|
const QUICK_ACTIONS = [
|
||||||
{ href: '/components', label: 'Komponenten verwalten', icon: 'cube', description: 'SW/FW/AI/HMI Baum bearbeiten' },
|
{ 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' },
|
{ 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' },
|
{ 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 }) {
|
function RiskGauge({ label, value, max, color }: { label: string; value: number; max: number; color: string }) {
|
||||||
const pct = max > 0 ? Math.round((value / max) * 100) : 0
|
const pct = max > 0 ? Math.round((value / max) * 100) : 0
|
||||||
return (
|
return (
|
||||||
@@ -150,12 +115,15 @@ export default function ProjectOverviewPage() {
|
|||||||
mitCount = live.total_mitigations || 0
|
mitCount = live.total_mitigations || 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate dynamic completeness percentage
|
// Calculate dynamic completeness percentage from CE process steps
|
||||||
const compCount = json.components?.length || 0
|
const compCount = json.components?.length || 0
|
||||||
const gates = (json.completeness_gates || json.gates || [])
|
const limitsForm = json.metadata?.limits_form || {}
|
||||||
const gatesPassed = gates.filter((g: Record<string, unknown>) => g.passed === true).length
|
const hasLimits = Object.keys(limitsForm).length > 0
|
||||||
const gatesTotal = gates.length || 1
|
const hasComponents = compCount > 0
|
||||||
const completeness = Math.round((gatesPassed / gatesTotal) * 100)
|
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({
|
setProject({
|
||||||
...json,
|
...json,
|
||||||
@@ -163,6 +131,7 @@ export default function ProjectOverviewPage() {
|
|||||||
component_count: compCount,
|
component_count: compCount,
|
||||||
hazard_count: hazCount,
|
hazard_count: hazCount,
|
||||||
mitigation_count: mitCount,
|
mitigation_count: mitCount,
|
||||||
|
metadata: json.metadata,
|
||||||
risk_summary: {
|
risk_summary: {
|
||||||
critical: rs.critical || 0,
|
critical: rs.critical || 0,
|
||||||
high: rs.high || 0,
|
high: rs.high || 0,
|
||||||
@@ -170,13 +139,6 @@ export default function ProjectOverviewPage() {
|
|||||||
low: rs.low || 0,
|
low: rs.low || 0,
|
||||||
total: rs.total || hazCount,
|
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) {
|
} catch (err) {
|
||||||
console.error('Failed to fetch project:', err)
|
console.error('Failed to fetch project:', err)
|
||||||
@@ -327,15 +289,44 @@ export default function ProjectOverviewPage() {
|
|||||||
</div>
|
</div>
|
||||||
</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">
|
<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="flex items-center justify-between mb-3">
|
||||||
<div className="space-y-1">
|
<h2 className="text-sm font-semibold text-gray-900 dark:text-white">Projektfortschritt</h2>
|
||||||
{project.gates && project.gates.length > 0 ? (
|
<span className="text-sm font-bold text-purple-600">{project.completeness_pct}%</span>
|
||||||
project.gates.map((gate) => <GateIndicator key={gate.id} gate={gate} />)
|
</div>
|
||||||
) : (
|
<div className="w-full bg-gray-200 rounded-full h-2.5 mb-4">
|
||||||
<p className="text-sm text-gray-400">Keine Gates definiert</p>
|
<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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -19,21 +19,51 @@ interface TechFileSection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SECTION_TYPES: Record<string, { icon: string; description: string }> = {
|
const SECTION_TYPES: Record<string, { icon: string; description: string }> = {
|
||||||
risk_assessment_report: {
|
// Annex IV mandatory sections (EU Machinery Regulation 2023/1230)
|
||||||
icon: '📊',
|
general_description: {
|
||||||
description: 'Zusammenfassung der Risikobeurteilung mit allen bewerteten Gefaehrdungen',
|
icon: '🏭',
|
||||||
|
description: 'Anhang IV.1 — Allgemeine Beschreibung der Maschine mit bestimmungsgemaesser Verwendung',
|
||||||
},
|
},
|
||||||
hazard_log: {
|
design_specifications: {
|
||||||
icon: '⚠️',
|
icon: '📐',
|
||||||
description: 'Vollstaendiges Gefaehrdungsprotokoll mit S/E/P-Bewertungen',
|
description: 'Anhang IV.2 — Gesamtplan, Schaltplaene und Systemarchitektur',
|
||||||
},
|
},
|
||||||
component_list: {
|
component_list: {
|
||||||
icon: '🔧',
|
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: '📋',
|
icon: '📋',
|
||||||
description: 'Regulatorische Klassifikation (AI Act, MVO, CRA, NIS2)',
|
description: 'Grundlegende Anforderungen (EHSR) nach MVO Anhang III',
|
||||||
},
|
},
|
||||||
mitigation_report: {
|
mitigation_report: {
|
||||||
icon: '🛡️',
|
icon: '🛡️',
|
||||||
@@ -47,17 +77,30 @@ const SECTION_TYPES: Record<string, { icon: string; description: string }> = {
|
|||||||
icon: '📎',
|
icon: '📎',
|
||||||
description: 'Index aller Nachweisdokumente mit Verknuepfungen',
|
description: 'Index aller Nachweisdokumente mit Verknuepfungen',
|
||||||
},
|
},
|
||||||
declaration_of_conformity: {
|
classification_report: {
|
||||||
icon: '📜',
|
icon: '🏷️',
|
||||||
description: 'EU-Konformitaetserklaerung',
|
description: 'Regulatorische Klassifikation (AI Act, MVO, CRA, NIS2)',
|
||||||
},
|
|
||||||
instructions_for_use: {
|
|
||||||
icon: '📖',
|
|
||||||
description: 'Sicherheitshinweise fuer Betriebsanleitung',
|
|
||||||
},
|
},
|
||||||
monitoring_plan: {
|
monitoring_plan: {
|
||||||
icon: '📡',
|
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)',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -158,178 +158,3 @@ func (h *IACEHandler) VerifyMitigation(c *gin.Context) {
|
|||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "mitigation verified"})
|
c.JSON(http.StatusOK, gin.H{"message": "mitigation verified"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
|
||||||
// Evidence & Verification
|
|
||||||
// ============================================================================
|
|
||||||
|
|
||||||
// UploadEvidence handles POST /projects/:id/evidence
|
|
||||||
// Creates a new evidence record for a project.
|
|
||||||
func (h *IACEHandler) UploadEvidence(c *gin.Context) {
|
|
||||||
projectID, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
MitigationID *uuid.UUID `json:"mitigation_id,omitempty"`
|
|
||||||
VerificationPlanID *uuid.UUID `json:"verification_plan_id,omitempty"`
|
|
||||||
FileName string `json:"file_name" binding:"required"`
|
|
||||||
FilePath string `json:"file_path" binding:"required"`
|
|
||||||
FileHash string `json:"file_hash" binding:"required"`
|
|
||||||
FileSize int64 `json:"file_size" binding:"required"`
|
|
||||||
MimeType string `json:"mime_type" binding:"required"`
|
|
||||||
Description string `json:"description,omitempty"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := rbac.GetUserID(c)
|
|
||||||
|
|
||||||
evidence := &iace.Evidence{
|
|
||||||
ProjectID: projectID,
|
|
||||||
MitigationID: req.MitigationID,
|
|
||||||
VerificationPlanID: req.VerificationPlanID,
|
|
||||||
FileName: req.FileName,
|
|
||||||
FilePath: req.FilePath,
|
|
||||||
FileHash: req.FileHash,
|
|
||||||
FileSize: req.FileSize,
|
|
||||||
MimeType: req.MimeType,
|
|
||||||
Description: req.Description,
|
|
||||||
UploadedBy: userID,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := h.store.CreateEvidence(c.Request.Context(), evidence); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit trail
|
|
||||||
newVals, _ := json.Marshal(evidence)
|
|
||||||
h.store.AddAuditEntry(
|
|
||||||
c.Request.Context(), projectID, "evidence", evidence.ID,
|
|
||||||
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{"evidence": evidence})
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListEvidence handles GET /projects/:id/evidence
|
|
||||||
// Lists all evidence records for a project.
|
|
||||||
func (h *IACEHandler) ListEvidence(c *gin.Context) {
|
|
||||||
projectID, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
evidence, err := h.store.ListEvidence(c.Request.Context(), projectID)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if evidence == nil {
|
|
||||||
evidence = []iace.Evidence{}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
|
||||||
"evidence": evidence,
|
|
||||||
"total": len(evidence),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateVerificationPlan handles POST /projects/:id/verification-plan
|
|
||||||
// Creates a new verification plan for a project.
|
|
||||||
func (h *IACEHandler) CreateVerificationPlan(c *gin.Context) {
|
|
||||||
projectID, err := uuid.Parse(c.Param("id"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid project ID"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req iace.CreateVerificationPlanRequest
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Override project ID from URL path
|
|
||||||
req.ProjectID = projectID
|
|
||||||
|
|
||||||
plan, err := h.store.CreateVerificationPlan(c.Request.Context(), req)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Audit trail
|
|
||||||
userID := rbac.GetUserID(c)
|
|
||||||
newVals, _ := json.Marshal(plan)
|
|
||||||
h.store.AddAuditEntry(
|
|
||||||
c.Request.Context(), projectID, "verification_plan", plan.ID,
|
|
||||||
iace.AuditActionCreate, userID.String(), nil, newVals,
|
|
||||||
)
|
|
||||||
|
|
||||||
c.JSON(http.StatusCreated, gin.H{"verification_plan": plan})
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateVerificationPlan handles PUT /verification-plan/:vid
|
|
||||||
// Updates a verification plan with the provided fields.
|
|
||||||
func (h *IACEHandler) UpdateVerificationPlan(c *gin.Context) {
|
|
||||||
planID, err := uuid.Parse(c.Param("vid"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var updates map[string]interface{}
|
|
||||||
if err := c.ShouldBindJSON(&updates); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
plan, err := h.store.UpdateVerificationPlan(c.Request.Context(), planID, updates)
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if plan == nil {
|
|
||||||
c.JSON(http.StatusNotFound, gin.H{"error": "verification plan not found"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"verification_plan": plan})
|
|
||||||
}
|
|
||||||
|
|
||||||
// CompleteVerification handles POST /verification-plan/:vid/complete
|
|
||||||
// Marks a verification plan as completed with a result.
|
|
||||||
func (h *IACEHandler) CompleteVerification(c *gin.Context) {
|
|
||||||
planID, err := uuid.Parse(c.Param("vid"))
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid verification plan ID"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var req struct {
|
|
||||||
Result string `json:"result" binding:"required"`
|
|
||||||
}
|
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userID := rbac.GetUserID(c)
|
|
||||||
|
|
||||||
if err := h.store.CompleteVerification(
|
|
||||||
c.Request.Context(), planID, req.Result, userID.String(),
|
|
||||||
); err != nil {
|
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "verification completed"})
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -35,19 +35,30 @@ func (h *IACEHandler) GenerateTechFile(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Define the standard CE technical file sections to generate
|
// Define sections per EU Machinery Regulation 2023/1230 Annex IV structure.
|
||||||
|
// Core Annex IV sections come first, then supplementary CE-Akte sections.
|
||||||
sectionDefinitions := []struct {
|
sectionDefinitions := []struct {
|
||||||
SectionType string
|
SectionType string
|
||||||
Title string
|
Title string
|
||||||
}{
|
}{
|
||||||
{"general_description", "General Description of the Machinery"},
|
// Annex IV mandatory sections
|
||||||
{"risk_assessment_report", "Risk Assessment Report"},
|
{"general_description", "Anhang IV.1 — Allgemeine Beschreibung der Maschine"},
|
||||||
{"hazard_log_combined", "Combined Hazard Log"},
|
{"design_specifications", "Anhang IV.2 — Gesamtplan und Schaltplaene"},
|
||||||
{"essential_requirements", "Essential Health and Safety Requirements"},
|
{"component_list", "Anhang IV.3 — Detailplaene und Komponentenliste"},
|
||||||
{"design_specifications", "Design Specifications and Drawings"},
|
{"risk_assessment_report", "Anhang IV.4 — Risikobeurteilung"},
|
||||||
{"test_reports", "Test Reports and Verification Results"},
|
{"standards_applied", "Anhang IV.5 — Angewandte harmonisierte Normen"},
|
||||||
{"standards_applied", "Applied Harmonised Standards"},
|
{"test_reports", "Anhang IV.6 — Pruefberichte und Ergebnisse"},
|
||||||
{"declaration_of_conformity", "EU Declaration of Conformity"},
|
{"instructions_for_use", "Anhang IV.7 — Betriebsanleitung"},
|
||||||
|
{"declaration_of_conformity", "Anhang IV.8 — EU-Konformitaetserklaerung"},
|
||||||
|
{"assembly_declaration", "Anhang IV.9 — Einbauerklaerung (falls zutreffend)"},
|
||||||
|
// Supplementary CE-Akte sections
|
||||||
|
{"hazard_log_combined", "Gefaehrdungsprotokoll (Hazard Log)"},
|
||||||
|
{"essential_requirements", "Grundlegende Anforderungen (EHSR)"},
|
||||||
|
{"mitigation_report", "Massnahmenbericht (3-Stufen-Verfahren)"},
|
||||||
|
{"verification_report", "Verifikationsbericht"},
|
||||||
|
{"evidence_index", "Nachweisverzeichnis"},
|
||||||
|
{"classification_report", "Regulatorischer Klassifizierungsbericht"},
|
||||||
|
{"monitoring_plan", "Post-Market-Monitoring-Plan"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if project has AI components for additional sections
|
// Check if project has AI components for additional sections
|
||||||
@@ -184,6 +195,7 @@ func (h *IACEHandler) GenerateSingleSection(c *gin.Context) {
|
|||||||
"evidence_index": "Evidence Index",
|
"evidence_index": "Evidence Index",
|
||||||
"instructions_for_use": "Instructions for Use",
|
"instructions_for_use": "Instructions for Use",
|
||||||
"monitoring_plan": "Post-Market Monitoring Plan",
|
"monitoring_plan": "Post-Market Monitoring Plan",
|
||||||
|
"assembly_declaration": "Anhang IV.9 — Einbauerklaerung (falls zutreffend)",
|
||||||
"ai_intended_purpose": "AI System Intended Purpose",
|
"ai_intended_purpose": "AI System Intended Purpose",
|
||||||
"ai_model_description": "AI Model Description and Training Data",
|
"ai_model_description": "AI Model Description and Training Data",
|
||||||
"ai_risk_management": "AI Risk Management System",
|
"ai_risk_management": "AI Risk Management System",
|
||||||
|
|||||||
@@ -403,6 +403,10 @@ func registerIACERoutes(v1 *gin.RouterGroup, h *handlers.IACEHandler) {
|
|||||||
iaceRoutes.POST("/projects/:id/verification-plan", h.CreateVerificationPlan)
|
iaceRoutes.POST("/projects/:id/verification-plan", h.CreateVerificationPlan)
|
||||||
iaceRoutes.PUT("/verification-plan/:vid", h.UpdateVerificationPlan)
|
iaceRoutes.PUT("/verification-plan/:vid", h.UpdateVerificationPlan)
|
||||||
iaceRoutes.POST("/verification-plan/:vid/complete", h.CompleteVerification)
|
iaceRoutes.POST("/verification-plan/:vid/complete", h.CompleteVerification)
|
||||||
|
iaceRoutes.GET("/projects/:id/verifications", h.ListVerificationPlans)
|
||||||
|
iaceRoutes.POST("/projects/:id/verifications", h.CreateVerificationAlias)
|
||||||
|
iaceRoutes.DELETE("/projects/:id/verifications/:vid", h.DeleteVerificationPlan)
|
||||||
|
iaceRoutes.POST("/projects/:id/verifications/:vid/complete", h.CompleteVerificationAlias)
|
||||||
iaceRoutes.POST("/projects/:id/tech-file/generate", h.GenerateTechFile)
|
iaceRoutes.POST("/projects/:id/tech-file/generate", h.GenerateTechFile)
|
||||||
iaceRoutes.GET("/projects/:id/tech-file", h.ListTechFileSections)
|
iaceRoutes.GET("/projects/:id/tech-file", h.ListTechFileSections)
|
||||||
iaceRoutes.PUT("/projects/:id/tech-file/:section", h.UpdateTechFileSection)
|
iaceRoutes.PUT("/projects/:id/tech-file/:section", h.UpdateTechFileSection)
|
||||||
|
|||||||
@@ -247,6 +247,15 @@ func (s *Store) getVerificationPlan(ctx context.Context, id uuid.UUID) (*Verific
|
|||||||
return &vp, nil
|
return &vp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteVerificationPlan deletes a verification plan by ID
|
||||||
|
func (s *Store) DeleteVerificationPlan(ctx context.Context, id uuid.UUID) error {
|
||||||
|
_, err := s.pool.Exec(ctx, `DELETE FROM iace_verification_plans WHERE id = $1`, id)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("delete verification plan: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Reference Data Operations
|
// Reference Data Operations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ const (
|
|||||||
SectionEvidenceIndex = "evidence_index"
|
SectionEvidenceIndex = "evidence_index"
|
||||||
SectionInstructionsForUse = "instructions_for_use"
|
SectionInstructionsForUse = "instructions_for_use"
|
||||||
SectionMonitoringPlan = "monitoring_plan"
|
SectionMonitoringPlan = "monitoring_plan"
|
||||||
|
SectionAssemblyDeclaration = "assembly_declaration"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -68,6 +69,8 @@ var sectionSystemPrompts = map[string]string{
|
|||||||
SectionInstructionsForUse: `Erstelle eine Gliederung fuer die Betriebsanleitung gemaess EU-Maschinenverordnung 2023/1230 Anhang III Abschnitt 1.7.4. Enthalten: 1) Bestimmungsgemaesse Verwendung, 2) Inbetriebnahme, 3) Sicherer Betrieb, 4) Wartung, 5) Restrisiken und Warnhinweise, 6) Ausserbetriebnahme. Beruecksichtige identifizierte Gefaehrdungen.`,
|
SectionInstructionsForUse: `Erstelle eine Gliederung fuer die Betriebsanleitung gemaess EU-Maschinenverordnung 2023/1230 Anhang III Abschnitt 1.7.4. Enthalten: 1) Bestimmungsgemaesse Verwendung, 2) Inbetriebnahme, 3) Sicherer Betrieb, 4) Wartung, 5) Restrisiken und Warnhinweise, 6) Ausserbetriebnahme. Beruecksichtige identifizierte Gefaehrdungen.`,
|
||||||
|
|
||||||
SectionMonitoringPlan: `Erstelle einen Post-Market-Monitoring-Plan fuer das Produkt. Enthalten: 1) Ueberwachungsziele, 2) Datenquellen (Kundenfeedback, Vorfaelle, Updates), 3) Ueberwachungsintervalle, 4) Eskalationsverfahren, 5) Dokumentationspflichten, 6) Verantwortlichkeiten. Beruecksichtige AI Act Art. 72 (Post-Market Monitoring) falls KI-Komponenten vorhanden.`,
|
SectionMonitoringPlan: `Erstelle einen Post-Market-Monitoring-Plan fuer das Produkt. Enthalten: 1) Ueberwachungsziele, 2) Datenquellen (Kundenfeedback, Vorfaelle, Updates), 3) Ueberwachungsintervalle, 4) Eskalationsverfahren, 5) Dokumentationspflichten, 6) Verantwortlichkeiten. Beruecksichtige AI Act Art. 72 (Post-Market Monitoring) falls KI-Komponenten vorhanden.`,
|
||||||
|
|
||||||
|
SectionAssemblyDeclaration: `Erstelle eine Einbauerklaerung gemaess EU-Maschinenverordnung 2023/1230 Anhang IV.9 fuer eine unvollstaendige Maschine. Enthalten: 1) Hersteller-Angaben, 2) Bezeichnung der unvollstaendigen Maschine, 3) Erklaerung, dass die Inbetriebnahme untersagt ist bis die Gesamtmaschine konform erklaert wurde, 4) Liste der eingehaltenen Anforderungen, 5) Angaben zur technischen Dokumentation. Falls die Maschine vollstaendig ist, vermerke "Nicht zutreffend — vollstaendige Maschine".`,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -95,6 +98,7 @@ func buildRAGQuery(sectionType string) string {
|
|||||||
SectionEvidenceIndex: "Nachweisdokumente Evidence Konformitaetsnachweis Dokumentenindex",
|
SectionEvidenceIndex: "Nachweisdokumente Evidence Konformitaetsnachweis Dokumentenindex",
|
||||||
SectionInstructionsForUse: "Betriebsanleitung Benutzerinformation Maschinenverordnung Abschnitt 1.7.4 Sicherheitshinweise",
|
SectionInstructionsForUse: "Betriebsanleitung Benutzerinformation Maschinenverordnung Abschnitt 1.7.4 Sicherheitshinweise",
|
||||||
SectionMonitoringPlan: "Post-Market-Monitoring Ueberwachungsplan AI Act Art. 72 Marktbeobachtung",
|
SectionMonitoringPlan: "Post-Market-Monitoring Ueberwachungsplan AI Act Art. 72 Marktbeobachtung",
|
||||||
|
SectionAssemblyDeclaration: "Einbauerklaerung unvollstaendige Maschine Maschinenverordnung Anhang IV",
|
||||||
}
|
}
|
||||||
|
|
||||||
if q, ok := ragQueries[sectionType]; ok {
|
if q, ok := ragQueries[sectionType]; ok {
|
||||||
@@ -133,6 +137,7 @@ func sectionDisplayName(sectionType string) string {
|
|||||||
SectionEvidenceIndex: "Nachweisverzeichnis",
|
SectionEvidenceIndex: "Nachweisverzeichnis",
|
||||||
SectionInstructionsForUse: "Betriebsanleitung (Gliederung)",
|
SectionInstructionsForUse: "Betriebsanleitung (Gliederung)",
|
||||||
SectionMonitoringPlan: "Post-Market-Monitoring-Plan",
|
SectionMonitoringPlan: "Post-Market-Monitoring-Plan",
|
||||||
|
SectionAssemblyDeclaration: "Einbauerklaerung",
|
||||||
}
|
}
|
||||||
if name, ok := names[sectionType]; ok {
|
if name, ok := names[sectionType]; ok {
|
||||||
return name
|
return name
|
||||||
|
|||||||
Reference in New Issue
Block a user