"""Kompakte Per-Engine-Projektion eines ConsentTestResult für die Browser-Matrix. Die Matrix braucht NICHT die volle `/scan`-Antwort — nur die Felder, die je Browser-Zeile angezeigt + persistiert werden: Cookies vor Consent / nach Ablehnen, ob „Ablehnen" respektiert wurde, Oberflächen-Signale, Screenshot. Bewusst schlank gehalten, damit der in `banner_result.browser_matrix` (JSONB) persistierte Block klein bleibt — 6 Engines × voller Cookie-Liste + Screenshot würde sonst schnell mehrere MB groß (BMW: ~780 Cookies je Phase). """ from __future__ import annotations from typing import Any # Cookie-Namen je Phase deckeln — die Matrix zeigt Zahlen + Beispiele, nicht # die volle Liste (die steckt im textbasierten Cookie-Modul). _NAME_CAP = 40 _TRACK_CAP = 20 def _vdict(v: Any) -> dict: """Violation (dataclass/obj/dict) → serialisierbares dict.""" if isinstance(v, dict): return v return getattr(v, "__dict__", None) or {"text": str(v)} def matrix_scan_dict(result: Any) -> dict: """`ConsentTestResult` → dict in der Form, die `multi_browser_scanner._extract_dimensions` liest (phases/banner_checks) plus ein kompakter `summary`-Block für Frontend + Persistenz. Defensiv via getattr — funktioniert auch, falls der Scanner mal ein bereits serialisiertes dict liefert (dann greifen die Defaults).""" before = list(getattr(result, "before_cookies", []) or []) after = list(getattr(result, "reject_cookies", []) or []) before_violations = list(getattr(result, "before_violations", []) or []) reject_violations = list(getattr(result, "reject_violations", []) or []) reject_new_tracking = list(getattr(result, "reject_new_tracking", []) or []) banner_text_violations = list( getattr(result, "banner_text_violations", []) or []) provider = getattr(result, "banner_provider", "") or "" summary = { "cookies_before_consent": len(before), "cookies_after_reject": len(after), "cookies_before_names": before[:_NAME_CAP], "cookies_after_reject_names": after[:_NAME_CAP], # „Ablehnen respektiert" = nach dem Klick auf „Ablehnen" keine Verstöße # UND kein neuer Tracker. Verbleibende essentielle Cookies (z.B. die # gespeicherte Consent-Entscheidung selbst) sind erlaubt → NICHT über # die reine Cookie-Zahl bewerten (sonst False Positive). "reject_respected": (len(reject_violations) == 0 and len(reject_new_tracking) == 0), "reject_new_tracking": reject_new_tracking[:_TRACK_CAP], "banner_detected": bool(getattr(result, "banner_detected", False)), "banner_provider": provider, "banner_screenshot_b64": getattr(result, "banner_screenshot_b64", "") or "", "surface": { "has_impressum_link": bool( getattr(result, "banner_has_impressum_link", False)), "has_dse_link": bool( getattr(result, "banner_has_dse_link", False)), "banner_text_issues": len(banner_text_violations), }, # Oberflächen-Befunde je Engine (die 20 Banner-Checks: Button-Prominenz, # Toggle-Vorauswahl, Einleitungstext/Links …) — Text + Severity + # Norm-Bezug. Aggregierte Maßnahmen folgen im Cross-Finding. "banner_findings": [ {"text": d.get("text", ""), "severity": d.get("severity", "MEDIUM"), "legal_ref": d.get("legal_ref", ""), "service": d.get("service", "")} for d in (_vdict(v) for v in banner_text_violations) ][:20], "violations": { "before_consent": len(before_violations), "after_reject": len(reject_violations), "banner_text": len(banner_text_violations), }, } return { "banner_detected": bool(getattr(result, "banner_detected", False)), "banner_provider": provider, # Minimal-Form für _extract_dimensions (nur cookies-Listen + violations): "phases": { "before_consent": {"cookies": before}, "after_reject": {"cookies": after}, }, "banner_checks": { "violations": [_vdict(v) for v in banner_text_violations], }, "summary": summary, }