gwoe-antragspruefer/app/templates/v2/screens/antrag_detail.html
Dotty Dotter 70d9790b4b feat(#177): Programm-Treue im BELEGE-Layout — pro Partei zwei aufklappbare Blöcke (WP+PP)
- _row_to_detail liefert zitate inline pro Wahlprogramm/Parteiprogramm-Block
- Template rendert <details>: Summary mit Score-Chip, Body mit Einschätzung+Belege
- v2.css: neue Klassen v2-treue-block/-label/-body, v2-pill, v2-einschaetzung
- Separate "Belege — Partei"-Sektion entfernt (ist jetzt inline pro Programm)

Tests: tests/test_v2_pdf_consistency.py (#176 generalisiert) bleibt grün —
fraktions_scores trägt zusätzliche zitate-Felder, ändert aber keine
Score/Begründungs-Werte aus dem Vergleich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:21:42 +02:00

1135 lines
55 KiB
HTML
Raw 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.

{% 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 %}
{# Verbesserungsvorschläge: alle verbesserungen rendern wenn vorhanden #}
{% if antrag.verbesserungen %}
<h3 class="v2-h3" style="margin-top:24px;">Verbesserungsvorschlä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;">Verbesserungsvorschlag</h3>
{% from "v2/components/redline.html" import redline %}
{{ redline(segments=antrag.redline.segments) }}
{% endif %}
</div>{# .left #}
{# ── Rechte Spalte: Bewertungs-Panel ── #}
<div class="right">
{# Bewertungs-Header mit Merken-Button rechts — visuelle Einheit
(User-Wunsch: „Bewertung passend zu Merken verschieben"). #}
<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:6px;">
<div class="v2-antrag-id">Bewertung</div>
<button id="v2-merkliste-btn"
onclick="v2DetailMerklisteToggle()"
style="display:inline-flex;align-items:center;gap:6px;padding:4px 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);">
<span id="v2-merkliste-star"></span>
<span id="v2-merkliste-label">Merken</span>
</button>
</div>
{{ score_hero(antrag.score | default(0), antrag.verdict_title | default(""), antrag.verdict_body | default("")) }}
{# ── 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 #}
{# ── 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",
} %}
{# Konsistenz-Hinweis: GWÖ-Empfehlung vs. tatsächlicher Beschluss.
Logik in app/marker.py — siehe ADR 0010. #}
{% set _state = consistency_state(antrag.verdict_title, antrag.plenum_votes) %}
{% set _decisive = decisive_outcome(antrag.plenum_votes) %}
{% if _state %}
<div style="margin-bottom:10px;padding:8px 12px;border-radius:6px;font-size:12px;line-height:1.5;
background:{% if _state == 'conflict' %}color-mix(in srgb,#cf222e 8%,transparent){% else %}color-mix(in srgb,#2da44e 8%,transparent){% endif %};
border-left:3px solid {% if _state == 'conflict' %}#cf222e{% else %}#2da44e{% endif %};">
<strong>{% if _state == 'conflict' %}Mehrheit kontra GWÖ-Empfehlung{% else %}Mehrheit deckt sich mit GWÖ-Empfehlung{% endif %}</strong>
— Empfohlen: <em>{{ antrag.verdict_title }}</em>; Beschluss: <em>{{ _decisive | capitalize }}</em>.
</div>
{% endif %}
{% 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 %}
{# Mehrheits-Bar: Fraktions-Anzahlen pro Lager als Stacked Bar #}
{% set _n_ja = v.fraktionen_ja | length %}
{% set _n_nein = v.fraktionen_nein | length %}
{% set _n_enth = v.fraktionen_enthaltung | length %}
{% set _n_total = _n_ja + _n_nein + _n_enth %}
{% if _n_total > 0 %}
<div style="margin:6px 0 10px;">
<div style="display:flex;height:10px;border-radius:3px;overflow:hidden;border:1px solid var(--hairline);"
title="Fraktions-Mehrheit: {{ _n_ja }} Ja · {{ _n_nein }} Nein · {{ _n_enth }} Enth. (kein Sitz-/Stimm-Anteil)">
{% if _n_ja %}<div style="width:{{ (100 * _n_ja / _n_total) }}%;background:#2da44e;"></div>{% endif %}
{% if _n_enth %}<div style="width:{{ (100 * _n_enth / _n_total) }}%;background:#6e7781;"></div>{% endif %}
{% if _n_nein %}<div style="width:{{ (100 * _n_nein / _n_total) }}%;background:#cf222e;"></div>{% endif %}
</div>
<div style="font-family:var(--font-mono);font-size:9px;opacity:0.5;margin-top:3px;">
{{ _n_ja }}/{{ _n_total }} Fraktionen Ja · {{ _n_nein }} Nein · {{ _n_enth }} Enth.
<span style="opacity:0.7;cursor:help;" title="Mehrheit nach Fraktionsanzahl, nicht nach Sitzen — Plenarprotokoll liefert keine Sitz-/Stimm-Counts."></span>
</div>
</div>
{% endif %}
<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 %}
{% set _opp_match = opportunismus_score(f, antrag.fraktions_scores) %}
<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 }}{% if _opp_match is not none %}<span style="margin-left:4px;cursor:help;font-style:italic;color:#bf8700;" title="Diese Fraktion stimmte mit Ja, obwohl der Antrag nicht zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_opp_match) }}/10).">!</span>{% endif %}</span>
{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_nein %}
<div><span style="color:#cf222e;font-weight:700;">Nein:</span>
{% for f in v.fraktionen_nein %}
{% set _wp_match = heuchelei_score(f, antrag.fraktions_scores) %}
<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 }}{% if _wp_match is not none %}<span style="margin-left:4px;cursor:help;" title="Diese Fraktion stimmte mit Nein, obwohl der Antrag gut zum eigenen Wahlprogramm passt (WP-Score {{ '%.0f' | format(_wp_match) }}/10)."></span>{% endif %}</span>
{% endfor %}
</div>
{% endif %}
{% if v.fraktionen_enthaltung %}
<div><span style="color:#6e7781;font-weight:700;cursor:help;border-bottom:1px dotted currentColor;" title="Enth. — Enthaltung: weder Zustimmung noch Ablehnung.">Enth.:</span>
{% 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 #}
{% if antrag.matrix %}
<h3 class="v2-h3">Matrix 2.0 · 25 Felder</h3>
{{ matrix_mini(antrag.matrix) }}
{% endif %}
{# Programm-Treue im BELEGE-Layout: pro Partei zwei aufklappbare Blöcke
(Wahlprogramm + Parteiprogramm). Summary zeigt Bewertung, expand
enthält Einschätzung + Belege. #}
{% 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 %}
{% set wp_score = fs.wahlprogramm.score | float %}
{% set pp_score = fs.parteiprogramm.score | float %}
<div class="v2-fraktion-row">
<div class="v2-fraktion-head">
<span class="v2-fraktion-label">{{ fs.fraktion }}</span>
{% if fs.ist_antragsteller %}<span class="v2-pill v2-pill-antrag">Antragsteller:in</span>{% endif %}
{% if fs.ist_regierung %}<span class="v2-pill v2-pill-reg">Regierungsfraktion</span>{% endif %}
</div>
<details class="v2-treue-block">
<summary>
<span class="v2-treue-label">Wahlprogramm</span>
<span class="v2-treue-spacer"></span>
<span class="v2-score-chip {% if wp_score >= 7 %}chip-green{% elif wp_score >= 4 %}chip-mid{% else %}chip-red{% endif %}">{{ "%.0f"|format(wp_score) }}/10</span>
<span class="v2-treue-caret" aria-hidden="true"></span>
</summary>
<div class="v2-treue-body">
{% if fs.wahlprogramm.begruendung %}
<div class="v2-einschaetzung">
<div class="v2-einschaetzung-label">Einschätzung</div>
<div class="v2-einschaetzung-text">{{ fs.wahlprogramm.begruendung }}</div>
</div>
{% endif %}
{% if fs.wahlprogramm.zitate %}
<div class="v2-belege">
<div class="v2-einschaetzung-label">Belege</div>
{% for z in fs.wahlprogramm.zitate %}
{{ quote_card(z.text, z.source, True, False, z.pdf_href) }}
{% endfor %}
</div>
{% endif %}
</div>
</details>
<details class="v2-treue-block">
<summary>
<span class="v2-treue-label">Parteiprogramm</span>
<span class="v2-treue-spacer"></span>
<span class="v2-score-chip {% if pp_score >= 7 %}chip-green{% elif pp_score >= 4 %}chip-mid{% else %}chip-red{% endif %}">{{ "%.0f"|format(pp_score) }}/10</span>
<span class="v2-treue-caret" aria-hidden="true"></span>
</summary>
<div class="v2-treue-body">
{% if fs.parteiprogramm.begruendung %}
<div class="v2-einschaetzung">
<div class="v2-einschaetzung-label">Einschätzung</div>
<div class="v2-einschaetzung-text">{{ fs.parteiprogramm.begruendung }}</div>
</div>
{% endif %}
{% if fs.parteiprogramm.zitate %}
<div class="v2-belege">
<div class="v2-einschaetzung-label">Belege</div>
{% for z in fs.parteiprogramm.zitate %}
{{ quote_card(z.text, z.source, True, False, z.pdf_href) }}
{% endfor %}
</div>
{% endif %}
</div>
</details>
</div>
{% endfor %}
</div>
{% endif %}
{# ── News-Match-Box: aktuelle News passend zu diesem Antrag (#170) ── #}
<div id="ad-news-box" style="display:none;margin-top:32px;background:var(--ecg-card-bg);border:1px solid var(--ecg-border);border-radius:6px;padding:16px;">
<h3 class="v2-h3" style="margin:0 0 8px;">Aktuelle News passend zu diesem Antrag</h3>
<p style="font-size:11px;font-family:var(--font-mono);opacity:0.65;margin:0 0 12px;">
Embedding-Match aus den letzten 90 Tagen. Quelle: Tagesschau-API + Bundestag-RSS.
</p>
<div id="ad-news-list">
<div style="font-family:var(--font-mono);font-size:12px;opacity:0.5;">Lade …</div>
</div>
</div>
{# 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>
{# ── Share-Block (analog v1) ───────────────────────────────────── #}
<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;">
<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>
<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="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>
<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>
</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 }};
window.ANTRAG_TOPICS = {{ (antrag.themen or []) | tojson }};
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 ────────────────────────────────────────────────────── */
// Limits pro Plattform (konservativ, mit Permalink-Reserve).
// Mastodon: meiste Instances erlauben 500 — minus Permalink (~80) = 420.
var SHARE_LIMITS = {threads: 460, mastodon: 420};
function buildShareText(platform) {
var limit = SHARE_LIMITS[platform]; // null = kein Limit (Copy/E-Mail)
var text;
if (platform === 'threads' && SHARE_THR) text = SHARE_THR;
else if (platform === 'mastodon' && SHARE_MAS) text = SHARE_MAS;
else if (platform === 'threads' && SHARE_MAS) text = SHARE_MAS; // Fallback Threads ← Mastodon
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 (limit && text.length > limit) text = text.substring(0, limit - 1) + '…';
return text;
}
// Vollständiger Share-Body fuer LinkedIn/E-Mail/Copy — keine Längen-Cuts.
function buildLongShareText() {
var emoji = SCORE >= 8 ? '🟢' : SCORE >= 5 ? '🟡' : SCORE >= 3 ? '🟠' : '🔴';
var lines = [
emoji + ' GWÖ-Score ' + SCORE + '/10 für „' + TITLE + '" (' + DRS + ')',
'',
'Bewertet nach der Gemeinwohl-Matrix 2.0 — Würde, Solidarität,',
'Nachhaltigkeit, Gerechtigkeit, Demokratie. Vollständige Auswertung',
'mit Fraktions-Programmtreue, Verbesserungsvorschlägen und Belegen:',
'',
PERMALINK,
'',
'#Gemeinwohl #GWÖ #Antragspruefer'
];
return lines.join('\n');
}
window.v2DetailShare = function(platform) {
if (platform === 'linkedin') {
// LinkedIn legacy share-offsite akzeptiert nur url. Aber wir
// prefillen den Text via Clipboard + öffnen Composer parallel,
// damit der User mit Strg-V einfügen kann.
var body = buildLongShareText();
var urlOnly = 'https://www.linkedin.com/sharing/share-offsite/?url=' + encodeURIComponent(PERMALINK);
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(body).then(function() {
window.open(urlOnly, '_blank', 'noopener');
v2ShareToast('LinkedIn-Composer geöffnet — Text liegt in der Zwischenablage (Strg/⌘-V einfügen)');
}, function() {
window.open(urlOnly, '_blank', 'noopener');
});
} else {
window.open(urlOnly, '_blank', 'noopener');
}
return;
}
var text = buildShareText(platform) + '\n' + PERMALINK;
var urls = {
threads: 'https://www.threads.net/intent/post?text=' + encodeURIComponent(text),
};
if (urls[platform]) window.open(urls[platform], '_blank', 'noopener');
};
window.v2DetailShareCopy = function(evt) {
// Kompletter Body, keine Längen-Cuts. PERMALINK enthalten.
var text = buildLongShareText();
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(function() {
var btn = (evt && evt.currentTarget) || (window.event && window.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 emoji = SCORE >= 8 ? '🟢' : SCORE >= 5 ? '🟡' : SCORE >= 3 ? '🟠' : '🔴';
var body = [
emoji + ' GWÖ-Score ' + SCORE + '/10',
'',
'Antrag: ' + TITLE,
'Drucksache: ' + DRS,
'',
(SHARE_MAS || SHARE_THR || ('Eine Auswertung aus Sicht der Gemeinwohl-Ökonomie zum Antrag „' + TITLE.substring(0, 80) + '".')),
'',
'Vollständige Bewertung mit Matrix 2.0, Programm-Treue pro Fraktion,',
'Verbesserungsvorschlägen und Belegen:',
'',
PERMALINK,
'',
'— GWÖ-Antragsprüfer · gwoe.toppyr.de'
].join('\n');
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';
// Magnific: license-Selection vorausgewählt (selection=1, last_filter=selection).
var url = 'https://www.magnific.com/search?format=search'
+ '&last_filter=selection&last_value=1'
+ '&query=' + encodeURIComponent(query)
+ '&selection=1';
window.open(url, '_blank', 'noopener');
};
window.v2DetailShareMastodon = function() {
// Encoding-Fix: encodeURIComponent kümmert sich um UTF-8 — wir
// dürfen den Text NICHT vorher manuell escapen.
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');
};
// Kleine Toast-Helper-Funktion — verschwindet nach 2 s
function v2ShareToast(msg) {
var t = document.createElement('div');
t.textContent = msg;
t.style.cssText = 'position:fixed;bottom:24px;left:50%;transform:translateX(-50%);'
+ 'background:var(--ecg-teal);color:#fff;padding:10px 18px;border-radius:4px;'
+ 'font-family:var(--font-mono);font-size:12px;z-index:9999;'
+ 'box-shadow:0 4px 12px rgba(0,0,0,0.15);';
document.body.appendChild(t);
setTimeout(function(){ t.remove(); }, 2500);
}
/* ── 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 + ' &nbsp;<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 = '';
}
}
/* ── News-Match-Box (#170 Reverse-Sicht) ──────────────────────── */
async function loadNewsMatches() {
const ds = "{{ antrag.drucksache | e }}";
const box = document.getElementById('ad-news-box');
const list = document.getElementById('ad-news-list');
if (!box || !list) return;
try {
// Parallel: News-Matches + Cluster-Map (fuer Cluster-Indicator)
const [matchesResp, clusterResp] = await Promise.all([
fetch('/api/aktuelle-themen/news-fuer-antrag?drucksache='
+ encodeURIComponent(ds)
+ '&top_k=5&min_similarity=0.4&days=90'),
fetch('/api/aktuelle-themen/cluster?days=90&min_cluster_size=2'),
]);
const matchData = await matchesResp.json();
const matches = matchData.matches || [];
if (!matches.length) return; // Box bleibt unsichtbar
// Cluster-Lookup-Map: URL → {clusterIdx, size, otherTitles}
let clusterByUrl = {};
try {
const clusterData = await clusterResp.json();
(clusterData.clusters || []).forEach((c, i) => {
(c.members || []).forEach(m => {
clusterByUrl[m.url] = {
size: c.size,
tags: c.top_tags || [],
others: (c.members || [])
.filter(o => o.url !== m.url)
.map(o => `${o.source}: ${o.titel}`)
.slice(0, 5),
};
});
});
} catch (_) { /* clusters optional */ }
box.style.display = '';
let html = '';
for (const n of matches) {
const d = (n.datum || '').slice(0, 10);
const tags = (n.tags || []).slice(0, 3).map(
t => '<span style="display:inline-block;padding:1px 6px;background:var(--ecg-bg-subtle);border-radius:3px;font-family:var(--font-mono);font-size:10px;margin-right:4px;">' + t + '</span>'
).join('');
const summary = n.summary
? '<p style="font-size:12px;margin:4px 0 8px;opacity:0.85;line-height:1.5;">' + n.summary + '</p>'
: '';
const clusterInfo = clusterByUrl[n.url];
const clusterBadge = clusterInfo
? '<span style="display:inline-block;padding:1px 7px;background:rgba(0,157,165,0.15);border-radius:11px;font-family:var(--font-mono);font-size:10px;margin-left:6px;color:var(--ecg-teal);" '
+ 'title="' + clusterInfo.others.map(s => s.replace(/"/g, '&quot;')).join(' • ') + '">'
+ '🔗 Cluster (' + clusterInfo.size + ' News)</span>'
: '';
html += '<div style="border-bottom:1px dotted var(--ecg-border);padding:10px 0;">';
html += '<div style="font-family:var(--font-mono);font-size:10px;opacity:0.6;margin-bottom:3px;">'
+ d + ' · ' + n.source + (n.ressort ? ' / ' + n.ressort : '')
+ ' · sim ' + n.similarity + clusterBadge + '</div>';
html += '<a href="' + n.url + '" target="_blank" rel="noopener" '
+ 'style="color:var(--ecg-teal);text-decoration:none;font-weight:500;">'
+ n.titel + '</a>';
html += summary;
if (tags) html += '<div style="margin-bottom:6px;">' + tags + '</div>';
html += '<button onclick="adGeneratePresse(\''
+ ds.replace(/'/g, "\\'") + '\', \''
+ encodeURIComponent(n.url) + '\', this)" '
+ 'style="font-family:var(--font-mono);font-size:11px;padding:4px 10px;'
+ 'border:1px solid var(--ecg-teal);background:var(--ecg-card-bg);'
+ 'color:var(--ecg-teal);border-radius:3px;cursor:pointer;">'
+ 'PM-Vorschlag generieren</button>';
html += '</div>';
}
list.innerHTML = html;
} catch (e) {
// Bei Fehler: Box bleibt unsichtbar — kein Stoerfaktor
}
}
window.adGeneratePresse = async function(drucksache, newsUrlEnc, btn) {
if (!confirm('Pressemitteilung für ' + drucksache + ' anzeigen / generieren?\n\n'
+ 'Falls bereits ein Entwurf existiert, wird dieser ohne LLM-Call zurückgegeben.\n'
+ 'Sonst: qwen-max generiert (~6 Cent, ~30 s).')) return;
btn.disabled = true;
btn.textContent = '…';
try {
const r = await fetch('/api/aktuelle-themen/generate-presse'
+ '?drucksache=' + encodeURIComponent(drucksache)
+ '&news_url=' + newsUrlEnc, { method: 'POST' });
if (!r.ok) {
const err = await r.json().catch(() => ({}));
alert('Fehler: ' + (err.detail || r.statusText));
return;
}
const d = await r.json();
const note = d._was_existing
? '(bereits generiert am ' + (d.created_at || '').slice(0, 10) + ')'
: '(neu generiert)';
alert(d.titel + '\n' + note + '\n\n' + d.body
+ '\n\n— Auf /aktuelle-themen sichtbar im Tab "PM-Entwürfe".');
} catch (e) {
alert('Fehler: ' + e);
} finally {
btn.disabled = false;
btn.textContent = 'PM-Vorschlag generieren';
}
};
/* ── Init ─────────────────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function() {
initAuth();
initMerkliste();
loadHistory();
loadNewsMatches();
});
})();
</script>
{% endif %}
{% endblock %}