Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:
1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)
Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.
Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.
Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
Web-Detail zeigt diese Marker bereits — pro NEIN-Fraktion einen ⚠ wenn
der eigene Wahlprogramm-Score ≥ 7/10 ist (Heuchelei: stimmt gegen die
eigenen Versprechen), pro JA-Fraktion einen ! wenn der Wahlprogramm-
Score < 3/10 (Opportunismus: stimmt zu obwohl Antrag inhaltlich nicht
zum eigenen Programm passt). Im PDF fehlten sie bisher.
Daten-Pfad: report.py rechnet die Marker einmal vor (heuchelei_score /
opportunismus_score aus app/marker.py, gefüttert mit umgemappten
fraktions_scores aus assessment.wahlprogramm_scores) und reicht zwei
Maps fraktion → score ans Template. Template macht nur noch Lookup:
``opportunismus_by_fraktion.get(f)`` neben jeder JA-Fraktion,
``heuchelei_by_fraktion.get(f)`` neben jeder NEIN-Fraktion. Plus
kompakte Legende unter dem Vote-Block, falls überhaupt Marker
vorkommen.
Stimmverhalten und Programm-Treue-Begründungen sind im PDF schon da
(verifiziert bei der Code-Inspektion). Damit ist die "PDF auf Augenhöhe
mit Web-Detail"-Liste aus #175 bis auf News-Match abgehakt; News-Match
explizit out-of-scope nach User-Entscheidung.
Drei Mitigations:
1) Admin-Queue-Polling 5s → 15s. Die Queue ändert sich pro Sekunde
ohnehin nicht spürbar; senkt CPU + Network ohne UX-Verlust.
2) ``pagehide``-Listener in admin_queue.html, admin_stand.html und
auswertungen.html. Zerstört Chart.js-Instanzen + cleart setInterval-
Handles, sobald die Page in den Back/Forward-Cache geht oder
geschlossen wird. Bisher hingen sie bis Browser-GC.
3) /auswertungen: zentrales Cleanup für ``_histChart``, alle ``_svCharts.*``
und ``window._zeitreiheModalChart`` beim pagehide. Bisher zerstört
nur die einzelnen Render-Funktionen ihre Vorgänger; beim Page-Verlassen
blieben sie alle stehen.
Was nicht abgedeckt ist (für eventuelle Folge-Iteration mit konkretem
Heap-Snapshot):
- Lazy-Render lange News-/Drucksachen-Listen via IntersectionObserver
- Detaillierte Detached-DOM-Untersuchung pro Seite
Bestehende Maßnahmen (bereits da, hier nicht angefasst): chart.destroy()
vor jedem neuen Chart, sim.stop() in cluster.html, visibilitychange-
Pause für Polling.
Threads-Encoding: rendered Sonderzeichen als ? oder Rauten, weil der
Text mit zerlegten Codepoints (z.B. ``a`` + Combining Diaeresis statt
``ä``) ankam — Threads' Composer kommt damit nicht klar. Fix: NFC-
Normalisierung (``str.normalize('NFC')``) vor encodeURIComponent. Das
vereinigt zerlegte Umlaute und typografische Anführungszeichen.
Instagram-Share auf macOS: bisher versuchten wir auch auf Desktop den
``navigator.share()``-Pfad mit File. Das öffnet das macOS-Share-Sheet,
zeigt aber nur AirDrop / Mail / Notizen — Instagram-App ist auf Desktop
nicht installiert, also nutzlos. Fix: Mobile-Detection via User-Agent
+ maxTouchPoints (für iPad-iOS-13+-Maskierung). Auf Desktop direkt zu
Pfad B (Download + Clipboard) statt OS-Sheet.
Zwei kleine UI-Bugs:
1) Score in Merkliste fehlte. Ursache: /api/assessment liefert ``gwoeScore``
(camelCase), das Merkliste-Template las ``a.gwoe_score`` (snake_case).
Fix: beide Schreibweisen akzeptieren.
2) Metadaten-Zeile im Antrag-Detail rendert mit Whitespace-Müll, weil die
Jinja-If-Blöcke ohne Whitespace-Steuerung Newlines durchlassen, die
der Browser zu Spaces collapst:
- "13.04.2026 , qwen-plus" (Komma mit Leerzeichen davor) statt
"13.04.2026, qwen-plus".
- "NRW-WP18. Wahlperiode" statt "18. Wahlperiode": ``antrag.wahlperiode``
ist der Filter-Key wie "NRW-WP18", nicht die reine Zahl.
Fix: Whitespace-Steuerung ``{%- ... -%}`` an allen relevanten If-Tags
in der Metadaten-Zeile + Byline. Plus neues Feld
``antrag.wahlperiode_zahl`` (nur die Zahl) im _row_to_detail-Mapping,
das bevorzugt vor ``antrag.wahlperiode`` zur Anzeige genutzt wird.
Vorher: NRW · Drs. 18/18246 · Antrag · NRW-WP18. Wahlperiode · eingebracht 18.03.2026
Eingebracht von SPD — Analyse 13.04.2026 , qwen-plus · 5 Zitate verifiziert
Nachher: NRW · Drs. 18/18246 · Antrag · 18. Wahlperiode · eingebracht 18.03.2026
Eingebracht von SPD — Analyse 13.04.2026, qwen-plus · 5 Zitate verifiziert
User-Feedback: vorherige Pillen-Variante zu schmal/dicht. Stattdessen
gewünscht: Layout wie ursprünglich (head + 2 Programm-Reihen), aber
mit weniger Whitespace und ohne Trennlinien — und die Partei in eine
linke Spalte gezogen, damit head und Programm-Rows nebeneinander
sitzen statt stapeln.
Layout: ``.v3-fraktion`` als CSS-Grid mit zwei Spalten:
- links (140px–25%): Fraktion-Name + Pillen vertikal
- rechts (1fr): zwei Programm-Reihen (Wahlprogramm / Parteiprogramm)
ohne border-top zwischen den Reihen, padding 3px statt 10/8,
font kleiner (11px statt 12px).
Pro Fraktion bisher ~137px (Head + Wahlprogramm-Reihe + Parteiprogramm-
Reihe in drei vertikalen Blöcken). Nutzer wollte deutlich kompakter.
Layout: Fraktion-Name + Antragsteller-/Regierungs-Pillen + zwei
Programm-Pillen (WP / PP) jetzt in EINER flex-row. Pro Programm-Pille
ein klickbares <details> mit Mini-Score-Chip; beim Aufklappen fließt
der Body (Begründung + Zitate) dank ``flex-basis: 100%`` unter die
Zeile in voller Breite.
Höhe collapsed: ~40-45px pro Fraktion (von vorher ~137px). Begründung
in der ausgeklappten Box bekommt zusätzlich einen ``Wahlprogramm:``-/
``Parteiprogramm:``-Präfix, da der Programm-Typ aus der Pille nicht
mehr im Body explizit auftaucht.
Doppelte v3-prog-text-Regel im CSS entfernt (war vergessenes Cruft).
Browser-Quirk: in einem <form> blieb der fetch nach Submit hängen,
auch mit preventDefault() im submit-Handler. Status-Text bekam
"Suche läuft …", aber die Response kam nie an — der Browser hat den
fetch durch den Submit-State des Forms blockiert.
Lösung: <form> → <div>, Button auf type="button". Click direkt an
runSearch binden, Enter via keydown auf dem Input. Keine Form-Submit-
Semantik mehr.
Bei E2E-Smoketest mit Playwright reproduzierbar gefixt.
Folge-Fix zu 501f32b: das body_scripts-Skript läuft am Ende des Body,
da ist DOMContentLoaded oft schon vorbei. Der bisherige Wrapper
``document.addEventListener('DOMContentLoaded', ...)`` wurde dann nie
gefeuert, der Submit-Listener nie gebunden — Suchknopf weiter still.
Lösung: IIFE direkt aufrufen + Idempotenz-Marker (``_quellenBound``)
gegen Doppelbindung.
Bug: ``runSearch`` ist async und returnt damit Promise<false>. Im
Inline-Handler ``onsubmit="return runSearch(event)"`` interpretiert der
Browser den Promise als truthy → Default-Form-Submit läuft an, die
Page navigiert, und der gerade abgesetzte fetch bricht mit
"Failed to fetch" ab. Im echten Browser merkt man's beim Klick auf
"Suchen" oder Enter im Suchfeld: Status bleibt auf "Suche läuft …"
hängen, keine Ergebnisse erscheinen.
Fix: Bindung über ``addEventListener('submit', ...)`` mit
``event.preventDefault()`` synchron vor dem async runSearch-Aufruf.
JS-Direktaufruf von ``runSearch()`` (z.B. bei Filter-Wechsel)
funktioniert weiterhin.
Gefunden bei E2E-Browser-Smoketest mit Playwright.
Folge-Schritt zu #222 (Commit bd591b9): Das große Datenliteral in
``embeddings.py`` (~280 Einträge, 712 Zeilen) wird durch einen
Re-Export aus ``programme.PROGRAMME`` ersetzt. Damit existieren die
Programm-Stammdaten endgültig nur noch an einer Stelle.
Schema-Brücke: Die ``chunks``-Tabelle führt ``typ`` als alte Sammel-
Bezeichnung (``wahlprogramm`` oder ``parteiprogramm``).
``programme.PROGRAMME`` differenziert ``grundsatzprogramm-bund`` vs.
``grundsatzprogramm-land``. Beim Re-Export werden beide auf
``parteiprogramm`` gemappt — alte Chunks und neue Indexierungen
tragen denselben typ-String, der Filter in
``get_relevant_quotes_for_antrag`` (typ="parteiprogramm") deckt
beide Grundsatzprogramm-Varianten weiter ab.
embeddings.py schrumpft von 1574 auf 926 Zeilen (−648). Tests
unverändert grün (1217 passed).
Schließt #224. ADR 0013 hat die Datenbasis für historische Bewertung
geschaffen (programme.PROGRAMME mit gueltig_ab/gueltig_bis); jetzt
nutzt der Analyzer sie auch tatsächlich.
Vorher: get_relevant_quotes_for_antrag suchte über ALLE Wahl- und
Grundsatzprogramme einer Partei in einem BL — egal aus welcher WP.
Folge: ein Antrag aus 2018 in NRW konnte Zitate aus dem cdu-nrw-2022
Wahlprogramm (das er noch nicht kennen konnte) zugeordnet bekommen.
Anachronismus-Halluzination.
Nachher: Wenn ``datum`` (ISO YYYY-MM-DD) durchgereicht wird, filtert
``find_relevant_chunks`` die Chunks auf Programme mit
``[gueltig_ab, gueltig_bis)`` ⊇ datum. Programme, die zum
Antragszeitpunkt nicht galten, werden komplett ausgelassen.
Signaturen erweitert (alle additiv, datum=None ⇒ altes Verhalten):
- embeddings.find_relevant_chunks(..., datum=None)
- embeddings.get_relevant_quotes_for_antrag(..., datum=None)
- analyzer.analyze_antrag(..., datum=None)
main.run_drucksache_analysis: reicht doc.datum durch (DIP/OPAL liefern
das Antragsdatum vor dem LLM-Call, kein Zwei-Pass-Workaround nötig).
Tests:
- test_embeddings.test_datum_param_is_passed_through_to_find_relevant_chunks
- test_bug_regressions.test_analyzer_propagates_datum_to_embeddings
1244/1244 Unit-Tests grün.
parteien.py listet 'BiW' als kanonischen Key (Mixed-Case), 'BIW' nur als
Alias. Bestehende HB-Assessments tragen bereits 'BiW' in
wahlprogramm_scores. Beim Block 2.4 hatte ich die historischen BiW-
Wahlprogramme aber mit "BIW" angelegt — Folge: find_relevant_chunks
mit parteien=["BiW"] hätte die 131 Chunks der biw-hb-*-PDFs nicht
gefunden, weil der partei-Filter exakt matcht.
Geändert:
- embeddings.PROGRAMME: 3 biw-hb-* Einträge "partei": "BIW" → "BiW"
- wahlprogramme.WAHLPROGRAMME["HB"]: Key "BIW" → "BiW", "partei" Feld
"BIW Bremen" → "BiW Bremen"
- test_hb_has_five_parteien: erwartete Set entsprechend angepasst
Folge-Schritt: chunks.partei in der embeddings.db muss von 'BIW'
auf 'BiW' migriert werden — die 131 betroffenen Chunks werden beim
nächsten Reindex der biw-hb-* Programme ohnehin überschrieben.
92/92 Programme-Tests grün, 1242 Unit-Tests grün.
Zwei kleine Verbesserungen, die beim Schreiben der Drift-Tests in
bf5400a aufgefallen sind:
1. HB-Datum-Typo: regierungsbildung war 2023-07-04 (Tag der Konstituierung
der 21. Bürgerschaft), korrekt ist 2023-07-05 (Vereidigung Senat
Bovenschulte II). 5 Einträge angepasst (SPD/CDU/GRÜNE/LINKE/BIW).
Kommentar im Header ebenfalls.
2. Helper-Docstrings (regierungsbildung_for, regierungsende_for,
regierung_aktuell) explizit darüber, dass das Datum die ERSTE
Regierung der WP ist, NICHT die aktuell amtierende. Wichtig bei
Sukzessionen wie RP WP18 (Dreyer III 2021-05-18 → Schweitzer I
2024-07-10) — die 2021er Wahlprogramme bleiben wirksam, auch nach
MP-Wechsel. Für aktuell amtierende Regierung gibt es
``legislaturen.aktuelle_regierung``.
92/92 Programme-Tests grün.
ADR 0013 hatte als offene Folge "Doppelter Daten-Bestand zwischen
WAHLPROGRAMME und embeddings.PROGRAMME ist nicht aufgelöst — Risk:
stille Drift". Der invasive Compat-Shim (#222) ist aufwendig; bis
dahin fängt eine neue Test-Klasse die Drift bidirektional ab:
TestWahlprogrammeProgrammeConsistency (4 Tests):
- Jeder WAHLPROGRAMME-Eintrag hat ein passendes aktuelles Programm in
PROGRAMME (sonst liefert aktuelles_wahlprogramm None)
- pdf-Dateinamen müssen übereinstimmen (file == pdf)
- Partei-Kurzform muss übereinstimmen
- Jedes aktuelle Wahlprogramm in PROGRAMME muss auch in WAHLPROGRAMME
registriert sein (orphan-check andere Richtung)
Drift-Funde dabei:
- BIW (Bürger in Wut) HB war in PROGRAMME (biw-hb-2023, biw-hb-2019,
biw-hb-2015), aber NICHT in WAHLPROGRAMME-HB. Bewertungs-Pipeline
hätte BIW-Anträge gegen kein Wahlprogramm geprüft. Eintrag ergänzt:
BÜRGER IN WUT — Programm Bürgerschaftswahl 2023 (26 Seiten).
- Test test_hb_has_four_parteien → test_hb_has_five_parteien.
92/92 Programme-Tests grün.
GET /api/quellen/search?q=&filter=current|all&top_k=&bundesland=&partei=
nutzt text-embedding-v4 für wortunscharfe Suche (Endungen + Synonyme).
Filter:
- filter=current: nur Programme mit gueltig_bis IS NULL (Default)
- filter=all: auch historische Programme
Response liefert pro Treffer name, partei, bundesland, gueltig_ab/bis,
seite, gekürztes Snippet, similarity, plus pdf_url mit Direkt-Sprung
ins highlightete PDF (über /api/wahlprogramm-cite).
UI auf /quellen oben über der BL-Auflistung:
- Suchfeld + Submit
- Radio-Toggle "nur aktuelle Programme" / "auch historische"
- Treffer-Karten mit Partei-Badge, gültig-Pille (grün/grau),
Seite + Relevanz-%, Snippet, Direktlink ins PDF
- Filter-Wechsel triggert automatischen Re-Run
Smoketest dev: "Klimaschutz" liefert 13 Treffer in aktuellen Programmen
mit korrekter Similarity-Sortierung; "Solidarität" mischt Wahl- und
Grundsatzprogramme. Zugriff erfordert keinen Login (read-only).
5 Programme zur LTW NRW 14.05.2017 als historische Wahlprogramme im
Embeddings-Index — erster Datensatz für die zeitpunktige Bewertung
historischer Antraege:
- cdu-nrw-2017 (Laschet, 120 S., 172 chunks)
- spd-nrw-2017 (Kraft, 116 S., 169 chunks)
- gruene-nrw-2017 (131 S., 322 chunks)
- fdp-nrw-2017 (Lindner, 56 S., 92 chunks)
- afd-nrw-2017 (84 S., 78 chunks)
Geltungszeitraum 2017-05-14 (Wahltag WP17) bis 2022-05-15 (Wahltag
WP18, exklusiv). Eintraege liegen NUR in embeddings.PROGRAMME — die
WAHLPROGRAMME[NRW]-Struktur bleibt single-current (cdu-nrw-2022).
programme._migrate_from_legacy hat einen neuen Schritt 2b, der
typ=wahlprogramm-Eintraege aus embeddings.PROGRAMME mit explizitem
gueltig_ab/_bis als historische Wahlprogramme registriert. Damit
liefert wahlprogramm_zum_zeitpunkt() jetzt fuer NRW-Antraege aus dem
Zeitraum 2017-2022 das passende Programm.
Live-Verifikation auf gwoe-antragspruefer-dev:
- 2018-09-01 -> cdu-nrw-2017 (WP17)
- 2024-01-01 -> cdu-nrw-2022 (WP18)
- Grenze: 14.05.2022 -> WP17, 15.05.2022 -> WP18
Tests: 116 gruen, plus neue test_grenze_zwischen_wp17_und_wp18 und
angepasstes test_datum_vor_aktueller_wp_nrw_wp17.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Antwort auf B1 + B2 aus der Roadmap:
- B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022,
vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende
Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die
Vereidigung.
- B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere
Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen
intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber
willkuerlich. Programme werden zur Wahl beschlossen und sind
Wahlversprechen ab dem Tag der Wahl.
Implementation in programme._migrate_from_legacy:
- gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf
altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege)
- ``wahl``-Feld auf Wahltag gesetzt
- ``wp``-Feld aus aktuelle_legislatur ergaenzt
Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und
versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert
"Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt
laeuft das primaer ueber legislaturen.REGIERUNGEN).
UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt
"gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06"
(Vereidigung Merz I).
Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme
+ test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und
test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik
explizit ab.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Sektion auf /methodik (zwischen "Analyse-Pipeline" und "Stimmverhalten
& Marker"), die transparent macht, gegen welches Wahlprogramm der Prüfer
einen Antrag misst. Erklärt:
- Wahlperiode → Programm-Geltung (Konstituierung, nicht Wahltag)
- Regierung zur Antragszeit (mit Sukzession-Beispiel Dreyer III →
Schweitzer I)
- Wahlprogramm-Liste pro Antragsteller-Fraktion mit Geltungsdatum
- Snapshot-Hinweis (Bewertungsdatum + Modell)
Plus ehrliche Einschraenkung: aktuell nur die jeweils gegenwaertigen
Wahlprogramme im Embeddings-Index. Architektur ist fuer historische
Indexierung vorbereitet (siehe ADR 0013).
Inhaltsverzeichnis-Link ergaenzt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Erweiterung des Geltungs-Kontext-Blocks (Antrag-Detail):
- Programm-Titel als Link auf das PDF ({titel} klickbar →
/static/referenzen/{pdf}, opens in new tab).
- Seitenzahl als ergaenzende Info: "116 S." neben Geltungsdatum.
- Snapshot-Zeile am Block-Ende: "Diese Bewertung wurde am
{datum} mit {modell} gegen den oben genannten Programm-Stand
erzeugt." — macht klar, dass die Anzeige eine Momentaufnahme der
damaligen LLM-Bewertung ist und nicht "live" gegen heutige
Programme misst.
CSS:
- v3-geltung-pdf: ECG-blauer Link mit dezenter underline-Linie.
- v3-geltung-snapshot: kursiv, getrennt durch hairline-border, gedaempft.
Tests: 88 grün (test_legislaturen + test_wahlprogramme + test_embeddings).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vor der Programm-Treue-Sektion eine kompakte Info-Box, die transparent
macht, was zur Antragszeit fuer das jeweilige Parlament galt:
- **Wahlperiode** (Nummer + Konstituierung-bis-Ende-Spanne) ueber
legislatur_zum_zeitpunkt(bl, antrag_datum)
- **Regierung zur Antragszeit** (Name + Koalitionsparteien + Vereidigung,
ggf. Endedatum bei Sukzessionen wie Dreyer III -> Schweitzer I) ueber
regierung_zum_zeitpunkt(bl, antrag_datum)
- **Bewertet gegen die folgenden Wahlprogramme** (pro Antragsteller-
Fraktion mit gueltig-seit-Datum) ueber wahlprogramm_zum_zeitpunkt
pro Partei
Daten kommen aus den neuen Modulen app/legislaturen.py + app/programme.py.
Helper laufen historisch korrekt — z.B. ein Antrag aus 2020-02-15 in TH
wuerde "Kemmerich I (FDP)" zurueckliefern.
Aktuell zeigen alle Antraege die jeweils "aktuelle" Regierung & das
aktuelle Programm, weil keine historischen Wahlprogramme im Embeddings-
Index sind. Die Architektur ist aber fuer den Tag vorbereitet, wo
historische Programme indiziert werden.
Implementation:
- main.py: _render_antrag_detail laedt geltung_kontext und gibt es ans
Template weiter. ISO-Datum aus row["datum"] (nicht aus dem display-
formatierten antrag["datum"]).
- v3/screens/antrag_detail.html: neue Sektion v3-geltung vor Programm-
Treue-Block.
- static/v3/v3.css: neue v3-geltung-Klassen mit dezentem Doku-Look.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: 'Aber für diesen speziellen Antrag müssten doch alle Programme
verfügbar sein. https://gwoe-dev.toppyr.de/antrag/21/1594'
Ursache: WAHLPROGRAMME (in app/wahlprogramme.py) hatte keinen 'BUND'-
Eintrag, daher hat check_missing_programmes() fuer jeden Bundestags-
Antrag ALLE 8 Fraktionen als fehlend markiert. Im Embedding-Index
(app/embeddings.py) sind die *-grundsatzprogramm.pdf Dateien aber
laengst registriert (typ=parteiprogramm, ohne bundesland-Bindung).
Die Lookup-Tabellen waren inkonsistent.
Fix: WAHLPROGRAMME['BUND']-Eintrag mit den 6 Grundsatzprogrammen
(CDU/SPD/GRUENE/FDP/AfD/LINKE) ergaenzt — entspricht der Realitaet
im embeddings.py-Index. CSU + BSW haben keine indizierten Programme
und werden weiterhin als fehlend gemeldet (was korrekt ist).
Bestehende BUND-Assessments mit fehlende_programme=[8 Parteien] in
der DB bleiben erst mal so (waehrend einer Re-Analyse korrekt). Issue
#186 (historische BTW-Wahlprogramme) bleibt offen — Grundsatzprogramme
sind nur ein Notbehelf gegen die 'alle fehlen'-Anzeige.
Plus: Permalink-Klick kopiert jetzt die absolute URL in die Zwischen-
ablage statt zur Page zu navigieren. window.v3CopyPermalink in
v2/screens/antrag_detail.html (wird via super() von v3 mitvererbt).
Link-Text 'Permalink kopieren', 1.6s 'Permalink kopiert ✓'-Flash
nach Copy. Fallback auf window.prompt() wenn Clipboard-API fehlt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: '?? Hinweis: Für folgende Parteien lag kein Wahl-/Parteiprogramm
vor — keine Treue-Bewertung möglich: CDU, CSU, AfD, SPD, GRÜNE, LINKE,
BSW, FDP.' Vorher zeigte die UI den Disclaimer + trotzdem die LLM-
halluzinierten Programm-Treue-Scores aller 8 BT-Parteien — schlechte UX.
WAHLPROGRAMME['BUND'] ist aktuell leer (keine Bundestags-Wahlprogramme
indiziert), daher wird check_missing_programmes alle BT-Fraktionen als
'fehlend' markieren. Bisher wurden trotzdem die LLM-Scores rausgespielt.
Fix in v3/screens/antrag_detail.html und v3/pdf/antrag_pdf.html:
- Wenn _all_missing (alle Fraktionen fehlen) → ganze Programm-Treue-
Sektion ausblenden, nur klare Disclaimer-Box zeigen ('Programm-Treue
nicht verfuegbar — fuer dieses Parlament sind aktuell keine Programme
indiziert').
- Wenn nur einzelne Fraktionen fehlen → die einzelnen Karten via
{% if fs.fraktion not in _missing_set %} skippen, Disclaimer fuer
die wegfallenden Fraktionen zeigen ('werden hier nicht aufgefuehrt').
Damit keine LLM-halluzinierten Scores mehr gezeigt werden, wo es keine
Quelle gibt. Issue #186 (historische Programme indizieren) ist die
langfristige Loesung — diese Aenderung macht die UI bis dahin ehrlich.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: 'Kannst du die Schriftarten eher nachempfinden? Und die Matrix
ist so sehr raumgreifend ... das könnte deutlich in der Höhe gestaucht
sein.'
Fonts:
- Google-Fonts-Link für Inter + JetBrains Mono ergänzt im PDF-Template.
- font-family-Stack: 'Inter', dann Avenir/Helvetica als Fallback (falls
WeasyPrint die CDN-Fonts nicht laden kann).
- Mono-Stack: 'JetBrains Mono' bevorzugt, dann Source Code Pro.
Matrix in der Höhe gestaucht (war ca. 320pt hoch, jetzt ~165pt):
- Zellen-Höhe 60pt → 28pt (Symbole bleiben mit 11pt-Schrift gut lesbar)
- Zellen-Breite 60pt → 52pt (knapper, Cells leicht breiter als hoch)
- Zeilen-Label-Spalte 90pt → 76pt
- Header-Reihe 24pt → 18pt
- Gap 2pt → 1.5pt
- Legend kleiner: 8pt → 7.5pt, swatches 12pt → 9pt
Resultat: Matrix-Block fast halbiert, Schwerpunkte-erklärt-Sektion
darunter rückt einen Seitenwechsel weniger in den Hintergrund.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: 'immer noch doppelte Borders ... Inhalte zu klein skaliert
nach oben links gerutscht (800 px breit statt 1080)'
Ursachen:
1. Canvas-Content-Mismatch (Inhalt 75% der PNG-Breite):
WeasyPrint rechnet 1 CSS-px = 0.75 PDF-pt (96dpi → 72dpi). @page
war auf {width}pt × {height}pt (1080×1350) gesetzt, body aber auf
1080×1350 CSS-px. Folge: Body fuellte nur 1080*0.75=810pt der
1080pt-Page → Content top-left, 25% rechts/unten leer; PyMuPDF
rasterisiert mit zoom=1 → 1080×1350 PNG, Content nur in den linken
810×1012 px → 'Inhalte zu klein nach oben links gerutscht'.
Fix: @page-Groesse auf (width * 0.75)pt × (height * 0.75)pt setzen.
Body fuellt jetzt die volle Canvas-Breite. PyMuPDF kompensiert mit
zoom = scale * 4/3, damit die PNG wieder die gewuenschten Pixel-
Dimensionen hat (1080×1350 für scale=1).
2. Doppel-Borders auf field-chip + party-pill:
WeasyPrint hat einen bekannten Render-Bug bei
'border + border-radius' auf inline-flex-Elementen — der Border
wird zweimal gezeichnet (innen + aussen). 1.5px → 2px hat das
nicht behoben, weil's nicht am Subpixel-Wert lag.
Fix: border ersetzt durch box-shadow: inset 0 0 0 2px var(--rule).
Inset-Shadow rendert sauber, kein Doppel-Effekt. border-radius
bleibt erhalten.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Im PNG-Export wurde 9.0/10 manchmal auf zwei Zeilen gerendert (9.0/
in einer Zeile, 10 in einer neuen). Ursache: WeasyPrint laedt Inter
ueber das Google-Fonts-CDN nicht zuverlaessig (CSP-/Font-Loading-
Issue), Fallback auf System-Sans hat andere Metriken → Score-Inhalt
wird breiter als 320px Score-Box → Umbruch.
Fix: white-space: nowrap auf .score .num und .score .num small.
Erzwingt 1-zeilige Darstellung egal welcher Font-Fallback geladen
wird.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User hat die zweite ZIP geliefert: 'Scorecard Formate.html' mit
Spec fuer drei zusaetzliche Formate. Plus Anmerkung: 'doppelte Borders'
und 'Export viel zu gross'.
Vier Formate jetzt im selben Template scorecard_portrait.html:
- format=portrait (DEFAULT) → 1080×1350 · 4:5 · IG-Feed
- format=square → 1080×1080 · 1:1 · IG/LinkedIn
- format=story → 1080×1920 · 9:16 · Story/Reels
- format=wide → 1920×1080 · 16:9 · OG/Slide/Twitter
Wide hat 2-spaltigen Body-Aufbau (Story-Spalte links, Daten-Spalte
rechts, Header+Footer ueber volle Breite), die anderen drei nutzen
das gemeinsame 1-spaltige Body-Markup. Aller Formate teilen sich die
Daten-Aggregation (Chips, Fraktions-Bars, Beschluss).
Bug-Fixes aus dem User-Feedback:
1. 'Doppelte Borders um die Partei und Field-Chips' — die 1.5-px-
Borders im Cloud-Design wurden von WeasyPrint als zwei einzelne
1-px-Linien gerendert (Subpixel-Bug bei fractional border-widths).
Alle 1.5px → 2px (integer).
2. 'Export viel zu gross' — der Download-Button hatte scale=2 als
Default → 2160×2700 PNG-Pixel. Fuer IG-Upload reicht 1080×1350
exakt (Instagram skaliert hochgeladene Bilder ohnehin). Default
jetzt scale=1, der ?scale=2-Param bleibt verfuegbar fuer Retina.
3. Statusleiste mit Format-Switcher: vier Pills (4:5 Feed / 1:1 Square
/ 9:16 Story / 16:9 Wide), aktuelles Format hervorgehoben. Klick
wechselt URL-format-Param. Plus PNG- und PDF-Download-Buttons,
die das aktuelle Format mitfuehren.
main.py: dimensions-Mapping um story+wide erweitert in
scorecard_template UND _render_scorecard_pdf. Format-Validation
ebenfalls erweitert. format-Variable an's Template durchgeschleift
(damit der Template-Switch fuer card-portrait/square/story/wide
funktioniert).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Wunsch: 'Fuege auf der Webseite zur Ansicht der Scorecard unten
eine Statusleiste hinzu mit einem Download-Knopf, der dann das Dokument
als PNG oder JPEG herunterlaedt.'
Statusleiste am unteren Viewport-Rand (position: fixed) mit:
- Label links: 'Scorecard · NRW · Drs. 18/17449 · 1080×1350 (Instagram 4:5)'
- Buttons rechts:
· primaerer 'PNG herunterladen' (Akzent-gruen, scale=2 = 2160×2700 px)
· sekundaerer 'PDF' (Outline-Style, format=portrait)
Nutzt das bestehende /api/assessment/scorecard.png-Endpoint und das
download-Attribut sorgt fuer den richtigen Dateinamen
('gwoe-scorecard-18-17449.png').
JPEG-Variante ist nicht implementiert — der Endpoint liefert PNG, was
fuer Scorecards mit Text/scharfen Kanten qualitativ besser ist als
JPEG. Falls explizit JPEG gewollt: separater Endpoint noetig.
Body-Padding bottom 80 px auf @media screen, damit die fixed Toolbar
keinen Inhalt verdeckt. PDF-Render ist unbeeinflusst (Toolbar via
@media print { display:none }).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: 'Skaliere die Scorecard so, dass sie die Hoehe des Browser
Viewports Minus 10% einnimmt'
Vorherige Logik nahm scaleByWidth, capped auf 1.0. Jetzt zwei Faktoren
und das kleinere gewinnt:
- scaleByHeight = window.innerHeight * 0.9 / 1350
- scaleByWidth = (window.innerWidth - 40) / 1080
- scale = min(beiden, max 1.0)
Auf einem 1440×900-Desktop ergibt das scale 0.6 (= 90% × 900 / 1350),
Card 648×810. Auf einem 390-px-Phone gewinnt die Breite-Begrenzung,
Card skaliert kleiner damit horizontal nichts abgeschnitten wird.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mein vorheriger CSS-Versuch transform: scale(calc(min(1080px, 100vw-40px)
/ 1080)) liefert eine px-Ausgabe — scale() braucht aber unitless. Folge:
keine Skalierung, Card wird nur abgeschnitten (auf Phone sah man nur
linkes Ecke).
Fix: JS-basiert. adjustScale() berechnet das Verhaeltnis
window.innerWidth/1080 (max 1.0 bei Desktop), setzt transform: scale()
auf .card und passt die .card-viewport-Dimensionen an. Re-fired bei
window.resize + load.
WeasyPrint fuehrt kein JS aus → PDF unbeeinflusst (Card bleibt 1:1
1080×1350).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User: 'Kannst du den Viewport so veraendern, dass ich da scrollen kann?
Oder im Idealfall, dass sie ihn klein angezeigt wird.'
Bisher hatte die Scorecard-Page body { width:1080px; height:1350px;
overflow:hidden } — fest fuer den PDF-Render. Im Browser wurde die
Card deshalb nicht skaliert oder scrollbar gemacht; auf kleinerem
Viewport einfach abgeschnitten.
Loesung: @media screen-Block, den nur Browser sehen (WeasyPrint
rendert mit media=print, ignoriert ihn). Wrapper-Div .card-viewport
bekommt aspect-ratio 1080/1350 und max-width 1080px (oder
viewport-40px). Die Card wird per CSS-transform scale() proportional
zur Viewport-Breite verkleinert. Auf einem normalen Desktop
(1920x1080) erscheint die Card jetzt in Originalgroesse zentriert
mit dunklem Rahmen drumherum, auf Mobile skaliert sie sich passend
auf die Bildschirmbreite.
PDF-Generierung unbeeinflusst: WeasyPrint sieht nur die Default-CSS
(body 1080x1350, card 1080x1350, kein transform).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Bericht: 'Die obere Menueleiste verschwindet hinter den Tabs in
Safari. Und ich kann nicht mehr auf Anmelden klicken.'
Drei zusammenhaengende Probleme im Mobile-Media-Query:
1. body.v2 { overflow-x: hidden } war als Notbremse fuer Layout-Overflow
gedacht, aber Safari (WebKit) interpretiert das so, dass position:
sticky-Kinder nicht ueber den scrollenden Container kommen — die
Topbar haftet nicht mehr richtig und kann hinter Safaris eigener
Tab-Bar verschwinden. Raus damit.
minmax(0, 1fr) auf v2-shell reicht als Overflow-Schutz.
2. flex-wrap: wrap auf der Topbar liess die Auth-/Theme-/Bundesland-
Items in eine zweite Zeile umbrechen. Sticky-Elements mit
variabler Hoehe haben in Safari Render-Bugs (clipping, click-
absorption). Stattdessen jetzt: weniger wichtige Items
(Methodik/Quellen-Links + Bundesland-Selector) auf Mobile via
display:none ausgeblendet. Brand, Auth-Control und Theme-Toggle
bleiben — passen problemlos in eine 32-px-Zeile.
3. padding-top: env(safe-area-inset-top, 0) fuer iOS-Safari, damit
die Topbar Saafais Chrome-Overlap respektiert (Notch, URL-Bar,
Tab-Bar im 'Compact'-Mode).
Plus z-index: 200 auf der Topbar — schaerfer als alle Page-Elemente,
sodass Anmelden-Button garantiert klickbar bleibt selbst wenn
darunterliegende Elemente in Edge-Cases ueberlappen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
wahlperiode_for() liefert 'NRW-WP18' (Bundesland-Praefix + WPnn). Ich
hatte im Template nochmal 'WP' davor gehaengt → 'WPNRW-WP18'. Fix:
Suffix-Teil nach Bindestrich nehmen ('WP18'), oder fallback 'WP'+Wert
falls kein Bindestrich.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drei Aufgaben in einem Schwung:
1. Werkstatt-Link im Admin
admin_stand bekommt eine Sektion 'Design-Werkstaetten' mit Link auf
/v2/scorecard-werkstatt — damit Admins den Live-Editor finden ohne
die URL kennen zu muessen.
2. Share-Block nur fuer angemeldete User
Der ganze Share-Block (Kopieren, Threads, Mastodon, LinkedIn,
Instagram, E-Mail, Scorecard, Stock-Bild) bekommt id=v2-share-block
und wird per initAuth() display:none/block geschaltet — analog zum
Comment-Form. Default im Markup: display:none, damit Gaeste ihn
nicht waehrend des Auth-Roundtrips kurz sehen. Funktioniert in v2
und v3 (gleicher JS-Handler via super-Inheritance).
3. PDF-Layout = v3-Layout
Neues Template v3/pdf/antrag_pdf.html (single column, A4) reused die
Visuallogik aus der Online-Detailseite:
- Score-Hero-Block mit Farb-Tint
- Matrix 5×5 mit Achsen-Labels (Werte oben, Berührungsgruppen links)
- Programm-Treue pro Fraktion mit Begruendung + Zitaten + Fallback-
Hinweis bei fehlenden Zitaten
- Verbesserungsvorschlaege mit Redline-Format
- Abstimmungsergebnis (best-effort via get_plenum_votes) inkl.
Konsistenz-Hinweis
Online-spezifisches gestrichen: Merken-Button, Vote-treffend, Share,
Kommentare, News-Box, Reanalyze, Historie, Modals.
NEU im PDF: 'Schwerpunkte erklaert'-Sektion direkt unter der Matrix.
Listet die Top-4 positiven und Top-4 negativen Matrix-Felder mit
ihrem LLM-generierten label + aspect — Ersatz fuer den interaktiven
Klick, der im PDF nicht funktioniert.
report.generate_html_report_v3() neu, generate_pdf_report() ruft
diese statt der alten Inline-HTML-Variante. Alte generate_html_report
bleibt als Fallback erhalten.
WeasyPrint rendert mit @page A4, Footer mit Drucksache + Branding +
Seitenzahl 'Seite X von Y'.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Wunsch: 'Baue eine Entwicklungsseite, wo wir all das in CSS code
zusammenschreiben und länger daran arbeiten können ohne jedes mal png
erzeugen zu müssen. Können wir hinterher auch nutzen, um irgendwo
mal schnell eine Übersicht einzublenden.'
Neue Route /v2/scorecard-werkstatt mit Split-Layout:
- Links: Live-iframe-Vorschau der /v2/scorecard, mit Zoom-Toolbar
(Fit / 40 / 50 / 65 / 80 / 100 %).
- Rechts: Drucksachen-Selector (Top-60 Anträge), Format-Pills
(Portrait / Square / OG), CSS-Editor-Textarea + Apply-Button.
- Apply schreibt das User-CSS als <style>-Element in den iframe →
keine Server-Roundtrips, kein PNG-Render, instantane Iteration.
- Strg/⌘+Enter im Editor wendet sofort an. Tab fuegt 2 Spaces ein.
- Direkt-Link + Iframe-Snippet werden generiert — die Card laesst sich
also direkt embedden (z.B. Übersicht in einer anderen App).
Plus: Cache-Buster `&_=Date.now()` am Scorecard-Button im v3-Detail,
damit die Vorschau-Anzeige nach Layout-Aenderungen nicht weiter eine
gecachete Version zeigt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorige Version hatte ~110 px Slack zwischen Matrix-Legende und Begruendung.
Matrix-Cells von 88×88 auf 110×110 hochgezogen, Label-Spalte 130→150 px,
Symbol-Schrift 19→24 pt, Zeilen-Header 36→40 px Hoehe.
Resultat: Matrix-Grid jetzt ca. 700×586 px (vorher 570×476). Slack im
matrix-block (flex-grow) deutlich kleiner, Card visueller dichter
gefuellt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback nach Browser-Inspektion: 'Bei mir sieht das immer noch
nicht so aus' — und tatsächlich, das vorherige Layout hatte zwei
sichtbare Probleme:
1. justify-content: space-between auf portrait-body verteilte den
Slack-Raum nicht symmetrisch, sondern haeufte ihn unten zwischen
Matrix-Block und Begruendung an. Folge: ~270 px Luecke zwischen
diesen Sektionen.
2. Die Matrix war 'stilisiert' nur in Form (5×5 Farb-Grid) — aber
ohne Achsen-Beschriftungen muessten Buerger:innen wissen was A1,
B2 etc. bedeuten. Kommt nicht an.
Redesign:
- Layout-Strategie: portrait-matrix-block bekommt flex-grow:1 und
absorbiert allen verbleibenden vertikalen Platz; Matrix bleibt
zentriert. Andere Sektionen sitzen in natuerlicher Hoehe oben/
unten. Kein space-between.
- Matrix stilisiert mit Achsen:
· Spalten-Header: Wuerde / Solidaritaet / Nachhaltigkeit /
Gerechtigkeit / Transparenz (Brand-Color, Mono-Caps)
· Zeilen-Header: A·Lieferant:innen, B·Finanzen, C·Verwaltung,
D·Buerger:innen, E·Gesellschaft & Natur
· Cells in 88×88 Quadraten, gap 4 px
· Legende horizontal unter der Matrix statt seitlich
- Begruendung: line-clamp 5, sitzt am Boden, mit Trennlinie und Sublabel.
- Cache-Control: no-store auf /v2/scorecard, damit Browser nach
Layout-Aenderungen nicht die alte HTML-Variante zeigt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Im rebalanced Render war die Matrix immer noch loechrig: r-0 (#d8d8d2)
hatte zu wenig Kontrast gegen den weissen Matrix-Frame, sodass die
neutralen Cells visuell wie Luecken wirkten.
Zwei Fixes:
- r-0 dunkler: #b8b8b2 statt #d8d8d2 (deutliche Grau-Praesenz)
- Matrix-Frame raus: kein weisser Hintergrund mehr, der Karten-Gradient
wird durch die 4px-Gaps sichtbar — Cells stehen als klare Farbflaechen
heraus, kein konkurrierender weisser Untergrund.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visuelle Probleme im vorigen Render: Title dominierte oben (5+ Zeilen,
36pt), Matrix wirkte loechrig (Neutral-Cells #f0f0f0 verschwanden im
Karten-Hintergrund), Score-Row war optisch zu zart fuer den Anker.
Neue Komposition:
- Title kompakt: 26pt, line-height 1.18, line-clamp 3 — beschreibt aber
dominiert nicht mehr.
- Score-Hero-Block: tonierter Hintergrund passend zur Score-Farbe
(gruen/orange/rot) plus dicker Border-left, full-width — wird zum
visuellen Anker statt nur zwischen Trennlinien zu sitzen. Score 132pt,
Verdict 30pt.
- Matrix: 480x480 mit weissem Frame + zarter Border, 6px gap. Neutral-
Cells (r-0) jetzt #d8d8d2 statt #f0f0f0 → klar sichtbar im Grid,
Loch-Look weg.
- Legende: Swatches 22px statt 18px, gleiche Sichtbarkeit wie Cells.
- Begruendung: line-clamp 4 (statt 9), eigene 'Begruendung'-Sublabel,
obere Trennlinie — bewusst schmal als Ausklang.
Hierarchie: Score-Hero ist optisch dominant (Anker), Matrix das datenreiche
Zentrum, Title deskriptiv aber zurueckhaltend, Begruendung ergaenzt ohne
zu konkurrieren.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Route-Default ist seit letztem Commit portrait, der Button-Aufruf
ueberschrieb das aber explizit mit ?format=og. Param raus, jetzt
laeuft alles ueber den Server-Default (portrait, 1080×1350).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>