diff --git a/app/main.py b/app/main.py index 201d4c8..d5e84a3 100644 --- a/app/main.py +++ b/app/main.py @@ -285,13 +285,23 @@ async def index(request: Request, current_user: Optional[dict] = Depends(get_cur }) -@app.get("/antrag/{drucksache:path}", response_class=HTMLResponse) -async def antrag_detail(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)): - """v2-Antragsdetail-Route — Server-Side-Rendering mit echten DB-Daten.""" +async def _render_antrag_detail( + request: Request, + drucksache: str, + current_user: Optional[dict], + template_name: str, +) -> HTMLResponse: + """Gemeinsame Render-Logik fuer v2- und v3-Antrag-Detail-Routes. + + Der einzige Unterschied zwischen den Modi ist `template_name` — die + DB-Reads, _row_to_detail-Aufbereitung, Plenum-Votes-Anreicherung und + der Template-Context sind identisch. So bleibt die Datenbasis fuer + beide Modi automatisch in Sync. + """ try: drucksache = validate_drucksache(drucksache) except Exception: - return templates.TemplateResponse("v2/screens/antrag_detail.html", { + return templates.TemplateResponse(template_name, { "request": request, "v2_active_nav": "durchsuchen", "error": f"Ungültige Drucksachen-ID: {drucksache}", @@ -300,7 +310,7 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona row = await get_assessment(drucksache) if not row: - return templates.TemplateResponse("v2/screens/antrag_detail.html", { + return templates.TemplateResponse(template_name, { "request": request, "v2_active_nav": "durchsuchen", "error": f"Antrag {drucksache} wurde nicht gefunden.", @@ -308,13 +318,11 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona }, status_code=404) antrag = _row_to_detail(row) - # #106 Phase 1: namentliche Abstimmungsdaten ergänzen (optional, kann None sein) try: antrag["abstimmungsverhalten"] = await get_abstimmungsverhalten(drucksache) except Exception: logger.exception("Fehler beim Laden von Abstimmungsverhalten für %s", drucksache) antrag["abstimmungsverhalten"] = None - # #106 Phase 2: fraktions-aggregierte Plenum-Abstimmungen aus Plenarprotokollen try: from .database import get_plenum_votes as _gpv antrag["plenum_votes"] = await _gpv(antrag.get("bundesland") or "NRW", drucksache) @@ -322,12 +330,22 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona logger.exception("Fehler beim Laden plenum_vote_results für %s", drucksache) antrag["plenum_votes"] = [] from .models import MATRIX_LABELS - return templates.TemplateResponse("v2/screens/antrag_detail.html", { + return templates.TemplateResponse(template_name, { "request": request, "v2_active_nav": "durchsuchen", "antrag": antrag, "assessment_count": None, - "matrix_explanations": { + "matrix_explanations": _MATRIX_EXPLANATIONS, + "matrix_labels": MATRIX_LABELS, + **_v2_template_context(current_user), + }) + + +# Allgemeine Felderklärungen für die GWÖ-Matrix — alltagssprachlich, nicht +# antragsspezifisch (die antragsspezifische Begründung kommt aus dem +# LLM-Output via _row_to_detail/matrix_dict). Geteilt zwischen / (Index) +# und /antrag/ /v3/antrag/. +_MATRIX_EXPLANATIONS = { "A1": "Wenn Ihre Stadt Büromöbel kauft: Wurden die unter menschenwürdigen Bedingungen hergestellt? Oder in einer Fabrik, in der Arbeiter:innen ausgebeutet werden? Hier geht es darum, ob die öffentliche Hand beim Einkauf auf Menschenrechte achtet.", "A2": "Beauftragt die Stadt den Handwerksbetrieb aus dem Ort — oder den billigsten Konzern aus dem Ausland? Bleibt das Geld in der Region und schafft Arbeitsplätze vor Ort?", "A3": "Werden bei öffentlichen Aufträgen Klimastandards verlangt? Kommt das Schulessen von regionalen Bauernhöfen oder wird es quer durch Europa gekarrt?", @@ -353,10 +371,28 @@ async def antrag_detail(request: Request, drucksache: str, current_user: Optiona "E3": "Denkt Ihre Kommune beim Einkauf auch an den CO₂-Fußabdruck? An die Abholzung von Regenwäldern für billiges Papier? An den Wasserverbrauch in Dürregebieten?", "E4": "Unterstützt Ihre Stadt strukturschwache Regionen? Gibt es Partnerschaften mit Kommunen im globalen Süden?", "E5": "Setzt sich Ihre Kommune für mehr Demokratie ein — auch auf Landes- und Bundesebene? Werden internationale Abkommen unterstützt?", - }, - "matrix_labels": MATRIX_LABELS, - **_v2_template_context(current_user), - }) +} + + +@app.get("/antrag/{drucksache:path}", response_class=HTMLResponse) +async def antrag_detail(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)): + """v2-Antrag-Detail (Profi-Modus): volles GWÖ-Dashboard pro Drucksache.""" + return await _render_antrag_detail( + request, drucksache, current_user, "v2/screens/antrag_detail.html" + ) + + +@app.get("/v3/antrag/{drucksache:path}", response_class=HTMLResponse) +async def antrag_detail_v3(request: Request, drucksache: str, current_user: Optional[dict] = Depends(get_current_user)): + """v3-Antrag-Detail (Bürger:innen-Modus, Beta): vereinfachte Vorschau. + + Sandbox fuer Issues #184 (CD-Konformitaet) und #185 (Bürgerinnen- + Perspektive). Initial identisch zu v2; iterative Vereinfachungen + folgen pro PR. v2-Endpoint bleibt unangetastet. + """ + return await _render_antrag_detail( + request, drucksache, current_user, "v3/screens/antrag_detail.html" + ) def _v2_template_context(current_user=None) -> dict: diff --git a/app/static/v3/v3.css b/app/static/v3/v3.css new file mode 100644 index 0000000..f7c0d99 --- /dev/null +++ b/app/static/v3/v3.css @@ -0,0 +1,46 @@ +/* + * v3.css — Bürgerinnen-Modus-Erweiterung über v2.css + * + * v3 = Sandbox für Issues #184 (CD-Konformität) und #185 (Bürgerinnen- + * Perspektive). Lädt nach v2.css und überschreibt selektiv. v2-Endpoints + * bleiben unverändert. + * + * Konvention: + * - Klassen mit Präfix `v3-` sind v3-spezifisch. + * - v2-Klassen werden nur überschrieben, wenn das Citizen-Bedürfnis es + * verlangt (Wort-Etiketten statt Symbol, weniger Tiefe als Default). + * - Inline-Styles in v3-Templates sind verboten (Lint-Hook folgt in #184). + */ + +/* ── v3-Beta-Indikator in der Topbar ────────────────────────────────── */ +.v3-beta-badge { + font-family: var(--font-mono); + font-size: 9px; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 2px 7px; + border-radius: 9px; + background: var(--ecg-green); + color: #fff; + font-weight: 700; +} + +/* ── Modus-Toggle: zwischen Profi (/antrag/...) und Bürgerin (/v3/...) ── */ +.v3-modus-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + font-family: var(--font-mono); + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--ecg-dark); + opacity: 0.85; + border-bottom: 1px solid rgba(0, 157, 165, 0.35); + padding-bottom: 1px; +} + +.v3-modus-toggle:hover { + opacity: 1; + color: var(--ecg-blue); +} diff --git a/app/templates/v2/screens/antrag_detail.html b/app/templates/v2/screens/antrag_detail.html index 3a49f47..498cea3 100644 --- a/app/templates/v2/screens/antrag_detail.html +++ b/app/templates/v2/screens/antrag_detail.html @@ -649,6 +649,21 @@ document.addEventListener('keydown', function (e) { history.back(); } }); + +/* v3-Toggle in der Topbar: nur bei vorhandenem Antrag, Link auf + /v3/antrag/. Injektion via JS, damit v2/base.html unangetastet bleibt. */ +{% if antrag is defined and antrag and antrag.drucksache %} +(function () { + var bar = document.querySelector('.v2-topbar'); + if (!bar) return; + var link = document.createElement('a'); + link.href = '/v3/antrag/' + encodeURIComponent({{ antrag.drucksache | tojson }}); + link.textContent = '→ Bürger:innen-Modus · v3 Beta'; + link.title = 'Vereinfachte Vorschau für Erst-Leser:innen (v3 Beta).'; + link.style.marginLeft = 'auto'; + bar.appendChild(link); +})(); +{% endif %} {# Matrix-Erklärungen als JSON in den Browser übertragen #} diff --git a/app/templates/v3/base.html b/app/templates/v3/base.html new file mode 100644 index 0000000..b4a4256 --- /dev/null +++ b/app/templates/v3/base.html @@ -0,0 +1,17 @@ +{# ───────────────────────────────────────────────────────────────────── + v3/base.html — Bürgerinnen-Modus-Shell + + Erweitert v2/base.html und kann perspektivisch Sidebar/Topbar/Footer + ueberschreiben. Initial: ein zusaetzliches v3.css fuer Bürgerinnen- + spezifische Komponenten + ein Beta-Indikator in der Topbar. + + Der v3-Endpoint (Routes: /v3/antrag/...) ist die Sandbox fuer Issue + #184 (CD-Konformitaet) und #185 (Buergerinnen-Perspektive). v2 bleibt + als Profi-Modus unangetastet. +───────────────────────────────────────────────────────────────────── #} +{% extends "v2/base.html" %} + +{% block head_extra %} +{{ super() }} + +{% endblock %} diff --git a/app/templates/v3/screens/antrag_detail.html b/app/templates/v3/screens/antrag_detail.html new file mode 100644 index 0000000..8d6c0e9 --- /dev/null +++ b/app/templates/v3/screens/antrag_detail.html @@ -0,0 +1,47 @@ +{# ───────────────────────────────────────────────────────────────────── + v3/screens/antrag_detail.html — Bürgerinnen-Modus, Schritt 0 + + Initial extendet diese Datei das v2-Antrag-Detail unverändert und + fügt nur den v3-Beta-Indikator + Modus-Toggle in die Topbar ein. + Folgende Iterationen ersetzen Profi-Blöcke durch Bürgerinnen- + Varianten (siehe #185): + - Score-Hero: Wort-Etikett statt 0–10-Zahl + - Matrix 5×5 → 5 Werte (Berührungsgruppen kollabiert) + - Heuchelei/Opportunismus-Marker mit Klartext-Tooltip + - Verbesserungsvorschläge default kollabiert + - Glossar-Tooltips auf Schlüsselbegriffen +───────────────────────────────────────────────────────────────────── #} +{% extends "v2/screens/antrag_detail.html" %} + +{# v3-Indikator + Toggle: erscheint im topbar-Slot, der in v2/base.html + aktuell nicht als Block exponiert ist. Wir nutzen daher head_extra + als Eingangstor und injizieren via JS einen kleinen Topbar-Pill. + Das vermeidet ein Refactor von v2/base.html, der v2 beruehren wuerde. #} +{% block head_extra %} +{{ super() }} + +{% endblock %} + +{% block body_scripts %} +{{ super() }} + +{% endblock %}