Commit Graph

87 Commits

Author SHA1 Message Date
Dotty Dotter
1350fc7f52 feat: Scorecard Portrait — Claude-Design 1:1 übernommen (1080×1350)
User hat eine Design-Vorgabe als ZIP geliefert (GWÖ Antrag Score Card.zip,
Claude Design Output, Stand 2026-05-07). Komplett anderes Konzept als
mein vorheriges Layout — nicht eine 5×5-Matrix als Datenviz, sondern eine
editorial-magazin-mässige Card mit klaren Modulen.

Übernommen 1:1:
- Inter + JetBrains Mono via Google Fonts
- 3-Zonen-Grid: Header 88px / Body / Footer 96px
- Paper-BG #f5f1ea, Ink #1a1a1a, GWÖ-Grün gedeckt #0a5d3f
- Header-Strip: Brand-Dot + 'GWÖ-Antragsprüfer' / 'Matrix 2.0' / 'NRW · WP18'
- Topline: Drs.-ID + Antragsteller-Pill (Partei-Farbpunkt)
- H1 Antragstitel 78pt, weight 800, line-height 0.95
- Lede: 1-Satz-Zusammenfassung, max 180 Zeichen
- Score-Block: 320px breite gruene Kachel mit 9.0/10 + 'Empfehlung' kicker
  + Verdict-Text + 3 Schwerpunkt-Chips (Code + Wert-kurz + Symbol)
- Fraktions-Grid: bis zu 5 Spalten, pro Fraktion Name + WP-Bar + Score
  + Vote-Label (Ja ✓ / Nein ✗); weak-Klasse (rot) bei Score<5 oder Nein
- Decision-Bar: invertiert schwarz, Beschluss-Text mit ✓/✗-Akzent
- Footer: URL + /antrag/-Pfad + CC BY 4.0 + QR-Pattern

Datenaggregation in main.py erweitert:
- matrix_chips: Top-3 positive Felder (rating > 0) mit Code+Wert+Symbol
- fraktionen_bars: aus wahlprogramm_scores + plenum_votes-Lookup
  (WP-Bar + Vote-Label pro Fraktion)
- beschluss: aus erstem plenum_vote (ergebnis + Mehrheits-Verhältnis)
- wahlperiode: via wahlperioden.wahlperiode_for(datum, BL)

Routing:
- /v2/scorecard?format=portrait → scorecard_portrait.html (NEU)
- /v2/scorecard?format=square|og → scorecard.html (alt, unveraendert)
- /api/assessment/scorecard.png?format=portrait nutzt ebenfalls das neue
  Portrait-Template fuer die PNG-Generierung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:55:16 +02:00
