'use client' import React, { useEffect, useState, useMemo } from 'react' import { use as useUnwrap } from 'react' import FindingsTab from './FindingsTab' import BannerTab from './BannerTab' type MCRow = { id: number doc_type: string mc_id: string label: string passed: number skipped: number severity: string regulation: string matched_text: string hint: string } type ScorecardRow = { regulation: string total: number passed: number failed: number skipped: number pct: number severity: Record } type AuditResponse = { found: boolean run?: { check_id: string ts: string site_name: string base_domain: string doc_count: number scorecard: { by_regulation: ScorecardRow[]; totals: any } vvt_summary: { total?: number; internal?: number; external?: number } } mc_count?: number results?: MCRow[] } // P8: MC-Audit ist eine Checkliste, KEINE Severity-Drohung. Statt // rotem HIGH-Badge zeigen wir die Quellen-Prioritaet (Gesetz vs. // Behoerden-Leitlinie vs. Best-Practice) und einen 3-Tier-Status // (erfuellt / nicht erfuellt / selbst pruefen). const PRIORITY_BADGE: Record = { Gesetz: 'bg-slate-800 text-white', 'Behoerden-Leitlinie': 'bg-blue-100 text-blue-800', 'Best-Practice': 'bg-gray-100 text-gray-600', '—': 'bg-gray-50 text-gray-400', } function regulationToPriority(reg: string): keyof typeof PRIORITY_BADGE { const r = (reg || '').toLowerCase() if (/dsgvo|gdpr|eprivacy|tdddg|tkg|bdsg|ttdsg/.test(r)) return 'Gesetz' if (/edpb|dsk|cnil|lfdi|eugh|orientierungshilfe|leitlinie|guideline/.test(r)) return 'Behoerden-Leitlinie' if (/iso|nist|bsi|cobit|sox/.test(r)) return 'Best-Practice' return '—' } const _CONDITIONAL_RE = /\b(falls|sofern|wenn|soweit|ggf\.|gegebenenfalls)\b/i function rowReviewStatus(r: MCRow): 'pass' | 'fail' | 'review' | 'na' { if (r.passed) return 'pass' if (r.skipped) return 'na' // failed: harter Fail nur bei matched_text-Beleg ODER nicht-konditionalem Label if (!r.matched_text && _CONDITIONAL_RE.test(r.label || '')) return 'review' return 'fail' } const STATUS_FILTERS = [ { value: 'all', label: 'Alle' }, { value: 'fail', label: 'Nicht erfuellt' }, { value: 'review', label: 'Selbst pruefen' }, { value: 'pass', label: 'Erfuellt' }, { value: 'na', label: 'Nicht anwendbar' }, ] as const export default function AuditPage( { params }: { params: Promise<{ checkId: string }> }, ) { const { checkId } = useUnwrap(params) const [data, setData] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [filterStatus, setFilterStatus] = useState('fail') const [filterReg, setFilterReg] = useState('') const [filterDoc, setFilterDoc] = useState('') const [expanded, setExpanded] = useState(null) const [tab, setTab] = useState<'mc' | 'all' | 'banner'>('all') useEffect(() => { let cancelled = false setLoading(true) fetch(`/api/sdk/v1/agent/audit/${checkId}`) .then(r => r.json()) .then(d => { if (!cancelled) setData(d) }) .catch(e => { if (!cancelled) setError(String(e)) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [checkId]) const allRows = data?.results ?? [] const docTypes = useMemo( () => Array.from(new Set(allRows.map(r => r.doc_type))).sort(), [allRows], ) const regulations = useMemo( () => Array.from(new Set(allRows.map(r => r.regulation).filter(Boolean))).sort(), [allRows], ) const filtered = allRows.filter(r => { if (filterStatus !== 'all' && rowReviewStatus(r) !== filterStatus) return false if (filterReg && r.regulation !== filterReg) return false if (filterDoc && r.doc_type !== filterDoc) return false return true }) if (loading) { return
Lade Audit…
} if (error || !data?.found) { return (
Audit nicht gefunden{error ? `: ${error}` : ''}.
) } const run = data.run! const scorecard = run.scorecard?.by_regulation ?? [] const totals = run.scorecard?.totals ?? { total: 0, passed: 0, failed: 0, pct: 0 } return (
{/* Header */}

MC-Audit: {run.site_name}

check_id {checkId} ·{' '} {new Date(run.ts).toLocaleString('de-DE')} · {run.doc_count} Dokumente ·{' '} {data.mc_count} MC-Eintraege

{/* Tab switcher */}
{([ { key: 'all', label: 'Voll-Audit (alle Findings)' }, { key: 'banner', label: 'Cookie-Banner-Analyse' }, { key: 'mc', label: 'Nur MC-Scorecard' }, ] as const).map(t => ( ))}
{tab === 'all' && } {tab === 'banner' && } {tab === 'mc' && <> {/* Scorecard */}

Compliance-Scorecard nach Regulation {totals.pct}% ({totals.passed} bestanden, {totals.failed} Fail,{' '} {totals.skipped} skipped — {totals.total} gesamt)

{scorecard.map(row => ( setFilterReg(row.regulation === filterReg ? '' : row.regulation)}> ))}
Regulation Passed Failed HIGH MEDIUM Score
{row.regulation} {row.passed} {row.failed} {(row.severity.HIGH || 0) + (row.severity.CRITICAL || 0)} {row.severity.MEDIUM || 0} = 80 ? 'text-green-700' : row.pct >= 50 ? 'text-amber-700' : 'text-red-700' }`}>{row.pct}%
{/* Filters */}
{STATUS_FILTERS.map(f => ( ))}
{filtered.length} von {allRows.length}
{/* Results */}
{filtered.map(row => ( setExpanded(expanded === row.id ? null : row.id)}> {expanded === row.id && ( )} ))} {filtered.length === 0 && ( )}
Status Doc Regulation MC Prioritaet
{(() => { const st = rowReviewStatus(row) if (st === 'pass') return if (st === 'na') return if (st === 'review') return ? return })()} {row.doc_type} {row.regulation || '—'} {row.label} {(() => { const prio = regulationToPriority(row.regulation) return ( {prio} ) })()}
MC-ID: {row.mc_id}
{row.matched_text && (
Treffer: "{row.matched_text}"
)} {row.hint && (
{row.hint}
)}
Keine MCs entsprechen den aktuellen Filtern.
}
) }