feat(iace): add data-driven Architektur & Datenfluss explainer tab
Adds an auditor-facing view of the IACE engine: a clickable 10-stage pipeline flow (Grenzen-Formular → ParseNarrative → Pattern-Gates → Relevanz → Caps → Gefährdungen → Maßnahmen → Risiko → Normen → Matrix), plus live library counts, the data-source/license register (incl. the DIN/Beuth + DGUV exclusions), and the norm-matching logic that reconciles DIN/ISO/OSHA machine-type vocabulary via canonicalMachineType folding. Backend: BuildArchitecture() with LIVE counts so the diagram can never drift; GET /iace/architecture; collectAllNorms() extracted from SuggestNorms as the single source of truth for the norm-library count. Frontend: useArchitecture hook + page + new IACE nav tab. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
export interface ArchStage {
|
||||
id: string
|
||||
title: string
|
||||
summary: string
|
||||
input: string
|
||||
logic: string
|
||||
data_source: string
|
||||
example: string
|
||||
}
|
||||
|
||||
export interface ArchLibrary {
|
||||
name: string
|
||||
count: number
|
||||
source_file: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export interface ArchDataSource {
|
||||
name: string
|
||||
license: string
|
||||
usage: string
|
||||
status: string // "verwendet" | "ausgeschlossen"
|
||||
}
|
||||
|
||||
export interface RiskEvidence {
|
||||
mode: string
|
||||
label: string
|
||||
stat: string
|
||||
source: string
|
||||
license: string
|
||||
attribution: string
|
||||
retrieved: string
|
||||
}
|
||||
|
||||
export interface Architecture {
|
||||
stages: ArchStage[]
|
||||
libraries: ArchLibrary[]
|
||||
data_sources: ArchDataSource[]
|
||||
norm_matching: string[]
|
||||
evidence: RiskEvidence[]
|
||||
}
|
||||
|
||||
/** Loads the data-driven IACE engine self-description (global, not per project). */
|
||||
export function useArchitecture() {
|
||||
const [data, setData] = useState<Architecture | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
async function load() {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/iace/architecture')
|
||||
const json = res.ok ? ((await res.json()) as Architecture) : null
|
||||
if (!cancelled) setData(json)
|
||||
} catch (err) {
|
||||
console.error('Failed to load IACE architecture:', err)
|
||||
} finally {
|
||||
if (!cancelled) setLoading(false)
|
||||
}
|
||||
}
|
||||
load()
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { data, loading }
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { useArchitecture, type ArchStage } from './_hooks/useArchitecture'
|
||||
|
||||
export default function ArchitekturPage() {
|
||||
const { data, loading } = useArchitecture()
|
||||
const [open, setOpen] = useState<string | null>('grenzen')
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-sm text-gray-500 dark:text-gray-400 p-1">Lade Engine-Architektur…</div>
|
||||
}
|
||||
if (!data) {
|
||||
return <div className="text-sm text-red-600">Architektur konnte nicht geladen werden.</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8 max-w-[1100px]">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Architektur & Datenfluss</h1>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-3xl mt-1">
|
||||
Nachvollziehbar für Auditoren: <strong>woher jede Information stammt</strong> und{' '}
|
||||
<strong>wie die Risikobeurteilung zustande kommt</strong> — jede Station, jedes Gate, jede
|
||||
Bibliothek und Datenquelle, in Reihenfolge. Die Zahlen sind <strong>live</strong> aus der
|
||||
laufenden Engine.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Pipeline flow */}
|
||||
<section className="space-y-2">
|
||||
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Deterministische Pipeline</h2>
|
||||
<div className="space-y-1">
|
||||
{data.stages.map((s, i) => (
|
||||
<StageRow
|
||||
key={s.id}
|
||||
stage={s}
|
||||
last={i === data.stages.length - 1}
|
||||
open={open === s.id}
|
||||
onToggle={() => setOpen(open === s.id ? null : s.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Libraries */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Wissensbasen (Live-Bestand)</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{data.libraries.map((l) => (
|
||||
<div key={l.name} className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-3">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="text-sm font-medium text-gray-800 dark:text-gray-200">{l.name}</span>
|
||||
<span className="text-lg font-bold text-purple-600 tabular-nums">{l.count.toLocaleString('de-DE')}</span>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-1">{l.description}</p>
|
||||
<code className="text-[10px] text-gray-400 mt-1 block">{l.source_file}</code>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Norm matching */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Normen-Matching (DIN / ISO / OSHA)</h2>
|
||||
<ul className="space-y-2">
|
||||
{data.norm_matching.map((n, i) => (
|
||||
<li key={i} className="flex gap-2 text-xs text-gray-600 dark:text-gray-300">
|
||||
<span className="text-purple-500 mt-0.5 shrink-0">▸</span>
|
||||
<span dangerouslySetInnerHTML={{ __html: inlineCode(n) }} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{/* Data sources & licenses */}
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Datenquellen & Lizenzen</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="text-gray-500 border-b border-gray-200 dark:border-gray-700 text-left">
|
||||
<th className="py-1.5 pr-3">Quelle</th>
|
||||
<th className="py-1.5 pr-3">Lizenz</th>
|
||||
<th className="py-1.5 pr-3">Nutzung</th>
|
||||
<th className="py-1.5">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.data_sources.map((d) => (
|
||||
<tr key={d.name} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
|
||||
<td className="py-1.5 pr-3 text-gray-700 dark:text-gray-300 font-medium">{d.name}</td>
|
||||
<td className="py-1.5 pr-3 text-gray-500">{d.license}</td>
|
||||
<td className="py-1.5 pr-3 text-gray-500">{d.usage}</td>
|
||||
<td className="py-1.5">
|
||||
<span
|
||||
className={`inline-block rounded px-1.5 py-0.5 font-medium ${
|
||||
d.status === 'verwendet'
|
||||
? 'bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300'
|
||||
: 'bg-gray-100 text-gray-500 dark:bg-gray-700 dark:text-gray-400'
|
||||
}`}
|
||||
>
|
||||
{d.status}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{data.evidence.length > 0 && (
|
||||
<p className="text-[11px] text-gray-400">
|
||||
Belegte Kontaktmodus-Quoten (ESAW):{' '}
|
||||
{data.evidence.map((e) => `${e.label} ${e.stat}`).join(' · ')} — {data.evidence[0]?.attribution}.
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function StageRow({
|
||||
stage,
|
||||
last,
|
||||
open,
|
||||
onToggle,
|
||||
}: {
|
||||
stage: ArchStage
|
||||
last: boolean
|
||||
open: boolean
|
||||
onToggle: () => void
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
onClick={onToggle}
|
||||
className={`w-full text-left rounded-lg border p-3 transition-colors ${
|
||||
open
|
||||
? 'border-purple-300 bg-purple-50/60 dark:border-purple-700 dark:bg-purple-900/20'
|
||||
: 'border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 hover:bg-gray-50 dark:hover:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-sm font-semibold text-gray-800 dark:text-gray-200">{stage.title}</div>
|
||||
<div className="text-xs text-gray-500 mt-0.5">{stage.summary}</div>
|
||||
</div>
|
||||
<span className="text-gray-400 text-xs shrink-0">{open ? '▲' : '▼'}</span>
|
||||
</div>
|
||||
{open && (
|
||||
<dl className="mt-3 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-2 text-xs">
|
||||
<Field label="Input" value={stage.input} />
|
||||
<Field label="Logik" value={stage.logic} />
|
||||
<Field label="Datenquelle" value={stage.data_source} mono />
|
||||
<Field label="Beispiel" value={stage.example} />
|
||||
</dl>
|
||||
)}
|
||||
</button>
|
||||
{!last && <div className="flex justify-center text-gray-300 dark:text-gray-600 text-xs leading-none py-0.5">↓</div>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Field({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
|
||||
return (
|
||||
<div>
|
||||
<dt className="text-[10px] uppercase tracking-wide text-gray-400">{label}</dt>
|
||||
<dd className={`text-gray-600 dark:text-gray-300 ${mono ? 'font-mono text-[11px]' : ''}`}>{value}</dd>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Renders `inline code` (single backticks) as <code> — the norm-matching bullets
|
||||
// use backticks for function/identifier names.
|
||||
function inlineCode(text: string): string {
|
||||
const escaped = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
||||
return escaped.replace(/`([^`]+)`/g, '<code class="text-[11px] bg-gray-100 dark:bg-gray-700 rounded px-1">$1</code>')
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import IACEFlowFAB from './[projectId]/_components/IACEFlowFAB'
|
||||
|
||||
const IACE_NAV_ITEMS = [
|
||||
{ id: 'overview', label: 'Uebersicht', href: '', icon: 'grid' },
|
||||
{ id: 'architektur', label: 'Architektur & Datenfluss', href: '/architektur', icon: 'activity' },
|
||||
{ id: 'order', label: 'Auftrag', href: '/order', icon: 'briefcase' },
|
||||
{ id: 'interview', label: 'Grenzen & Verwendung', href: '/interview', icon: 'chat' },
|
||||
{ id: 'operational-states', label: 'Betriebszustaende', href: '/operational-states', icon: 'activity' },
|
||||
|
||||
Reference in New Issue
Block a user