Dotty Dotter
8ae2b92313 feat: Scorecard-Werkstatt — Live-Editor unter /v2/scorecard-werkstatt
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>
2026-05-07 13:39:45 +02:00
Dotty Dotter
d470e03caf feat: Scorecard Portrait redesign — Matrix mit Achsen-Labels, flex-grow gegen Slack
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>
2026-05-07 13:32:44 +02:00
Dotty Dotter
0e5b2180ab feat: Scorecard-Default = portrait, Instagram via Web-Share-API, Padding gestrafft
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>
2026-05-07 13:06:46 +02:00
Dotty Dotter
8c8dfbe625 feat: Scorecard im Instagram 4:5 Hochformat (1080×1350) — Title + Score + Matrix prominenter
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>
2026-05-07 13:02:04 +02:00
Dotty Dotter
1ef5578e02 feat(v3): v3 wird Default unter /antrag/{drs}, v2 zieht nach /v2/antrag/{drs}
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>
2026-05-07 11:29:06 +02:00
Dotty Dotter
f471586f33 feat(v3): Themen, Kernpunkte, Schwerpunkt, Konfidenz, fehlende Programme, Wahlperiode + Ähnliche Anträge
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>
2026-05-07 11:01:18 +02:00
Dotty Dotter
c4750d3274 feat(v3): Bürger:innen-Modus-Sandbox unter /v3/antrag/{drs}
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>
2026-05-07 09:55:06 +02:00
Dotty Dotter
c4f8ce398a feat: Matrix-Klick mit Antrags-Begründung + Vote-Block-Verbesserungen
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>
2026-05-07 09:33:53 +02:00
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
Dotty Dotter
045461492f fix: ist_antragsteller/ist_regierung Fallback aus Drucksachen-Metadaten
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.
2026-05-07 09:10:49 +02:00
Dotty Dotter
d8999f8a64 fix(#180): @page-Größe in pt statt px für korrekte PNG-Pixel-Dimensionen
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.
2026-05-07 08:04:56 +02:00
Dotty Dotter
52ff36a136 feat(#180): PNG-Scorecards via WeasyPrint→PyMuPDF
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>
2026-05-07 08:03:38 +02:00
Dotty Dotter
727d7d2976 fix(#179): Scorecard via WeasyPrint-PDF (Container hat kein Chromium)
Container hat keine Playwright/Chromium-Installation — PNG-Render
deferred. WeasyPrint-PDF mit exaktem Seiten-Setup (1200x630pt fuer
og, 1080x1080pt fuer square) liefert jetzt /api/assessment/scorecard.pdf.

Folge-Issue fuer PNG: Chromium ins Image bauen oder pypdfium2/pdf2image
fuer pdf→png-Konvertierung ergaenzen.
2026-05-06 23:46:01 +02:00
Dotty Dotter
1faf4e9220 feat(#179 Phase 15): Scorecards — HTML-Template + PNG-Endpoint
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>
2026-05-06 23:44:21 +02:00
Dotty Dotter
ee93fcd76a fix(#177): _row_to_detail Matrix-Rating-Shift entfernt — Wurzel von '+/++ gleichfarbig'
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>
2026-05-06 23:39:52 +02:00
Dotty Dotter
5667259bff feat(#6 Phase 11): Bundesrats-Drucksachen erkennen + markieren + ablehnen
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>
2026-05-06 23:29:12 +02:00
Dotty Dotter
1dfdb59954 feat(#189 Phase 10.2): Empfehlungs-Konsistenz-Drilldown analog zu Heuchelei
- 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>
2026-05-06 17:11:34 +02:00
Dotty Dotter
62636b5a78 quality: 9 Tests fuer auto_rate_runs + ruff F401 in main.py
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>
2026-05-06 16:58:49 +02:00
Dotty Dotter
092a68ac02 fix(#177): f-string ohne Backslash (Py3.9-Kompat fuer Local-Tests) 2026-05-06 16:21:17 +02:00
Dotty Dotter
a2b8f8c6fe feat(#178 Phase 4.2): PM-Variante 'thread' fuer Mastodon/Twitter-Threads
- 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>
2026-05-06 16:11:16 +02:00
Dotty Dotter
4bb267aace fix(#177): PDF-Endpoint auf /drafts/pdf/{id} (Routing-Konflikt mit {draft_id:int}) 2026-05-06 16:06:57 +02:00
Dotty Dotter
d8d7c3f02f feat(#177 Phase 4.1): PM-PDF-Render via WeasyPrint
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>
2026-05-06 16:05:57 +02:00
Dotty Dotter
c241d329aa feat(#173): Vote-Orphans-Auto-Bewertung als Cron-Job + Tracking
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>
2026-05-06 16:02:33 +02:00
Dotty Dotter
7a1c37afe4 feat(#170 Phase 2.2): Drilldown-Modal von Heuchelei-Bar zu Antragsliste
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>
2026-05-06 15:54:51 +02:00
Dotty Dotter
741faae8ff feat(#169): eigene /stimmverhalten-View als linker Nav-Eintrag
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>
2026-05-06 15:52:09 +02:00
Dotty Dotter
5823828fec feat: Opportunismus-Marker bei JA-Stimmen mit WP-Score < 3
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>
2026-05-06 15:48:06 +02:00
Dotty Dotter
9498ca4b97 refactor + tests: marker.py + pm_render.py mit 56 Unit-Tests
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>
2026-05-06 15:44:12 +02:00
Dotty Dotter
d30fcb132a feat: Stand-Dashboard, Score-Histogram, PM-Markdown, Live-Polling, Cluster-Indicator
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>
2026-05-06 02:49:06 +02:00
Dotty Dotter
8136a1a10b feat(#172): Vote-Orphans-Banner + Bulk-Auto-Bewerten-Endpoint
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>
2026-05-06 02:03:23 +02:00
Dotty Dotter
48a272a87d feat: Batch-Analyse mit "Alle Bundeslaender"-Modus
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>
2026-05-06 02:00:03 +02:00
Dotty Dotter
7c1e0fa0b0 feat(#170): Chart-Click-Tag-Filter + Transparenz-Banner + top_k 50 default
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>
2026-05-03 21:24:38 +02:00
Dotty Dotter
e27dfc30a2 feat(#170 followup 2): Pre-Filter, Cluster, Antrags-Initiative, PM-Versionierung, Mail-Link
User-Feedback: Aktuelle-Themen-Dashboard war "Detective-Modus" — durch
viele News scrollen, Match-Stärke selbst interpretieren. Komplett-Refactor
zur kuratierten Sicht mit Tabs.

**1. Pre-Filter + GWÖ-Relevanz-Score (#134)**

`compute_relevance(matches)`: Score = max(antrag.gwoe_score × similarity).
Level: high (≥4.0) / mid (≥2.5) / low (>0) / none.
Pro News in der UI ein farbiger Pill (gruen/orange/grau) + Reason-Text:
"GWÖ-9.0/10-Antrag „Klimaschutzgesetz" (GRÜNE) passt mit Similarity 0.55."

Default-Filter "Nur GWÖ-relevant" aktiv (only_relevant=true) — zeigt
nur high/mid News, blendet Rauschen aus. Toggle-Checkbox.

`/api/aktuelle-themen/top` neuer Param `only_relevant=true|false`.

**2. PM-Versionierung im Modal (#135)**

`list_drafts_for(drucksache, news_url)`: alle Versionen, neueste oben.
Endpoint `/api/aktuelle-themen/drafts-versions`. Modal zeigt Dropdown
wenn >1 Version, Switch ohne LLM-Call. Force-Regen bleibt als Button
im "bestehender Entwurf"-Banner.

**3. News-Cluster-View (#136)**

`aggregate_news_cluster(intra_threshold=0.55, min_cluster_size=2)`:
Greedy-Embedding-Cluster + zentralster Antrags-Match per Centroid-
Vektor. Zweiter Tab "Themen-Cluster": 5 News über "Pflege" → 1 Cluster
mit gemeinsamem Antrag-Vorschlag, statt 5 separate Cards.
Endpoint: `/api/aktuelle-themen/cluster`.

**4. Mail-Direkt-Link + Clipboard (#137)**

Im PM-Modal zwei Buttons:
- "📧 Per Mail versenden" (mailto: mit subject + body, ~1900 Char Limit)
- "📋 In Zwischenablage kopieren" (navigator.clipboard.writeText)
- Bei langem PM (>1900 Char): mailto-Link wird ausgegraut, Hinweis
  "PM zu lang für Mail-Link — Clipboard nutzen"

**5. Antrags-Initiative (#138)**

`aggregate_top_antraege_with_news(min_gwoe_score=8.0, days=14)`:
Reverse-Sicht — pro Antrag mit GWÖ ≥ 8 die News-Resonanz. Antraege
ohne Match werden trotzdem angezeigt mit "keine News"-Pill.
Dritter Tab "GWÖ-Top-Anträge". Endpoint `.../top-antraege`.

**UI-Restrukturierung:** statt einer langen Scroll-Liste jetzt
5 Tabs mit gemeinsamer Filter-Bar:
- News × Anträge (Default, kuratiert via Pre-Filter)
- Themen-Cluster (Bündel ähnlicher News)
- GWÖ-Top-Anträge (Reverse)
- News-Volumen (Chart)
- PM-Entwürfe (Drafts-Liste)

Default min_similarity 0.40 → 0.50 erhoeht (weniger Rauschen).

Tests: 14 neue (compute_relevance × 5, only_relevant + sort × 3,
cluster × 3, top_antraege × 3). Suite 1067 gruen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:41:31 +02:00
Dotty Dotter
2bff943e8a feat(#170 followup): PM-Generator Idempotenz + qwen-max + Wrapper-Verbesserungen
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>
2026-05-03 13:10:20 +02:00
Dotty Dotter
d54ce23e42 feat(#170): Aktuelle-Themen-Dashboard — News × Anträge × Pressemitteilungen
Vollständiges 4-Phasen-Feature:

**Phase 1 — News-Aggregator** (`app/news_aggregator.py`)
- Tagesschau-API (`/api2u/news?ressort=...`) für inland/ausland/wirtschaft/wissen
- Bundestag-RSS für aktuellethemen / pressemitteilungen / hib
- DB-Tabelle `news_articles` (URL-PK, idempotent)
- Embeddings via existierender qwen-v4-Pipeline
- Cron-Script `scripts/auto-fetch-news.sh`
- Bewusst NICHT: RND.de (robots.txt bannt explizit ClaudeBot, GPTBot,
  CCBot, ChatGPT-User, Google-Extended). Nur AI-erlaubende, öffentlich-
  rechtliche/parlamentarische Quellen
- Volltexte werden NICHT persistiert (nur Titel + erster Satz)

**Phase 2 — Themen × Anträge Matching** (`app/themen_matching.py`)
- News-Embedding × Assessment-summary_embedding via Cosine-Similarity
- `find_anträge_for_news`: pro News die Top-K passenden Anträge
- `find_news_for_antrag`: pro Antrag Top-K News mit Datums-Fenster (90d)
- `aggregate_top_themen`: primärer Dashboard-Endpoint
- `aggregate_themen_zeitreihe`: News-Volumen pro Tag × Source

**Phase 3 — Dashboard-View** (`/aktuelle-themen`)
- Neuer linker Nav-Eintrag „Aktuelle Themen"
- Stacked-Area-Chart News-Volumen pro Quelle (30d)
- Pro News-Card: Titel + Summary + Tags + Top-3-Antrags-Match-Liste
  mit GWÖ-Score-Pill, Drucksache-Link, PM-Vorschlag-Button
- Filter: Zeitfenster, Top-N, min_similarity
- Auth-protected (require_auth)

**Phase 4 — Pressemitteilungs-Generator** (`app/presse_generator.py`)
- LLM-Prompt-Template (200-250 Worte, GWÖ-Sicht, JSON-Output)
- Reuse von `QwenBewerter` aus app/adapters/qwen_bewerter.py
- DB-Tabelle `presse_drafts` (Persistenz)
- POST `/api/aktuelle-themen/generate-presse` rate-limited 5/min,
  auth-only (LLM-Kosten)
- GET `/api/aktuelle-themen/drafts` + `/drafts/{id}` für Liste/Detail
- Manueller Trigger via UI-Button, kein Auto-Versand
- Modal-Anzeige des generierten Texts

**Compliance:**
- robots.txt-respektierend (ClaudeBot-Bann von RND vermieden, AI-
  erlaubende Quellen verwendet)
- UI zeigt nur Titel+URL+Datum+erster Satz, keine Volltext-Reproduktion
- Pressemitteilungen sind explizit Drafts, nicht Auto-Versand
- LLM-Calls rate-limited, auth-only

**Tests:** 43 neue Tests (19 news_aggregator + 16 themen_matching +
8 presse_generator). Suite jetzt 1048 grün.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 12:39:36 +02:00
Dotty Dotter
1e381d23ab feat(#168): Über-Zeit-Drift im Stimmverhalten-Tab
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>
2026-04-29 23:03:53 +02:00
Dotty Dotter
79003d6056 feat(#166): Berührungsgruppen-Aufschlüsselung im Stimmverhalten-Tab
Stimm-Index pro Beruehrungsgruppe (Matrix-Zeilen A-E) zusaetzlich zur
bestehenden Werte-Aufschluesselung (Spalten 1-5). Toggle-Buttons in der
3. Sub-Section schalten zwischen Werte/Gruppen.

- `aggregate_stimm_index_pro_gruppe()` analog zu `_pro_wert`, aber
  gruppiert nach `field[0]` (A-E) statt `field[-1]` (1-5).
- `_gruppen_score_for_assessment()` Helper.
- `GET /api/auswertungen/stimm-index-pro-gruppe`.
- UI-Toggle "Pro GWÖ-Wert" / "Pro Berührungsgruppe" mit `setMatrixAxis()`.
- 6 neue Tests, Suite jetzt 995 grün.

Beruehrungsgruppen-Labels (aus app/models.py:MATRIX_LABELS gekuerzt):
- A: Ausgelagerte Betriebe / Lieferant:innen
- B: Finanzpartner:innen / Steuerzahler:innen
- C: Politische Führung / Verwaltung / Ehrenamt
- D: Bürger:innen und Wirtschaft
- E: Staat, Gesellschaft und Natur

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 23:00:35 +02:00
Dotty Dotter
d81753c4fb feat(#167): Empfehlungs-Konsistenz + CSV-Export Stimmverhalten
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>
2026-04-29 22:56:35 +02:00
Dotty Dotter
5eabe0d9b3 feat: Stimmverhalten × Gemeinwohl-Orientierung in /auswertungen
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>
2026-04-29 15:30:02 +02:00
Dotty Dotter
30d51da5f7 feat(#106 Folge): Datenquellen-Tabelle auf /methodik um Plenum-Vote-Spalte erweitert
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
Abstimmungs­ergebnisse 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).
2026-04-28 23:12:58 +02:00
Dotty Dotter
7e0f0117e6 feat(#106): UI-Block 'Abstimmungsergebnis' auf Antrag-Detail
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).
2026-04-28 08:04:32 +02:00
Dotty Dotter
0d26cad549 feat(#145): LLM-Prompts auf /methodik als Transparenz-Block
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
2026-04-28 01:50:25 +02:00
Dotty Dotter
6d587c1f3a feat(feedback): konfigurierbare Issue-Labels via GITEA_FEEDBACK_LABELS
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.
2026-04-28 01:31:25 +02:00
Dotty Dotter
a8d7b72702 feat(v2): Feedback-Widget mit Audit-Trail + Screenshot + direkter Gitea-Anbindung
- Component v2/components/feedback_widget.html: Button unten links oberhalb der
  Queue, Klick oeffnet Modal mit vorausgefuellten Kontext-Feldern (URL,
  Drucksache, Viewport, User-Agent, letzte 15 Klicks, letzte 10 Console-Errors,
  letzte 5 Page-Loads). Eingaben: Titel, Beschreibung, optional Screenshot
- Audit-Trail-Sammler in localStorage (Ringbuffer 30 Klicks, 10 Errors)
- Screenshot via self-hosted html2canvas 1.4.1 (194 KB unter app/static/v2/lib/)
- Backend POST /api/feedback (rate-limit 5/h):
  - validiert + html-strippt Inputs
  - erstellt Gitea-Issue per API mit Label 'feedback' (Label wird idempotent angelegt)
  - laedt Screenshot als Issue-Asset hoch (Gitea Issue-Attachment-API)
- 4 neue Settings: gitea_token, gitea_api_url, gitea_repo_owner, gitea_repo_name
- Server .env um GITEA_TOKEN ergaenzt
- 10 neue Unit-Tests (mit gemocktem httpx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:00:44 +02:00
Dotty Dotter
98787c8684 fix(v2): Cache-Buster fuer CSS via ?v=app_version
Browser-Cache zeigte alte v2.css ohne v2-menu-toggle-display:none-Regel.
Mit ?v=1.0.0 wird auf Versionsspruenge sauber neu geladen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:33:18 +02:00
Dotty Dotter
7a64335e64 feat(auth): 'Passwort vergessen?'-Link im v2-Login-Modal
Klick öffnet /api/auth/forgot-password → 302 zur Keycloak-Reset-Page mit
client_id + redirect_uri (auf eigene Domain). Keycloak schickt Mail mit
Reset-Link, User setzt neues Passwort, kommt zurück.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:21:02 +02:00
Dotty Dotter
c1926ada4f feat(#143): Registrierungs-Bestätigungsmail an User direkt nach Anmeldung
Vorher: User registriert -> Keycloak-User mit enabled=false angelegt -> KEINE
Mail bis Admin manuell freischaltet. UX-Luecke: User weiss zwischen Klick und
Admin-Freischaltung nicht, ob etwas passiert ist.

Jetzt: nach erfolgreichem Keycloak-User-Create wird sofort eine Bestaetigungs-
Mail an die angegebene Adresse geschickt mit Hinweis auf den 3-Schritt-Flow
(Anmeldung -> Admin-Freischaltung -> Passwort-Setzen-Mail). Plain-Text + HTML.
Fehler beim Mail-Versand wird geloggt aber nicht weitergereicht — User-Anlage
ist davon unabhaengig.

Response-Message angepasst: 'Wir haben dir eine Bestaetigung per E-Mail geschickt.'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:40:04 +02:00
Dotty Dotter
6581acd28e ux(v2): Partei-Dropdown statt Freitext in /v2/abos und /v2/feed
Beide Routes liefern jetzt all_canonical_keys() (ohne Landesregierung) als Dropdown-
Optionen. Verhindert Tippfehler und gibt nur tatsaechlich erkannte Parteien zur Auswahl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:37:31 +02:00
Dotty Dotter
7cbd46f88d feat(v2): Atom-Feed-Konfig-Seite + Eigene-Abos-Verwaltung
Backend (Filter sind seit jeher da):
- /api/feed.xml?bundesland=&partei=&limit=
- /api/subscriptions GET/POST/DELETE

UI:
- /v2/feed: Form mit BL/Partei/Limit, generiert Feed-URL live, Buttons Oeffnen/
  URL-Kopieren/In-Feedly. Default-BL aus Header-Selektor uebernommen
- /v2/abos: Liste eigener Abos + Form zum Anlegen/Loeschen, BL-Dropdown,
  Partei-Freitext, Frequenz daily/weekly
- Sidebar 'Daten'-Gruppe um beide Eintraege erweitert (statt Direkt-Link auf
  /api/feed.xml)
- Beide Routen mit Depends(require_auth) — Anonyme bekommen 401-Redirect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:34:55 +02:00
Dotty Dotter
7f070b5e6c fix(v2): Topbar harte Hoehe 32px + Kleine-Anfragen-Heuristik in Landtag-Suche
Topbar:
- height: 32px (statt auto), line-height: 1, alle children max 24px
- Topbar-Icons explizit auf 12x12 (statt 14)
- selects/buttons/a mit fester Hoehe 22px, padding 2px 6px

Landtag-Suche:
- search_landtag filtert jetzt Drucksachen aus, deren Titel typische
  Frage-Praefixe haben (Welche/Wie viele/Wann/Was/Hat/Ist/...)  oder mit '?'
  enden — bei NRW-OPAL liefert der Adapter alle als 'sonstige', daher
  Title-Heuristik. Server-side, damit alle Adapter profitieren.
- Neuer Helper drucksache_typen.likely_kleine_anfrage_titel()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:23:22 +02:00