From 1b8e9881bb885f58d3dbc4f7f0b12ae2d4c7e565 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 10 May 2026 07:55:12 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Banner-Check=20=E2=80=94=20Historie,=20?= =?UTF-8?q?persistentes=20Ergebnis,=20E-Mail-Report?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../sdk/agent/_components/BannerCheckTab.tsx | 132 +++++++++++------- .../compliance/api/agent_doc_check_routes.py | 49 ++++++- 2 files changed, 130 insertions(+), 51 deletions(-) diff --git a/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx index d037095..e99ea7c 100644 --- a/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx +++ b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx @@ -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(null) - const [result, setResult] = useState(null) + const [result, setResult] = useState(() => { + 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(['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' }`} > - toggleCategory(cat.id)} - className="sr-only" - /> + toggleCategory(cat.id)} className="sr-only" /> {categories.includes(cat.id) && ( @@ -188,68 +209,89 @@ export function BannerCheckTab() { {result && (
- {/* 3-Phase Summary Card */} {result.phases && (
- - {result.banner_detected ? '\u{1F6E1}\u{FE0F}' : '\u26A0\u{FE0F}'} - + {result.banner_detected ? '\u{1F6E1}\u{FE0F}' : '\u26A0\u{FE0F}'}

{result.banner_detected ? `Banner erkannt: ${result.banner_provider || 'Unbekannter Anbieter'}` : 'Kein Cookie-Banner erkannt'}

-

- 3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion -

+

3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion

- - + - + + violations={0} />
)} - {/* Structured L1/L2 Checklist */} {hasStructured && (
)} + {result.email_status && ( +
+ + E-Mail: {result.email_status === 'sent' ? 'Gesendet' : result.email_status} +
+ )} + {!result.banner_detected && !hasStructured && (

- 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.

)}
)} + + {/* History */} + {history.length > 0 && ( +
+

Letzte Banner-Checks

+
+ {history.map((h, i) => ( + + ))} +
+
+ )} ) } @@ -261,14 +303,8 @@ function PhaseBox({ label, icon, cookies, scripts, violations }: {
{icon}
{label}
-
- {cookies} Cookies, {scripts} Scripts -
- {violations > 0 && ( -
- {violations} Verstoesse -
- )} +
{cookies} Cookies, {scripts} Scripts
+ {violations > 0 &&
{violations} Verstoesse
}
) } diff --git a/backend-compliance/compliance/api/agent_doc_check_routes.py b/backend-compliance/compliance/api/agent_doc_check_routes.py index 3639a05..d5c23df 100644 --- a/backend-compliance/compliance/api/agent_doc_check_routes.py +++ b/backend-compliance/compliance/api/agent_doc_check_routes.py @@ -110,9 +110,52 @@ async def run_banner_check(req: BannerCheckRequest): "categories": req.categories, }, ) - if resp.status_code == 200: - return resp.json() - return {"error": f"Consent-Tester: HTTP {resp.status_code}"} + if resp.status_code != 200: + return {"error": f"Consent-Tester: HTTP {resp.status_code}"} + + result = resp.json() + + # Send email report + checks = result.get("structured_checks", []) + violations = [c for c in checks if not c.get("passed") and not c.get("skipped")] + passes = [c for c in checks if c.get("passed")] + provider = result.get("banner_provider", "Unbekannt") + comp_pct = result.get("completeness_pct", 0) + + html = [ + '
', + f'

Banner-Check: {req.url}

', + f'

Banner: {provider} | Vollstaendigkeit: {comp_pct}%

', + ] + if violations: + html.append(f'

{len(violations)} Verstoesse

') + for v in violations: + html.append( + f'
' + f' ' + f'{v.get("label","")}' + ) + if v.get("hint"): + html.append( + f'
{v["hint"]}
' + ) + html.append('
') + if passes: + html.append(f'

{len(passes)} Bestanden

') + for p in passes: + html.append(f'
' + f' {p.get("label","")}
') + html.append('
') + + email_result = send_email( + recipient="dsb@breakpilot.local", + subject=f"[BANNER-CHECK] {provider} — {req.url}", + body_html="\n".join(html), + ) + result["email_status"] = email_result.get("status", "failed") + return result except Exception as e: return {"error": str(e)[:200]}