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:
@@ -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 >100 | Hoch 60-100 | Mittel 20-60 | Niedrig <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>
|
||||
|
||||
Reference in New Issue
Block a user