Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/ArchitekturView.tsx
T
Benjamin Admin ce6b4c58e3 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>
2026-06-21 11:25:28 +02:00

303 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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>
)
}