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
@@ -0,0 +1,172 @@
'use client'
import { useState, useEffect } from 'react'
interface NormStats {
total: number
byType: Record<string, number>
categories: string[]
}
const TYPE_INFO: Record<string, { label: string; color: string }> = {
A: { label: 'A-Normen (Grundnormen)', color: 'bg-red-50 text-red-800 border-red-200' },
B1: { label: 'B1-Normen (Sicherheitsgrundnormen)', color: 'bg-blue-50 text-blue-800 border-blue-200' },
B2: { label: 'B2-Normen (Sicherheitsfachgrundnormen)', color: 'bg-green-50 text-green-800 border-green-200' },
C: { label: 'C-Normen (Maschinenspezifisch)', color: 'bg-purple-50 text-purple-800 border-purple-200' },
}
const CATEGORY_DESCRIPTIONS: Record<string, string> = {
A: 'ISO 12100 (Grundnorm Risikobeurteilung)',
B1: 'ISO 13849-1/2, IEC 62061, IEC 61508 (SIL/PL, Funktionale Sicherheit)',
B2: 'Elektrik, Ergonomie, Vibration, Laerm, Brandschutz, Hydraulik/Pneumatik, Software-Safety, Emissionen, Schutzeinrichtungen, Zugaenge, Signale',
C: '',
}
export function NormsCoverage() {
const [stats, setStats] = useState<NormStats | null>(null)
const [loading, setLoading] = useState(true)
const [expanded, setExpanded] = useState(false)
useEffect(() => {
fetch('/api/sdk/v1/iace/norms-library')
.then((r) => (r.ok ? r.json() : null))
.then((json) => {
if (!json?.norms) return
const norms = json.norms as Array<{ norm_type: string; machine_types?: string[] }>
const byType: Record<string, number> = {}
const machineTypes = new Set<string>()
for (const n of norms) {
byType[n.norm_type] = (byType[n.norm_type] || 0) + 1
if (n.machine_types) {
for (const mt of n.machine_types) machineTypes.add(mt)
}
}
// Group machine types into readable categories
const catMap: Record<string, string> = {
press: 'Pressen', hydraulic_press: 'Pressen', mechanical_press: 'Pressen', press_brake: 'Pressen',
robot: 'Roboter', industrial_robot: 'Roboter', robot_cell: 'Roboter',
collaborative_robot: 'Kollaborierende Roboter', cobot: 'Kollaborierende Roboter',
woodworking: 'Holzbearbeitung', saw: 'Holzbearbeitung', circular_saw: 'Holzbearbeitung',
panel_saw: 'Holzbearbeitung', table_saw: 'Holzbearbeitung', miter_saw: 'Holzbearbeitung',
log_saw: 'Holzbearbeitung', planer: 'Holzbearbeitung', router: 'Holzbearbeitung',
lathe: 'Metallbearbeitung', turning_machine: 'Metallbearbeitung', large_lathe: 'Metallbearbeitung',
small_lathe: 'Metallbearbeitung', milling_machine: 'Metallbearbeitung', drilling_machine: 'Metallbearbeitung',
grinding_machine: 'Metallbearbeitung', metal_saw: 'Metallbearbeitung', band_saw: 'Metallbearbeitung',
cold_saw: 'Metallbearbeitung', shearing_machine: 'Metallbearbeitung', bending_machine: 'Metallbearbeitung',
cnc: 'Metallbearbeitung', machining_center: 'Metallbearbeitung', transfer_machine: 'Metallbearbeitung',
injection_molding: 'Kunststoff/Gummi', plastics_machine: 'Kunststoff/Gummi',
compression_molding: 'Kunststoff/Gummi', blow_molding: 'Kunststoff/Gummi',
extruder: 'Kunststoff/Gummi', plastics_press: 'Kunststoff/Gummi',
rubber_machine: 'Kunststoff/Gummi', two_roll_mill: 'Kunststoff/Gummi',
reaction_molding: 'Kunststoff/Gummi', calender: 'Kunststoff/Gummi',
food_machine: 'Lebensmittel', meat_grinder: 'Lebensmittel', bread_slicer: 'Lebensmittel',
bakery: 'Lebensmittel', mixer: 'Lebensmittel', cooker: 'Lebensmittel',
cutter: 'Lebensmittel', food_cutter: 'Lebensmittel', filling_machine: 'Lebensmittel',
packaging_machine: 'Verpackung', palletizer: 'Verpackung', pallet_wrapper: 'Verpackung',
wrapping_machine: 'Verpackung', strapping_machine: 'Verpackung',
textile_machine: 'Textilmaschinen', spinning_machine: 'Textilmaschinen',
weaving_machine: 'Textilmaschinen', dyeing_machine: 'Textilmaschinen',
nonwoven_machine: 'Textilmaschinen',
agricultural_machine: 'Landmaschinen', combine_harvester: 'Landmaschinen',
mower: 'Landmaschinen', baler: 'Landmaschinen', sprayer: 'Landmaschinen', tiller: 'Landmaschinen',
crane: 'Krane/Hebezeuge', bridge_crane: 'Krane/Hebezeuge', gantry_crane: 'Krane/Hebezeuge',
tower_crane: 'Krane/Hebezeuge', mobile_crane: 'Krane/Hebezeuge', hoist: 'Krane/Hebezeuge',
winch: 'Krane/Hebezeuge', slewing_crane: 'Krane/Hebezeuge',
elevator: 'Aufzuege', lift: 'Aufzuege', construction_hoist: 'Aufzuege',
conveyor: 'Foerdertechnik', belt_conveyor: 'Foerdertechnik', screw_conveyor: 'Foerdertechnik',
transfer_system: 'Foerdertechnik', rotary_transfer_machine: 'Foerdertechnik',
forklift: 'Flurfoerderzeuge', industrial_truck: 'Flurfoerderzeuge',
earth_moving: 'Erdbaumaschinen', excavator: 'Erdbaumaschinen',
wheel_loader: 'Erdbaumaschinen', bulldozer: 'Erdbaumaschinen',
welding_machine: 'Schweissmaschinen', arc_welder: 'Schweissmaschinen',
printing_press: 'Druckmaschinen', coating_machine: 'Druckmaschinen',
pump: 'Pumpen/Kompressoren', compressor: 'Pumpen/Kompressoren', vacuum_pump: 'Pumpen/Kompressoren',
foundry_machine: 'Giesserei', casting_machine: 'Giesserei', die_casting: 'Giesserei',
industrial_furnace: 'Industrieoefen', heat_treatment: 'Industrieoefen',
dryer: 'Trockner/Oefen', oven: 'Trockner/Oefen', kiln: 'Trockner/Oefen',
paper_machine: 'Papiermaschinen', slitter_rewinder: 'Papiermaschinen', pulper: 'Papiermaschinen',
centrifuge: 'Zentrifugen',
aerial_platform: 'Hubarbeitsbuehnen', cherry_picker: 'Hubarbeitsbuehnen',
scissor_lift: 'Hubtische', lift_table: 'Hubtische',
powered_gate: 'Tore/Tueren', industrial_door: 'Tore/Tueren',
laser_machine: 'Lasermaschinen', laser_cutter: 'Lasermaschinen',
silo: 'Schuettgutanlagen', bunker: 'Schuettgutanlagen',
suspended_platform: 'Haengebuehnen', scaffold: 'Haengebuehnen',
storage_retrieval: 'Lagertechnik', automated_warehouse: 'Lagertechnik',
pressure_vessel: 'Druckbehaelter', hydraulic_accumulator: 'Druckbehaelter',
}
const cats = new Set<string>()
for (const mt of machineTypes) {
cats.add(catMap[mt] || mt)
}
const sortedCats = Array.from(cats).sort()
setStats({ total: norms.length, byType, categories: sortedCats })
})
.catch(() => {})
.finally(() => setLoading(false))
}, [])
if (loading || !stats) return null
const cDesc = stats.categories.join(', ')
return (
<div className="p-4 bg-purple-50 dark:bg-purple-900/10 border border-purple-200 dark:border-purple-800 rounded-lg">
<button onClick={() => setExpanded(!expanded)} className="w-full text-left">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<svg className="w-5 h-5 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
<span className="text-sm font-semibold text-purple-800 dark:text-purple-300">
Normen-Bibliothek: {stats.total} Normen in {stats.categories.length} Branchen
</span>
</div>
<svg className={`w-4 h-4 text-purple-400 transition-transform ${expanded ? 'rotate-180' : ''}`} fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</div>
</button>
{expanded && (
<div className="mt-3 space-y-2">
<table className="w-full text-xs">
<thead>
<tr className="border-b border-purple-200 dark:border-purple-700">
<th className="text-left py-1.5 px-2 font-semibold text-purple-800 dark:text-purple-300 w-48">Typ</th>
<th className="text-center py-1.5 px-2 font-semibold text-purple-800 dark:text-purple-300 w-16">Anzahl</th>
<th className="text-left py-1.5 px-2 font-semibold text-purple-800 dark:text-purple-300">Abdeckung</th>
</tr>
</thead>
<tbody>
{(['A', 'B1', 'B2', 'C'] as const).map((type) => {
const info = TYPE_INFO[type]
const count = stats.byType[type] || 0
const desc = type === 'C' ? cDesc : CATEGORY_DESCRIPTIONS[type]
return (
<tr key={type} className="border-b border-purple-100 dark:border-purple-800/50">
<td className="py-2 px-2">
<span className={`inline-block px-2 py-0.5 rounded border text-xs font-medium ${info.color}`}>
{info.label}
</span>
</td>
<td className="py-2 px-2 text-center font-bold text-purple-900 dark:text-purple-200">{count}</td>
<td className="py-2 px-2 text-gray-700 dark:text-gray-300">{desc}</td>
</tr>
)
})}
</tbody>
</table>
<div className="pt-2 text-xs text-purple-600 dark:text-purple-400">
Alle Normen mit Abschnittsnummern und{' '}
<a href="https://www.beuth.de" target="_blank" rel="noopener noreferrer" className="underline font-medium hover:text-purple-800">
Beuth-Kauflinks
</a>{' '}
hinterlegt. Die vollstaendige Bibliothek ist unter &quot;Normenrecherche&quot; in jedem Projekt einsehbar.
</div>
</div>
)}
</div>
)
}