Files
breakpilot-compliance/admin-compliance/app/sdk/agent/audit/[checkId]/page.tsx
T
Benjamin Admin 6c223c7c9b
CI / detect-changes (push) Successful in 10s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / validate-canonical-controls (push) Successful in 14s
CI / loc-budget (push) Failing after 15s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m43s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
feat(compliance-check): exec-summary + voll-audit + TDM-respect + cookie-KB-extended + saving-scan-funnel
P1 — Exec-Summary oben im Email-Report (4 KPIs + 2 CTAs, dunkler Gradient)
P3 — no_direct_sales-Flag fuer OEM-Konfigurator-Sites; AGB/Widerruf/AGB als
     "NICHT ANWENDBAR" (grau) statt "NICHT GEFUNDEN" (rot)
P5 — Voll-Audit Unification: alle Findings (MC + Pflichtangaben + Vendor +
     Redundanz) in /data/compliance_audits.db.unified_findings; neuer
     /api/compliance/agent/findings/<id> Endpoint + FindingsTab im Audit-UI
     mit Filter + CSV-Export
P7 — Crawl-Hardening: TDM-Reservation-Check (robots.txt / ai.txt / Header /
     Meta) vor jedem Run mit 24h-Cache; HeadlessChrome-UA (Firma noch nicht
     gegruendet — Switch via BREAKPILOT_BRANDED_UA env); per-Domain
     Rate-Limit 1 req/s + max 2 concurrent
P2 — Cookie-Knowledge-DB additiv erweitert (35 -> 74 Cookies): Adobe, Meta,
     Microsoft, LinkedIn, TikTok, HubSpot, Marketo, Salesforce, Hotjar,
     FullStory, Mouseflow, Intercom, Drift, Zendesk, Cloudflare, Stripe,
     OneTrust/Cookiebot/Usercentrics, Matomo, Pinterest, Snapchat, X/Twitter,
     YouTube, Vimeo, Klaviyo, Mailchimp, Mixpanel, Segment, Amplitude,
     Optimizely, Datadog; Wire-in in cookie_function_classifier liefert
     compliance_risk-Label (kritisch/hoch/mittel/gering) pro Vendor
A  — k-Anonymitaets-Helper (benchmark_k_anonymity) fuer P6-Vorbereitung
B  — Cross-Tenant-Domain-Assertion im /findings-Endpoint (expected_domain
     Query-Param -> 403 bei Mismatch)
C  — Saving-Scan-Funnel: /api/compliance/agent/saving-scan/start mit
     Validierung + 24h-Rate-Limit pro Domain + Lead-Persistenz in
     saving_scan_leads + Auto-Discovery via _run_compliance_check; 6 Tests
D  — Risk-Badge im Email-Vendor-Row

Rechtliche Leitplanken (Memory feedback_oem_data_legal.md): nur eigene
Knapp-Bewertungen + Source-Pointer, keine 1:1-Kopien fremder CMP-Texte.
TDM-Opt-Out-Respect nach § 44b UrhG. KEINE Schema-Aenderungen — alles in
Sidecar-SQLite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 23:48:34 +02:00

300 lines
11 KiB
TypeScript

