Commit Graph

364 Commits

Author SHA1 Message Date
Dotty Dotter
56c68d3398 fix: Cluster-Liste leer — Backend-Felder drucksachen/avg_gwoe_score
Backend liefert seit ueber den Refactor fields drucksachen/
avg_gwoe_score; Frontend-Template las members/avg_score → leere
Cluster-Cards. Beide Schluessel akzeptieren (Backwards-Compat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:52:36 +02:00
Dotty Dotter
c3d4ab186f fix: icon()-Macro mit ignore-missing + Coverage-Test
Folge zum scales.svg-Vorfall (commit 01ea766):

1. icon.html: `{% include … ignore missing %}` — fehlende SVG-Files
   rendern jetzt leeren Span statt einen 500 auszuloesen. data-icon-
   Attribut zeigt den angefragten Namen, hilft im DevTools-Inspector.
2. tests/test_icons.py: scannt alle templates/-Files nach
   icon("name")-Aufrufen und prueft, dass jedes referenzierte Icon
   als SVG-File existiert. 4 Tests, alle gruen — verhindert dass
   solche Aufrufe in Zukunft unentdeckt durchrutschen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:35:59 +02:00
Dotty Dotter
01ea7665cc fix: 500 nach Login durch fehlendes 'scales'-Icon — circle-half nutzen
Der Stimmverhalten-Nav-Eintrag (#169) referenzierte ein
phosphor/scales.svg-Icon, das nicht im Repo liegt. Folge: Jinja-
TemplateNotFound bei jedem Render von base.html nach Auth → 500
auf jeder authenticated Page.

Fix: circle-half (existierendes Icon, semantisch passend fuer
Pro/Contra-Balance).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:32:38 +02:00
Dotty Dotter
db2fdda66b test(#191 Phase 10.4): 10 Tests fuer presse_generator Style-Switch
MockBewerter zeichnet system_prompt + user_prompt + model auf, damit
der Style-Switch isoliert testbar ist.

Coverage:
- TestStyleSwitch: 'pm'/'thread'/'invalid' nutzen den richtigen Prompt
- TestPersist: style-Wert wird korrekt in presse_drafts gespeichert
- TestIdempotenz: gleiche (ds, url, style) liefert Cache-Treffer; pm und
  thread fuer gleiches Paar liefern getrennte Drafts; force=True umgeht
  den Cache
- TestThreadAutoSplit: Auto-Splitter aktiviert sich bei zu langen
  Threads ohne \\n\\n-Trenner; bereits gesplittete Threads bleiben

10 passed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:15:21 +02:00
Dotty Dotter
f660c89a63 feat(#190 Phase 10.3): Vote-Mehrheits-Bar im Antrag-Detail
Stacked Bar (Ja gruen / Enth grau / Nein rot) zeigt die Fraktions-
Mehrheit pro Plenum-Vote. Caveat-Tooltip ⓘ stellt klar: Anzahl
Fraktionen, nicht Sitz-/Stimm-Anteile (Plenarprotokoll liefert
keine Sitz-Counts).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:12:39 +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
ba1f104c8e feat(#178 Folge): Thread-Auto-Splitter + Quality-Audit-Skript
- _split_into_thread_posts() splittet zu lange Bodies an Satzgrenzen
  in mehrere Posts ≤ max_chars (Default 280). Greedy: möglichst viele
  Sätze pro Post. Hashtags am Ende bleiben erhalten.
- generate_draft(style='thread') ruft den Splitter auf, wenn das LLM
  weniger als 3 Posts oder Posts > 290 chars liefert.
- 7 Unit-Tests fuer den Splitter (test_thread_splitter.py).
- scripts/pm-quality-audit.sh: prueft alle PM-Drafts gegen Verbotsliste
  (GWÖ-Score, Matrix-Codes, Floskeln) + Wortzahl + Absatzzahl + Post-Laengen.
  Markdown-Report-Output. Audit von 23 Drafts: 4/23 ohne Auffaelligkeit;
  Hauptbefund: PMs haeufig zu kurz, Threads splittten ohne Auto-Splitter
  nicht zuverlaessig — Splitter behebt das.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:08:57 +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
eb74ae647e fix(#178): Thread-Prompt verschaerft fuer 280-Zeichen-Posts 2026-05-06 16:14:11 +02:00
Dotty Dotter
6a78dee2d1 feat(#179 Phase 4.3): pm-sample-bundle.sh fuer 5 PMs (PM + Thread) zur Sichtung
Skript laeuft fuer N_SAMPLES (Default 5) hochbewertete Antraege jeweils
generate_draft() mit style='pm' und style='thread' aus. Idempotent ueber
das presse_drafts.style-Schema.

Manueller Aufruf:
  ./scripts/pm-sample-bundle.sh gwoe-antragspruefer-dev

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:12:41 +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
3bfe9f425f fix(#173): docker exec -e Flags vor Container-Name 2026-05-06 16:03:25 +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
1a94b27a22 ux(#171): Mobile-Polish fuer Tab-Bars + Drilldown-Modal
- auswertungen.html: .auswert-tabs scrollt jetzt horizontal (overflow-x:auto)
  + nowrap-Buttons + kleinere Padding/Font auf <600px.
- aktuelle-themen.html: .at-tab Buttons whitespace:nowrap, Tab-Container
  ebenfalls scrollbar.
- Drilldown-Modal: 8px statt 20px Padding aussen, Tabelle in
  overflow-x-Wrapper, max-height 90vh statt 80vh.

Visueller Test auf 375px steht aus (kein Browser im Build-Setup),
diese Aenderungen folgen aus statischer CSS-Audit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:57:40 +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
a8f85bf3ee test: 11 weitere Smoketests fuer Stimmverhalten-Endpoints
Smoketests fuer alle 7 Stimmverhalten-Aggregat-Endpoints (stimm-index,
heuchelei, empfehlungs-konsistenz, pro-wert, cross-bl, zeitreihe) plus
zwei CSV-Tests (Header-Spalten + konsistente Datenzeilen-Spaltenzahl).

Refs: ADR 0010

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:45:59 +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
abb6cf81a8 docs: API-Reference + mkdocs-Nav um Stimmverhalten/Aktuelle-Themen/Admin
api.md ergaenzt um die ~20 neuen Endpoints (Stimmverhalten-Aggregate,
Aktuelle-Themen, PM-Drafts, Admin-Stand, Auth, Score-Histogram,
Vote-Orphans). Filter-Parameter-Tabelle dokumentiert.

mkdocs.yml-Nav vollstaendig auf alle 11 ADRs erweitert plus
Plenum-Vote-Parser-Roadmap unter "Analysen".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:40:58 +02:00
Dotty Dotter
1ba9d8e5d9 docs(adr): 0010 Stimmverhalten×GWÖ-Aggregat + 0011 Aktuelle-Themen+PM
ADR 0010 dokumentiert die JOIN-Aggregat-Entscheidung (Option B) mit
4 Aggregat-Funktionen, Heuchelei- und Konsistenz-Markern im Detail,
Sample-Size-Ehrlichkeit und Cache-Strategie.

ADR 0011 dokumentiert Aktuelle-Themen mit Persona-Prompt-Strategie,
Versionierung statt Force-Override, AI-Bann-Quellen-Filter und
json_object_mode-Recovery.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:39:29 +02:00
Dotty Dotter
c158cd5fb8 fix(#162): Konsistenz-Hinweis bevorzugt definitives Outcome
Wenn ein Antrag mehrere Plenum-Votes hat (Überweisung → Endabstimmung),
nimmt der Konsistenz-Block jetzt das erste mit angenommen/abgelehnt/
bestätigt. Vorher wurde stur [0] verwendet — das war oft "überwiesen"
und der Block blieb leer trotz vorhandenem Endbeschluss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:40:19 +02:00
Dotty Dotter
76f03e72ee feat(#162): Empfehlung-vs-Vote-Konsistenz im Antrag-Detail
Direkt unter "Abstimmungsergebnis" steht ein Hinweis-Block:
- "Mehrheit kontra GWÖ-Empfehlung" (rot) wenn Empfehlung "unterstützen"
  und Beschluss "abgelehnt" oder umgekehrt.
- "Mehrheit deckt sich mit GWÖ-Empfehlung" (grün) bei aligniertem Fall.
- Bei "überwiesen" oder ambivalenter Empfehlung kein Block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:38:43 +02:00
Dotty Dotter
db0c0681c6 feat(#171): Heuchelei-Marker bei NEIN-Stimmen mit hoher Programm-Treue
Neben jeder NEIN-Fraktion erscheint ein dezentes ⚠ wenn der eigene
Wahlprogramm-Score >= 7 lag. Tooltip nennt den Score. Macht im Detail
sichtbar wer gegen das eigene Programm stimmt — gleicher Befund wie im
Stimmverhalten-Tab, aber pro Antrag punktgenau.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:36:59 +02:00
Dotty Dotter
b6b0ce752a feat(#170): sparsame **fett**-Hervorhebungen + Smoke-Tests fuer Histogram/Stand
PM-Prompt erlaubt nun max. eine Markdown-Bold-Markierung pro Absatz
(Schluessel-Zahl/Effekt). Force-Regen-Test bestaetigt: qwen-max liefert
**30 %** wie im Beispiel; renderPmBody im Frontend rendert das als
<strong>. Smoketests gegen die neuen Endpoints (score-histogram x4,
admin/stand x2 Auth-Walls) absichern Regressionen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 09:32:54 +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
0377cf4bd9 fix(#170): PM-Prompt — Paragraphen-Trennung mit \\n\\n erzwingen
User-Beobachtung im Draft #6: qwen-max nutzte einsame Anfuehrungs-
zeichen (") als Paragraph-Trenner statt \\n\\n. Optisch wirkte das
wie inkorrekte JSON-Escapes mitten im Text.

Zwei Mechanismen:

**1. Prompt-Erweiterung:**
Neuer Abschnitt "Paragraphen-Formatierung" mit explizitem Beispiel:
`"body": "Lead.\\n\\nWirkung 1.\\n\\nWirkung 2.\\n\\n..."`. Klar:
keine Anfuehrungszeichen oder Sonderzeichen als Trenner.

**2. Post-Process-Heuristik:**
Regex `([.!?])"([A-ZÄÖÜ])` → `\\1\\n\\n\\2`. Wenn ein " genau zwischen
Punkt+Whitespace und Großbuchstabe steht, ist es wahrscheinlich ein
Trenn-Klumpen, kein semantischer Anfuehrer. Wird durch echten
Paragraph-Break ersetzt.

Konservativ: nur dieses spezifische Pattern wird touched. Echte
Quotes (z.B. "Es ist Zeit, …", sagt X) bleiben unangetastet, weil sie
nicht direkt nach Satzschluss-Punkt stehen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:30:58 +02:00
Dotty Dotter
aef8f83a08 feat: Antrag-Detail News-Match-Box + Test-Coverage fuer aktuelle-themen
**News-Match-Box im Antrag-Detail:**
Reverse-Sicht zur /aktuelle-themen-Seite — pro Antrag-Detail-Page eine
Box "Aktuelle News passend zu diesem Antrag" mit den Top-5 Matches der
letzten 90 Tage. Pro News-Card direkter "PM-Vorschlag generieren"-Button
mit Idempotenz-Check (bestehender Draft wird ohne LLM-Call zurueckgegeben).

Loesst das User-Feedback "ich oeffne ja meist Antrags-Detail, nicht den
News-Tab — da fehlt mir die News-Sicht". Box laedt lazy via fetch und
bleibt komplett versteckt wenn keine Matches existieren (kein Noise).

**Test-Coverage fuer die heutigen Backend-Aenderungen:**

`tests/test_llm_bewerter.py`:
- 6 Tests fuer `_recover_unescaped_newlines` (clean, raw newline, tab+cr,
  outside-string, makes-invalid-valid, preserves-already-escaped)
- 2 Tests fuer `json_object_mode` pass-through (off → kein Param,
  on → response_format={"type":"json_object"})
- 1 Integration: Recovery greift im bewerte()-Loop ohne Retry

`tests/test_endpoints_smoke.py`:
- Vote-Orphans-Endpoint (GET) Smoke
- Vote-Orphans-Auto-Rate Auth-Wall
- Batch-Analyze Auth-Wall (incl. ALL-Modus)
- Aktuelle-Themen-Endpoints (top, zeitreihe, top-antraege, cluster,
  drafts-list, drafts-versions) — 8 Tests

`tests/test_batch_helpers.py`:
- 4 Unit-Tests fuer _enqueue_for_bl-Logik via Inline-Repro mit Mocks
  (already-rated skip, no-adapter, limit-cap, empty-text-skip)

Suite: 1084 passed, 50 skipped (Smoke-Tests skippen lokal weil
FastAPI nicht importbar, greifen aber gegen dev/CI).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:22:22 +02:00
Dotty Dotter
0fd8a72958 feat(#171): logrotate-Config fuer /var/log/gwoe-*.log
scripts/logrotate-gwoe.conf — wird bei sudo-Install zu
/etc/logrotate.d/gwoe kopiert.

Schema:
- weekly, rotate 8 (8 Wochen Historie)
- compress + delaycompress (letzte Rotation greppable)
- missingok + notifempty (Cron-Logs koennen ja leer sein)
- create 0644 dotty dotty (passt zur Permissions-Konvention der
  bestehenden gwoe-*.log)
- sharedscripts (zukunftssicher fuer postrotate-Hooks)

Install via sudo cp + sudo logrotate -d (Dry-Run), siehe Inline-
Doku im File. Kein Server-seitiger Auto-Install — root-Aktion
liegt beim Maintainer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 02:04:17 +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
f008570cff feat(#173): BL-Filter im Stimmverhalten-Tab unabhaengig vom globalen
User-Wunsch: Stimmverhalten-Tab soll Querschnitt ueber alle BL zeigen
koennen, auch wenn der globale Header-BL-Filter auf einem einzelnen BL
steht. Bisher: Tab nutzte v2GetGlobalBl() → bei Header=BW wurde nur BW
angezeigt, bei Datensparse 0 Zeilen.

Aenderungen:
- Lokaler BL-Selector im Stimmverhalten-Caveat-Bereich.
  Default-Option: "— Alle Bundeslaender —"
- svGetBl() Helper liest den lokalen Selector
- loadStimmverhalten + loadMatrixHeatmap + downloadStimmverhaltenCsv
  nutzen svGetBl() statt v2GetGlobalBl()
- v2-bl-changed Event triggert das Stimmverhalten-Panel NICHT mehr
  (eigener Filter)

Andere Tabs (BL × Partei, Themen × Fraktion) reagieren weiter auf den
globalen BL-Filter.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:58:03 +02:00
Dotty Dotter
39ef248a66 fix(#170): PM-Body literal \\n → echte Newlines
Beobachtung beim ersten Pressereferent-Output: qwen-max liefert
manchmal literale Backslash-n Sequenzen (2 chars: \\ + n) statt echter
Newline-Bytes im JSON-Body. Auch mit response_format=json_object aktiv.

Post-Process im PM-Generator: \\n / \\r / \\t Sequenzen durch echte
Newlines / CR / Tab ersetzen. Konservativ (nur diese drei).
Macht das Modal richtig formatiert mit Paragraphen-Breaks.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:55:58 +02:00
Dotty Dotter
a0559333e8 fix(#170): JSON-Parse-Fehler im PM-Generator (unescaped Newlines)
Beobachtung beim Force-Regen: alle 2 Retries scheiterten mit
"Invalid control character at: line 3 column 275". qwen-max produziert
JSON mit rohen \n statt \\n im body-String, was json.loads sprengt.

Zwei Fixes parallel:

**1. response_format={"type": "json_object"}** als optionaler Mode im
LlmRequest. PM-Generator setzt das jetzt. DashScope unterstuetzt das
fuer qwen-max + qwen-plus und zwingt valide JSON-Strings.

**2. Newline-Recovery als Fallback** im QwenBewerter:
`_recover_unescaped_newlines` iteriert char-weise mit String-Tracking,
ersetzt unescaped \n/\r/\t in Strings durch \\n/\\r/\\t. Backslash-
Folgen bleiben unangetastet. Wird vor dem Retry-Re-throw versucht.

Bewertungs-Pfad (analyzer.py) bekommt json_object_mode=False als Default,
um die bewaehrte Retry-Semantik nicht zu aendern.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 01:53:29 +02:00
Dotty Dotter
6e78e92ddf fix: Matrix-Faerbung bei rating ±3 / ±4 / ±5 inkonsistent
User-Bug-Report: "+ mal blassrot, ++ mal blassgruen oder gruen".

Ursache: matrix_mini-Macro hatte rating_class() nur fuer
rating ∈ {-2, -1, 0, 1, 2} definiert. Aber die echte
Bewertungs-Skala ist −5..+5 (siehe app/models.py:MatrixEntry).

Effekt:
- rating=3, symbol="+" → m-0 neutral angezeigt (sollte m-p gruen sein)
- rating=4, symbol="++" → m-0 neutral (sollte m-pp ECG-Gruen)
- rating=-3, symbol="−" → m-0 neutral (sollte m-n rot)
- rating=-4, symbol="−−" → m-0 neutral (sollte m-nn dunkelrot)

Fix: rating_class abdeckt jetzt die volle Skala −5..+5 analog
zu MatrixEntry.to_symbol():
- rating ≥  4  → m-pp
- rating  1..3 → m-p
- rating  0    → m-0
- rating -1..-3→ m-n
- rating ≤ -4  → m-nn

Doku im Macro-Header korrigiert (war "-2 bis 2", jetzt "-5 bis +5").

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:51:24 +02:00
Dotty Dotter
cbc303f765 fix: Admin-Queue-Ansicht — Daten wurden nicht angezeigt
Bug: Template erwartete data.running, data.queued, data.failed.
API liefert aber data.jobs (mit status-Feld pro Job). Daher waren
alle drei Tabellen IMMER leer, selbst bei laufenden Jobs.

Fix:
- jobs nach status filtern (running | queued/pending | completed | failed)
- Neue Sektion "Zuletzt abgeschlossen" — vorher gar nicht angezeigt
  (20 completed Jobs auf dev waren unsichtbar)
- 4. Stat-Kachel "Abgeschlossen (Total)" mit data.processed_total
- Konfig-Info-Zeile: workers_running, max_size, avg_job_duration_seconds,
  estimated_wait_seconds — alles vorher ungenutzt im API-Response
- Spalte "Gestartet" → "Dauer (s)" (Daten-mismatch, started_at gibt's
  im API nicht)
- Wartende Jobs: bundesland-Spalte raus (nicht im API), durch
  Job-ID-Kurzform ersetzt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 22:49:13 +02:00
Dotty Dotter
e2dbb796e6 feat: Rolle im User-Profil anzeigen (Topbar-Badge)
Topbar zeigt jetzt:
- Username (wie bisher)
- "ADMIN"-Badge (teal) wenn user.roles enthaelt 'admin' oder 'gwoe-admin'
- Tooltip mit allen Rollen beim Hover

Macht sichtbar, ob man Admin-Rechte hat — wichtig fuer Sichtbarkeit
von /v2/batch und /v2/admin/* Eintraegen.

Plus: Rolle gwoe-admin in Keycloak (Realm collaboration) angelegt
+ User tobias zugewiesen. Auth-Code prueft realm_access.roles auf
'admin' ODER 'gwoe-admin'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:35:01 +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
3bf1de15b5 fix(#174): PM-Prompt mit harter Verbotsliste + Few-Shot
User-Feedback nach Live-Test: PMs waren kuerzer + nicht anschaulicher.
Im Output stand "Score von 4,0/10", "in den Bereichen Buerger:innen,
Wirtschaft, Staat, Gesellschaft und Natur" (Matrix-Zeilen D+E),
"staerkt Solidaritaet, Wuerde und Demokratie" (GWÖ-Werte-Liste),
Floskeln wie "innovative Loesungen" und "faktenbasierter Dialog".

Komplett-Refactor:

**ABSOLUT VERBOTEN im PM-Text:**
- Numerische Scores ("GWÖ-Score 4/10", "X von 10 Punkten")
- GWÖ-Wert-Listen als Aufzaehlung
- Beruehrungsgruppen-Sprache ("Bereiche Buerger, Wirtschaft, Staat, ...")
- Matrix-Codes ("Feld D2", "A1")
- GWÖ-Begriffe als Schlagwort (max 1× pro Begriff, nur konkret)
- Floskeln (zukunftsweisend, innovativ, faktenbasierter Dialog, ...)

**PFLICHT: Mindestens 3 Buerger:innen-Lebenslagen mit konkreter Wirkung:**
- Familien mit Kindern (Beträge, KiTa-Plätze)
- Pflegebeduerftige + Angehoerige (Wartezeiten, Kosten)
- Auszubildende / Studierende (Abbruchrisiko, BAföG)
- Pendler:innen (Spritpreis, ÖPNV-Tarif)
- Mieter:innen (Mietniveau, Nebenkosten)
- Rentner:innen / Geringverdiener:innen (Kaufkraft in Euro)
- Selbststaendige / kleine Betriebe (Buerokratie-Stunden, Steuern)

Pro Lebenslage: konkreter quantifizierter Effekt
("verlaengert Wartezeit auf Heimplatz von 8 auf 12 Wochen",
"spart einer vierkoepfigen Familie etwa 1.800 €/Jahr").

**Few-Shot:** Schlechtes Beispiel + Gutes Beispiel im Prompt.
Das gute Beispiel zeigt 30%-Abbrecherquote, 2 Stunden Beratung,
800 zusaetzliche Pflegekraefte in 5 Jahren — konkret quantifizierte
Wirkungen aus echten Zahlen.

**Laenger:** 320–380 Worte (vorher 220–280) — konkrete Beispiele
brauchen Platz.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 21:22:00 +02:00
Dotty Dotter
a3d13e984b fix(#170): default min_similarity 0.40 + PM-Prompt als Pressereferent (Issue tba)
**1. Default min_similarity 0.40 statt 0.50.** Live-Test auf dev:
mit 0.50 zeigt only_relevant=true 0 buckets, weil zu strikt fuer die
aktuelle Sparse-Datenlage (77 Bewertungen × 30 News). Mit 0.40 bleiben
1 high + 2 mid News pro 7-Tage-Fenster — genau die kuratierte Sicht,
die wir wollen.

**2. PM-System-Prompt umgeschrieben** als Pressereferent statt
Redakteur. User-Wunsch: "Bürger:innen anschaulich machen, was sich
durch den Antrag konkret im Leben vor Ort aendert".

Pflicht-Elemente im neuen Prompt:
- Konkrete Alltagswirkung (mindestens 2 Beispiele aus Lebenslagen:
  Pflegekraefte, Familien, Mieter:innen, Pendler:innen, ...)
- GWÖ-Verbesserungspotential bei nicht voll ueberzeugenden Antraegen
  (was fehlt, wie ginge es besser aus GWÖ-Sicht)
- Bei negativen Antraegen: klar benennen was verschlechtert wird,
  konkret quantifiziert wo moeglich
- 220–280 Worte (vorher 200–250)
- Aktive Verben, kurze Saetze, keine Floskeln
- Strukturierter Aufbau: Lead → Beispiele + GWÖ-Bewertung →
  Verbesserungspotential → Forderung

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-03 13:45:40 +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