"""
P18 — Erweiterter Banner-Block fuer die Email.
Rendert die Daten aus dem consent-tester die heute weggeworfen wurden:
- 3-Phasen-Cookie-Tabelle (before_consent / after_reject / after_accept)
- Banner-Quality-Score (completeness/correctness/violations)
- Per-Category-Tracker-Listing
- Violations-Liste mit Rechtsgrundlagen
"""
from __future__ import annotations
def _color_for(pct: int) -> str:
return ("#16a34a" if pct >= 80 else
"#d97706" if pct >= 50 else "#dc2626")
def _short_phase_label(key: str) -> str:
return {
"before_consent": "Vor Consent",
"after_reject": "Nach Ablehnung",
"after_accept": "Nach Annahme",
}.get(key, key)
def _phase_color(key: str, cookie_count: int) -> str:
if key == "before_consent":
return "#16a34a" if cookie_count == 0 else "#dc2626"
if key == "after_reject":
return "#16a34a" if cookie_count <= 1 else "#d97706"
return "#94a3b8"
def build_banner_deep_html(banner_result: dict | None) -> str:
"""Render: Banner-Quality + Phases + Violations.
Konsumiert das volle consent-tester-Response. Komplementiert
`build_provider_list_html` (das nur Summary + TCF-Vendor-Tabelle macht).
"""
if not banner_result:
return ""
parts: list[str] = [
'
'
'
'
'Cookie-Banner — technische Analyse
'
]
# 1) Quality-Score-Cards
compl = banner_result.get("completeness_pct")
corr = banner_result.get("correctness_pct")
summary = banner_result.get("summary") or {}
n_critical = summary.get("critical", 0)
n_high = summary.get("high", 0)
if compl is not None or corr is not None:
parts.append(
'
'
)
if compl is not None:
c = _color_for(int(compl))
parts.append(
f'| '
f' '
f'Vollstaendigkeit '
f'{compl}% '
f' | '
)
if corr is not None:
c = _color_for(int(corr))
parts.append(
f''
f' '
f'Korrektheit '
f'{corr}% '
f' | '
)
viol_c = ("#dc2626" if n_critical + n_high > 0 else
"#d97706" if (summary.get("total_violations") or 0) > 0 else
"#16a34a")
parts.append(
f''
f' '
f'Verstoesse '
f''
f'{summary.get("total_violations", 0)}'
f''
f'(crit:{n_critical} high:{n_high}) | '
)
parts.append('
')
# 2) 3-Phasen-Tabelle
phases = banner_result.get("phases") or {}
if phases:
parts.append(
'
Cookie-Setzungen pro Phase '
'(echter Browser-Test):
'
'
'
''
'| Phase | '
'Cookies | '
'Tracker | '
'Auffaelligkeiten | '
'
'
)
for key in ("before_consent", "after_reject", "after_accept"):
ph = phases.get(key) or {}
if not isinstance(ph, dict): continue
cookies = ph.get("cookies") or []
trackers = ph.get("tracking_services") or []
new_track = ph.get("new_tracking") or []
violations = ph.get("violations") or []
undoc = ph.get("undocumented") or []
color = _phase_color(key, len(cookies))
issues_parts = []
if violations: issues_parts.append(f"{len(violations)} Verstoss")
if new_track: issues_parts.append(f"{len(new_track)} neue Tracker")
if undoc: issues_parts.append(f"{len(undoc)} undokumentiert")
issues_str = ", ".join(issues_parts) or "—"
parts.append(
f''
f'| '
f''
f'{_short_phase_label(key)} | '
f'{len(cookies)} | '
f'{len(trackers)} | '
f'{issues_str} | '
f'
'
)
parts.append('
')
# 3) Per-Category-Tracker
cats = banner_result.get("category_tests") or []
if cats:
non_essential = [c for c in cats if c.get("category") != "necessary"]
if non_essential:
parts.append(
'
Provider-Listing pro Banner-Kategorie:
'
'
'
''
'| Kategorie | '
'Anbieter | '
'Hinweis | '
'
'
)
for c in non_essential:
n = len(c.get("tracking_services") or [])
label = c.get("category_label") or c.get("category", "?")
pdv = c.get("provider_details_visible")
# P19: echtes Signal aus Click-Through-Test
if pdv is False:
color, hint = "#dc2626", ("Banner zeigt KEINE Provider-"
"Details — keine informierte Einwilligung")
elif pdv is True:
color, hint = "#16a34a", ""
elif n == 0:
color, hint = "#d97706", ("Keine Anbieter erkannt (vermutlich "
"kein Provider-Listing im Banner)")
else:
color, hint = "#16a34a", ""
parts.append(
f''
f'| {label} | '
f'{n} | '
f''
f'{hint} |
'
)
parts.append('
')
# 4) Violations mit Rechtsgrundlage
violations = (banner_result.get("banner_checks") or {}).get("violations", [])
if violations:
parts.append(
'
Erkannte Banner-Verstoesse:
'
'
')
# 5) P59b: Cookie-Behavior-Findings (deklariert vs. tatsaechlich)
cb_findings = banner_result.get("cookie_behavior_findings") or []
if cb_findings:
parts.append(
'
'
'
Cookie-Verhaltens-Check '
'(P59 — deklarierter Zweck vs. tatsaechliches Verhalten)
'
'
')
# 6) P61: Untergeschobene Cookies/Vendors (Vendor-Package)
impl_findings = banner_result.get("implicit_vendor_findings") or []
if impl_findings:
# Gruppiert nach primary_vendor: pro Primary die mitgelaufenen Items
by_primary: dict[str, list[dict]] = {}
for f in impl_findings:
by_primary.setdefault(f["primary_vendor"], []).append(f["implicit"])
parts.append(
'
'
'
Untergeschobene Cookies / Vendors '
'(P61 — mit Hauptanbieter automatisch mitgeladen)
'
'
'
'Diese Cookies/Vendors kommen automatisch mit dem deklarierten '
'Hauptanbieter mit — Marketing-Manager waehlen sie oft nicht '
'bewusst aus, sie sind aber zustimmungspflichtig.
'
)
for primary, impls in by_primary.items():
parts.append(
f'
'
f'{primary} bringt automatisch:
'
'
')
parts.append('
')
parts.append('
')
return "".join(parts)