- **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>
- **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>
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>
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>
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 " 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>
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>
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>
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>
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>
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>
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>
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>
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>
- 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>
- Schema additiv: presse_drafts.style TEXT NOT NULL DEFAULT 'pm' via
ALTER TABLE (idempotent in init_db).
- presse_generator.generate_draft(style='pm'|'thread') nutzt eigenen
SYSTEM_PROMPT_THREAD (3-5 Posts à ≤280 Zeichen, Hook + Lebenslagen +
Forderung, Hashtags am Schluss; keine **fett**-Markdown).
- _find_existing_draft, list_drafts, list_drafts_for, get_draft liefern
jetzt auch das style-Feld zurueck.
- Endpoint /api/aktuelle-themen/generate-presse?style=thread baut den
Switch ein. Ohne Param weiterhin 'pm'.
- Frontend: PM-Modal zeigt den style-Tag (📰 PM / 🐦 Thread) im Banner
und bietet einen Knopf "Auch als Thread / Auch als PM" generieren.
Idempotenz pro (drucksache, news_url, style)-Tripel.
Refs: #170, #178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Endpoint GET /api/aktuelle-themen/drafts/{id}.pdf rendert den
gespeicherten PM-Body inkl. **fett**-Markdown als A4-PDF mit Header
(Drucksache-Link, GWÖ-Markup) und Footer-Quellenangabe.
PM-Modal in /aktuelle-themen bekommt zusätzlich einen 📄 PDF-Button
neben Mail/Clipboard. Dateiname `PM-DRUCKSACHE-DRAFTID.pdf`.
Refs: #170, #177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 (Vote-Orphans-Auto-Bewertung):
- Neue Tabelle `auto_rate_runs` (additiv) mit started_at, source,
bundesland, limit_requested, n_attempted/succeeded/failed/skipped,
error_summary.
- Neue DB-Helper: record_auto_rate_run, list_auto_rate_runs,
auto_rate_today_total.
- POST /api/auswertungen/vote-orphans/auto-rate erweitert um source,
daily_cap und Run-Persistenz. Throttled gegen Tagessumme.
- Neuer Endpoint GET /api/auto-rate-runs (admin) — letzte N Runs +
Tagessumme.
- scripts/auto-rate-orphans.sh: Cron-Wrapper (analog auto-fetch-news.sh)
mit MAX_PER_RUN=30 / MAX_PER_DAY=200 Defaults, BUNDESLAND-Filter
optional, ruft direkt die Python-Worker-Funktion via docker exec.
- Admin-Stand-Dashboard: KPI-Zeile "heute X Runs / Y versucht" + Tabelle
der letzten 5 Runs mit BL/Counts/Notiz.
Refs: #173, ADR 0010
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Klick auf eine Heuchelei-Bar oeffnet ein Modal mit der konkreten
Liste der Antraege wo die Fraktion mit Nein gestimmt hat, obwohl
der Antrag inhaltlich zum eigenen Wahlprogramm passt.
- Backend: app.auswertungen.get_heuchelei_cases() + Endpoint
GET /api/auswertungen/heuchelei-cases?partei=X[&bundesland=Y].
- Backend: _load_assessments_with_votes liefert jetzt zusaetzlich
das ergebnis-Feld (additiv im SELECT).
- Frontend: onClick-Handler im Heuchelei-Bar-Chart, Modal-Markup
wird lazy injiziert, Tabellen-Drilldown mit Drucksachen-Link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Route /stimmverhalten rendert dieselbe auswertungen.html, aber
mit default_tab='stimmverhalten' und v2_active_nav='stimmverhalten'.
Linker Nav-Eintrag 'Stimmverhalten' (Icon scales) zwischen
Auswertungen und Aktuelle Themen.
Beim Page-Load aktiviert das DOMContentLoaded-Handler den im Context
gesetzten Tab — fuer /auswertungen ist es 'bl-partei' (Default), fuer
/stimmverhalten direkt 'stimmverhalten'. Kein Code-Duplikat im
Tab-Inhalt.
Refs: #169
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetrisch zur Heuchelei-Logik: bei JA-Fraktionen, deren eigener
Wahlprogramm-Score < 3 ist, erscheint ein dezenter italic '!' mit
Tooltip. 11 echte Cases gefunden auf dev (NRW + BB).
app/marker.py: opportunismus_score() — neun neue Tests (test_marker.py
jetzt 44 grün).
Refs: ADR 0010, Phase 2.4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logik aus dem Jinja-Template (Heuchelei-Marker, Konsistenz-Block,
decisive-Outcome-Selection) in app/marker.py extrahiert. Template
ruft die drei Helper als Jinja-Globals auf. Damit ist die Logik
testbar ohne Render-Kontext.
Plus: app/pm_render.py als Python-Spiegelbild des JS-Mini-Markdown-
Renderers in aktuelle-themen.html — fuer Tests und potenzielle
Server-side-Render-Optionen (z.B. PM-Mail).
Tests:
- tests/test_marker.py (35 Cases): heuchelei_score, decisive_outcome,
consistency_state inkl. Multi-Vote, ambivalente Empfehlung,
Edge-Cases.
- tests/test_pm_render.py (21 Cases): Bold, Italic, Listen,
HTML-Escape, Paragraph-Splitting, snake_case-Schutz.
Refs: ADR 0010, ADR 0011
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
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>
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>
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>
**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>
Datenlage auf dev: 7281 Plenum-Votes, 96 Bewertungen, nur 19 Matches.
Stimmverhalten-Tab zeigt fast nichts, weil die meisten Vote-Drucksachen
keine Bewertung haben. Issue #172 schliesst die Luecke.
**Banner im Stimmverhalten-Tab:**
- Zeigt Anzahl + Verteilung pro BL der "Vote-only"-Drucksachen
- Nur sichtbar wenn count > 0
- Aktion: "Auto-Bewerten Top-N" mit Limit-Selector (5/10/20)
**Endpoint `GET /api/auswertungen/vote-orphans`:**
LEFT JOIN plenum_vote_results vs assessments, count + by_bundesland +
Top-N items sortiert nach parsed_at desc.
**Endpoint `POST /api/auswertungen/vote-orphans/auto-rate`:**
Admin-only, rate-limited 3/min. Nimmt Top-N Orphans, lädt Antragstext
per Adapter, enqueued einen Bewertungs-Job pro Drucksache. Defaults
limit=10, max 50. Per-skipped-reason-Liste in der Response (Adapter
fehlt, Empty-Text, Queue-full, etc.).
**Tests:** 4 neue (`TestGetVoteOrphans`), Suite 1071 gruen.
Helper `_enqueue_for_bl` aus dem Batch-Endpoint wird hier indirekt
wiederverwendet (gleiche Job-Queue-Pipeline).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Wunsch: Batch-Analyse soll auch Anträge aus mehreren BL gleichzeitig
ranziehen koennen, nicht nur einen einzelnen.
- Neue Dropdown-Option "— Alle aktiven Bundesländer (Limit verteilt) —"
als Default
- Backend: bei `bundesland=ALL` iteriert ueber `aktive_bundeslaender()`
und verteilt das Limit proportional (limit // N pro BL).
- Helper `_enqueue_for_bl()` extrahiert die BL-spezifische Logik.
- Adapter-Fehler pro BL werden geloggt + skipt, blockieren nicht die
anderen BL.
- Response-Erweiterung: `per_bundesland`-Liste mit Per-BL-Stats
(enqueued / skipped_existing / error).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-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>
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>
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>
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>
**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>
User-Feedback nach Live-Test:
**1. Idempotenz** — Pressemitteilungen wurden ungespeichert generiert,
doppelter Klick erzeugte doppelten Draft + LLM-Kosten.
- Neuer Helper `_find_existing_draft(drucksache, news_url)` der den
neuesten Draft fuer das Paar zurueckgibt
- `generate_draft()` prueft per Default zuerst den Lookup, liefert
existing zurueck mit `_was_existing=True` (kein LLM-Call)
- `force=True` Parameter fuer bewusste Neu-Generierung
- Endpoint nimmt `?force=true` Query-Param entgegen
- UI: Modal zeigt klar "Bestehender Entwurf vs Neu generiert" Banner,
mit "Neu generieren"-Button im existing-Banner
**2. Premium-Modell statt Default** — User wollte hoehere Sprachqualitaet
("Opus oder sowas"). Da das Projekt Qwen via DashScope nutzt (kein
Anthropic), Wechsel auf `settings.llm_model_premium` (qwen-max).
- Tradeoff: ~3× teurer (~6 Cent statt 2 Cent) und ~2× langsamer
(~30 s statt 15 s) — aber spuerbare Qualitaetsverbesserung in
Pressemitteilungs-Diktion
- confirm-Dialog im Frontend nennt jetzt 6 Cent + 30 s
**3. Wrapper-Verbesserungen** — `auto-fetch-news.sh` aufgeraeumt:
- Container-Check (skip wenn down) analog zu run-digest.sh
- START/END-Timestamps
- Ausfuehrliche cron-install-Doku im Header
- Auto-Backfill: wenn erster Run >= 100 Embeddings (Limit gehit),
wird embed_pending_articles bis zu 500 weitere nachgeholt
Tests: 5 neue (idempotency, force, _find_existing_draft × 3). Suite
1053 gruen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stimm-Index pro Fraktion über Quartale. Linien-Chart pro Fraktion,
Lücken bei Quartalen mit n<3 (Ja UND Nein). Macht sichtbar, ob sich die
Gemeinwohl-Affinität einer Fraktion innerhalb der Wahlperiode verschiebt.
- `_quarter_for(datum)` Helper: ISO-Datum → "YYYY-Qn".
- `aggregate_stimm_index_zeitreihe()` analog zu pro_wert/pro_gruppe,
aber nach Quartal-Bucket statt Achse.
- `GET /api/auswertungen/stimm-index-zeitreihe?parteien=CDU,SPD,...`
- 4. Sub-Section im Stimmverhalten-Tab: Multi-Linien-Chart mit
Partei-Farben (CDU schwarz, SPD rot, GRÜNE grün, FDP gelb, AfD blau,
LINKE pink, BSW lila, SSW navy, BVB-FW orange).
Bei aktueller Sparse-Datenmenge (35 Assessments × 4 Quartale) ist der
Chart heute meist leer — Infrastruktur ist ready, fuellt sich automatisch
mit Issue #44 Batch-Bewertung.
Tests: 10 neue (4 _quarter_for, 6 aggregate). Suite jetzt 1005 grün.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase-2-Erweiterungen des Stimmverhalten-Tabs:
**1. Empfehlungs-Konsistenz (#167):**
Pro Fraktion: Anteil der Anträge mit GWÖ-Empfehlung
"Uneingeschränkt unterstützen" oder "Unterstützen mit Änderungen",
bei denen die Fraktion trotzdem NEIN gestimmt hat. Orthogonal zur
Heuchelei-Quote — prüft NICHT gegen Wahlprogramm-Treue, sondern gegen
die GWÖ-Empfehlung des Systems.
- `aggregate_empfehlungs_konsistenz()` in app/auswertungen.py
- `GET /api/auswertungen/empfehlungs-konsistenz`
- 5. Chart-Sub-Section im Stimmverhalten-Tab (rote Bar Chart, 0..100%)
**2. CSV-Export (Phase-1-Querschnitts-TODO):**
Long-Format-CSV mit Spalten: drucksache, bundesland, wahlperiode, datum,
gwoe_score, empfehlung, partei, vote, ist_antragsteller. Macht alle
Stimmverhalten-Aussagen wissenschaftlich auswertbar (R/pandas/Excel).
- `export_stimmverhalten_csv()` in app/auswertungen.py
- `GET /api/auswertungen/stimmverhalten.csv` mit
Filter-Parametern bundesland/wahlperiode/exclude_antragsteller
- "CSV-Export"-Button im Stimmverhalten-Tab neben dem Toggle
**Tests:** 27 Stimmverhalten-Tests (war 18, +4 Empfehlungs-Konsistenz,
+5 CSV-Export). Fixture um `empfehlung`-Spalte erweitert.
Suite: 989 Tests grün (war 980).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Auswertungs-Sicht: Welche Fraktionen stimmen häufiger gemeinwohl-
orientierten Anträgen zu? Verschneidet GWÖ-Bewertung pro Antrag mit
dem tatsächlichen Plenum-Stimmverhalten der Fraktionen.
Vier Aussagen, alle hinter dem neuen Tab "Stimmverhalten":
1. **Gemeinwohl-Stimm-Index** pro Fraktion: Ø-GWÖ-Score der JA-Anträge
minus Ø-GWÖ-Score der NEIN-Anträge. Domain −10..+10. Positiv = stimmt
eher Gemeinwohl-affinen Anträgen zu.
2. **Heuchelei-Quote** pro Fraktion: Anteil der Anträge mit
wahlprogramm_score ≥ 7 (passt zum eigenen Wahlprogramm), bei denen
die Fraktion trotzdem NEIN gestimmt hat.
3. **Stimm-Index pro GWÖ-Wert** als Heatmap: 5 Spalten (Würde,
Solidarität, Nachhaltigkeit, Gerechtigkeit, Demokratie) aus den
gwoe_matrix-Suffix-Spalten. Domain −5..+5 pro Zelle.
4. **Cross-BL-Vergleich** als Grouped Bar: gleiche Fraktion in
mehreren Ländern. Nur Fraktionen in ≥2 BL mit ausreichender
Datenbasis.
Querschnitt:
- `exclude_antragsteller=True` per Default (Toggle-Checkbox in UI),
weil Antragsteller-Fraktionen quasi immer JA stimmen → würde Index
verzerren. Toggle macht den Effekt sichtbar.
- `min_n=5` pro Fraktion fuer Stimm-Index, n=3 fuer Heatmaps.
Fraktionen unter dem Cutoff werden als "Nicht aussagekräftig" separat
gelistet.
- Caveat-Banner mit `n_assessments_matched` über jedem Chart.
Implementation:
- `app/auswertungen.py`: `_load_assessments_with_votes()` JOIN-Helper
+ 4 Aggregat-Funktionen analog zu `aggregate_matrix`-Pattern.
Reuse: `normalize_partei` für Aliasing (BÜNDNIS 90/DIE GRÜNEN →
GRÜNE), `wahlperiode_for` für WP-Filter.
- `app/main.py`: 4 neue read-only GET-Endpoints unter
`/api/auswertungen/stimm-index|heuchelei|stimm-index-pro-wert|
stimm-index-cross-bl`.
- `app/templates/v2/screens/auswertungen.html`: 4. Tab "Stimmverhalten"
mit 4 Sub-Sektionen, Chart.js Bars + HTML-Heatmap-Tabelle.
- `tests/test_auswertungen_stimmverhalten.py`: 18 neue Tests
(Fixture-DB mit 13 Assessments + 13 Vote-Results, Edge-Cases:
GRÜNE-positiver-Index, AfD-negativer-Index, exclude_antragsteller-
Effekt, min_n-Cutoff, leere DB).
Sparse-Data-Realität: aktuell 35 Assessments im prod, dünne Datenbasis
fuer einige Fraktionen. Feature wächst mit Issue #44 Batch-Bewertung.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pro BL zeigt die Tabelle nun:
- Doku-System (wie bisher)
- Drucksachen: alle aktiv (Adapter laufen)
- Plenum-Votes: 'aktiv' wenn Parser registriert (NRW), sonst 'Stub'
Plus Erklär-Hinweis: 'Plenum-Votes = fraktions-aggregierte
Abstimmungsergebnisse aus den Plenarprotokollen (#106). Stubs sind
Tracking-Stellen fuer kuenftige Implementierungen (Issues #148-#163).'
main.py reicht supported_bundeslaender() aus protokoll_parsers an die
Template-Context durch (plenum_vote_parsers-Set).
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.
Antrag-Detail-Endpoint liest plenum_votes via get_plenum_votes() und
reicht sie an antrag_detail.html durch.
Block rendert pro Plenum-Abstimmung eine Karte:
- Ergebnis (angenommen/abgelehnt/...) farb-kodiert
- 'einstimmig'-Annotation falls gesetzt
- Quelle (Protokoll-ID, mit URL als Tooltip)
- Fraktions-Chips fuer Ja/Nein/Enthaltung
Mehrfach-Abstimmungen einer Drucksache (Ueberweisung + finale
Beschlussfassung) erzeugen mehrere Karten — chronologisch via
parsed_at DESC im Repository sortiert.
Block erscheint nur, wenn Eintraege existieren (kein leerer Header).
System- und User-Prompt-Template stehen jetzt collapsed unter dem
neuen Abschnitt 'LLM-Prompts'. Der User-Prompt wird auf eine eigene
Konstante USER_PROMPT_TEMPLATE umgestellt und via .format(...) gerendert,
sodass das gleiche Template auf der Methodik-Seite gezeigt werden kann
ohne den f-string-Code zu duplizieren.
Closes#145
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