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
{% extends "v2/base.html" %}
{% from "v2/components/score_hero.html" import score_hero %}
{% from "v2/components/matrix_mini.html" import matrix_mini %}
{% from "v2/components/quote_card.html" import quote_card %}
{% from "v2/components/kasten.html" import kasten %}
{% from "v2/components/redline.html" import redline %}
{% block title %}
{% if antrag is defined and antrag %}{{ antrag.title }} — {% endif %}GWÖ-Antragsprüfer
{% endblock %}
{% block head_extra %}
{% if antrag is defined and antrag %}
{# ── Open-Graph / Twitter-Card-Meta (#141) ────────────────────────── #}
{% set _og_img = "/api/og/" ~ (antrag.drucksache | urlencode) ~ ".png" %}
< meta property = "og:type" content = "article" >
< meta property = "og:title" content = "{{ antrag.title }} — GWÖ-Antragsprüfer" >
< meta property = "og:description" content = "GWÖ-Score {{ '%.1f'|format(antrag.score|float) }}/10 — {{ antrag.zusammenfassung | truncate(160, True) }}" >
< meta property = "og:image" content = "{{ _og_img }}" >
< meta property = "og:image:width" content = "1200" >
< meta property = "og:image:height" content = "630" >
{% if antrag.updated_at_raw %}
< meta property = "og:updated_time" content = "{{ antrag.updated_at_raw }}" >
{% endif %}
< meta name = "twitter:card" content = "summary_large_image" >
< meta name = "twitter:title" content = "{{ antrag.title }} — GWÖ-Antragsprüfer" >
< meta name = "twitter:description" content = "GWÖ-Score {{ '%.1f'|format(antrag.score|float) }}/10 — {{ antrag.zusammenfassung | truncate(160, True) }}" >
< meta name = "twitter:image" content = "{{ _og_img }}" >
{% endif %}
{% endblock %}
{% set v2_active_nav = "durchsuchen" %}
{% block main %}
{# ── Fehlerfall ──────────────────────────────────────────────────── #}
{% if error is defined and error %}
< div class = "v2-kasten" style = "border-color:var(--redline-contra);margin-top:32px;" >
< h3 style = "color:var(--redline-contra);" > Antrag nicht gefunden< / h3 >
< p > {{ error }}< / p >
< p > < a href = "/" > ← Zurück zur Übersicht< / a > < / p >
< / div >
{% elif antrag is not defined or not antrag %}
{# ── Demo-Daten wenn kein echtes Antrag-Objekt übergeben ────────── #}
{% set antrag = {
"drucksache": "18/4412",
"bundesland": "BW",
"parlament": "Landtag",
"typ": "Antrag",
"datum": "12.04.2026",
"analysiert": "14.04.2026",
"modell": "qwen-plus",
"parteien": ["GRÜNE", "SPD"],
"zitate_count": 3,
"title": "Kommunale Wärmeplanung bis 2028 verpflichtend machen",
"score": 9.1,
"verdict_title": "Vorbildlich",
"verdict_body": "Starker Beitrag zur ökologischen Nachhaltigkeit und Transparenz auf kommunaler Ebene.",
"zusammenfassung": "Der Antrag verpflichtet Kommunen ab 10 000 Einwohner:innen zur Erstellung einer kommunalen Wärmeplanung bis Ende 2028.",
"staerkster_wert": {
"titel": "Ökologische Nachhaltigkeit",
"text": "Verpflichtende Wärmeplanung führt zu messbaren Klimazielen. E3 = ++, D3 = ++."
},
"schwaechster_wert": {
"titel": "Soziale Gerechtigkeit",
"text": "Kostenverteilung auf Mieter:innen versus Eigentümer:innen ist im Antrag nicht geregelt."
},
"redline": {
"segments": [
{"type": "ctx", "text": "§ 3 Abs. 2 "},
{"type": "del", "text": "auf Antrag"},
{"type": "ins", "text": "verpflichtend"},
{"type": "ctx", "text": " eine sozialverträgliche Umlage"}
]
},
"matrix": {
"A1": {"rating": 0, "symbol": "○"}, "A2": {"rating": 1, "symbol": "+"},
"A3": {"rating": 2, "symbol": "++"}, "A4": {"rating": 0, "symbol": "○"},
"A5": {"rating": 1, "symbol": "+"},
"B1": {"rating": 0, "symbol": "○"}, "B2": {"rating": 1, "symbol": "+"},
"B3": {"rating": 2, "symbol": "++"}, "B4": {"rating": -1, "symbol": "− "},
"B5": {"rating": 1, "symbol": "+"},
"C1": {"rating": 0, "symbol": "○"}, "C2": {"rating": 1, "symbol": "+"},
"C3": {"rating": 1, "symbol": "+"}, "C4": {"rating": 0, "symbol": "○"},
"C5": {"rating": 2, "symbol": "++"},
"D1": {"rating": 1, "symbol": "+"}, "D2": {"rating": 1, "symbol": "+"},
"D3": {"rating": 2, "symbol": "++"}, "D4": {"rating": 1, "symbol": "+"},
"D5": {"rating": 2, "symbol": "++"},
"E1": {"rating": 1, "symbol": "+"}, "E2": {"rating": 2, "symbol": "++"},
"E3": {"rating": 2, "symbol": "++"}, "E4": {"rating": 1, "symbol": "+"},
"E5": {"rating": 1, "symbol": "+"}
},
"zitate": [
{
"text": "Wir verpflichten alle Kommunen zu einer verbindlichen kommunalen Wärmeplanung bis 2028.",
"source": "Wahlprogramm GRÜNE 2022 · S. 84",
"partei": "GRÜNE",
"verified": True,
"contra": False,
"pdf_href": "/api/wahlprogramm-cite?pid=gruene-nrw-2022& seite=84& q=Wärmeplanung"
}
],
"verbesserungen": [],
"staerken": [],
"schwaechen": []
} %}
{# Fallthrough: Demo-Daten rendern wie echte Daten #}
{% set _render = True %}
{% else %}
{% set _render = True %}
{% endif %}
{# ── Eigentlicher Detail-Inhalt ──────────────────────────────────── #}
{% if _render is defined and _render and antrag is defined and antrag %}
{# ── Zurück-Link ─────────────────────────────────────────────────── #}
< p style = "font-family:var(--font-mono);font-size:11px;margin-bottom:20px;" >
< a href = "/" > ← Zurück zur Übersicht< / a >
< / p >
{# ── Split-Layout ────────────────────────────────────────────────── #}
< div class = "v2-detail" >
{# ── Linke Spalte: Redaktionelle Analyse ── #}
< div class = "left" >
< div class = "v2-antrag-id" >
{{ antrag.bundesland | default("") }}
{% if antrag.drucksache %} · Drs. {{ antrag.drucksache }}{% endif %}
{% if antrag.typ %} · {{ antrag.typ }}{% endif %}
{% if antrag.datum %} · eingebracht {{ antrag.datum }}{% endif %}
< / div >
< h1 class = "v2-big-title" > {{ antrag.title | default("Antrag") }}< / h1 >
{% if antrag.parteien or antrag.analysiert %}
< div class = "v2-byline" >
{% if antrag.parteien %}Eingebracht von {{ antrag.parteien | join(", ") }}{% endif %}
{% if antrag.analysiert %} — Analyse {{ antrag.analysiert }}{% endif %}
{% if antrag.modell %}, {{ antrag.modell }}{% endif %}
{% if antrag.zitate_count %} · {{ antrag.zitate_count }} Zitat{{ "e" if antrag.zitate_count != 1 else "" }} verifiziert{% endif %}
< / div >
{% endif %}
{% if antrag.zusammenfassung %}
< h3 class = "v2-h3" > Zusammenfassung< / h3 >
< p style = "font-size:14.5px;line-height:1.55" > {{ antrag.zusammenfassung }}< / p >
{% endif %}
{# Stärkster Wert #}
{% if antrag.staerkster_wert and antrag.staerkster_wert.text %}
< div class = "v2-kasten outline-green" style = "margin-top:20px;" >
< h4 > Stärkster Wert{% if antrag.staerkster_wert.titel %} — {{ antrag.staerkster_wert.titel }}{% endif %}< / h4 >
< p > {{ antrag.staerkster_wert.text }}< / p >
< / div >
{% elif antrag.staerken %}
< div class = "v2-kasten outline-green" style = "margin-top:20px;" >
< h4 > Stärken< / h4 >
< ul style = "margin:0;padding-left:1.2em;" >
{% for s in antrag.staerken %}
< li style = "font-size:13.5px;line-height:1.5;" > {{ s }}< / li >
{% endfor %}
< / ul >
< / div >
{% endif %}
{# Schwächster Wert #}
{% if antrag.schwaechster_wert and antrag.schwaechster_wert.text %}
< div class = "v2-kasten outline-blue" >
< h4 > Schwächster Wert{% if antrag.schwaechster_wert.titel %} — {{ antrag.schwaechster_wert.titel }}{% endif %}< / h4 >
< p > {{ antrag.schwaechster_wert.text }}< / p >
< / div >
{% elif antrag.schwaechen %}
< div class = "v2-kasten outline-blue" >
< h4 > Schwächen< / h4 >
< ul style = "margin:0;padding-left:1.2em;" >
{% for s in antrag.schwaechen %}
< li style = "font-size:13.5px;line-height:1.5;" > {{ s }}< / li >
{% endfor %}
< / ul >
< / div >
{% endif %}
{# Redline-Vorschläge: alle verbesserungen rendern wenn vorhanden #}
{% if antrag.verbesserungen %}
< h3 class = "v2-h3" style = "margin-top:24px;" > Redline-Vorschläge< / h3 >
{% for v in antrag.verbesserungen %}
< div style = "margin-bottom:16px;" >
{% if antrag.verbesserungen | length > 1 %}
< div style = "font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);opacity:0.65;margin-bottom:4px;" >
Vorschlag {{ loop.index }} von {{ antrag.verbesserungen | length }}
< / div >
{% endif %}
{% from "v2/components/redline.html" import redline %}
{% if v.segments %}
{{ redline(original=v.original | default(""), segments=v.segments) }}
{% else %}
{{ redline(original=v.original | default(""), vorschlag=v.vorschlag | default("")) }}
{% endif %}
{% if v.begruendung %}
< p style = "font-size:12px;color:var(--ecg-dark);opacity:0.75;margin:4px 0 0;font-family:var(--font-mono);" >
{{ v.begruendung }}
< / p >
{% endif %}
< / div >
{% endfor %}
{% elif antrag.redline and antrag.redline.segments %}
< h3 class = "v2-h3" style = "margin-top:24px;" > Redline-Vorschlag< / h3 >
{% from "v2/components/redline.html" import redline %}
{{ redline(segments=antrag.redline.segments) }}
{% endif %}
< / div > {# .left #}
{# ── Rechte Spalte: Bewertungs-Panel ── #}
< div class = "right" >
< div class = "v2-antrag-id" > Bewertung< / div >
{{ score_hero(antrag.score | default(0), antrag.verdict_title | default(""), antrag.verdict_body | default("")) }}
{# ── Merkliste-Stern (#140) ── #}
< div style = "margin-top:12px;margin-bottom:4px;" >
< button id = "v2-merkliste-btn"
onclick="v2DetailMerklisteToggle()"
style="display:inline-flex;align-items:center;gap:6px;padding:5px 12px;
border:1px solid var(--hairline);border-radius:4px;background:none;
cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
< span id = "v2-merkliste-star" > ☆< / span >
< span id = "v2-merkliste-label" > Merken< / span >
< / button >
< / div >
{# ── Namentliche Abstimmung (#106 Phase 1) ── #}
{% if antrag.abstimmungsverhalten %}
{% set aw = antrag.abstimmungsverhalten %}
< h3 class = "v2-h3" style = "margin-top:24px;" > Namentliche Abstimmung< / h3 >
< div style = "font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);opacity:0.75;margin-bottom:10px;" >
{% if aw.datum %}{{ aw.datum }} · {% endif %}
{% if aw.accepted %}Angenommen{% else %}Abgelehnt{% endif %}
< / div >
{% for f in aw.fraktionen %}
{% set total = (f.yes + f.no + f.abstain + f.no_show) | int %}
{% if total > 0 %}
< div style = "margin-bottom:8px;" >
< div style = "font-family:var(--font-mono);font-size:11px;margin-bottom:3px;display:flex;justify-content:space-between;" >
< span > {{ f.partei }}< / span >
< span style = "opacity:0.65;" > {{ f.yes }}✓ {{ f.abstain }}○ {{ f.no }}✗< / span >
< / div >
< div style = "display:flex;height:8px;border-radius:4px;overflow:hidden;background:var(--ecg-light,#f0f0f0);" >
{% if f.yes > 0 %}
< div style = "flex:{{ f.yes }};background:#2da44e;" title = "{{ f.yes }} Ja" > < / div >
{% endif %}
{% if f.abstain > 0 %}
< div style = "flex:{{ f.abstain }};background:#adb5bd;" title = "{{ f.abstain }} Enthaltung" > < / div >
{% endif %}
{% if f.no > 0 %}
< div style = "flex:{{ f.no }};background:#cf222e;" title = "{{ f.no }} Nein" > < / div >
{% endif %}
< / div >
< / div >
{% endif %}
{% endfor %}
{% endif %}{# abstimmungsverhalten #}
2026-04-28 08:04:32 +02:00
{# ── Fraktions-aggregierte Plenum-Abstimmung aus Plenarprotokoll (#106) ── #}
{% if antrag.plenum_votes %}
< h3 class = "v2-h3" style = "margin-top:24px;" > Abstimmungsergebnis< / h3 >
{% set ergebnis_color = {
"angenommen": "#2da44e",
"abgelehnt": "#cf222e",
"überwiesen": "#0969da",
"zurückgezogen": "#8250df",
"bestätigt": "#2da44e",
"sammel": "#0969da",
} %}
{% for v in antrag.plenum_votes %}
< div style = "border:1px solid var(--hairline);border-radius:6px;padding:12px 14px;margin-bottom:10px;background:var(--paper);" >
< div style = "display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;" >
< span style = "font-family:var(--font-display);font-size:14px;font-weight:700;color:{{ ergebnis_color.get(v.ergebnis, '#6e7781') }};" >
{{ v.ergebnis | capitalize }}{% if v.einstimmig %} · einstimmig{% endif %}
< / span >
< span style = "font-family:var(--font-mono);font-size:10px;opacity:0.6;" title = "{% if v.quelle_url %}{{ v.quelle_url }}{% endif %}" >
{{ v.quelle_protokoll }}{% if v.quelle_url %} ↗{% endif %}
< / span >
< / div >
{% if v.fraktionen_ja or v.fraktionen_nein or v.fraktionen_enthaltung %}
< div style = "display:flex;flex-wrap:wrap;gap:12px;font-family:var(--font-mono);font-size:11px;" >
{% if v.fraktionen_ja %}
< div > < span style = "color:#2da44e;font-weight:700;" > Ja:< / span >
{% for f in v.fraktionen_ja %}< span style = "display:inline-block;padding:1px 6px;background:color-mix(in srgb,#2da44e 15%,transparent);color:#1a7f37;border-radius:3px;margin-right:3px;" > {{ f }}< / span > {% endfor %}
< / div >
{% endif %}
{% if v.fraktionen_nein %}
< div > < span style = "color:#cf222e;font-weight:700;" > Nein:< / span >
{% for f in v.fraktionen_nein %}< span style = "display:inline-block;padding:1px 6px;background:color-mix(in srgb,#cf222e 15%,transparent);color:#a40e26;border-radius:3px;margin-right:3px;" > {{ f }}< / span > {% endfor %}
< / div >
{% endif %}
{% if v.fraktionen_enthaltung %}
2026-04-28 08:46:27 +02:00
< div > < span style = "color:#6e7781;font-weight:700;cursor:help;border-bottom:1px dotted currentColor;" title = "Enth. — Enthaltung: weder Zustimmung noch Ablehnung." > Enth.:< / span >
2026-04-28 08:04:32 +02:00
{% for f in v.fraktionen_enthaltung %}< span style = "display:inline-block;padding:1px 6px;background:color-mix(in srgb,#6e7781 15%,transparent);color:#57606a;border-radius:3px;margin-right:3px;" > {{ f }}< / span > {% endfor %}
< / div >
{% endif %}
< / div >
{% endif %}
< / div >
{% endfor %}
< div style = "font-family:var(--font-mono);font-size:10px;opacity:0.5;margin-top:-4px;margin-bottom:8px;" >
Quelle: Plenarprotokoll · automatisch extrahiert
< / div >
{% endif %}{# plenum_votes #}
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
{% if antrag.matrix %}
< h3 class = "v2-h3" > Matrix 2.0 · 25 Felder< / h3 >
{{ matrix_mini(antrag.matrix) }}
{% endif %}
{# Fraktions-Score-Tabelle (Fix 2+3): auch Fraktionen ohne Zitate sichtbar #}
{% if antrag.fraktions_scores %}
< h3 class = "v2-h3" style = "margin-top:24px;" > Programm-Treue pro Fraktion< / h3 >
< div class = "v2-fraktions-scores" >
{% for fs in antrag.fraktions_scores %}
< div class = "v2-fraktion-row" >
< div class = "v2-fraktion-label" >
{{ fs.fraktion }}
2026-04-28 08:46:27 +02:00
{% if fs.ist_antragsteller %}< span class = "v2-badge-antragsteller" title = "A — Antragstellende Fraktion: hat den Antrag eingereicht." > A< / span > {% endif %}
{% if fs.ist_regierung %}< span class = "v2-badge-regierung" title = "R — Regierungsfraktion: trägt die aktuelle Mehrheit im Landtag." > R< / span > {% endif %}
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
< / div >
< div class = "v2-fraktion-scores" >
{% set wp_score = fs.wahlprogramm.score | float %}
{% set pp_score = fs.parteiprogramm.score | float %}
< span class = "v2-score-chip {% if wp_score >= 7 %}chip-green{% elif wp_score >= 4 %}chip-mid{% else %}chip-red{% endif %}"
2026-04-28 08:46:27 +02:00
title="WP — Wahlprogramm-Treue (0– 10): wie gut passt der Antrag zum aktuellen Wahlprogramm dieser Fraktion? {{ fs.wahlprogramm.begruendung }}">
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
WP {{ "%.0f"|format(wp_score) }}/10
< / span >
< span class = "v2-score-chip {% if pp_score >= 7 %}chip-green{% elif pp_score >= 4 %}chip-mid{% else %}chip-red{% endif %}"
2026-04-28 08:46:27 +02:00
title="PP — Parteiprogramm-Treue (0– 10): wie gut passt der Antrag zum Grundsatzprogramm dieser Partei? {{ fs.parteiprogramm.begruendung }}">
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
PP {{ "%.0f"|format(pp_score) }}/10
< / span >
< / div >
< / div >
{% endfor %}
< / div >
{% endif %}
{# Zitate nach Partei gruppiert; Fraktion ohne Zitate erhält Hinweis via fraktions_scores-Block oben #}
{% if antrag.zitate %}
{% set current_partei = namespace(value="") %}
{% for z in antrag.zitate %}
{% if z.partei != current_partei.value %}
{% set current_partei.value = z.partei %}
< h3 class = "v2-h3" style = "margin-top:24px;" > Belege — {{ z.partei }}< / h3 >
{% endif %}
{{ quote_card(z.text, z.source, z.verified | default(True), z.contra | default(False), z.pdf_href | default("")) }}
{% endfor %}
{% endif %}
{# Aktions-Links #}
< div style = "margin-top:24px;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);opacity:0.85;display:flex;gap:16px;flex-wrap:wrap;" >
< a href = "/api/assessment/pdf?drucksache={{ antrag.drucksache | urlencode }}"
style="color:var(--ecg-blue);border-bottom:1px solid rgba(0,157,165,0.35);">
PDF-Bericht
< / a >
< a href = "/api/assessment?drucksache={{ antrag.drucksache | urlencode }}"
style="color:var(--ecg-blue);border-bottom:1px solid rgba(0,157,165,0.35);">
JSON-Export
< / a >
< a href = "/antrag/{{ antrag.drucksache }}"
style="color:var(--ecg-blue);border-bottom:1px solid rgba(0,157,165,0.35);">
Permalink
< / a >
< / div >
{# ── Voting-Block ─────────────────────────────────────────────── #}
< div style = "margin-top:24px;" >
< div style = "font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;" > Bewertung treffend?< / div >
< div id = "v2-vote-overall" style = "display:flex;gap:8px;align-items:center;" >
< button id = "v2-vote-up"
onclick="v2DetailCastVote('{{ antrag.drucksache | e }}','up')"
style="display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:12px;color:var(--ecg-dark);">
👍 < span id = "v2-vote-up-count" > 0< / span >
< / button >
< button id = "v2-vote-down"
onclick="v2DetailCastVote('{{ antrag.drucksache | e }}','down')"
style="display:inline-flex;align-items:center;gap:5px;padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:12px;color:var(--ecg-dark);">
👎 < span id = "v2-vote-down-count" > 0< / span >
< / button >
< / div >
< / div >
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
{# ── Share-Block (analog v1) ───────────────────────────────────── #}
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
< div style = "margin-top:20px;" >
< div style = "font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;" > Teilen< / div >
< div style = "display:flex;gap:8px;flex-wrap:wrap;" >
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
< button onclick = "v2DetailShareCopy()"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
📋 Kopieren
< / button >
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
< button onclick = "v2DetailShare('threads')"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
Threads
< / button >
< button onclick = "v2DetailShare('twitter')"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
𝕏
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
< / button >
< button onclick = "v2DetailShareMastodon()"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
Mastodon
< / button >
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
< button onclick = "v2DetailShare('linkedin')"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
LinkedIn
< / button >
< button onclick = "v2DetailShareEmail()"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
📧 E-Mail
< / button >
< button onclick = "v2DetailShareImage()"
style="padding:5px 10px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">
🖼 Bild
< / button >
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
< / div >
< / div >
{# ── Re-Analyze-Block ─────────────────────────────────────────── #}
< div style = "margin-top:20px;" >
< button id = "v2-reanalyze-btn"
onclick="v2DetailReAnalyze(this)"
style="padding:5px 12px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);opacity:0.8;">
Neu analysieren
< / button >
< / div >
{# ── Bewertungs-Historie ───────────────────────────────────────── #}
< div style = "margin-top:24px;" >
< div style = "font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;" > Bewertungs-Historie< / div >
< div id = "v2-history-list" >
< span style = "font-family:var(--font-mono);font-size:12px;color:var(--ecg-dark);opacity:0.45;" > Lade…< / span >
< / div >
< / div >
< / div > {# .right #}
< / div > {# .v2-detail #}
{# ── Kommentare ───────────────────────────────────────────────────────── #}
< div style = "margin-top:40px;border-top:1px solid var(--hairline);padding-top:28px;" >
< h3 class = "v2-h3" style = "margin-bottom:16px;" > Kommentare< / h3 >
< div id = "v2-comments-list" style = "margin-bottom:20px;" >
< span style = "font-family:var(--font-mono);font-size:12px;color:var(--ecg-dark);opacity:0.5;" > Lade…< / span >
< / div >
{# Kommentar-Formular — wird per JS eingeblendet wenn angemeldet #}
< div id = "v2-comment-form" style = "display:none;" >
< div style = "font-family:var(--font-mono);font-size:10px;text-transform:uppercase;letter-spacing:0.07em;color:var(--ecg-dark);opacity:0.6;margin-bottom:8px;" > Kommentar hinzufügen< / div >
< textarea id = "v2-comment-input"
rows="3"
placeholder="Kommentar…"
style="width:100%;box-sizing:border-box;padding:8px 10px;border:1px solid var(--hairline);border-radius:4px;font-family:var(--font-sans);font-size:13px;background:var(--surface);color:var(--ecg-dark);resize:vertical;margin-bottom:8px;outline:none;">< / textarea >
< div style = "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" >
< select id = "v2-comment-visibility"
style="padding:5px 8px;border:1px solid var(--hairline);border-radius:4px;font-family:var(--font-mono);font-size:11px;background:var(--surface);color:var(--ecg-dark);">
< option value = "all" > Öffentlich< / option >
< option value = "authenticated" > Nur Angemeldete< / option >
< option value = "private" > Nur ich< / option >
< / select >
< button onclick = "v2DetailAddComment('{{ antrag.drucksache | e }}')"
style="padding:5px 14px;border:none;border-radius:4px;background:var(--ecg-blue);color:#fff;cursor:pointer;font-family:var(--font-mono);font-size:11px;font-weight:700;">
Absenden
< / button >
< / div >
< / div >
< div id = "v2-comment-login-hint" style = "display:none;" >
< button onclick = "v2AuthModalOpen()"
style="padding:5px 12px;border:1px solid var(--hairline);border-radius:4px;background:none;cursor:pointer;font-family:var(--font-mono);font-size:11px;color:var(--ecg-blue);">
Anmelden um zu kommentieren
< / button >
< / div >
< / div >
{# ── Matrix-Feld-Info-Modal ───────────────────────────────────────────── #}
< div id = "v2-matrix-field-modal"
role="dialog" aria-modal="true" aria-label="Matrix-Feld Erklärung"
style="display:none;position:fixed;inset:0;background:rgba(0,0,0,0.45);
z-index:9100;align-items:center;justify-content:center;"
onclick="if(event.target===this)this.style.display='none'">
< div style = "background:var(--ecg-card-bg,#fff);border:1px solid var ( --ecg-border , # ddd ) ;
border-radius:8px;padding:28px 32px;min-width:280px;max-width:480px;
font-family:var(--font-sans);font-size:14px;color:var(--ecg-dark);
line-height:1.55;box-shadow:0 8px 32px rgba(0,0,0,0.18);">
< div style = "display:flex;justify-content:space-between;align-items:baseline;margin-bottom:12px;" >
< strong id = "v2-matrix-field-title"
style="font-family:var(--font-display,inherit);font-size:16px;font-weight:900;
color:var(--ecg-teal,#009da5);letter-spacing:0.03em;">< / strong >
< button onclick = "document.getElementById('v2-matrix-field-modal').style.display='none'"
style="background:none;border:none;font-size:18px;cursor:pointer;color:var(--ecg-dark);
opacity:0.55;padding:0;line-height:1;"
aria-label="Schließen">× < / button >
< / div >
< p id = "v2-matrix-field-text" style = "margin:0;" > < / p >
< / div >
< / div >
{% endif %}{# _render #}
{% endblock %}
{% block body_scripts %}
< script >
/* Escape auf der Detailseite → history.back() (außer wenn Matrix-Modal offen) */
document.addEventListener('keydown', function (e) {
if (e.key === 'Escape' & & !e.target.matches('input, textarea, select')) {
var modal = document.getElementById('v2-matrix-field-modal');
if (modal & & modal.style.display === 'flex') {
modal.style.display = 'none';
return;
}
e.preventDefault();
history.back();
}
});
< / script >
{# Matrix-Erklärungen als JSON in den Browser übertragen #}
{% if matrix_explanations is defined %}
< script >
window._v2MatrixExplanations = {{ matrix_explanations | tojson }};
window.v2ShowMatrixFieldInfo = function(field) {
var explains = window._v2MatrixExplanations || {};
var text = explains[field] || '';
var titleEl = document.getElementById('v2-matrix-field-title');
var textEl = document.getElementById('v2-matrix-field-text');
var modal = document.getElementById('v2-matrix-field-modal');
if (!modal) return;
if (titleEl) titleEl.textContent = 'Feld ' + field;
if (textEl) textEl.textContent = text || '(Keine Erklärung vorhanden)';
modal.style.display = 'flex';
};
< / script >
{% endif %}
{% if antrag is defined and antrag and antrag.drucksache %}
< script >
(function () {
var DRS = {{ antrag.drucksache | tojson }};
var BL = {{ antrag.bundesland | tojson }};
var SHARE_THR = {{ (antrag.share_threads or '') | tojson }};
var SHARE_TWI = {{ (antrag.share_twitter or '') | tojson }};
var SHARE_MAS = {{ (antrag.share_mastodon or '') | tojson }};
var TITLE = {{ antrag.title | tojson }};
var SCORE = {{ antrag.score | tojson }};
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
window.ANTRAG_TOPICS = {{ (antrag.themen or []) | tojson }};
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
var PERMALINK = 'https://gwoe.toppyr.de/antrag/' + encodeURIComponent(DRS);
var currentUser = null;
/* ── Auth-State laden ─────────────────────────────────────────── */
async function initAuth() {
try {
var resp = await fetch('/api/auth/me');
var data = await resp.json();
currentUser = (data & & data.authenticated) ? data : null;
} catch (_) { currentUser = null; }
var form = document.getElementById('v2-comment-form');
var loginHint = document.getElementById('v2-comment-login-hint');
if (currentUser) {
if (form) form.style.display = 'block';
if (loginHint) loginHint.style.display = 'none';
} else {
if (form) form.style.display = 'none';
if (loginHint) loginHint.style.display = 'block';
}
loadComments();
loadVotes();
}
/* ── Kommentare ───────────────────────────────────────────────── */
async function loadComments() {
var container = document.getElementById('v2-comments-list');
if (!container) return;
try {
var comments = await fetch('/api/comments?drucksache=' + encodeURIComponent(DRS)).then(function(r){ return r.json(); });
var visible = comments.filter(function(c) {
if (c.visibility === 'all') return true;
if (!currentUser) return false;
if (c.visibility === 'authenticated') return true;
if (c.visibility === 'private') return c.user_id === currentUser.sub;
if (c.visibility & & c.visibility.startsWith('group:')) return true;
return false;
});
if (visible.length === 0) {
container.innerHTML = '< span style = "font-family:var(--font-mono);font-size:12px;color:var(--ecg-dark);opacity:0.45;" > Noch keine Kommentare.< / span > ';
return;
}
function visBadge(v) {
var lbl = v === 'private' ? '👤' : v === 'authenticated' ? '🔒' : '🌐';
return '< span style = "font-family:var(--font-mono);font-size:10px;background:var(--surface);border:1px solid var(--hairline);padding:1px 4px;border-radius:2px;" > ' + lbl + '< / span > ';
}
container.innerHTML = visible.map(function(c) {
var dateStr = new Date(c.created_at).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'});
var delBtn = (currentUser & & currentUser.sub === c.user_id)
? '< button onclick = "v2DetailDeleteComment(' + c.id + ')" style = "margin-left:auto;background:none;border:none;color:#c33;cursor:pointer;font-family:var(--font-mono);font-size:11px;padding:0;" title = "Löschen" > ✕< / button > '
: '';
return '< div style = "padding:10px 0;border-bottom:1px solid var(--hairline);" > '
+ '< div style = "font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);opacity:0.75;margin-bottom:5px;display:flex;align-items:center;gap:6px;" > '
+ '< strong > ' + (c.user_name || 'Anonym') + '< / strong > '
+ visBadge(c.visibility)
+ '< span style = "opacity:0.6;" > ' + dateStr + '< / span > '
+ delBtn
+ '< / div > '
+ '< div style = "font-size:13.5px;line-height:1.5;" > ' + c.text + '< / div > '
+ '< / div > ';
}).join('');
} catch (_) {
document.getElementById('v2-comments-list').innerHTML = '';
}
}
window.v2DetailAddComment = async function(drucksache) {
var input = document.getElementById('v2-comment-input');
var visSel = document.getElementById('v2-comment-visibility');
if (!input || !input.value.trim()) return;
var visibility = visSel ? visSel.value : 'all';
await fetch('/api/comment', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'drucksache=' + encodeURIComponent(drucksache)
+ '& text=' + encodeURIComponent(input.value)
+ '& visibility=' + visibility
});
input.value = '';
loadComments();
};
window.v2DetailDeleteComment = async function(commentId) {
await fetch('/api/comment/' + commentId, {method: 'DELETE'});
loadComments();
};
/* ── Voting ───────────────────────────────────────────────────── */
async function loadVotes() {
try {
var data = await fetch('/api/votes?drucksache=' + encodeURIComponent(DRS)).then(function(r){ return r.json(); });
var counts = (data.counts & & data.counts.overall) ? data.counts.overall : {up:0, down:0};
var myVote = data.my_votes & & data.my_votes.overall;
var upEl = document.getElementById('v2-vote-up-count');
var downEl = document.getElementById('v2-vote-down-count');
var upBtn = document.getElementById('v2-vote-up');
var downBtn = document.getElementById('v2-vote-down');
if (upEl) upEl.textContent = counts.up || 0;
if (downEl) downEl.textContent = counts.down || 0;
if (upBtn) {
upBtn.style.background = myVote === 'up' ? 'rgba(137,158,51,0.12)' : '';
upBtn.style.borderColor = myVote === 'up' ? 'var(--ecg-green)' : '';
}
if (downBtn) {
downBtn.style.background = myVote === 'down' ? 'rgba(220,53,69,0.10)' : '';
downBtn.style.borderColor = myVote === 'down' ? '#dc3545' : '';
}
} catch (_) {}
}
window.v2DetailCastVote = async function(drucksache, vote) {
if (!currentUser) { v2AuthModalOpen(); return; }
try {
var resp = await fetch('/api/vote', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'drucksache=' + encodeURIComponent(drucksache) + '& target=overall& vote=' + vote
});
if (resp.ok) loadVotes();
} catch (_) {}
};
/* ── Share ────────────────────────────────────────────────────── */
function buildShareText(platform) {
var LIMITS = {twitter: 240, threads: 460, mastodon: 460};
var limit = LIMITS[platform] || 460;
var text;
if (platform === 'twitter' & & SHARE_TWI) text = SHARE_TWI;
else if (platform === 'threads' & & SHARE_THR) text = SHARE_THR;
else if (platform === 'mastodon' & & SHARE_MAS) text = SHARE_MAS;
else {
var emoji = SCORE >= 8 ? '🟢' : SCORE >= 5 ? '🟡' : SCORE >= 3 ? '🟠' : '🔴';
text = emoji + ' GWÖ-Score ' + SCORE + '/10: „' + TITLE.substring(0, 70) + '" (' + DRS + ')\n\n#Gemeinwohl #GWÖ';
}
if (text.length > limit) text = text.substring(0, limit - 1) + '…';
return text;
}
window.v2DetailShare = function(platform) {
var text = buildShareText(platform) + '\n' + PERMALINK;
var urls = {
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
twitter: 'https://twitter.com/intent/tweet?text=' + encodeURIComponent(text),
threads: 'https://www.threads.net/intent/post?text=' + encodeURIComponent(text),
linkedin: 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(PERMALINK)
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
};
if (urls[platform]) window.open(urls[platform], '_blank', 'noopener');
};
fix(v2): Topbar-Höhe runter, Share-Felder erweitert (Kopieren/LinkedIn/Email/Bild), Smoke-Test 401-Pattern
- Topbar padding 10px -> 4px, min-height 32px (User: 'Header weniger hoch')
- Share-Buttons im Antragsdetail erweitert auf 7 Plattformen analog v1:
Kopieren (Clipboard), Threads, X, Mastodon, LinkedIn, E-Mail (mailto), Bild (Freepik)
- v2DetailShareCopy/Email/Image-Helper, ANTRAG_TOPICS ans Template uebergeben
- Smoke-Test akzeptiert 401 fuer auth-protected Routen (curl ohne Accept-Header
bekommt 401-JSON, echte Browser bekommen 302-Redirect via _auth_redirect_handler)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 21:57:04 +02:00
window.v2DetailShareCopy = function() {
var text = buildShareText('twitter') + '\n' + PERMALINK;
if (navigator.clipboard & & navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
// kleiner visueller Hinweis: Button-Text temporär
var btn = event & & event.currentTarget;
if (btn) {
var orig = btn.textContent;
btn.textContent = '✓ kopiert';
setTimeout(function(){ btn.textContent = orig; }, 1500);
}
});
} else {
prompt('Zum Kopieren markieren und Cmd/Strg-C drücken:', text);
}
};
window.v2DetailShareEmail = function() {
var subject = 'GWÖ-Bewertung: ' + (TITLE.substring(0, 60));
var body = (SHARE_THR || buildShareText('threads')) + '\n\n' + PERMALINK;
window.location.href = 'mailto:?subject=' + encodeURIComponent(subject) + '& body=' + encodeURIComponent(body);
};
window.v2DetailShareImage = function() {
var topics = (window.ANTRAG_TOPICS || []).slice(0, 2).join(' ');
var query = (topics || TITLE.substring(0, 40)) + ' Politik';
window.open('https://www.freepik.com/search?format=search& query=' + encodeURIComponent(query), '_blank', 'noopener');
};
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
window.v2DetailShareMastodon = function() {
var text = buildShareText('mastodon') + '\n' + PERMALINK;
var instance = localStorage.getItem('mastodon_instance');
if (!instance) {
instance = prompt('Deine Mastodon-Instanz (z.B. mastodon.social):');
if (!instance) return;
instance = instance.trim().replace(/^https?:\/\//, '').replace(/\/+$/, '');
localStorage.setItem('mastodon_instance', instance);
}
window.open('https://' + instance + '/share?text=' + encodeURIComponent(text), '_blank', 'noopener');
};
/* ── Re-Analyze ───────────────────────────────────────────────── */
function pollReAnalyze(jobId, btn) {
fetch('/status/' + jobId)
.then(function(r){ return r.json(); })
.then(function(data) {
if (data.status === 'completed') {
btn.textContent = 'Fertig — lade neu…';
setTimeout(function(){ location.reload(); }, 800);
} else if (data.status === 'failed') {
btn.textContent = 'Analyse fehlgeschlagen';
btn.disabled = false;
} else {
setTimeout(function(){ pollReAnalyze(jobId, btn); }, 2000);
}
})
.catch(function() { btn.textContent = 'Polling-Fehler'; btn.disabled = false; });
}
window.v2DetailReAnalyze = async function(btn) {
if (!currentUser) { v2AuthModalOpen(); return; }
btn.disabled = true;
btn.textContent = 'Lösche alte Bewertung…';
try {
var delResp = await fetch('/api/assessment/delete?drucksache=' + encodeURIComponent(DRS), {method: 'DELETE'});
if (delResp.status === 401) {
btn.textContent = 'Nicht angemeldet';
setTimeout(function(){ btn.textContent = 'Neu analysieren'; btn.disabled = false; }, 3000);
return;
}
btn.textContent = 'Analyse läuft…';
var resp = await fetch('/api/analyze-drucksache', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'drucksache=' + encodeURIComponent(DRS) + '& bundesland=' + encodeURIComponent(BL)
});
if (resp.status === 401) {
btn.textContent = 'Nicht angemeldet';
setTimeout(function(){ btn.textContent = 'Neu analysieren'; btn.disabled = false; }, 3000);
return;
}
var data = await resp.json();
if (data.status === 'queued') {
btn.textContent = 'Wird analysiert…';
pollReAnalyze(data.job_id, btn);
} else {
btn.textContent = 'Fehler: ' + (data.detail || 'unbekannt');
setTimeout(function(){ btn.textContent = 'Neu analysieren'; btn.disabled = false; }, 4000);
}
} catch (e) {
btn.textContent = 'Fehler: ' + e.message;
setTimeout(function(){ btn.textContent = 'Neu analysieren'; btn.disabled = false; }, 4000);
}
};
/* ── Merkliste (#140) ────────────────────────────────────────── */
var _merklisteActive = false;
async function initMerkliste() {
if (!currentUser) return;
try {
var resp = await fetch('/api/me/merkliste');
if (!resp.ok) return;
var entries = await resp.json();
var isInList = entries.some(function(e) { return e.antrag_id === DRS; });
_updateMerkliste(isInList);
} catch (_) {}
}
function _updateMerkliste(active) {
_merklisteActive = active;
var star = document.getElementById('v2-merkliste-star');
var label = document.getElementById('v2-merkliste-label');
var btn = document.getElementById('v2-merkliste-btn');
if (!star) return;
star.textContent = active ? '★' : '☆';
label.textContent = active ? 'Gemerkt' : 'Merken';
if (btn) {
btn.style.background = active ? 'rgba(0,157,165,0.10)' : '';
btn.style.borderColor = active ? 'var(--ecg-teal)' : '';
btn.style.color = active ? 'var(--ecg-teal)' : '';
}
}
window.v2DetailMerklisteToggle = async function() {
if (!currentUser) { v2AuthModalOpen(); return; }
try {
if (_merklisteActive) {
var r = await fetch('/api/me/merkliste/' + encodeURIComponent(DRS), { method: 'DELETE' });
if (r.ok) _updateMerkliste(false);
} else {
var r = await fetch('/api/me/merkliste', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ antrag_id: DRS })
});
if (r.ok) _updateMerkliste(true);
}
} catch (_) {}
};
/* ── Bewertungs-Historie ─────────────────────────────────────── */
async function loadHistory() {
var container = document.getElementById('v2-history-list');
if (!container) return;
try {
var entries = await fetch('/api/assessment/history?drucksache=' + encodeURIComponent(DRS)).then(function(r){ return r.json(); });
if (!Array.isArray(entries) || entries.length === 0) {
container.innerHTML = '< span style = "font-family:var(--font-mono);font-size:12px;color:var(--ecg-dark);opacity:0.45;" > Nur eine Version vorhanden.< / span > ';
return;
}
container.innerHTML = entries.map(function(v, i) {
var dateStr = v.created_at
? new Date(v.created_at).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',year:'numeric',hour:'2-digit',minute:'2-digit'})
: '– ';
var version = v.version !== undefined ? v.version : (entries.length - i);
return '< div style = "display:flex;justify-content:space-between;align-items:baseline;'
+ 'padding:5px 0;border-bottom:1px solid var(--hairline);'
+ 'font-family:var(--font-mono);font-size:11px;color:var(--ecg-dark);">'
+ '< span > v' + version + ' < span style = "opacity:0.55;" > ' + dateStr + '< / span > < / span > '
+ '< a href = "/api/assessment?drucksache=' + encodeURIComponent(DRS) + '&version=' + version + '" target = "_blank" rel = "noopener" '
+ ' style="color:var(--ecg-blue);border-bottom:1px solid rgba(0,157,165,0.35);">JSON< / a > '
+ '< / div > ';
}).join('');
} catch (_) {
if (container) container.innerHTML = '';
}
}
/* ── Init ─────────────────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function() {
initAuth();
initMerkliste();
loadHistory();
});
})();
< / script >
{% endif %}
{% endblock %}