Commit Graph

271 Commits

Author SHA1 Message Date
Dotty Dotter
481a791934 fix(#183): Browser-Speicher — Modal-Chart-destroy, Force-Sim-stop, Polling-Pause
- **Zeitreihe-Modal-Chart**: vor Re-Render alte Chart.destroy(),
  bei closeModal() + Escape ebenfalls. Vorher akkumulierten sich
  Chart-Instanzen bei jedem Modal-Open (Listener bleiben hängen).
- **Cluster-Force-Sim**: `_currentForceSim` global gemerkt, beim
  Verlassen Detail-View (showList) und vor neuem renderClusterGraph
  per sim.stop() beendet. Vorher liefen tick-Listener weiter, hielten
  nodes/links/SVG-Refs.
- **Polling-Pause auf visibilitychange** in queue_widget, admin_stand
  und admin_queue. Wenn Tab versteckt → clearInterval, beim Show wieder
  starten. Spart CPU/Akku + verhindert verwaiste Polls.

Refs: #183. Heap-Snapshot-Verifikation in Folge-Schritt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:01:48 +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
eeedf85d7e refactor(Phase 17 follow-up): copyDraftToClipboard nutzt data-pm-id statt JSON.stringify im onclick
Vorher: `onclick="copyDraftToClipboard(this, ${JSON.stringify(...).replace(/"/g, '&quot;')}, ...)"`
— funktional korrekt, aber Pattern-anfaellig (gleiche Klasse wie der
merkliste-bug aus Phase 17). Plus < und > waren nicht escaped.

Nachher: Button traegt nur eine numerische data-pm-id; der Handler
fetched den Draft per API und kopiert den Body. Robuster, weniger
Quote-Escaping, einheitlicher mit dem versionsHtml-Pattern oben in
derselben Datei.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 08:01:00 +02:00
Dotty Dotter
53f8d2cad5 fix(Phase 17 audit): Cluster-Sort nutzte members.length statt size/drucksachen.length
Audit-Befund: alte UI sortierte _clusters nach (members || []).length —
Backend liefert aber size + drucksachen, members ist leer. Folge: alle
Cards hatten size 0 als Sort-Wert, Reihenfolge war effektiv random.

Backwards-compat-Lookup mit drucksachen → members → size-Fallback.

(Andere c.members-Lookups in antrag_detail.html + aktuelle-themen.html
betreffen News-Cluster, deren API tatsächlich 'members' liefert — kein Bug.)
2026-05-06 23:52:17 +02:00
Dotty Dotter
bdbfc1ff7d feat(Phase 18): PM-Prompt verschaerft + Auto-Re-Generate bei zu kurzem Output
- SYSTEM_PROMPT mit explizitem 'Mindestens 320 Worte, < 280 ist
  Verstoss' + Hinweis 'wenn Substanz ausgeht: Lebenslage vertiefen
  statt abbrechen'.
- Output-Format-Beispiel mit MINDESTENS-Hinweis.
- generate_draft prüft nach LLM-Call die Wortzahl. Bei <280 Worten:
  ein einzelner Re-Prompt mit höherer Temperatur (0.5) und Hint zur
  ersten zu-kurzen Wortzahl. Wenn der zweite Versuch laenger ist,
  wird er übernommen — sonst bleibt der erste.
- max_retries=1 fuer den zweiten Call (nicht endlos).

Audit-Hauptbefund war 15/19 PMs unter Soll 320–380 Worten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:47:42 +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
c268d889fa fix(#175 #176 Phase 14): PDF-Matrix-Coloring + Symbol an v2 angleichen
- get_rating_symbol nutzt jetzt -5..+5-Skala (vorher: rating>=2 → ++,
  was bei rating=2 oder 3 falsche '++' gab; jetzt: rating>=4 → ++).
- PDF-Tabelle nutzt 5 Klassen (rating-pp/-p/-0/-n/-nn) statt 3
  (positive/negative/neutral). Heller Grün/Rot-Tint für mid-strength
  ratings, kräftiges Grün/Rot für Extreme. Visuell deutlich
  unterscheidbar.
- Beibehaltung der alten Klassennamen für Backwards-Compat falls
  irgendwo zwischengespeicherte HTML-Reports liegen.

Damit ist die v2/PDF-Konsistenz fuer NRW/18/18246 (#176) bezüglich
Matrix-Symbole und -Farben hergestellt.

Refs: #175, #176

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:42:15 +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
c87b99f778 feat(#177 Phase 13): Matrix-Coloring + Programm-Treue mit Begründung
- **Matrix-Coloring konsistent**: Symbol wird jetzt aus rating
  abgeleitet (rating_symbol-Macro) statt vom LLM übernommen. Bisher
  wurde z.B. rating=4 + symbol="+" geliefert → Template zeigte "+"
  aber mit m-pp-Klasse (kräftiges Grün) → "++/+ wirkten gleichfarbig".
  Stichprobe: 7/30 Assessments hatten rating/symbol-Mismatch.
- **„Antragsteller:in" / „Regierungsfraktion"** als Pill ausgeschrieben
  statt 1-Buchstabe-Badges A/R.
- **Programm-Treue mit Begründung sichtbar**: Wahlprogramm- und
  Parteiprogramm-Begründung als Block direkt unter den Score-Chips.
  Vorher nur Tooltip — auf Mobil schwer zugänglich.
- **„Redline" → „Verbesserungsvorschläge"** in beiden Heading-Pfaden.

Layout-Umstellung (Matrix↔Vote oben, Programm-Treue↔Verbesserung unten)
in #177 als Follow-up — braucht gerichtete Session mit Browser-Vorschau.

Refs: #177

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:38:01 +02:00
Dotty Dotter
a06bcb4d89 fix(#178 Phase 12): Teilen-Funktion umfassend gefixt
- **X (Twitter) raus** — Button + Logik entfernt.
- **Copy & Paste**: vollständiger Body ohne Länge-Cut, mehrzeilig
  strukturiert (Score, Titel, Drucksache, Beschreibung, Permalink,
  Hashtags). Statt 240-Zeichen-Twitter-Variante.
- **Threads**: encodeURIComponent kümmert sich um UTF-8 — keine
  Sonderzeichen-Probleme.
- **Mastodon**: gleicher Body wie Threads, Limit auf 420 Zeichen
  (mit Permalink-Reserve), Instance-Prompt bleibt.
- **LinkedIn**: Composer öffnet nur den Permalink (LinkedIn-API-
  Limitation), aber der vollständige Body landet parallel in der
  Zwischenablage. Toast informiert User.
- **E-Mail**: strukturierter Body mit Umbrüchen — Score-Zeile, Titel,
  Drucksache, Beschreibung, Permalink, Footer. Statt der knappen
  Threads-Variante.
- **Magnific**: korrekte URL mit `last_filter=selection&last_value=1
  &selection=1` — license-Filter vorausgewählt.

Refs: #178

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:34:09 +02:00
Dotty Dotter
d853101275 feat(Phase 11b): Bypass-DB-Logging + Auto-Rotation-Skript
- auth_bypass_uses-Tabelle additiv (used_at, client_ip, path, user_agent).
- _check_debug_token schreibt jeden Use als Best-Effort-Insert
  (Try/Except, kein Fehler an User).
- scripts/rotate-debug-token.sh: wöchentlicher Cron, generiert
  neues Secret + re-creates dev-Container.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:31:51 +02:00
Dotty Dotter
c13292133c fix: Validator akzeptiert Bundesrats-3-Komponenten-Drucksachen
Damit /api/analyze-drucksache die Bundesrats-spezifische 400-Meldung
liefern kann (vorher haengen blieb am Path-Traversal-Validator mit
generischem 'Ungueltige Drucksache-ID').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:30:29 +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
03948038c4 fix: Merkliste-Lösch-Row blieb im DOM — ID-Lookup vs. escAttr inkonsistent
Row-ID wird via escAttr gebildet ([^a-zA-Z0-9_-] → '_'), z.B. '18/18089'
landet als id='merkliste-row-18_18089'. Der getElementById-Lookup nutzte
aber CSS.escape, das 18/18089 zu 18\\/18089 escaped — zwei verschiedene
Strings, getElementById lieferte null, el.remove() lief nicht.

Plus: getElementById akzeptiert ohnehin keinen CSS-Selektor — der
CSS.escape-Lookup war doppelt falsch.

Fix: gleiche Sanitization-Regex wie escAttr im Lookup nutzen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:40:27 +02:00
Dotty Dotter
c599e5f6b5 fix: Merkliste-Löschen ging nicht — onclick-Attribut HTML-broken
JSON.stringify(a.drucksache) lieferte einen JSON-String mit Doublequotes
(z.B. "18/18089"). Eingesetzt in onclick="merkliste_remove("18/18089")"
brach das das HTML-Attribut beim ersten inneren Doublequote, der Browser
warf 'Unexpected end of input' beim Click und der DELETE-Request kam nie
beim Server an.

Fix: escHtml() um den JSON-String, sodass Quotes als &quot; gerendert
werden — onclick-Attribut bleibt valide.

Bug headless mit Playwright + DEBUG_AUTH_TOKEN gefunden (commit f8cfa42).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:39:16 +02:00
Dotty Dotter
cb6971ff86 fix: ungeschlossene <div class='controls-bar'> verschluckte 3 Tabs
In panel-themen wurde <div class='controls-bar'> geöffnet, aber nie
geschlossen. Folge: alle nachfolgenden Panels (Stimmverhalten,
Score-Verteilung, Cluster-Link) rutschten als Kinder in panel-themen
rein und erbten dessen display:none.

Bei aktiviertem Tab-Switch wurde das richtige Panel zwar mit class
'active' versehen, aber sein PARENT (panel-themen) blieb display:none
— daher 0×0 Bounding-Box auf allen Charts.

Ohne Debug-Bypass headless gefunden — Diagnose-Skript zeigte panel-
stimmverhalten als Kind von panel-themen im DOM-Stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:30:21 +02:00
Dotty Dotter
f8cfa42d9f feat: DEBUG_AUTH_TOKEN Bypass fuer Diagnose-Sessions auf dev
Wenn ENV `DEBUG_AUTH_TOKEN` gesetzt ist, akzeptieren require_auth +
require_admin einen Header `X-Debug-Token: <secret>` oder einen
Query-Param `?__debug_token=<secret>` und liefern einen Admin-Mock-
User. Jeder Use wird mit logger.warning protokolliert.

Default: leer = inaktiv (auch in prod, weil prod-compose das nicht
durchreicht).

Damit kann ein Diagnose-Tool (Playwright, curl) ohne Keycloak-Login
auf admin-only-Endpoints zugreifen — fuer Browser-Console-Auswertung
bei UI-Bugs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:26:39 +02:00
Dotty Dotter
60db39d5b3 fix: Force-Graph zeigt nichts — Edges sind Index-basiert, nicht ID-basiert
Backend liefert edges als {a: 0, b: 1, sim: ...} mit Indizes in der
nodes-Liste. d3.forceLink mappt per id-Lookup und fand 'drucksache' als
Lookup-Key nicht. Folge: keine Links, Force-Sim degeneriert ohne Layout.

Fix: Index-Strings als id verwenden.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 22:11:16 +02:00
Dotty Dotter
d72a6f30df fix: Cluster-Detail-View unsichtbar — display:'' griff CSS-default 'none'
Beim Klick auf eine Cluster-Card setzte showCluster() detail.style.display = ''.
Da #cluster-detail per CSS aber 'display:none' hat, fiel der Style auf
'none' zurueck — Detail-View blieb unsichtbar, Force-Graph wurde nie gesehen.

Fix: explizit 'block' setzen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:32:28 +02:00
Dotty Dotter
fdac89ab47 feat: Force-Graph-Visualisierung im /v2/cluster-Detail
Beim Klick auf einen Cluster wird jetzt zusätzlich zur Antragsliste
ein d3-Force-Graph eingeblendet. Knoten = Drucksachen, Kantendicke =
Cosine-Similarity, Knotenfarbe = dominante Fraktion. Klick auf einen
Knoten oeffnet das Antrag-Detail.

Daten kommen aus dem bereits vorhandenen /api/clusters-Response
(nodes/edges-Felder, vorher ungenutzt). Layout: forceSimulation mit
link/charge/center/collide. d3.v7.min.js wird im head_extra geladen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 18:21:15 +02:00
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
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
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
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
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
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
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
Dotty Dotter
33bb564ed1 feat(#149): BB-Parser produktiv — Brandenburger Plenarprotokolle (Status-Only)
URL-Pattern verifiziert WP8 Sitzung 22:
https://www.parlamentsdokumentation.brandenburg.de/starweb/LBB/ELVIS/parladoku/w8/plpr/{n}.pdf

**Wichtig:** parladoku-PDF-URL liefert 403 ohne Cookie-Session. Erst
GET auf portal/browse.tt.html?wp=8 zur Cookie-Akquise, dann mit
gesetztem Cookie die PDF-URL aufrufen. Ingest-Cron implementiert
diesen Flow per http.cookiejar.CookieJar in Python.

Anchor-Pattern (NRW-aehnlich):
- "Damit ist [Subj] (mehrheitlich|einstimmig)? (angenommen|abgelehnt|ueberwiesen)"
- Drucksachen-Lookup: Drucksache 8/N rueckwaerts vom Anchor

Vote-Style: Handzeichen-only (kein Fraktionen-Listing). Daher
Vote-Listen leer; einstimmig=True setzt JA=alle WP8-Fraktionen
(SPD, AfD, CDU, BSW, GRÜNE).

Tests: 14 BB-Tests, Verifikation S22 → 26 Vote-Anchors extrahiert.
Stand: 10 produktive Parser
(NRW, BUND, BE, HH, TH, HE, SH, HB, SL, BB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 02:04:21 +02:00
Dotty Dotter
d0f7b9217c feat(#161): SL-Parser produktiv — Saarland HTML-Abstimmungsergebnisse
Saarland publiziert keine Wortprotokolle, sondern eigene HTML-Seiten
mit strukturierten Abstimmungsergebnissen pro Sitzung:

  <p>Drucksache 17/2076 ... in Erster Lesung mit Stimmenmehrheit
  angenommen ... [SPD: dafür; CDU und AfD: dagegen]</p>

Daher Input ist HTML, nicht PDF. Parser nutzt LI-Block-Iteration und
extrahiert pro Block:
- Drucksache aus "Drucksache N/M"
- Status aus "(einstimmig|mit Stimmenmehrheit)? (angenommen|abgelehnt)"
- Vote-Block aus "[SPD: dafür; CDU: dagegen; AfD: Enthaltung]"
- einstimmig=True falls Status enthaelt "einstimmig"

Vote-Bracket-Parser (eigenstaendig vs. Reden-Stil-Parser anderer BL):
- Splits per ; → "Phrase: Status"
- Phrase per Wortgrenzen-Regex auf {SPD,CDU,AfD} matchen
- Status-Map: dafür→ja, dagegen→nein, Enthaltung→enthaltung

URL-Pattern (nicht direkt vorhersagbar wegen Datums-Slug):
https://www.landtag-saar.de/aktuelles/mitteilungen/abstimmungsergebnisse-der-{n}-landtagssitzung-vom-{datum}/

Auto-Ingest via Index-Scrape (analog HH/HE/SH):
- /aktuelles/mitteilungen/ scrape
- WP16-URLs (mit "wahlperiode-vom") ueberspringen
- Pro neue Sitzung: HTML herunterladen, ingest_pdf-API auf .html-Datei

Tests: 18 SL-Tests (Verifikation Sitzung 46 → 18 Votes mit korrekten
JA/NEIN/ENTH-Listen). Stand: 9 produktive Parser
(NRW, BUND, BE, HH, TH, HE, SH, HB, SL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:53:51 +02:00
Dotty Dotter
d9ae0b0db8 feat(#153): HB-Parser produktiv — Bremer Beschlussprotokolle (Status-Only)
Bremen publiziert wie Hessen nur Beschlussprotokolle (TOPs + Status-Saetze),
KEINE Wortprotokolle mit Vote-Block. Daher minimaler Parser:
- Drucksache + Status (angenommen/abgelehnt/ueberwiesen)
- Vote-Listen bleiben leer (HB hat keine Fraktions-Detail)

Anchor-Regex: "Die Buergerschaft (Landtag|Stadtbuergerschaft) <verb> <rest> <terminator>"
Verb-Mapping:
- "lehnt ... ab" → abgelehnt
- "stimmt ... zu" → angenommen
- "beschliesst ..." → angenommen
- "verabschiedet ..." → angenommen
- "verweist|ueberweist|leitet" → ueberwiesen
- "nimmt ... Kenntnis" → uebersprungen (kein Vote)

Drucksachen-Aufloesung: erst Inline-Form "(21/N)", dann Block-Form
"Drucksache 21/N" rueckwaerts vom Anchor.

URL-Pattern (verifiziert WP21 Sitzung 33 Land):
https://www.bremische-buergerschaft.de/dokumente/wp21/land/protokoll/b21l{n4}.pdf

Cron unterstuetzt jetzt {n4}-Platzhalter (4-stellig). HB Land WP21
ingestiert via direktes URL-Probing (b21l0001.pdf … b21l9999.pdf).
Stadtbuergerschaft (b21s*) als Folge-Issue.

Tests: 21 HB-Tests, Verifikation S33 → 20 Beschluesse extrahiert.
Stand: 8 produktive Parser (NRW, BUND, BE, HH, TH, HE, SH, HB).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:41:40 +02:00
Dotty Dotter
7ebdc78331 feat(#160): SH-Parser produktiv — Schleswig-Holsteiner Plenarprotokolle
Verifiziert auf WP20 Sitzungen 115 + 116. Format ist TH-aehnlich:

Result-Anchor: "Damit ist [Subjekt] (mehrheitlich|einstimmig)? (angenommen|abgelehnt|überwiesen|so beschlossen)"
Vote-Block (Q+A im Reden-Stil):
  - JA: "Wer dem zustimmen will ... Das sind die Fraktionen von X"
  - NEIN: "Wer stimmt dagegen? ... Das sind die Fraktionen von Y"
  - ENTH: "Wer enthaelt sich? ... Z"
Drucksachen-Lookup: rueckwaerts vom Anchor

Besonderheiten:
- SSW (5%-Huerden-befreit) als feste Fraktion
- "Damit ist die Ausschussueberweisung einstimmig so beschlossen" → ergebnis="ueberwiesen"
- "Das sind alle anderen Fraktionen" → NEIN als Komplement von JA inferiert
- Soft-Hyphen-Reparatur (PDF-Zeilenumbruch "zustim- men" → "zustimmen")
- _last_match-Helper, weil 1500-char-Window mehrere Vote-Bloecke enthalten kann
  (TH-Limitierung gefixed)

URL-Pattern (verifiziert):
https://www.landtag.ltsh.de/export/sites/ltsh/infothek/wahl20/plenum/plenprot/{YYYY}/20-{n:03}_{MM-YY}.pdf

Datum-Anteile (YYYY-Pfad + MM-YY-Suffix) machen URL-Vorhersage unmoeglich
→ Auto-Ingest-Cron via Index-Scrape (analog HH/HE):
https://www.landtag.ltsh.de/infothek/wahl20/plenum/plenprot_seite/

Tests: 23 SH-Tests + Stub-Registry-Test angepasst.
Stand: 7 produktive Parser (NRW, BUND, BE, HH, TH, HE, SH).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:29:06 +02:00
Dotty Dotter
8125dbb731 feat(#154): HE-Parser produktiv — Hessen Beschlussprotokoll (Status-Only)
Hessen publiziert nur Beschlussprotokolle (Tagesordnung + Status), KEINE
Wortprotokolle mit Vote-Block. Daher minimaler Parser:
- Drucksache + Status (angenommen/abgelehnt/ueberwiesen)
- Vote-Listen bleiben leer (HE hat keine Fraktions-Detail)

URL-Pattern (verifiziert WP21 Sitzungen 61-63):
http://starweb.hessen.de/cache/hessen/landtag/Plenum/{wp}/Beschlussprotokoll_PL_{n}_{datum}.pdf

Datum-Teil DD-MM-YYYY → URL-Vorhersage unmoeglich, Auto-Ingest braucht
Index-Scrape via starweb.hessen.de/starweb/LIS/Pd_Eingang.htm (analog HH).

Status-Mapping:
- "angenommen" → ergebnis="angenommen"
- "Abgelehnt" → ergebnis="abgelehnt"
- "Nach (Aussprache|Lesung) an [Ausschuss]" → ergebnis="ueberwiesen"
- "Entgegengenommen", "Abgehalten", "Zur Kenntnis genommen" → uebersprungen

Tests: PROTOKOLL_PARSERS-Set jetzt {NRW, BUND, BE, HH, TH, HE}. STUB_BL_CODES
auf 11 BL reduziert (BB, BW, BY, HB, LSA, MV, NI, RP, SH, SL, SN bleiben).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 01:19:02 +02:00
Dotty Dotter
399dbc2639 feat(#163): TH-Parser produktiv — Thueringer Plenarprotokolle
Fuenfter produktiver Parser nach NRW + BUND + BE + HH.

URL-Pattern verifiziert (WP8 Sitzungen 1, 10, 20, 30, 40, 42):
  https://www.thueringer-landtag.de/uploads/tx_tltcalendar/protocols/Arbeitsfassung{n}.pdf

Anchor-Sprache (BE-aehnlich):
  Wer dem zustimmt, ... Das sind die Stimmen aus den Fraktionen der
  CDU, BSW, SPD und Die Linke. Wer stimmt gegen ...? Das sind die
  Stimmen aus der Fraktion der AfD. Damit ist [...] mehrheitlich
  angenommen.

Pattern:
- Result-Anchor: Damit ist [Subjekt] (mehrheitlich|einstimmig)?
  (angenommen|abgelehnt)
- Vote-Block: Wer dem zustimmt / Wer stimmt gegen / Wer enthaelt sich
- Drucksachen-Lookup: 'Drucksache 8/N' rueckwaerts

Fraktions-Mapping WP8 (ab Mai 2024): CDU, AfD, BSW, Linke, SPD
(WP7-Faktionen GRUENE/FDP fuer Backfill ebenfalls im Mapping).

Cron-PROTO_TARGETS um TH-WP8 erweitert. Stub-Test angepasst.
2026-04-29 01:11:58 +02:00
Dotty Dotter
5f97ae9fc3 feat(#155): HH-Parser produktiv — Hamburg Beschlussprotokolle
Vierter produktiver Plenarprotokoll-Parser nach NRW + BUND + BE.

Hamburg publiziert kompakte Beschlussprotokolle (Tabellen-Form mit
Vote-Block pro Beschluss):
  ... mehrheitlich mit den Stimmen der SPD und GRUENEN gegen die
  Stimmen der CDU und AfD bei Enthaltung der Linken angenommen

Pattern:
- einstimmig (angenommen|abgelehnt) — alle Fraktionen
- mehrheitlich mit den Stimmen X gegen die Stimmen Y bei Enthaltung Z
  (angenommen|abgelehnt)

Fraktions-Mapping WP23: SPD, GRUENE, CDU, AfD, Linke

URL-Discovery laeuft ueber die Protokoll-Liste der Buergerschaft
(Blob-IDs via Index-Page-Scrape). Cron-Eintrag erst sobald
URL-Discovery-Skript hier integriert ist.

Stub-Test angepasst (HH raus aus STUB_BL_CODES).
2026-04-29 00:57:58 +02:00
Dotty Dotter
c7d6ac7f5f feat(#150): BE-Parser produktiv — Berliner Abgeordnetenhaus-Plenarprotokolle
Dritter vollwertiger Plenarprotokoll-Parser nach NRW + BUND.

URL-Pattern verifiziert (WP19 Sitzungen 1, 10, 50, 80, 100):
  https://www.parlament-berlin.de/ados/{wp}/IIIPlen/protokoll/plen{wp}-{n:03}-pp.pdf

Anchor-Sprache (NRW-aehnlich, mit Berliner-Eigenheit 'pro forma'):
  Wer den Antrag auf Drucksache 19/X annehmen moechte, ... – Das sind
    die Fraktionen Buendnis 90/Die Gruenen und Die Linke.
  Wer stimmt dagegen? – Das sind die Fraktionen der CDU, SPD und AfD.
  Wer enthaelt sich, pro forma? – Das ist niemand.
  Damit ist der Antrag abgelehnt.

Pattern:
- Result-Anchor: Damit ist [Antrag/Aenderungsantrag/Gesetzentwurf/...]
  (angenommen|abgelehnt)
- Vote-Block: 3 Q+A-Paare im Reden-Stil (annehmen moechte / dagegen /
  enthaelt sich)
- Drucksachen-Lookup: 'Drucksache 19/N(-suffix)' rueckwaerts (1500-char Fenster)

Fraktions-Mapping WP19:
- Buendnis 90/Die Gruenen → GRÜNE
- Die Linke → LINKE
- CDU, SPD, AfD, FDP

21 Tests in test_protokoll_parsers_be.py.
Cron-PROTO_TARGETS erweitert um BE WP19 (~80 Sitzungen).
Stub-Test angepasst.

905 Tests gruen (889 → 905, +16 fuer BE).
2026-04-29 00:37:47 +02:00
Dotty Dotter
a83c770b93 docs(#151): BW-Datenmodell-Inkompatibilitaet vermerkt
Vertiefte Probe (WP17 Sitzung 50): BW stimmt 'pro Artikel'
('Damit ist Artikel 1 einstimmig zugestimmt'), nicht pro Drucksache.

Das ist andere Datenmodellierung als NRW (Drucksache→Vote) und BUND
(Beschlussempfehlung→Vote). Ein BW-Parser braucht entweder:
- Aggregations-Heuristik: alle Artikel angenommen → DS angenommen
- Schema-Erweiterung um 'artikel'-Spalte fuer per-Artikel-Records

Implementer muss vor Start mit Maintainer abstimmen, welcher Weg
gegangen wird. BW bleibt Stub bis Designwahl getroffen ist.
2026-04-28 23:29:31 +02:00
Dotty Dotter
22a2b63c35 feat(#148): BUND-Parser produktiv — Bundestags-XML-Plenarprotokolle
Erster vollwertiger Plenarprotokoll-Parser nach NRW. Quelle:
https://dserver.bundestag.de/btp/{wp}/{wp}{n:03}.xml

Anchor-Sprache (verifiziert WP20 Sitzungen 30 + 100):
  'Die Beschlussempfehlung ist mit den Stimmen der Koalitionsfraktionen
   gegen die Stimmen der CDU/CSU-Fraktion bei Enthaltung der AfD-Fraktion
   angenommen.'

Pattern:
- Subjekt: Beschlussempfehlung | Ueberweisungsvorschlag | Antrag | Gesetzentwurf
- Vote-Block: 'mit den Stimmen X / gegen die Stimmen Y / bei Enthaltung Z'
- Ergebnis: 'angenommen' oder 'abgelehnt'
- Drucksache rueckwaerts vom Anchor (1500 chars Window)
- Kind 'ueberweisung' invertiert ergebnis zu 'ueberwiesen'

Fraktions-Mapping (WP20 = Ampel):
- 'Koalitionsfraktionen' → SPD + GRÜNE + FDP
- 'Oppositionsfraktionen' → CDU/CSU + AfD + LINKE
- 'CDU/CSU-Fraktion', 'Fraktion Bündnis 90/Die Grünen', etc.

WP21 (ab 2025) braucht eigenes Mapping-Update.

26 Tests in test_protokoll_parsers_bund.py (Vote-Block-Parsing, Anchor-
Erkennung, Drucksachen-Lookup, End-to-End mit Mock-XML).

Cron + Ingest-CLI:
- PROTO_TARGETS-Format erweitert um PROTOKOLL_ID_PREFIX und {n3}-
  Placeholder fuer 3-stellig zero-gepaddetes BT-Schema (BTP20-N)
- ingest_votes URL-Suffix dynamisch (PDF vs XML) statt hardcoded .pdf
- Eintrag in PROTOKOLL_PARSERS (NRW + BUND)
- Stub-Test angepasst: BUND raus aus STUB_BL_CODES

889 Tests gruen (787 → 889, +102 fuer Phase-2 Stubs+Tests+BUND).
2026-04-28 23:21:39 +02:00
Dotty Dotter
7cfbd9f210 docs(#148): BUND-Stub um echte Anchor-Sprache aus WP20-Sitzung 30
Erste Probe (Sitzung 184) war Aussprache, daher 0 Beschluss-Anchors.
Sitzung 30 (572k chars, 5 angenommen-Anchors) zeigt die echte
BT-Vote-Sprache:

  'Die Beschlussempfehlung ist mit den Stimmen der Koalitions-
   fraktionen und der Fraktion Die Linke gegen die Stimmen der
   CDU/CSU-Fraktion bei Enthaltung der AfD-Fraktion angenommen.'

Pattern-Erkennung:
- Anchor-Verb 'angenommen' oder 'abgelehnt' am Satzende
- Vote-Block: 'mit den Stimmen [...] gegen die Stimmen [...]
  bei Enthaltung [...]'
- Fraktions-Phrasen: 'Fraktion X', 'X-Fraktion', 'Koalitionsfraktionen'
- Drucksachen rueckwaerts vom Anchor (oft 100+ Zeichen vorher)

Wichtig: BT-Anchor-Sprache ist viel laenger als NRW — Regex-Begrenzung
muss 200+ Zeichen tolerieren.

Sample-Sitzungen mit Beschluessen: WP20 30, 100, 150.
2026-04-28 23:15:36 +02:00
Dotty Dotter
171a05ed4d docs(#151): BW-Stub um konkrete Recherche-Befunde erweitert
Heutige Probe von WP17 Sitzung 50 (618 KB PDF) ergab:

URL-Pattern bestaetigt:
  https://www.landtag-bw.de/.../WP{wp}/Plp/{wp}_{n:04}.pdf
  4-stellige Sitzungs-Nr mit Padding (anders als NRW unkpaddet)

Anchor-Phrasen-Stichprobe:
  'einstimmig zugestimmt' x5 — Haupt-Anchor (NRW: 'angenommen')
  'Damit ist [...] einstimmig' x2 — NRW-aehnliche Struktur
  'angenommen' x1 — nur in einer Rede, KEIN Beschluss-Anchor!
  'Drucksache 17/N' x35 — DS-Pattern wie NRW
  'zugestimmt' x19 — dominierende Vote-Phrase

Fraktions-Auflistung pro Vote in BW deutlich weniger detailliert als
NRW — Parser wird oft nur 'einstimmig' / 'mit Mehrheit' extrahieren
koennen, kein ja/nein/enthaltung-Breakdown pro Fraktion.

Fuer den naechsten Implementer (BW-Session) wertvolle Vorarbeit.
2026-04-28 23:14:17 +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
c0692b3078 feat(#106 Folge): 16 Stub-Parser + Tracking-Issues fuer alle Bundeslaender
Pro BL (BUND + 15 Laender) ein Modul app/protokoll_parsers/<bl>.py mit:
- Recherche-Findings im Docstring (Doku-System, Base-URL, Format,
  URL-Discovery-Status, Familie, Aufwand-Schaetzung)
- parse_protocol() raised NotImplementedError mit Hinweis auf Issue-Tracker
- *Nicht* in PROTOKOLL_PARSERS-Registry → Auto-Ingest-Cron uebersieht sie

Tracking-Issues #148-#163 auf Gitea, jeweils mit den Recherche-Findings
und einer Checkliste fuer die Implementer-Session.

Roadmap-Doc (docs/protokoll-parser-roadmap.md) aktualisiert mit
Stub→Issue-Mapping-Tabelle.

Wenn der Implementer pro BL fertig ist:
1. NotImplementedError durch echten Parser ersetzen
2. Eintrag in app/protokoll_parsers/__init__.py::PROTOKOLL_PARSERS
3. PROTO_TARGETS in scripts/auto-ingest-protocols.sh ergaenzen

787 Tests gruen, NRW unveraendert.
2026-04-28 23:09:07 +02:00
Dotty Dotter
145ad1e8d4 docs(methodik): klarstellen wie System- und User-Prompt zusammenwirken
User-Frage zur Transparenz-Seite: 'Welcher Prompt wird ausgefuehrt?
Der System-Prompt ist deutlich umfangreicher.' Antwort: keiner allein —
beide werden in einem API-Call zusammen gesendet und gemeinsam
ausgewertet.

Auf /methodik#prompts neu vor den details-Bloecken:
- Erklaerung 'in einem einzigen API-Call', beide ins Kontextfenster
- 2-Spalten-Tabelle 'System (Wer/wie)' vs. 'User (Was)'
- Begruendung der Trennung (Caching, Compliance, Wartbarkeit)
- Code-Referenz zu qwen_bewerter.py:83-85 mit messages-Aufbau

Reine UI-Aenderung, keine Code-Logik betroffen.
2026-04-28 09:14:22 +02:00
Dotty Dotter
eb0669d6ac feat(#147): Hover-Tooltips fuer Abkuerzungen auf Antrag-Detail
User-Feedback: '(A)' hinter Partei, 'WP', 'PP' brauchen Erklaerung
fuer Erstleser:innen. Loesung: ausfuehrliche title-Tooltips plus
visuelle Affordanz (cursor:help).

Geaendert:
- v2-badge-antragsteller / -regierung: cursor:help
- v2-score-chip[title]: cursor:help
- (A) → 'A — Antragstellende Fraktion: hat den Antrag eingereicht.'
- (R) → 'R — Regierungsfraktion: traegt die aktuelle Mehrheit im Landtag.'
- WP-Chip: 'WP — Wahlprogramm-Treue (0–10): wie gut passt der Antrag
  zum aktuellen Wahlprogramm? + Begruendung'
- PP-Chip: analog fuer Parteiprogramm-Treue
- Score-Hero: Tooltip mit GWÖ-Score-Definition + Methodik-Verweis
- 'Enth.:' im Abstimmungs-Block: dotted underline + Tooltip 'Enth. —
  Enthaltung: weder Zustimmung noch Ablehnung'

Closes #147
2026-04-28 08:46:27 +02:00
Dotty Dotter
7de4df1fef feat(#126): protokoll_parsers/-Sub-Package + Registry-Pattern + ADR 0009
Architektur-Refactor zur Vorbereitung BL-uebergreifender Parser:

- app/protokoll_parser_nrw.py → app/protokoll_parsers/nrw.py
- app/ingest_votes_nrw.py → app/ingest_votes.py (BL-uebergreifend)
- Neue app/protokoll_parsers/__init__.py mit:
  - PROTOKOLL_PARSERS-Dict (BL-Code → Parser-Funktion, derzeit nur NRW)
  - parse_protocol(bundesland, pdf_path) als BL-uebergreifender Einstieg
  - supported_bundeslaender()-Helper
  - NotImplementedError mit hilfreicher Message bei unbekanntem BL

CLI bekommt --supported-Flag fuer BL-Discovery:
  python -m app.ingest_votes --supported  → 'NRW'

ADR 0009 dokumentiert das Muster (Sub-Package + Funktions-Registry,
analog zu ADR 0002 fuer ParlamentAdapter). Folge-BL bekommen je
eine eigene Datei und einen Eintrag in PROTOKOLL_PARSERS — kein
Refactoring der Bestands-Logik.

Tests:
- 7 neue Tests in test_protokoll_parsers.py fuer Registry und Dispatch
- Bestehende NRW-Tests umbenannt zu test_protokoll_parsers_nrw.py,
  Imports angepasst — keine Verhaltens-Aenderung
- Bestehende Ingest-Tests umbenannt zu test_ingest_votes.py

642 Tests gruen, kein Verhaltens-Drift.
2026-04-28 08:37:31 +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
e26607854f feat(#106): Ingest-CLI fuer NRW-Plenarprotokolle
app/ingest_votes_nrw.py: Pipeline PDF → protokoll_parser_nrw → DB.

CLI:
  python -m app.ingest_votes_nrw --pdf /pfad/MMP18-119.pdf
  python -m app.ingest_votes_nrw --url https://landtag.nrw.de/.../MMP18-119.pdf
  python -m app.ingest_votes_nrw --pdf x.pdf --protokoll-id MMP18-119 --bundesland NRW

Protokoll-ID wird default aus Datei-Stem abgeleitet (MMP18-119.pdf →
MMP18-119), URL-Mode parst sie aus dem letzten Pfadsegment.

ingest_pdf() ist die programmatische API (auch fuer Folge-Cron, falls
spaeter automatisch Plenarprotokoll-Sammelinges nachgeruestet wird).
Statistik-Dict: parsed/written/skipped_no_drucksache/errors.

6 Tests: Roundtrip, skip-bei-fehlender-Drucksache, default + override
fuer Protokoll-ID, BL-Override (fuer #126-Folge), idempotenter Re-Ingest.
2026-04-28 08:03:18 +02:00
Dotty Dotter
ae3f48be41 feat(#106): plenum_vote_results-Tabelle + Repository
DB-Schema fuer fraktions-aggregierte Plenum-Abstimmungsergebnisse:
- bundesland, drucksache, quelle_protokoll als Compound-PK
  (eine Drucksache kann mehrfach abgestimmt werden — Ausschuss-Empfehlung
  und finale Beschlussfassung leben nebeneinander)
- ergebnis (angenommen/abgelehnt/ueberwiesen/...), einstimmig-Flag
- fraktionen_ja/_nein/_enthaltung als JSON-Arrays
- quelle_protokoll (z.B. 'MMP18-119') + optional quelle_url
- Index auf (bundesland, drucksache) fuer Lookup-Path

Repository-API:
- upsert_plenum_vote(...) idempotent ueber Compound-PK
- get_plenum_votes(bl, drucksache) → Liste, neueste zuerst

7 Tests fuer Roundtrip, einstimmig-Flag, Idempotenz, Multi-Protokoll-Erhalt,
leere Queries, Unicode-Handling von 'GRÜNE'.

Refs #106 — naechster Schritt: Ingest-CLI gegen NRW-PDFs.
2026-04-28 08:01:26 +02:00
Dotty Dotter
d640734641 feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).

Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
  auch im Stub-conftest funktionieren

30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
  (neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match

Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
Dotty Dotter
5559f42c92 feat(#138): SHA-Lock-File schuetzt vor stillem PDF-Tausch
Hintergrund: abgeordnetenwatch hatte das CDU-BE-2023-PDF unter dem alten
Slug-Namen gegen das CDU-BE-2026-Wahlprogramm ersetzt — ohne den
Datei-Namen zu aendern. Die Embedding-Indexierung haette das anachronistische
Programm uebernommen, ohne dass es jemand bemerkt.

Loesung: app/wahlprogramm-shas.lock.json pinnt nach erstem erfolgreichen
Download den SHA-256 jedes Programmes. Spaetere Aufrufe von
fetch_and_verify() vergleichen den Server-Inhalt gegen den Lock; bei
Abweichung wird abgebrochen mit klarer Fehlermeldung. Nur mit explizitem
Maintainer-Override (--accept-new-sha) wird der Lock aktualisiert.

CLI:
  python -m app.wahlprogramm_fetch --pin-existing
    seedet den Lock einmalig aus den vorhandenen PDFs (52 Eintraege).
  python -m app.wahlprogramm_fetch --fetch BL PARTEI [--accept-new-sha]
    laedt mit Lock-Pruefung; --accept-new-sha bei bewusstem Update.

6 neue Tests in test_wahlprogramm_fetch.py decken den Pferdetausch-
Block, das initiale Pinnen, das Migration-Szenario (PDF da, Lock leer)
und den --accept-new-sha-Override ab.

Closes #138
2026-04-28 01:58:42 +02:00
Dotty Dotter
d0d941444d feat(#144): Matrix-Ueberschriften ausschreiben + Hover-Tooltips
Statt Abkuerzungen (Wuerde, Solid., Liefer., Verwalt., Gesell.) jetzt
voll ausgeschrieben: Menschenwuerde, Solidaritaet, Lieferant:innen,
Verwaltung, Gesellschaft & Natur, etc.

Hover-Tooltip pro Spalte/Zeile mit Erklaerung + Staatsprinzip
(Rechtsstaatsprinzip, Gemeinnutz, Umwelt-Verantwortung, ...).
Matrix-Felder bekommen Tooltip mit Feldname als Vorschau, der
volle Erklaerungstext bleibt im Click-Modal (showField).

Layout: rhdr-Spalte 130/150px, line-height 1.25, min-height 36px,
damit lange Begriffe sauber umbrechen koennen.

Closes #144
2026-04-28 01:53:38 +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
5f6bcac282 feat(#146): Fraktionen je Treffer in Landtag-Suche anzeigen
Adapter liefert fraktionen schon mit, das Frontend ignorierte sie bisher.
Treffer-Zeile bekommt jetzt unter dem Titel kleine Teal-Chips fuer jede
einreichende Fraktion (Beispiel: 'CDU SPD' bei kollektiven Antraegen).

Stylistisch konsistent zum Score-Chip-System (color-mix mit ecg-teal),
mono Font, uppercase 10px — bleibt auch bei vielen Fraktionen lesbar.

Closes #146
2026-04-28 01:47:54 +02:00
Dotty Dotter
09c29cac69 fix(#142): SL HTTP 5xx als Fehler raisen statt return []
Symptom: Monitoring-Scan zeigte bei SL seen=0 errors=OK, obwohl der
Umbraco-Backend HTTP 500 zurueckgab. Im _post_search wurde 5xx via
'logger.error + return []' geschluckt, sodass der Monitoring-Layer
die Fehlerursache nicht in monitoring_daily_summary persistierte.

Fix: bei resp.status_code != 200 httpx.HTTPStatusError raisen — das
propagiert durch search() ueber _search_adapter ins outer except in
daily_scan, das den Fehlertext in summary.errors schreibt.

Regression-Test test_search_propagates_http_500.

Closes #142
2026-04-28 01:46:35 +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
4b03448e29 fix(feedback): Screenshot scharf + ohne Feedback-UI
- Auflösung: scale = window.devicePixelRatio (statt min:2 cap) — Retina-scharf
- Vor dem html2canvas-Capture werden v2-feedback-{modal,overlay,btn} auf
  display:none gesetzt; finally-Block stellt UI zurueck. Damit ist die
  ausgegraute Modal-Schicht nicht im Bild
- Capture nur des sichtbaren Viewports (width/height/x/y/windowWidth/Height
  explizit), spart Bandbreite + zeigt was der User wirklich sieht
- MAX_W 800 -> 1600, JPEG 0.7 -> 0.85, imageSmoothingQuality high
- requestAnimationFrame x2 vor capture, damit Browser den Reflow vor dem Snap fertig hat
- app_version 1.0.1 -> 1.0.2 (Cache-Buster)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:10:36 +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
fab1bddd3c fix(v2): Hamburger-Toggle wirklich ausblenden (Specificity-Konflikt + Cache)
Bug: .v2-topbar button {display:inline-flex} ueberschreibt .v2-menu-toggle{display:none}
wegen hoeherer Specificity. Fix: Selektor .v2-topbar .v2-menu-toggle + !important.

Plus app_version 1.0.0 -> 1.0.1 als Cache-Buster fuer alle CSS-Refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:37:55 +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
b1ad2bd45d fix(v2): Hamburger-Menü-Toggle nur auf Mobile (< 900 px) sichtbar
Auf Desktop ist die Sidebar permanent — der Burger-Button hatte dort keine
Funktion. display: none default + @media max-width:900px → inline-flex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:28:51 +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