""" P82 — GF-1-Pager (Geschaeftsfuehrer-Kurzfassung). Eine kompakte 5-7-Bullet-Zusammenfassung ganz oben in der Mail. GF liest sonst die 124k-Char-Komplettpruefung nicht. Ton sachlich, keine Panik (Memory: feedback_breakpilot_tonalitaet). Bildet ab: - Compliance-Score + Vergleichswert (wenn Vorlauf vorhanden) - Top-3 priorisierte Themen (HIGH oder kritisches MEDIUM) - Aufwand-Schaetzung (4-8 Wochen) + Wer-macht-was (DSB / IT / Marketing) - Realer Risiko-Hinweis (ohne 4%-Weltumsatz-Drohung) Wird VOR Critical-Findings und Exec-Summary gerendert. """ from __future__ import annotations import logging from typing import Any logger = logging.getLogger(__name__) _AREA_LABEL = { "banner": "Cookie-Banner", "cookie": "Cookie-Richtlinie", "dse": "Datenschutzerklaerung", "impressum": "Impressum", "agb": "AGB", "library_mismatch": "Cookie-Klassifikation", "vendor": "Vendor-Liste / VVT", "consent": "Einwilligung", "rights": "Betroffenenrechte", } def _normalize_finding(item: dict) -> dict: sev = str(item.get("severity") or item.get("level") or "").upper() if sev not in ("HIGH", "MEDIUM", "LOW"): sev = "MEDIUM" label = (item.get("label") or item.get("title") or item.get("check") or item.get("name") or "").strip() if not label: return {} area = (item.get("area") or item.get("doc_type") or item.get("category") or "").lower() return { "severity": sev, "label": label[:200], "area": _AREA_LABEL.get(area, area.replace("_", " ").title() or "Allgemein"), "owner": item.get("owner") or _guess_owner(label, area), } def _guess_owner(label: str, area: str) -> str: """Heuristik: wer ist der wahrscheinliche Ansprechpartner.""" lab = label.lower() if any(w in lab for w in ("banner", "cookie", "consent", "einwilligung", "tracking")): return "DSB + Marketing/CMP-Admin" if any(w in lab for w in ("vendor", "avv", "auftragsverarbeitung", "drittland", "schrems")): return "DSB + Einkauf/Legal" if any(w in lab for w in ("impressum", "agb", "widerruf", "kontakt")): return "Legal + Web-Team" if any(w in lab for w in ("dsfa", "dsr", "loeschfrist", "art. 15", "auskunft", "betroffenenrecht")): return "DSB" if any(w in lab for w in ("tom", "verschluesselung", "backup", "incident", "logging")): return "IT-Security + DSB" if area in ("banner", "cookie"): return "DSB + Marketing" return "DSB" def _collect_top_findings( banner_result: dict | None, scorecard: dict | None, library_mismatch_findings: list[dict] | None, limit: int = 5, ) -> list[dict]: out: list[dict] = [] # 1) Banner deep-check findings (HIGH zuerst) if banner_result: for ph in (banner_result.get("phases") or {}).values(): if not isinstance(ph, dict): continue for f in (ph.get("findings") or []): if not isinstance(f, dict): continue n = _normalize_finding({**f, "area": "banner"}) if n: out.append(n) # 2) Library-Mismatch HIGH (Marketing-Cookies als essential deklariert) for mm in (library_mismatch_findings or []): if isinstance(mm, dict) and mm.get("severity") == "HIGH": out.append({ "severity": "HIGH", "label": f'Cookie "{mm.get("cookie","?")}" als ' f'{mm.get("declared_category","?")} deklariert, ' f'tatsaechlicher Zweck typischerweise ' f'{mm.get("library_category","?")}', "area": _AREA_LABEL["library_mismatch"], "owner": "DSB + Marketing/CMP-Admin", }) # 3) Scorecard FAILs (MC-Audit) if scorecard: for entry in (scorecard.get("failed") or scorecard.get("items") or []): if not isinstance(entry, dict): continue n = _normalize_finding(entry) if n and n["severity"] == "HIGH": out.append(n) # Sort: HIGH first, then MEDIUM, stable order. Dedup by label. seen: set[str] = set() order = {"HIGH": 0, "MEDIUM": 1, "LOW": 2} out.sort(key=lambda f: order.get(f["severity"], 3)) dedup: list[dict] = [] for f in out: key = f["label"].lower()[:80] if key in seen: continue seen.add(key) dedup.append(f) if len(dedup) >= limit: break return dedup def _score_color(score: float | int | None) -> str: if score is None: return "#64748b" try: s = float(score) except (TypeError, ValueError): return "#64748b" if s >= 80: return "#16a34a" if s >= 60: return "#ca8a04" return "#dc2626" def _delta_html(curr: float | None, prev: float | None) -> str: if curr is None or prev is None: return "" try: d = float(curr) - float(prev) except (TypeError, ValueError): return "" if abs(d) < 0.5: return ( ' ' '(unveraendert ggue. letztem Lauf)' ) arrow = "↑" if d > 0 else "↓" color = "#16a34a" if d > 0 else "#dc2626" return ( f' ' f'{arrow} {abs(d):.1f} Punkte ggue. letztem Lauf' ) def build_gf_one_pager_html( site_name: str, scorecard: dict | None = None, previous_scorecard: dict | None = None, banner_result: dict | None = None, library_mismatch_findings: list[dict] | None = None, scan_context: dict | None = None, ) -> str: """5-7-Bullet-Zusammenfassung. Leere Top-Findings: nur Status-Bullet.""" score = None if scorecard: score = scorecard.get("compliance_score") or scorecard.get("score") prev_score = None if previous_scorecard: prev_score = (previous_scorecard.get("compliance_score") or previous_scorecard.get("score")) top = _collect_top_findings( banner_result=banner_result, scorecard=scorecard, library_mismatch_findings=library_mismatch_findings, limit=5, ) n_high = sum(1 for f in top if f["severity"] == "HIGH") n_med = sum(1 for f in top if f["severity"] == "MEDIUM") if score is not None: score_str = f'{float(score):.0f}/100' else: score_str = "—" score_color = _score_color(score) ctx_line = "" if scan_context: bits: list[str] = [] if scan_context.get("industry"): bits.append(scan_context["industry"]) if scan_context.get("business_model"): bits.append(scan_context["business_model"].upper()) if scan_context.get("employee_count"): bits.append(f'{scan_context["employee_count"]} MA') if bits: ctx_line = ( '