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
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.
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).
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: '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>
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: '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>
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>
User-Feedback in drei Punkten:
1. 'Standard auch fuer die Scorecard sein' — /v2/scorecard und
/api/assessment/scorecard.{png,pdf} default jetzt format=portrait
statt og. Wer das alte OG-Layout will, muss explizit ?format=og
setzen (oder square). Externe OG-Tags sind nicht betroffen, die
nutzen ein anderes Template (v2/og_template.html).
2. 'Instagram-Button sollte den Teilen-Dialog aufrufen' — implementiert
mit navigator.share() + File-Blob. Auf Mobile (Safari iOS / Chrome
Android) oeffnet der native Share-Sheet und Instagram erscheint
direkt als Ziel; Bild + Text gehen mit. Auf Desktop / Browsern
ohne canShare({files:…}) falle auf den vorigen Fallback zurueck:
Bild in neuem Tab + Text in Zwischenablage.
3. 'Card nutzt Platz besser, viel Rand' — alle Paddings reduziert:
- Card-Padding portrait: 54/56/32 → 34/38/24
- Body gap: 22 → 16, margin-top: 26 → 16
- Title: 32pt → 36pt
- Score-Number: 110pt → 130pt
- Matrix: 380×380 → 460×460 (groesser, mehr Detail erkennbar)
- Footer: enger an den Rand
Inhalt nimmt jetzt mehr Platz ein, weniger Whitespace-Verschwendung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Frage: 'Mach die Scorecard im Instagram-Format hochkant. Was genau
ist das nochmal? Vier zu fünf?' — Ja, 1080×1350 ist das Instagram-Feed-
Format mit dem groessten vertikalen Real-Estate. Stories sind 9:16,
quadratisch ist 1:1.
Neuer format=portrait (1080×1350) in /v2/scorecard und
/api/assessment/scorecard.png. Layout speziell vertikal:
- Title gross (32 pt) mit reichlich Leading
- Fraktions-Pills in eigener Zeile
- Score-Row: HUGE Zahl (110 pt) links + Empfehlung-Wort rechts,
oben/unten zarte Trennlinien — klare Bewertungs-Anker
- Matrix 5×5 gross (380×380) mit Legende daneben — User sieht
sofort welche Felder + - + - sind
- gwoe_begruendung als 5-zeilige Zusammenfassung darunter
- Footer am Boden
Instagram-Button im Share-Block stellt von square auf portrait um.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routen:
- /antrag/{drs} → v3 (Standard, Bürger:innen-Modus single column)
- /v2/antrag/{drs} → v2 (alter Profi-Modus, weiterhin erreichbar)
- /v3/antrag/{drs} → Alias auf v3 (für alte Bookmarks)
UI-Hinweise auf alternative Ansichten ausgeblendet:
- v3-Topbar-Pill "Bürger:innen-Modus · Beta" + "→ Profi-Modus"-Toggle raus
- v2-Topbar-Link "→ Bürger:innen-Modus · v3 Beta" raus
Im Admin-Bereich (/v2/admin/stand) neuer Block "Alternative Ansichten"
mit Beispiel-Drucksache, Live-Link auf v3 (Default) und v2 (Profi).
Nur Admins sehen die Hinweise auf v2.
Trennlinien-Cleanup im Rest-Block:
- Doppellinie unter Abstimmungsergebnis aufgelöst (.v3-rest hatte
border-top, das v3-section.border-bottom doppelt war).
- Neue Trennlinien via Klasse v3-rest-divider-top vor:
· Teilen-Block (zwischen Ähnliche und Teilen)
· Aktions-Links (zwischen Teilen und administrativem Bereich)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DB-Felder die bisher in der UI fehlten, jetzt in v3 sichtbar:
- _row_to_detail() liefert themen, kernpunkte, schwerpunkt, link,
konfidenz, fehlende_programme, wahlperiode an's Frontend.
- _wahlperiode_silent() leitet die WP aus datum+bundesland ab via
wahlperioden.wahlperiode_for() — silent-fail bei Lookup-Fehler.
v3-Template:
- Wahlperiode in der Antrag-ID-Zeile ("18. Wahlperiode")
- Themen-Chips als Reihe unter byline
- Kernforderungen als Bullet-Liste in der Zusammenfassungs-Sektion
- Konfidenz-Pille (hoch/mittel/niedrig) neben der Empfehlung
- Schwerpunkt-Felder (Top-Matrix-Cells) als Chips über der Matrix
- Disclaimer "fehlende Programme" am Programm-Treue-Block
- Original-PDF-Link im Aktions-Block
- Ähnliche Anträge als eigener Block, geladen via JS aus
/api/assessment/similar (Re-Use des bestehenden Endpoints aus #108)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skelett für Issues #184 + #185 — minimal, nicht-disruptiv:
- v3/base.html extendet v2/base.html (Topbar/Sidebar/Footer geteilt)
- v3/screens/antrag_detail.html extendet vorerst v2-Screen 1:1 und
injiziert nur Beta-Pill + Toggle "→ Profi-Modus"
- v2/screens/antrag_detail.html bekommt Topbar-Link "→ Bürger:innen-
Modus (v3 Beta)" → /v3/antrag/<drs>
- _render_antrag_detail() teilt DB-Reads/Context zwischen v2 + v3 —
Datenbasis garantiert in Sync, Unterschied ist nur template_name
- _MATRIX_EXPLANATIONS auf Modul-Ebene ausgelagert (war bisher
inline im v2-Route, jetzt von beiden Modi referenziert)
- v3.css als Add-On nach v2.css (lädt im v3/base head)
Was v3 noch NICHT tut: Score-Hero-Vereinfachung, Matrix→5-Werte,
Glossar-Tooltips, Default-Collapsing der Profi-Blöcke (Verbesserungen,
Kommentare). Diese Iterationen folgen pro PR — v2 bleibt unberührt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matrix-Info-Modal:
- _row_to_detail liefert label+aspect je Matrix-Cell ans Frontend
- Modal zeigt antragsspezifische Bewertung (Label + ausformulierte
Begründung + Rating-Chip) UND die allgemeine Felderklärung
(was misst dieses Feld?). v1-Verhalten wiederhergestellt.
Abstimmungsergebnis (Vote-Block):
- Outlink-Pfeil ↗ ist jetzt ein klickbares <a> auf v.quelle_url
(target=_blank). Vorher: Span ohne Link, Pfeil tat nichts.
- Marker ⚠ (Heuchelei) und ! (Opportunismus) bekommen sichtbare
Hover-Affordanz (Hintergrund + dotted-border on focus). Native
title= bleibt fuer Screenreader-/Tooltip aktiv. tabindex=0+role=button
macht sie keyboard-erreichbar.
- Legende unter dem Vote-Pill-Block erklaert die Marker beim
ersten Auftreten in einer Liste — wird nur eingeblendet wenn auf
dem Block mindestens ein Marker tatsaechlich vorkommt.
- Vote-Pills via Klassen v2-vote-pill/-ja/-nein/-enth statt
Inline-Styles (CD-Annaeherung).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _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>
LLM-Output setzt diese Flags nicht zuverlaessig (alle Werte heute None
in NRW/18/18246-Beispiel). _row_to_detail leitet sie jetzt im
Fallback aus den Drucksachen-Meta ab:
- ist_antragsteller := Fraktion in row.fraktionen (Antragsteller-Liste)
- ist_regierung := Fraktion in BUNDESLAENDER[bl].regierungsfraktionen
Damit erscheinen die 'Antragsteller:in' / 'Regierungsfraktion'-Pills
auch bei alten Assessments ohne explizite LLM-Flags. LLM-Wert (falls
gesetzt) hat weiterhin Vorrang.
WeasyPrint konvertiert CSS-px zu PDF-pt mit 96/72=0.75-Faktor:
1080px CSS-Page → 810pt PDF-Page → 810px PNG (bei zoom=1).
Mit 'size: 1080pt 1080pt' wird die PDF-Page direkt 1080pt
und PyMuPDF rendert 1080×1080px wie erwartet.
PyMuPDF (fitz) ist bereits in requirements.txt — also kein extra
Dependency noetig. Render-Pipeline: HTML-Template → WeasyPrint-PDF →
fitz Pixmap → PNG-Bytes.
Endpoint: GET /api/assessment/scorecard.png?drucksache=&bundesland=
&format=og|square&scale=2.0
- scale=2.0 (Default) liefert Retina-Aufloesung.
- scale=1.0..4.0 erlaubt.
Refactor: gemeinsamer Helper _render_scorecard_pdf() fuer .pdf und .png
— vorher Code-Duplikat zwischen den Endpoints.
Refs: #179, #180
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mockup im GWÖ-Stil mit:
- Drucksachen-Header (Kicker + Datum)
- Titel + antragstellende Fraktionen als Pills
- Empfehlungs-Verdict
- 420-Zeichen-Zusammenfassung
- Big Score-Zahl (farbcodiert nach 8/5/3-Schwellen)
- 5x5 Mini-Matrix mit korrekten 5 Klassen (rating-pp/-p/-0/-n/-nn)
- Footer mit Brand + Drucksachen-ID
Endpoints:
- GET /v2/scorecard?drucksache=&bundesland=&format=og|square (HTML)
- GET /api/assessment/scorecard.png?... (PNG via Playwright,
1200x630 für og, 1080x1080 für square)
Pattern entlehnt von app/og_card.py (Playwright-Headless-Render).
Refs: #179
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DB speichert seit langem die volle -5..+5 Skala (siehe models.py
MatrixEntry mit ge=-5, le=5), aber _row_to_detail shiftet noch
'rating - 3' (Migration-Reste der alten 1..5 → -2..+2-Skala).
Folge: rating=5 wurde zu 2, rating=4 zu 1, beide landeten im
matrix_mini auf der m-p-Klasse (rating 1..3) → kraftiges Gruen
(m-pp) wurde fast nie ausgespielt.
Fix: kein Shift; defensive int-Konversion + Clamp -5..+5.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DIP-Drucksachen mit `herausgeber: 'BR'` (Bundesrat) haben Bundesländer
als Antragsteller (z.B. SN, HE) statt Fraktionen. Variante b — explizite
Behandlung statt nur ausschließen:
- Drucksache-dataclass: neue Felder `is_bundesrat: bool`,
`urheber_bundeslaender: list[str]`. Existierende Pfade unberührt.
- BundestagAdapter._doc_to_drucksache: liest herausgeber + urheber-Liste,
setzt Bundesländer-Codes (bezeichnung wie "SN") in
urheber_bundeslaender. fraktionen bleibt leer fuer BR — verhindert
dass Stimmverhalten-Aggregate verwirrt werden.
- /api/search-landtag liefert is_bundesrat + urheber_bundeslaender im
Response.
- /api/analyze-drucksache (POST) lehnt BR-Drucksachen mit HTTP 400 +
klarer Meldung ab statt crashen.
- v2-Search-UI rendert grayen Bundesrat-Sticker mit BL-Codes statt
Fraktionen, "Analysieren"-Button durch "nicht unterstuetzt" ersetzt.
is_bundesrat_drucksache() in drucksache_typen.py als Format-Helper
(N/M/JJ-Pattern) bleibt fuer Cases wo nur die Drucksache-ID ohne
Adapter-Metadaten verfuegbar ist.
Refs: #6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- get_empfehlungs_konsistenz_cases() liefert Antraege wo `partei` mit
NEIN gestimmt hat, obwohl die GWÖ-Empfehlung "Unterstuetzen" lautete.
- Endpoint GET /api/auswertungen/empfehlungs-konsistenz-cases
- Frontend: Konsistenz-Bar bekommt onClick → Modal-Tabelle mit Drucksache,
BL, Datum, GWÖ-Score, Empfehlung, Beschluss. Drucksachen-Link ins Detail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 8 (Code-Pflege):
- Neue Test-Datei tests/test_auto_rate_runs.py (9 Cases) deckt
record_auto_rate_run, list_auto_rate_runs, auto_rate_today_total
und das Schema ab.
- list_auto_rate_runs sortiert jetzt by id DESC (statt started_at DESC),
weil started_at nur sekundengenau ist und Sub-Sekunden-Inserts
unstabilen Output produzierten.
- ruff --select F401 --fix auf main.py: 7 ungenutzte Imports entfernt
(MAX_SEARCH_QUERY_LEN, import_json_assessments, KLEINE_ANFRAGE,
BUNDESLAENDER, lokale sqlite3/json/timezone-Reimports). Tests
weiterhin grün (74 passed).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Schema additiv: presse_drafts.style TEXT NOT NULL DEFAULT 'pm' via
ALTER TABLE (idempotent in init_db).
- presse_generator.generate_draft(style='pm'|'thread') nutzt eigenen
SYSTEM_PROMPT_THREAD (3-5 Posts à ≤280 Zeichen, Hook + Lebenslagen +
Forderung, Hashtags am Schluss; keine **fett**-Markdown).
- _find_existing_draft, list_drafts, list_drafts_for, get_draft liefern
jetzt auch das style-Feld zurueck.
- Endpoint /api/aktuelle-themen/generate-presse?style=thread baut den
Switch ein. Ohne Param weiterhin 'pm'.
- Frontend: PM-Modal zeigt den style-Tag (📰 PM / 🐦 Thread) im Banner
und bietet einen Knopf "Auch als Thread / Auch als PM" generieren.
Idempotenz pro (drucksache, news_url, style)-Tripel.
Refs: #170, #178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Endpoint GET /api/aktuelle-themen/drafts/{id}.pdf rendert den
gespeicherten PM-Body inkl. **fett**-Markdown als A4-PDF mit Header
(Drucksache-Link, GWÖ-Markup) und Footer-Quellenangabe.
PM-Modal in /aktuelle-themen bekommt zusätzlich einen 📄 PDF-Button
neben Mail/Clipboard. Dateiname `PM-DRUCKSACHE-DRAFTID.pdf`.
Refs: #170, #177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 (Vote-Orphans-Auto-Bewertung):
- Neue Tabelle `auto_rate_runs` (additiv) mit started_at, source,
bundesland, limit_requested, n_attempted/succeeded/failed/skipped,
error_summary.
- Neue DB-Helper: record_auto_rate_run, list_auto_rate_runs,
auto_rate_today_total.
- POST /api/auswertungen/vote-orphans/auto-rate erweitert um source,
daily_cap und Run-Persistenz. Throttled gegen Tagessumme.
- Neuer Endpoint GET /api/auto-rate-runs (admin) — letzte N Runs +
Tagessumme.
- scripts/auto-rate-orphans.sh: Cron-Wrapper (analog auto-fetch-news.sh)
mit MAX_PER_RUN=30 / MAX_PER_DAY=200 Defaults, BUNDESLAND-Filter
optional, ruft direkt die Python-Worker-Funktion via docker exec.
- Admin-Stand-Dashboard: KPI-Zeile "heute X Runs / Y versucht" + Tabelle
der letzten 5 Runs mit BL/Counts/Notiz.
Refs: #173, ADR 0010
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Klick auf eine Heuchelei-Bar oeffnet ein Modal mit der konkreten
Liste der Antraege wo die Fraktion mit Nein gestimmt hat, obwohl
der Antrag inhaltlich zum eigenen Wahlprogramm passt.
- Backend: app.auswertungen.get_heuchelei_cases() + Endpoint
GET /api/auswertungen/heuchelei-cases?partei=X[&bundesland=Y].
- Backend: _load_assessments_with_votes liefert jetzt zusaetzlich
das ergebnis-Feld (additiv im SELECT).
- Frontend: onClick-Handler im Heuchelei-Bar-Chart, Modal-Markup
wird lazy injiziert, Tabellen-Drilldown mit Drucksachen-Link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Route /stimmverhalten rendert dieselbe auswertungen.html, aber
mit default_tab='stimmverhalten' und v2_active_nav='stimmverhalten'.
Linker Nav-Eintrag 'Stimmverhalten' (Icon scales) zwischen
Auswertungen und Aktuelle Themen.
Beim Page-Load aktiviert das DOMContentLoaded-Handler den im Context
gesetzten Tab — fuer /auswertungen ist es 'bl-partei' (Default), fuer
/stimmverhalten direkt 'stimmverhalten'. Kein Code-Duplikat im
Tab-Inhalt.
Refs: #169
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetrisch zur Heuchelei-Logik: bei JA-Fraktionen, deren eigener
Wahlprogramm-Score < 3 ist, erscheint ein dezenter italic '!' mit
Tooltip. 11 echte Cases gefunden auf dev (NRW + BB).
app/marker.py: opportunismus_score() — neun neue Tests (test_marker.py
jetzt 44 grün).
Refs: ADR 0010, Phase 2.4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logik aus dem Jinja-Template (Heuchelei-Marker, Konsistenz-Block,
decisive-Outcome-Selection) in app/marker.py extrahiert. Template
ruft die drei Helper als Jinja-Globals auf. Damit ist die Logik
testbar ohne Render-Kontext.
Plus: app/pm_render.py als Python-Spiegelbild des JS-Mini-Markdown-
Renderers in aktuelle-themen.html — fuer Tests und potenzielle
Server-side-Render-Optionen (z.B. PM-Mail).
Tests:
- tests/test_marker.py (35 Cases): heuchelei_score, decisive_outcome,
consistency_state inkl. Multi-Vote, ambivalente Empfehlung,
Edge-Cases.
- tests/test_pm_render.py (21 Cases): Bold, Italic, Listen,
HTML-Escape, Paragraph-Splitting, snake_case-Schutz.
Refs: ADR 0010, ADR 0011
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sechs zusammengehoerige UX/Performance-Erweiterungen:
**1. /v2/admin/stand — System-Stand-Dashboard**
KPI-Kacheln (Bewertungen, Plenum-Votes, Match, Vote-Orphans, News, PM-
Drafts, Bookmarks) + GWÖ-Score-Histogram + Per-BL-Tabelle + News-Source-
Tabelle. Auto-Refresh 30 s. Endpoint /api/admin/stand liefert alles in
einem Roundtrip. Nav-Eintrag "Stand" in der Admin-Sektion.
**2. /auswertungen Score-Histogram-Tab**
4. Tab "Score-Verteilung" mit Bar-Chart 0–10. Endpoint
/api/auswertungen/score-histogram liefert Buckets, optional gefiltert
nach Bundesland + Wahlperiode. Reagiert auf den globalen BL-Filter.
**3. PM-Body Markdown-Rendering**
Mini-Renderer im Modal: **bold** / __bold__ / *italic* / _italic_ /
- list-bullets / Doppel-Newline-Paragraphen. Kein externer Markdown-
Parser, keine neue Dependency. Body wird HTML-escaped, Patterns dann
zu Tags umgesetzt.
**4. Performance-Cache fuer themen_matching**
TTL-Cache (60 s) fuer aggregate_top_themen und aggregate_news_cluster.
Cache-Key inkl. aller Filter-Parameter. Automatische Invalidation in
news_aggregator.run_aggregator nach erfolgreichem Insert/Embed.
4 neue Tests fuer cache_get/set/clear-Verhalten.
**5. Stimmverhalten Banner Live-Update**
Statt setTimeout(800) jetzt pollQueueUntilDrained: alle 4 s
GET /api/queue/status, Banner zeigt pending + elapsed live. Bei
pending=0 zwei Polls in Folge: Banner + Stimmverhalten-Charts neu
laden. Max 5 Min Polling-Timeout. Bricht ab wenn Tab gewechselt wird.
**6. Antrag-Detail Cluster-Indicator**
News-Match-Box im Antrag-Detail laedt parallel /aktuelle-themen/cluster
und mappt URL → Cluster. Pro News-Card ein "🔗 Cluster (N News)"-Badge
mit Hover-Tooltip der anderen Cluster-Members. Macht thematische
Bündel sichtbar, ohne Pop-Out auf den Cluster-Tab.
Suite: 1088 → 1092 grün (4 Cache-Tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Datenlage auf dev: 7281 Plenum-Votes, 96 Bewertungen, nur 19 Matches.
Stimmverhalten-Tab zeigt fast nichts, weil die meisten Vote-Drucksachen
keine Bewertung haben. Issue #172 schliesst die Luecke.
**Banner im Stimmverhalten-Tab:**
- Zeigt Anzahl + Verteilung pro BL der "Vote-only"-Drucksachen
- Nur sichtbar wenn count > 0
- Aktion: "Auto-Bewerten Top-N" mit Limit-Selector (5/10/20)
**Endpoint `GET /api/auswertungen/vote-orphans`:**
LEFT JOIN plenum_vote_results vs assessments, count + by_bundesland +
Top-N items sortiert nach parsed_at desc.
**Endpoint `POST /api/auswertungen/vote-orphans/auto-rate`:**
Admin-only, rate-limited 3/min. Nimmt Top-N Orphans, lädt Antragstext
per Adapter, enqueued einen Bewertungs-Job pro Drucksache. Defaults
limit=10, max 50. Per-skipped-reason-Liste in der Response (Adapter
fehlt, Empty-Text, Queue-full, etc.).
**Tests:** 4 neue (`TestGetVoteOrphans`), Suite 1071 gruen.
Helper `_enqueue_for_bl` aus dem Batch-Endpoint wird hier indirekt
wiederverwendet (gleiche Job-Queue-Pipeline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Wunsch: Batch-Analyse soll auch Anträge aus mehreren BL gleichzeitig
ranziehen koennen, nicht nur einen einzelnen.
- Neue Dropdown-Option "— Alle aktiven Bundesländer (Limit verteilt) —"
als Default
- Backend: bei `bundesland=ALL` iteriert ueber `aktive_bundeslaender()`
und verteilt das Limit proportional (limit // N pro BL).
- Helper `_enqueue_for_bl()` extrahiert die BL-spezifische Logik.
- Adapter-Fehler pro BL werden geloggt + skipt, blockieren nicht die
anderen BL.
- Response-Erweiterung: `per_bundesland`-Liste mit Per-BL-Stats
(enqueued / skipped_existing / error).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback: "Welche Meldungen werden da angezeigt? Es wurden ja viel
mehr indiziert."
**1. Transparenz-Banner im News-Tab**
Zeigt jetzt explizit:
- "X News angezeigt"
- "Y News im Zeitraum (mit Embedding)"
- "Z News insgesamt embedded"
- Hinweis wenn only_relevant aktiv ist
- Hinweis wenn top_k limitierend ist
**2. Chart als Filter** — Klick auf einen Tag im News-Volumen-Chart
wechselt zum News-Tab und filtert auf diesen Tag.
- Chart bekommt onClick-Handler ueber getElementsAtEventForMode
- Cursor wechselt bei Hover ueber Datenpunkte
- Im News-Tab erscheint Pill "Tag: 2026-05-01 [× Tag-Filter entfernen]"
**3. Backend `single_date`-Param**
`aggregate_top_themen(single_date="YYYY-MM-DD")` filtert auf genau
diesen Tag (overrides days_window). Endpoint: `/api/aktuelle-themen/top
?date=YYYY-MM-DD`. Response neu: `n_in_window`, `n_shown`,
`filter.single_date`.
**4. Default top_k 20 → 50** (max 200), damit weniger oft auf
"top_k limitierend" gestoßen wird.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback nach Live-Test:
**1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert,
doppelter Klick erzeugte doppelten Draft + LLM-Kosten.
- Neuer Helper `_find_existing_draft(drucksache, news_url)` der den
neuesten Draft fuer das Paar zurueckgibt
- `generate_draft()` prueft per Default zuerst den Lookup, liefert
existing zurueck mit `_was_existing=True` (kein LLM-Call)
- `force=True` Parameter fuer bewusste Neu-Generierung
- Endpoint nimmt `?force=true` Query-Param entgegen
- UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner,
mit "Neu generieren"-Button im existing-Banner
**2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet
("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein
Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max).
- Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer
(~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in
Pressemitteilungs-Diktion
- confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s
**3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt:
- Container-Check (skip wenn down) analog zu run-digest.sh
- START/END-Timestamps
- Ausfuehrliche cron-install-Doku im Header
- Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit),
wird embed_pending_articles bis zu 500 weitere nachgeholt
Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite
1053 gruen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stimm-Index pro Fraktion über Quartale. Linien-Chart pro Fraktion,
Lücken bei Quartalen mit n<3 (Ja UND Nein). Macht sichtbar, ob sich die
Gemeinwohl-Affinität einer Fraktion innerhalb der Wahlperiode verschiebt.
- `_quarter_for(datum)` Helper: ISO-Datum → "YYYY-Qn".
- `aggregate_stimm_index_zeitreihe()` analog zu pro_wert/pro_gruppe,
aber nach Quartal-Bucket statt Achse.
- `GET /api/auswertungen/stimm-index-zeitreihe?parteien=CDU,SPD,...`
- 4. Sub-Section im Stimmverhalten-Tab: Multi-Linien-Chart mit
Partei-Farben (CDU schwarz, SPD rot, GRÜNE grün, FDP gelb, AfD blau,
LINKE pink, BSW lila, SSW navy, BVB-FW orange).
Bei aktueller Sparse-Datenmenge (35 Assessments × 4 Quartale) ist der
Chart heute meist leer — Infrastruktur ist ready, fuellt sich automatisch
mit Issue #44 Batch-Bewertung.
Tests: 10 neue (4 _quarter_for, 6 aggregate). Suite jetzt 1005 grün.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase-2-Erweiterungen des Stimmverhalten-Tabs:
**1. Empfehlungs-Konsistenz (#167):**
Pro Fraktion: Anteil der Anträge mit GWÖ-Empfehlung
"Uneingeschränkt unterstützen" oder "Unterstützen mit Änderungen",
bei denen die Fraktion trotzdem NEIN gestimmt hat. Orthogonal zur
Heuchelei-Quote — prüft NICHT gegen Wahlprogramm-Treue, sondern gegen
die GWÖ-Empfehlung des Systems.
- `aggregate_empfehlungs_konsistenz()` in app/auswertungen.py
- `GET /api/auswertungen/empfehlungs-konsistenz`
- 5. Chart-Sub-Section im Stimmverhalten-Tab (rote Bar Chart, 0..100%)
**2. CSV-Export (Phase-1-Querschnitts-TODO):**
Long-Format-CSV mit Spalten: drucksache, bundesland, wahlperiode, datum,
gwoe_score, empfehlung, partei, vote, ist_antragsteller. Macht alle
Stimmverhalten-Aussagen wissenschaftlich auswertbar (R/pandas/Excel).
- `export_stimmverhalten_csv()` in app/auswertungen.py
- `GET /api/auswertungen/stimmverhalten.csv` mit
Filter-Parametern bundesland/wahlperiode/exclude_antragsteller
- "CSV-Export"-Button im Stimmverhalten-Tab neben dem Toggle
**Tests:** 27 Stimmverhalten-Tests (war 18, +4 Empfehlungs-Konsistenz,
+5 CSV-Export). Fixture um `empfehlung`-Spalte erweitert.
Suite: 989 Tests grün (war 980).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pro BL zeigt die Tabelle nun:
- Doku-System (wie bisher)
- Drucksachen: alle aktiv (Adapter laufen)
- Plenum-Votes: 'aktiv' wenn Parser registriert (NRW), sonst 'Stub'
Plus Erklär-Hinweis: 'Plenum-Votes = fraktions-aggregierte
Abstimmungsergebnisse aus den Plenarprotokollen (#106). Stubs sind
Tracking-Stellen fuer kuenftige Implementierungen (Issues #148-#163).'
main.py reicht supported_bundeslaender() aus protokoll_parsers an die
Template-Context durch (plenum_vote_parsers-Set).
Antrag-Detail-Endpoint liest plenum_votes via get_plenum_votes() und
reicht sie an antrag_detail.html durch.
Block rendert pro Plenum-Abstimmung eine Karte:
- Ergebnis (angenommen/abgelehnt/...) farb-kodiert
- 'einstimmig'-Annotation falls gesetzt
- Quelle (Protokoll-ID, mit URL als Tooltip)
- Fraktions-Chips fuer Ja/Nein/Enthaltung
Mehrfach-Abstimmungen einer Drucksache (Ueberweisung + finale
Beschlussfassung) erzeugen mehrere Karten — chronologisch via
parsed_at DESC im Repository sortiert.
Block erscheint nur, wenn Eintraege existieren (kein leerer Header).
System- und User-Prompt-Template stehen jetzt collapsed unter dem
neuen Abschnitt 'LLM-Prompts'. Der User-Prompt wird auf eine eigene
Konstante USER_PROMPT_TEMPLATE umgestellt und via .format(...) gerendert,
sodass das gleiche Template auf der Methodik-Seite gezeigt werden kann
ohne den f-string-Code zu duplizieren.
Closes#145
Dev-Container setzt GITEA_FEEDBACK_LABELS=feedback,dev, damit
Feedback-Issues aus gwoe-dev.toppyr.de unterscheidbar markiert werden.
Label-Farben: feedback rot, dev gelb, Sonst grau.
Teil der Container-Duplikation fuer v1.x-Entwicklung.