feat(agent-ui): add Architektur tab explaining the doc-check pipeline

Mirror the CE module's /sdk/iace/.../architektur tab for /sdk/agent: a
hand-authored schema (data-flow lanes, step-by-step pipeline accordion,
module-engine cards, Pruefer-Matrix) explaining orchestrator phases A-F,
the parallel specialist agents (Impressum/AGB/DSE), the 4-layer DSE engine,
and the verification/decision-method meta-model. Adds a page-level
Check | Architektur tab toggle (the page was flat).

Static content (the Python doc-check has no architecture endpoint, unlike
the Go IACE module); can be data-fed later.

NOTE: not yet lint/type/browser-verified -- the worktree has no node_modules.
Needs a visual check + next lint / tsc in an env with the toolchain.

dev-only, no deploy.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-06-21 11:25:28 +02:00
parent f6d018234b
commit ce6b4c58e3
2 changed files with 328 additions and 4 deletions
@@ -0,0 +1,302 @@
'use client'
// Erklärendes Architekturschema des Compliance-Check-Tools — Muster aus dem
// CE-Modul (/sdk/iace/.../architektur) übernommen: hand-kurierte Boxen/Pfeile +
// Schritt-Akkordeon. Inhalt spiegelt den Code-Pfad (api/agent_check/_orchestrator
// + services/specialist_agents). Bewusst statisch (der Doc-Check ist Python, hat
// keinen Architektur-Endpoint wie das Go-IACE-Modul) — bei Bedarf später aus einem
// Backend-Handler speisbar.
import { useState, type ReactNode } from 'react'
function Box({ title, sub, accent }: { title: string; sub?: string; accent?: 'purple' | 'amber' | 'green' | 'gray' }) {
const c =
accent === 'purple'
? 'border-purple-300 bg-purple-50/60 dark:border-purple-700 dark:bg-purple-900/20'
: accent === 'amber'
? 'border-amber-300 bg-amber-50/60 dark:border-amber-700 dark:bg-amber-900/20'
: accent === 'green'
? 'border-green-300 bg-green-50/60 dark:border-green-700 dark:bg-green-900/20'
: 'border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-800'
return (
<div className={`rounded-lg border ${c} px-2.5 py-1.5`}>
<div className="text-[11px] font-medium text-gray-800 dark:text-gray-200 leading-tight">{title}</div>
{sub && <div className="text-[10px] text-gray-500 leading-tight mt-0.5">{sub}</div>}
</div>
)
}
function Lane({ label, children }: { label: string; children: ReactNode }) {
return (
<div className="flex-1 min-w-[150px] space-y-2">
<div className="text-[10px] font-semibold uppercase tracking-wide text-gray-400 text-center">{label}</div>
<div className="space-y-1.5">{children}</div>
</div>
)
}
function Arrow() {
return (
<div className="flex items-center justify-center text-gray-300 dark:text-gray-600 shrink-0 px-0.5">
<span className="hidden lg:block text-lg"></span>
<span className="lg:hidden text-sm"></span>
</div>
)
}
type Stage = {
id: string
title: string
summary: string
input: string
logic: string
source: string
example: string
}
// Spiegelt run_compliance_check (Phasen AF) + die Spezialagenten-Schicht.
const STAGES: Stage[] = [
{
id: 'a',
title: 'Phase A — Auflösen & Crawl',
summary: 'URLs + hochgeladene Dokumente einsammeln, fehlende Pflichtseiten automatisch finden.',
input: 'Start-URL, Dokument-Uploads, 8 Wizard-Felder (scan_context)',
logic: 'Discovery (Sitemap/Heuristik) + Fetch je Seite, Text-Extraktion pro Doc-Typ',
source: 'consent-tester /dsi-discovery, Playwright',
example: 'Findet /impressum, /datenschutz, /agb ohne manuelle Eingabe',
},
{
id: 'b',
title: 'Phase B — Profil & Dokument-Checks',
summary: 'Geschäftsprofil erkennen, jedes Dokument gegen seine Controls prüfen.',
input: 'Doc-Texte je Typ + Business-Scope',
logic: 'Regex-Runner + MC-Keyword + BGE-M3-Embedding + LLM-Verify (nur unscharf)',
source: 'doc_check_controls (DB), mc_classification.db (Embeddings)',
example: 'DSE: 267 Text-MCs, Keyword + semantischer Recall',
},
{
id: 'agents',
title: 'Spezialagenten (nebenläufig)',
summary: 'Pro Dokumenttyp ein typisierter Agent → eigener Ergebnis-Tab, gefüllt per SSE.',
input: 'Doc-Text, Scope, scan_context',
logic: 'Impressum + AGB + DSE laufen parallel (asyncio.gather), je ein AgentOutput',
source: 'api/agent_check/_agent_outputs._TOPIC_AGENTS',
example: 'AGB-Tab + DSE-Tab erscheinen, sobald ihr Agent fertig ist',
},
{
id: 'c',
title: 'Phase C — Cookie-Banner',
summary: 'Consent-Banner + gesetzte Cookies vor/nach Einwilligung live prüfen.',
input: 'Live-Seite im Browser',
logic: 'Consent-Tester-Scan: Banner, Vendors, Enforcement, Browser-Matrix',
source: 'consent-tester /scan',
example: 'Cookie vor Einwilligung gesetzt → Verstoß-Kandidat',
},
{
id: 'd',
title: 'Phase D — Vendors & Plausibilität',
summary: 'Dritt-Dienste extrahieren + Findings auf Plausibilität prüfen.',
input: 'Banner-/Seiten-Daten, Findings',
logic: 'Vendor-Extraktion (+OCR-Fallback), Plausibilitäts-Check je FAIL',
source: 'Cookie-/Vendor-Kataloge, LLM-Kaskade',
example: 'Analytics ohne Rechtsgrundlage → bestätigtes Finding',
},
{
id: 'reconcile',
title: 'Cross-Finding-Abgleich',
summary: 'Findings über Dokumente hinweg abgleichen — Doppel & Scheinverstöße auflösen.',
input: 'Alle Modul-Findings',
logic: 'Deckt ein anderes Dokument die Pflicht ab, wird das Cross-Finding unterdrückt',
source: 'cross_doc_reconcile (B-Wirings)',
example: '§36 VSBG im Impressum statt DSE → kein Doppel-Finding',
},
{
id: 'f',
title: 'Phase E/F — Bericht & Snapshot',
summary: 'Ergebnis persistieren, Snapshot für die Historie speichern.',
input: 'Konsolidiertes Ergebnis',
logic: 'Mail-Render + DB-Persist + Snapshot (Tab-Ansicht ohne Re-Crawl)',
source: 'compliance_check_snapshots',
example: 'Historie erneut öffnen, ohne die Seite neu zu crawlen',
},
]
type ModuleEngine = { name: string; mechanism: string }
const MODULES: ModuleEngine[] = [
{ name: 'Impressum', mechanism: 'Scope-Gate + Feld-Matcher (§5 DDG / §18 MStV)' },
{ name: 'AGB', mechanism: 'decision_method-Routing: Keyword → Geschäftsmodell-Gate → Embedding/Reference/LLM' },
{ name: 'DSE', mechanism: '4-Layer: Regex-Boost → Keyword → BGE-M3-Recall (0.65) → Semantic-Validator' },
{ name: 'Cookie-Banner', mechanism: 'Consent-Tester: Banner, Vendors, Enforcement, Browser-Matrix' },
]
type Pruefer = { method: string; mechanism: string; deterministic: string; example: string }
// Meta-Modell: jede Pflicht → ein Prüfertyp (decision_method). Wenige
// wiederverwendbare Prüfer statt Logik pro Control.
const PRUEFER: Pruefer[] = [
{ method: 'REGEX', mechanism: 'Kuratierte Muster / Keyword', deterministic: 'ja', example: 'Pflicht-Stichwort im Text' },
{ method: 'EMBEDDING', mechanism: 'BGE-M3 Kosinus ≥ Schwelle', deterministic: 'ja (feste Funktion)', example: '„Recht auf Berichtigung" ≈ Umschreibung' },
{ method: 'REFERENCE', mechanism: 'Link-/Verweis-Auflösung', deterministic: 'ja', example: 'Verweis auf die Datenschutzerklärung' },
{ method: 'LLM', mechanism: 'Kaskade Qwen→OVH→Claude, nur unscharfe Fälle', deterministic: 'nein (eskaliert)', example: 'Speicherdauer inhaltlich erfüllt?' },
{ method: 'BEHAVIOR', mechanism: 'Playwright: Live-Verhalten', deterministic: 'ja', example: 'Cookies vor Einwilligung gesetzt?' },
{ method: 'SCANNER', mechanism: 'Repo-/Netzwerk-/Prozess-Scan', deterministic: 'ja', example: 'Geplant: technische Nachweise' },
]
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>
)
}
function StageRow({ stage, last, open, onToggle }: { stage: Stage; 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.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>
)
}
export function ArchitekturView() {
const [open, setOpen] = useState<string | null>('b')
return (
<div className="space-y-8">
<div>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">Architektur &amp; Datenfluss</h2>
<p className="text-sm text-gray-500 dark:text-gray-400 max-w-3xl mt-1">
Nachvollziehbar: <strong>woher jedes Finding stammt</strong> und <strong>wie es geprüft wird</strong>.
Die Engine ist überwiegend <strong>deterministisch</strong> (Regex + Embedding); ein LLM entscheidet nur
die unscharfen Fälle. Ergebnisse erscheinen pro Modul progressiv und werden am Ende per
Cross-Finding-Abgleich bereinigt.
</p>
</div>
<section className="space-y-2">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Datenfluss (Überblick)</h3>
<div className="rounded-xl border border-gray-200 dark:border-gray-700 bg-gray-50/50 dark:bg-gray-900/20 p-3 overflow-x-auto">
<div className="flex flex-col lg:flex-row gap-1.5 lg:items-stretch min-w-[280px]">
<Lane label="Eingabe">
<Box title="Website + Dokumente" sub="Impressum · DSE · AGB · Cookies" accent="purple" />
<Box title="Wizard-Kontext" sub="8 Felder: Shop, Drittland, Beruf…" accent="purple" />
</Lane>
<Arrow />
<Lane label="Crawl + Text">
<Box title="Discovery + Fetch" sub="consent-tester, Playwright" />
<Box title="Doc-Text je Typ" />
</Lane>
<Arrow />
<Lane label="Engine (deterministisch)">
<div className="rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 p-1.5 space-y-1">
{STAGES.map((s) => (
<div key={s.id} className="text-[10px] text-gray-600 dark:text-gray-300 leading-tight">
{s.title}
</div>
))}
</div>
</Lane>
<Arrow />
<Lane label="Ausgaben">
<Box title="Findings je Modul-Tab" sub="Impressum/AGB/DSE/Cookie" accent="green" />
<Box title="Severity + Maßnahme" accent="green" />
<Box title="Snapshot + Bericht" sub="ohne Re-Crawl" accent="green" />
</Lane>
</div>
<p className="text-[10px] text-gray-400 mt-2">
Linksrechts reproduzierbar. Embedding ist semantisch UND deterministisch (feste Funktion: gleicher
Text gleicher Vektor). Das LLM läuft nur für unscharfe Fälle und eskaliert mit Selbstkonfidenz.
</p>
</div>
</section>
<section className="space-y-2">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Pipeline (Schritt für Schritt)</h3>
<div className="space-y-1">
{STAGES.map((s, i) => (
<StageRow
key={s.id}
stage={s}
last={i === STAGES.length - 1}
open={open === s.id}
onToggle={() => setOpen(open === s.id ? null : s.id)}
/>
))}
</div>
</section>
<section className="space-y-3">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Modul-Engines (live)</h3>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{MODULES.map((m) => (
<div key={m.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">{m.name}</span>
<span className="inline-block rounded px-1.5 py-0.5 text-[10px] font-medium bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-300">
live
</span>
</div>
<p className="text-xs text-gray-500 mt-1">{m.mechanism}</p>
</div>
))}
</div>
</section>
<section className="space-y-3">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300">Prüfer-Matrix (Meta-Modell)</h3>
<p className="text-xs text-gray-500 max-w-3xl">
Jede Pflicht wird einem <strong>Prüfertyp</strong> zugeordnet so braucht es nicht pro Control eigene
Logik, sondern wenige wiederverwendbare Prüfer.
</p>
<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">Prüfer</th>
<th className="py-1.5 pr-3">Mechanismus</th>
<th className="py-1.5 pr-3">Deterministisch</th>
<th className="py-1.5">Beispiel</th>
</tr>
</thead>
<tbody>
{PRUEFER.map((p) => (
<tr key={p.method} className="border-b border-gray-100 dark:border-gray-700/50 align-top">
<td className="py-1.5 pr-3">
<code className="text-[11px] bg-gray-100 dark:bg-gray-700 rounded px-1">{p.method}</code>
</td>
<td className="py-1.5 pr-3 text-gray-600 dark:text-gray-300">{p.mechanism}</td>
<td className="py-1.5 pr-3 text-gray-500">{p.deterministic}</td>
<td className="py-1.5 text-gray-500">{p.example}</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
)
}
+24 -2
View File
@@ -4,10 +4,12 @@ import React, { useState } from 'react'
import { ComplianceCheckTab } from './_components/ComplianceCheckTab' import { ComplianceCheckTab } from './_components/ComplianceCheckTab'
import { ComplianceFAQ } from './_components/ComplianceFAQ' import { ComplianceFAQ } from './_components/ComplianceFAQ'
import { SnapshotHistoryList } from './_components/SnapshotHistoryList' import { SnapshotHistoryList } from './_components/SnapshotHistoryList'
import { ArchitekturView } from './_components/ArchitekturView'
export default function AgentPage() { export default function AgentPage() {
// Nach einem abgeschlossenen Check die Historie unten neu laden. // Nach einem abgeschlossenen Check die Historie unten neu laden.
const [historyKey, setHistoryKey] = useState(0) const [historyKey, setHistoryKey] = useState(0)
const [tab, setTab] = useState<'check' | 'architektur'>('check')
return ( return (
<div className="space-y-6 max-w-4xl"> <div className="space-y-6 max-w-4xl">
@@ -16,11 +18,31 @@ export default function AgentPage() {
<p className="text-gray-500 mt-1">Webseiten + Dokumente auf DSGVO-Konformität prüfen.</p> <p className="text-gray-500 mt-1">Webseiten + Dokumente auf DSGVO-Konformität prüfen.</p>
</div> </div>
<div className="flex gap-1 border-b border-gray-200 dark:border-gray-700">
{([['check', 'Check'], ['architektur', 'Architektur']] as const).map(([id, label]) => (
<button
key={id}
onClick={() => setTab(id)}
className={`px-3 py-2 text-sm font-medium -mb-px border-b-2 transition-colors ${
tab === id
? 'border-purple-500 text-purple-600 dark:text-purple-400'
: 'border-transparent text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'
}`}
>
{label}
</button>
))}
</div>
{tab === 'check' ? (
<>
<ComplianceCheckTab onComplete={() => setHistoryKey(k => k + 1)} /> <ComplianceCheckTab onComplete={() => setHistoryKey(k => k + 1)} />
<SnapshotHistoryList refreshKey={historyKey} /> <SnapshotHistoryList refreshKey={historyKey} />
<ComplianceFAQ /> <ComplianceFAQ />
</>
) : (
<ArchitekturView />
)}
</div> </div>
) )
} }