feat: IACE CE-Compliance Module — Normen, Risikobewertung, Production Lines

Major features:
- 215 norms library with section references + Beuth URLs (A/B1/B2/C norms)
- 173 hazard patterns with detail fields (scenario, trigger, harm, zone)
- Deterministic pattern matching: Component × Lifecycle × Pattern cross-product
- SIL/PL auto-calculation from S×E×P risk graph
- Risk assessment table with editable S/E/P dropdowns
- Production Line Dashboard with animated station flow (Running Dots)
- IACE process flow + norms coverage on start page
- Non-blocking cookie banner, ProcessFlow SSR fix
- 104 Playwright E2E tests passing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-07 10:53:26 +02:00
parent 3853a0838a
commit e7f2f98da3
59 changed files with 8326 additions and 525 deletions
@@ -3,6 +3,7 @@
import React, { useState, useEffect } from 'react'
import Link from 'next/link'
import { useParams } from 'next/navigation'
import { SuggestedNorms } from './_components/SuggestedNorms'
interface ProjectOverview {
id: string
@@ -120,11 +121,72 @@ export default function ProjectOverviewPage() {
async function fetchProject() {
try {
const res = await fetch(`/api/sdk/v1/iace/projects/${projectId}`)
if (res.ok) {
const json = await res.json()
setProject(json)
// Fetch project detail + live risk summary + mitigations count in parallel
const [projRes, riskRes, mitRes, hazRes] = await Promise.all([
fetch(`/api/sdk/v1/iace/projects/${projectId}`),
fetch(`/api/sdk/v1/iace/projects/${projectId}/risk-summary`),
fetch(`/api/sdk/v1/iace/projects/${projectId}/mitigations`),
fetch(`/api/sdk/v1/iace/projects/${projectId}/hazards`),
])
if (!projRes.ok) return
const json = await projRes.json()
// Live risk summary from dedicated endpoint
let rs = json.risk_summary || {}
if (riskRes.ok) {
const riskJson = await riskRes.json()
const live = riskJson.risk_summary || riskJson || {}
rs = {
critical: live.critical || 0,
high: live.high || 0,
medium: live.medium || 0,
low: live.low || 0,
negligible: live.negligible || 0,
total: live.total_hazards || live.total || 0,
}
}
// Live counts
let mitCount = 0
if (mitRes.ok) {
const mitJson = await mitRes.json()
mitCount = mitJson.total || (mitJson.mitigations || []).length || 0
}
let hazCount = 0
if (hazRes.ok) {
const hazJson = await hazRes.json()
hazCount = hazJson.total || (hazJson.hazards || []).length || 0
}
// Calculate dynamic completeness percentage
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)
setProject({
...json,
completeness_pct: completeness,
component_count: compCount,
hazard_count: hazCount,
mitigation_count: mitCount,
risk_summary: {
critical: rs.critical || 0,
high: rs.high || 0,
medium: rs.medium || 0,
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)
} finally {
@@ -229,15 +291,31 @@ export default function ProjectOverviewPage() {
</dl>
</div>
{/* Risk Summary */}
{/* Risk Summary — live from /risk-summary endpoint */}
<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">Risikozusammenfassung</h2>
<div className="flex items-center justify-around">
<RiskGauge label="Kritisch" value={project.risk_summary?.critical || 0} max={project.risk_summary?.total || 1} color="#EF4444" />
<RiskGauge label="Hoch" value={project.risk_summary?.high || 0} max={project.risk_summary?.total || 1} color="#F97316" />
<RiskGauge label="Mittel" value={project.risk_summary?.medium || 0} max={project.risk_summary?.total || 1} color="#EAB308" />
<RiskGauge label="Niedrig" value={project.risk_summary?.low || 0} max={project.risk_summary?.total || 1} color="#22C55E" />
{/* Risk level bars */}
<div className="space-y-2">
{[
{ label: 'Kritisch', value: project.risk_summary?.critical || 0, color: 'bg-red-500', text: 'text-red-700' },
{ label: 'Hoch', value: project.risk_summary?.high || 0, color: 'bg-orange-500', text: 'text-orange-700' },
{ label: 'Mittel', value: project.risk_summary?.medium || 0, color: 'bg-yellow-500', text: 'text-yellow-700' },
{ label: 'Niedrig', value: project.risk_summary?.low || 0, color: 'bg-green-500', text: 'text-green-700' },
].map((level) => {
const total = project.risk_summary?.total || 1
const pct = Math.round((level.value / total) * 100)
return (
<div key={level.label} className="flex items-center gap-3">
<span className={`text-xs font-medium w-16 ${level.text}`}>{level.label}</span>
<div className="flex-1 bg-gray-100 rounded-full h-4 overflow-hidden">
<div className={`${level.color} h-4 rounded-full transition-all`} style={{ width: `${pct}%` }} />
</div>
<span className="text-sm font-bold text-gray-900 dark:text-white w-8 text-right">{level.value}</span>
</div>
)
})}
</div>
{/* Counts */}
<div className="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700 grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">{project.component_count}</div>
@@ -252,6 +330,10 @@ export default function ProjectOverviewPage() {
<div className="text-xs text-gray-500">Massnahmen</div>
</div>
</div>
{/* RPZ threshold info */}
<div className="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700 text-xs text-gray-500">
RPZ-Schwellen: Kritisch &gt;100 | Hoch 60-100 | Mittel 20-60 | Niedrig &lt;20
</div>
</div>
{/* Completeness Gates */}
@@ -267,6 +349,9 @@ export default function ProjectOverviewPage() {
</div>
</div>
{/* Suggested Norms */}
<SuggestedNorms projectId={projectId} />
{/* Quick Actions */}
<div>
<h2 className="text-sm font-semibold text-gray-900 dark:text-white mb-4">Schnellzugriff</h2>