'use client'
import React, { useEffect, useState, useMemo } from 'react'
import { use as useUnwrap } from 'react'
import FindingsTab from './FindingsTab'
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<string, number>
}
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[]
}
const SEVERITY_COLOR: Record<string, string> = {
CRITICAL: 'bg-red-600 text-white',
HIGH: 'bg-red-100 text-red-800',
MEDIUM: 'bg-amber-100 text-amber-800',
LOW: 'bg-blue-100 text-blue-800',
INFO: 'bg-gray-100 text-gray-600',
}
const STATUS_FILTERS = [
{ value: 'all', label: 'Alle' },
{ value: 'failed', label: 'Nur Fail' },
{ value: 'passed', label: 'Nur Pass' },
{ value: 'skipped', label: 'Nur Skipped' },
] as const
export default function AuditPage(
{ params }: { params: Promise<{ checkId: string }> },
) {
const { checkId } = useUnwrap(params)
const [data, setData] = useState<AuditResponse | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [filterStatus, setFilterStatus] = useState<typeof STATUS_FILTERS[number]['value']>('failed')
const [filterReg, setFilterReg] = useState<string>('')
const [filterDoc, setFilterDoc] = useState<string>('')
const [expanded, setExpanded] = useState<number | null>(null)
const [tab, setTab] = useState<'mc' | 'all'>('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 === 'failed' && (r.passed || r.skipped)) return false
if (filterStatus === 'passed' && !r.passed) return false
if (filterStatus === 'skipped' && !r.skipped) return false
if (filterReg && r.regulation !== filterReg) return false
if (filterDoc && r.doc_type !== filterDoc) return false
return true
})
if (loading) {
return <div className="p-6 text-sm text-gray-500">Lade Audit</div>
}
if (error || !data?.found) {
return (
<div className="p-6 text-sm text-red-600">
Audit nicht gefunden{error ? `: ${error}` : ''}.
</div>
)
}
const run = data.run!
const scorecard = run.scorecard?.by_regulation ?? []
const totals = run.scorecard?.totals ?? { total: 0, passed: 0, failed: 0, pct: 0 }
return (
<div className="space-y-6 p-6 max-w-6xl">
{/* Header */}
<div>
<h1 className="text-xl font-semibold text-gray-900">
MC-Audit: {run.site_name}
</h1>
<p className="text-xs text-gray-500 mt-1">
check_id <code className="bg-gray-100 px-1 rounded">{checkId}</code> ·{' '}
{new Date(run.ts).toLocaleString('de-DE')} · {run.doc_count} Dokumente ·{' '}
{data.mc_count} MC-Eintraege
</p>
</div>
{/* Tab switcher */}
<div className="flex gap-2 border-b border-gray-200">
{([
{ key: 'all', label: 'Voll-Audit (alle Findings)' },
{ key: 'mc', label: 'Nur MC-Scorecard' },
] as const).map(t => (
<button key={t.key}
onClick={() => setTab(t.key)}
className={`px-4 py-2 text-sm border-b-2 -mb-px transition ${
tab === t.key
? 'border-blue-600 text-blue-700 font-medium'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}>{t.label}</button>
))}
</div>
{tab === 'all' && <FindingsTab checkId={checkId} />}
{tab === 'mc' && <>
{/* Scorecard */}
<div className="border rounded-lg overflow-hidden">
<div className="px-4 py-3 bg-blue-50 border-b border-blue-100">
<h2 className="text-sm font-medium text-blue-900">
Compliance-Scorecard nach Regulation
<span className="ml-2 text-blue-700 font-semibold text-base">
{totals.pct}%
</span>
<span className="ml-2 text-xs text-blue-600">
({totals.passed} bestanden, {totals.failed} Fail,{' '}
{totals.skipped} skipped {totals.total} gesamt)
</span>
</h2>
</div>
<table className="w-full text-xs">
<thead className="bg-gray-50 text-gray-600">
<tr>
<th className="px-3 py-2 text-left">Regulation</th>
<th className="px-3 py-2 text-center">Passed</th>
<th className="px-3 py-2 text-center">Failed</th>
<th className="px-3 py-2 text-center">HIGH</th>
<th className="px-3 py-2 text-center">MEDIUM</th>
<th className="px-3 py-2 text-right">Score</th>
</tr>
</thead>
<tbody>
{scorecard.map(row => (
<tr key={row.regulation} className="border-t hover:bg-blue-50/30 cursor-pointer"
onClick={() => setFilterReg(row.regulation === filterReg ? '' : row.regulation)}>
<td className="px-3 py-2 font-medium">{row.regulation}</td>
<td className="px-3 py-2 text-center text-green-700">{row.passed}</td>
<td className="px-3 py-2 text-center text-red-700">{row.failed}</td>
<td className="px-3 py-2 text-center text-red-700">
{(row.severity.HIGH || 0) + (row.severity.CRITICAL || 0)}
</td>
<td className="px-3 py-2 text-center text-amber-700">
{row.severity.MEDIUM || 0}
</td>
<td className={`px-3 py-2 text-right font-semibold ${
row.pct >= 80 ? 'text-green-700' :
row.pct >= 50 ? 'text-amber-700' : 'text-red-700'
}`}>{row.pct}%</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Filters */}
<div className="flex flex-wrap gap-3 items-center text-xs">
<div className="flex gap-1">
{STATUS_FILTERS.map(f => (
<button key={f.value}
onClick={() => setFilterStatus(f.value)}
className={`px-2.5 py-1 rounded-full border ${
filterStatus === f.value
? 'bg-blue-600 text-white border-blue-600'
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'
}`}>{f.label}</button>
))}
</div>
<select value={filterDoc} onChange={e => setFilterDoc(e.target.value)}
className="border border-gray-200 rounded px-2 py-1">
<option value="">Alle Doc-Types</option>
{docTypes.map(d => <option key={d} value={d}>{d}</option>)}
</select>
<select value={filterReg} onChange={e => setFilterReg(e.target.value)}
className="border border-gray-200 rounded px-2 py-1">
<option value="">Alle Regulations</option>
{regulations.map(r => <option key={r} value={r}>{r}</option>)}
</select>
<span className="text-gray-500">
{filtered.length} von {allRows.length}
</span>
</div>
{/* Results */}
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-xs">
<thead className="bg-gray-50 text-gray-600">
<tr>
<th className="px-3 py-2 text-left">Status</th>
<th className="px-3 py-2 text-left">Doc</th>
<th className="px-3 py-2 text-left">Regulation</th>
<th className="px-3 py-2 text-left">MC</th>
<th className="px-3 py-2 text-left">Severity</th>
</tr>
</thead>
<tbody>
{filtered.map(row => (
<React.Fragment key={row.id}>
<tr className="border-t cursor-pointer hover:bg-gray-50"
onClick={() => setExpanded(expanded === row.id ? null : row.id)}>
<td className="px-3 py-2">
{row.passed ? (
<span className="text-green-600"></span>
) : row.skipped ? (
<span className="text-gray-400"></span>
) : (
<span className="text-red-600"></span>
)}
</td>
<td className="px-3 py-2 text-gray-700">{row.doc_type}</td>
<td className="px-3 py-2 text-gray-500">{row.regulation || '—'}</td>
<td className="px-3 py-2 text-gray-900">{row.label}</td>
<td className="px-3 py-2">
<span className={`px-2 py-0.5 rounded text-[10px] font-medium ${
SEVERITY_COLOR[row.severity] || 'bg-gray-100'
}`}>{row.severity || '—'}</span>
</td>
</tr>
{expanded === row.id && (
<tr className="bg-gray-50/50">
<td colSpan={5} className="px-3 py-3 text-xs">
<div className="text-gray-500 mb-1">
MC-ID: <code>{row.mc_id}</code>
</div>
{row.matched_text && (
<div className="mb-2">
<span className="text-green-700 font-medium">Treffer: </span>
<span className="font-mono text-gray-700">
"{row.matched_text}"
</span>
</div>
)}
{row.hint && (
<div className="text-amber-700 bg-amber-50 border-l-2 border-amber-200 pl-2 py-1">
{row.hint}
</div>
)}
</td>
</tr>
)}
</React.Fragment>
))}
{filtered.length === 0 && (
<tr>
<td colSpan={5} className="px-3 py-6 text-center text-gray-400">
Keine MCs entsprechen den aktuellen Filtern.
</td>
</tr>
)}
</tbody>
</table>
</div>
</>}
</div>
)
}