gwoe-antragspruefer/app/og_card.py
Dotty Dotter 565849bd84 feat(#139,#129,#138,#141): v2-Frontend (ECOnGOOD-CD), Login-Modal, Auto-DL, OG-Cards
v2-Frontend (#139, ECOnGOOD CD Manual Juni 2024):
- app/static/v2/: tokens.css, fonts.css, v2.css, Nunito-Sans woff2, Phosphor-Icons (21 SVGs)
- app/templates/v2/: base.html + 11 Screens + 8 Component-Macros
- AppShell mit Sidebar (Lesen/Pruefen/Daten/Admin), v2-Detail mit allen Features
  (ScoreHero, MatrixMini, QuoteCard, Redline, Fraktions-Scores)
- v2 ist jetzt Default unter / — classic unter /classic
- Login-Modal in v2-Topbar mit Tabs Anmelden/Registrieren (#129)
- Phosphor-Icons in Sidebar + Topbar mit dynamischem Theme-Toggle
- Keyboard-Shortcuts (j/k/Enter/Esc/?/path), Landtag-Suche, Antrag-Historie,
  Sort-Dropdown, Matrix-Feld-Info-Modal, Bookmarks/Comments/Voting/Share/Re-Analyze

Backend-Erweiterungen:
- main.py: ~30 neue Routes (/v2/*, /antrag/{ds}, /api/auth/{login,refresh,logout},
  /api/me/merkliste/*, /api/admin/*, /v2/admin/*, OG-Cards, etc.)
- og_card.py + og_template: Open-Graph-Bilder via Playwright (#141)
- wahlprogramm_fetch.py + wahlprogramm-links.yaml: SHA-Gate Auto-DL (#138)
- auswertungen.py: BL-Filter + get_wahlperioden Helper (#137)
- auth.py: Direct-Access-Grant + Refresh-Token-Cookie

Classic-Updates:
- Header-DRY via _header.html, Auswertungen redirected, Batch-Inline raus

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 20:55:57 +02:00

122 lines
3.8 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Open-Graph-Bild-Rendering via Playwright (#141).
Rendert /v2/og-template?drucksache=X als PNG 1200×630.
Cache in data/og-cache/ mit Key SHA256(drucksache + updated_at).
Öffentliche API:
``render_og_card(drucksache, updated_at, base_url)``
→ PNG-Bytes oder None bei Fehler
``cache_key(drucksache, updated_at)``
→ Hex-String (SHA-256 Kurzform, 16 Zeichen)
``get_cached(drucksache, updated_at, cache_dir)``
→ Path der gecacheten Datei oder None
"""
from __future__ import annotations
import hashlib
import logging
from pathlib import Path
from typing import Optional
logger = logging.getLogger(__name__)
_DEFAULT_CACHE_DIR = Path(__file__).resolve().parent.parent / "data" / "og-cache"
def cache_key(drucksache: str, updated_at: str) -> str:
"""Berechnet den Cache-Schlüssel als 16-stelligen SHA-256-Präfix.
Args:
drucksache: Drucksachen-ID (z.B. "NRW-18/1234").
updated_at: ISO-Zeitstempel des letzten Updates aus der Datenbank.
Returns:
16 Hex-Zeichen (64-Bit-Präfix des SHA-256).
"""
raw = f"{drucksache}|{updated_at}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
def _cache_path(drucksache: str, updated_at: str, cache_dir: Path) -> Path:
key = cache_key(drucksache, updated_at)
safe_name = drucksache.replace("/", "_").replace(" ", "_")
return cache_dir / f"{safe_name}_{key}.png"
def get_cached(
drucksache: str,
updated_at: str,
cache_dir: Optional[Path] = None,
) -> Optional[Path]:
"""Gibt den Pfad der gecacheten PNG-Datei zurück, wenn sie existiert.
Args:
drucksache: Drucksachen-ID.
updated_at: ISO-Zeitstempel — ändert sich dieser, ist der Cache ungültig.
cache_dir: Verzeichnis für den Cache. Standard: data/og-cache/.
Returns:
Path-Objekt wenn Treffer, sonst None.
"""
cache_dir = cache_dir or _DEFAULT_CACHE_DIR
path = _cache_path(drucksache, updated_at, cache_dir)
return path if path.exists() else None
def render_og_card(
drucksache: str,
updated_at: str,
base_url: str = "http://127.0.0.1:8000",
cache_dir: Optional[Path] = None,
) -> Optional[bytes]:
"""Rendert die OG-Karte als PNG via Playwright und legt sie im Cache ab.
Bei Cache-Hit wird das Rendering übersprungen.
Args:
drucksache: Drucksachen-ID (URL-kodierbar).
updated_at: ISO-Zeitstempel für den Cache-Key.
base_url: Interne Basis-URL der App (Playwright greift darauf zu).
cache_dir: Cache-Verzeichnis. Standard: data/og-cache/.
Returns:
PNG-Bytes bei Erfolg, None bei Fehler.
"""
cache_dir = cache_dir or _DEFAULT_CACHE_DIR
cache_dir.mkdir(parents=True, exist_ok=True)
cached = get_cached(drucksache, updated_at, cache_dir)
if cached:
logger.debug("OG-Cache-Hit für %s", drucksache)
return cached.read_bytes()
dest = _cache_path(drucksache, updated_at, cache_dir)
try:
from playwright.sync_api import sync_playwright
import urllib.parse
encoded = urllib.parse.quote(drucksache, safe="")
url = f"{base_url}/v2/og-template?drucksache={encoded}"
with sync_playwright() as pw:
browser = pw.chromium.launch(args=["--no-sandbox"])
page = browser.new_page(viewport={"width": 1200, "height": 630})
page.goto(url, wait_until="networkidle", timeout=15000)
png_bytes = page.screenshot(
clip={"x": 0, "y": 0, "width": 1200, "height": 630},
type="png",
)
browser.close()
dest.write_bytes(png_bytes)
logger.info("OG-Karte gerendert: %s%s", drucksache, dest.name)
return png_bytes
except Exception:
logger.exception("Playwright-Render fehlgeschlagen für %s", drucksache)
return None