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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user