feat: Banner-Check — Historie, persistentes Ergebnis, E-Mail-Report

1. localStorage Persistenz: URL, letztes Ergebnis, Historie (30 Eintraege)
2. Historie: Zeigt URL, Datum, Provider, Violations, Prozent
3. Letztes Ergebnis bleibt nach Tab-Wechsel/Reload sichtbar
4. E-Mail-Report: HTML-formatiert mit Violations + Hints an mailpit
5. Email-Status Anzeige im Frontend

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-10 07:55:12 +02:00
parent c075ecb721
commit 1b8e9881bb
2 changed files with 130 additions and 51 deletions
@@ -31,6 +31,7 @@ interface BannerResult {
after_reject: { cookies: string[]; scripts: string[]; new_tracking: string[]; violations: any[] }
after_accept: { cookies: string[]; scripts: string[]; new_tracking: string[]; undocumented: string[] }
}
email_status?: string
}
const CATEGORIES = [
@@ -43,12 +44,24 @@ const CATEGORIES = [
]
export function BannerCheckTab() {
const [url, setUrl] = useState('')
const [url, setUrl] = useState(() =>
typeof window !== 'undefined' ? localStorage.getItem('banner-check-url') || '' : ''
)
const [loading, setLoading] = useState(false)
const [progress, setProgress] = useState('')
const [error, setError] = useState<string | null>(null)
const [result, setResult] = useState<BannerResult | null>(null)
const [result, setResult] = useState<BannerResult | null>(() => {
if (typeof window === 'undefined') return null
try { const s = localStorage.getItem('banner-check-result'); return s ? JSON.parse(s) : null } catch { return null }
})
const [categories, setCategories] = useState<string[]>(['all'])
const [history, setHistory] = useState<{ url: string; date: string; provider: string; violations: number; pct: number }[]>(() => {
if (typeof window === 'undefined') return []
try { return JSON.parse(localStorage.getItem('banner-check-history') || '[]') } catch { return [] }
})
// Persist URL
React.useEffect(() => { localStorage.setItem('banner-check-url', url) }, [url])
const toggleCategory = (id: string) => {
if (id === 'all') {
@@ -71,10 +84,7 @@ export function BannerCheckTab() {
setResult(null)
setProgress('Cookie-Banner wird analysiert...')
// 'all' selected = empty array (test everything)
const selectedCategories = categories.includes('all')
? []
: categories
const selectedCategories = categories.includes('all') ? [] : categories
try {
const res = await fetch('/api/sdk/v1/agent/banner-check', {
@@ -85,6 +95,20 @@ export function BannerCheckTab() {
if (!res.ok) throw new Error(`Fehler: ${res.status}`)
const data = await res.json()
setResult(data)
localStorage.setItem('banner-check-result', JSON.stringify(data))
// Add to history
const violations = data.structured_checks?.filter((c: CheckItem) => !c.passed && !c.skipped).length || 0
const entry = {
url: url.trim(),
date: new Date().toISOString(),
provider: data.banner_provider || 'Unbekannt',
violations,
pct: data.completeness_pct ?? 0,
}
const updated = [entry, ...history].slice(0, 30)
setHistory(updated)
localStorage.setItem('banner-check-history', JSON.stringify(updated))
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
@@ -93,12 +117,15 @@ export function BannerCheckTab() {
}
}
const loadFromHistory = (entry: { url: string }) => {
setUrl(entry.url)
}
const structuredChecks = result?.structured_checks || []
const hasStructured = structuredChecks.length > 0
const compPct = result?.completeness_pct ?? 0
const corrPct = result?.correctness_pct ?? 0
// Build ChecklistView-compatible result for structured checks
const checklistResults = hasStructured ? [{
label: `Cookie-Banner: ${result?.banner_provider || 'Unbekannt'}`,
url: url,
@@ -149,16 +176,10 @@ export function BannerCheckTab() {
: 'bg-gray-50 border-gray-200 text-gray-600 hover:bg-gray-100'
}`}
>
<input
type="checkbox"
checked={categories.includes(cat.id)}
onChange={() => toggleCategory(cat.id)}
className="sr-only"
/>
<input type="checkbox" checked={categories.includes(cat.id)}
onChange={() => toggleCategory(cat.id)} className="sr-only" />
<span className={`w-3 h-3 rounded-sm border flex items-center justify-center ${
categories.includes(cat.id)
? 'bg-purple-600 border-purple-600'
: 'border-gray-400'
categories.includes(cat.id) ? 'bg-purple-600 border-purple-600' : 'border-gray-400'
}`}>
{categories.includes(cat.id) && (
<svg className="w-2 h-2 text-white" fill="currentColor" viewBox="0 0 12 12">
@@ -188,68 +209,89 @@ export function BannerCheckTab() {
{result && (
<div className="space-y-4">
{/* 3-Phase Summary Card */}
{result.phases && (
<div className="bg-white border border-gray-200 rounded-xl shadow-sm overflow-hidden">
<div className="px-6 py-4 bg-gray-50 border-b border-gray-200">
<div className="flex items-center gap-3">
<span className="text-2xl">
{result.banner_detected ? '\u{1F6E1}\u{FE0F}' : '\u26A0\u{FE0F}'}
</span>
<span className="text-2xl">{result.banner_detected ? '\u{1F6E1}\u{FE0F}' : '\u26A0\u{FE0F}'}</span>
<div>
<h3 className="text-sm font-semibold text-gray-900">
{result.banner_detected
? `Banner erkannt: ${result.banner_provider || 'Unbekannter Anbieter'}`
: 'Kein Cookie-Banner erkannt'}
</h3>
<p className="text-xs text-gray-500 mt-0.5">
3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion
</p>
<p className="text-xs text-gray-500 mt-0.5">3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion</p>
</div>
</div>
</div>
<div className="px-6 py-3 grid grid-cols-3 gap-4">
<PhaseBox
label="Vor Consent"
icon="\uD83D\uDD12"
<PhaseBox label="Vor Consent" icon="\uD83D\uDD12"
cookies={result.phases.before_consent.cookies?.length ?? 0}
scripts={result.phases.before_consent.scripts?.length ?? 0}
violations={result.phases.before_consent.violations?.length ?? 0}
/>
<PhaseBox
label="Nach Ablehnen"
icon="\uD83D\uDEAB"
violations={result.phases.before_consent.violations?.length ?? 0} />
<PhaseBox label="Nach Ablehnen" icon="\uD83D\uDEAB"
cookies={result.phases.after_reject.cookies?.length ?? 0}
scripts={result.phases.after_reject.scripts?.length ?? 0}
violations={result.phases.after_reject.violations?.length ?? 0}
/>
<PhaseBox
label="Nach Akzeptieren"
icon="\u2705"
violations={result.phases.after_reject.violations?.length ?? 0} />
<PhaseBox label="Nach Akzeptieren" icon="\u2705"
cookies={result.phases.after_accept.cookies?.length ?? 0}
scripts={result.phases.after_accept.scripts?.length ?? 0}
violations={0}
/>
violations={0} />
</div>
</div>
)}
{/* Structured L1/L2 Checklist */}
{hasStructured && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<ChecklistView results={checklistResults} />
</div>
)}
{result.email_status && (
<div className="text-xs text-gray-500 flex items-center gap-2">
<span className={`w-2 h-2 rounded-full ${result.email_status === 'sent' ? 'bg-green-400' : 'bg-gray-300'}`} />
E-Mail: {result.email_status === 'sent' ? 'Gesendet' : result.email_status}
</div>
)}
{!result.banner_detected && !hasStructured && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<p className="text-sm text-gray-500">
Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach ss25 TDDDG Pflicht.
Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach §25 TDDDG Pflicht.
</p>
</div>
)}
</div>
)}
{/* History */}
{history.length > 0 && (
<div className="border border-gray-200 rounded-xl p-4">
<h4 className="text-sm font-medium text-gray-700 mb-2">Letzte Banner-Checks</h4>
<div className="space-y-1">
{history.map((h, i) => (
<button key={i} onClick={() => loadFromHistory(h)}
className="w-full flex items-center justify-between p-2.5 rounded-lg border border-gray-100 hover:border-purple-200 hover:bg-purple-50/30 transition-all text-left">
<div className="min-w-0 flex-1">
<div className="text-sm font-medium text-gray-900 truncate">{h.url}</div>
<div className="text-xs text-gray-500">
{new Date(h.date).toLocaleDateString('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit' })}
{' · '}{h.provider}
</div>
</div>
<div className="flex items-center gap-3 shrink-0 ml-3">
<span className={`text-xs font-medium ${h.violations > 0 ? 'text-red-600' : 'text-green-600'}`}>
{h.violations} Findings
</span>
<span className={`text-xs font-medium ${h.pct === 100 ? 'text-green-700' : h.pct >= 50 ? 'text-yellow-700' : 'text-red-700'}`}>
{h.pct}%
</span>
</div>
</button>
))}
</div>
</div>
)}
</div>
)
}
@@ -261,14 +303,8 @@ function PhaseBox({ label, icon, cookies, scripts, violations }: {
<div className="text-center">
<div className="text-lg">{icon}</div>
<div className="text-xs font-medium text-gray-700">{label}</div>
<div className="text-xs text-gray-500 mt-1">
{cookies} Cookies, {scripts} Scripts
</div>
{violations > 0 && (
<div className="text-xs text-red-600 font-medium">
{violations} Verstoesse
</div>
)}
<div className="text-xs text-gray-500 mt-1">{cookies} Cookies, {scripts} Scripts</div>
{violations > 0 && <div className="text-xs text-red-600 font-medium">{violations} Verstoesse</div>}
</div>
)
}