Commit Graph

292 Commits

Author SHA1 Message Date
Dotty Dotter
70d9790b4b feat(#177): Programm-Treue im BELEGE-Layout — pro Partei zwei aufklappbare Blöcke (WP+PP)
- _row_to_detail liefert zitate inline pro Wahlprogramm/Parteiprogramm-Block
- Template rendert <details>: Summary mit Score-Chip, Body mit Einschätzung+Belege
- v2.css: neue Klassen v2-treue-block/-label/-body, v2-pill, v2-einschaetzung
- Separate "Belege — Partei"-Sektion entfernt (ist jetzt inline pro Programm)

Tests: tests/test_v2_pdf_consistency.py (#176 generalisiert) bleibt grün —
fraktions_scores trägt zusätzliche zitate-Felder, ändert aber keine
Score/Begründungs-Werte aus dem Vergleich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:21:42 +02:00
Dotty Dotter
38e58e4ee0 feat(#175 Phase 23 step 1): Matrix-Block-Renderer via v2-Macro vorbereitet
Erste Stufe der 'one-source-of-truth'-Architektur (User-Vorschlag):
build_matrix_html_v2() rendert die GWÖ-Matrix mit dem matrix_mini-
Macro aus v2/components/matrix_mini.html — gleiche Quelle wie die
Web-View.

Noch nicht aktiv: das v2.css-Stylesheet muss erst im PDF-WeasyPrint-
Aufruf eingebunden werden (Klassen wie m-pp/m-p/m-0/m-n/m-nn). Bis
dahin nutzt der PDF-Renderer weiterhin build_matrix_html mit eigenem
inline-CSS.

Folge-Schritte (Refactor-Pattern):
1. v2.css als zusaetzliches Stylesheet in HTML(string=...).write_pdf()
2. Pro Block (Programmtreue, Verbesserungsvorschlaege, Vote): analog
   v2-Macro extrahieren + im PDF includen.
3. Wenn alle Blocks via Macro rendern: PDF-HTML wird zur reinen
   Block-Liste, Layout-Wechsel ist nur ein @page-CSS-Tausch.

Refs: #175

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:15:25 +02:00
Dotty Dotter
d80e6e7aad feat(#177 Phase 21+22): Verdict-Umfluss + Merken bei Bewertung
Phase 21 — Score-Hero auf Float-Layout:
- Score-Zahl floated links, Verdict-Text fließt rechts daneben UND
  unter sie weiter (vorher: flex-baseline, Text bricht nicht um).
- overflow:hidden auf Container fuer clean float-clearing.

Phase 22 — Merken-Button oben rechts auf gleicher Höhe wie 'Bewertung'-Label:
- Visuelle Einheit: 'Bewertung'-Header links, Merken-Button rechts.
- Vorher: Merken-Button stand alleinig zwischen Score und Vote-Block,
  optisch losgeloest.

Refs: #177

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:13:37 +02:00
Dotty Dotter
045461492f fix: ist_antragsteller/ist_regierung Fallback aus Drucksachen-Metadaten
LLM-Output setzt diese Flags nicht zuverlaessig (alle Werte heute None
in NRW/18/18246-Beispiel). _row_to_detail leitet sie jetzt im
Fallback aus den Drucksachen-Meta ab:
- ist_antragsteller := Fraktion in row.fraktionen (Antragsteller-Liste)
- ist_regierung := Fraktion in BUNDESLAENDER[bl].regierungsfraktionen

Damit erscheinen die 'Antragsteller:in' / 'Regierungsfraktion'-Pills
auch bei alten Assessments ohne explizite LLM-Flags. LLM-Wert (falls
gesetzt) hat weiterhin Vorrang.
2026-05-07 09:10:49 +02:00
Dotty Dotter
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
c06de002ca docs(Phase 19): ADR 0012 DEBUG_AUTH_TOKEN-Bypass
Dokumentiert das gesamte Bypass-Pattern mit Optionen-Vergleich
(Service-Account vs. langes JWT vs. ENV-Secret), Härtungsstrategie
(prod-compose ignoriert ENV, Audit-Logging, wöchentliche Rotation)
und Folgen für ADR 0005 (Keycloak Dev-Bypass).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:53:55 +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
f6220b52e0 test(Phase 16): 9 E2E-Smoketests gegen gwoe-dev mit DEBUG_AUTH_TOKEN
Regression-Schutz fuer die heutigen UI-Fixes:
- test_cluster_list_has_cards (#56c68d3 — drucksachen vs members)
- test_cluster_detail_visible_after_click (#d72a6f3 — display:'')
- test_cluster_force_graph_renders (#60db39d — Edge-IDs)
- test_stimmverhalten_chart_has_size (#cb6971f — controls-bar-leak)
- test_score_histogram_has_buckets
- test_antrag_detail_has_markers (Heuchelei + Konsistenz + Mehrheits-Bar)
- test_antrag_matrix_coloring (#ee93fcd — rating-Shift)
- test_merkliste_add_and_delete (#c599e5f + #0394803 — onclick-quoting + ID-Lookup)
- test_aktuelle_themen_5_tabs

Aktivierung: DEBUG_AUTH_TOKEN gesetzt + pytest -m e2e.
Lokaler Run: 9/9 passed in 52s.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 23:50:40 +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
db2fdda66b test(#191 Phase 10.4): 10 Tests fuer presse_generator Style-Switch
MockBewerter zeichnet system_prompt + user_prompt + model auf, damit
der Style-Switch isoliert testbar ist.

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

10 passed.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:12:39 +02:00
Dotty Dotter
1dfdb59954 feat(#189 Phase 10.2): Empfehlungs-Konsistenz-Drilldown analog zu Heuchelei
- get_empfehlungs_konsistenz_cases() liefert Antraege wo `partei` mit
  NEIN gestimmt hat, obwohl die GWÖ-Empfehlung "Unterstuetzen" lautete.
- Endpoint GET /api/auswertungen/empfehlungs-konsistenz-cases
- Frontend: Konsistenz-Bar bekommt onClick → Modal-Tabelle mit Drucksache,
  BL, Datum, GWÖ-Score, Empfehlung, Beschluss. Drucksachen-Link ins Detail.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 17:08:57 +02:00
Dotty Dotter
62636b5a78 quality: 9 Tests fuer auto_rate_runs + ruff F401 in main.py
Phase 8 (Code-Pflege):

- Neue Test-Datei tests/test_auto_rate_runs.py (9 Cases) deckt
  record_auto_rate_run, list_auto_rate_runs, auto_rate_today_total
  und das Schema ab.
- list_auto_rate_runs sortiert jetzt by id DESC (statt started_at DESC),
  weil started_at nur sekundengenau ist und Sub-Sekunden-Inserts
  unstabilen Output produzierten.
- ruff --select F401 --fix auf main.py: 7 ungenutzte Imports entfernt
  (MAX_SEARCH_QUERY_LEN, import_json_assessments, KLEINE_ANFRAGE,
  BUNDESLAENDER, lokale sqlite3/json/timezone-Reimports). Tests
  weiterhin grün (74 passed).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:58:49 +02:00
Dotty Dotter
092a68ac02 fix(#177): f-string ohne Backslash (Py3.9-Kompat fuer Local-Tests) 2026-05-06 16:21:17 +02:00
Dotty Dotter
eb74ae647e fix(#178): Thread-Prompt verschaerft fuer 280-Zeichen-Posts 2026-05-06 16:14:11 +02:00
Dotty Dotter
6a78dee2d1 feat(#179 Phase 4.3): pm-sample-bundle.sh fuer 5 PMs (PM + Thread) zur Sichtung
Skript laeuft fuer N_SAMPLES (Default 5) hochbewertete Antraege jeweils
generate_draft() mit style='pm' und style='thread' aus. Idempotent ueber
das presse_drafts.style-Schema.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:12:41 +02:00
Dotty Dotter
a2b8f8c6fe feat(#178 Phase 4.2): PM-Variante 'thread' fuer Mastodon/Twitter-Threads
- Schema additiv: presse_drafts.style TEXT NOT NULL DEFAULT 'pm' via
  ALTER TABLE (idempotent in init_db).
- presse_generator.generate_draft(style='pm'|'thread') nutzt eigenen
  SYSTEM_PROMPT_THREAD (3-5 Posts à ≤280 Zeichen, Hook + Lebenslagen +
  Forderung, Hashtags am Schluss; keine **fett**-Markdown).
- _find_existing_draft, list_drafts, list_drafts_for, get_draft liefern
  jetzt auch das style-Feld zurueck.
- Endpoint /api/aktuelle-themen/generate-presse?style=thread baut den
  Switch ein. Ohne Param weiterhin 'pm'.
- Frontend: PM-Modal zeigt den style-Tag (📰 PM / 🐦 Thread) im Banner
  und bietet einen Knopf "Auch als Thread / Auch als PM" generieren.
  Idempotenz pro (drucksache, news_url, style)-Tripel.

Refs: #170, #178

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:11:16 +02:00
Dotty Dotter
4bb267aace fix(#177): PDF-Endpoint auf /drafts/pdf/{id} (Routing-Konflikt mit {draft_id:int}) 2026-05-06 16:06:57 +02:00
Dotty Dotter
d8d7c3f02f feat(#177 Phase 4.1): PM-PDF-Render via WeasyPrint
Neuer Endpoint GET /api/aktuelle-themen/drafts/{id}.pdf rendert den
gespeicherten PM-Body inkl. **fett**-Markdown als A4-PDF mit Header
(Drucksache-Link, GWÖ-Markup) und Footer-Quellenangabe.

PM-Modal in /aktuelle-themen bekommt zusätzlich einen 📄 PDF-Button
neben Mail/Clipboard. Dateiname `PM-DRUCKSACHE-DRAFTID.pdf`.

Refs: #170, #177

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:05:57 +02:00
Dotty Dotter
3bfe9f425f fix(#173): docker exec -e Flags vor Container-Name 2026-05-06 16:03:25 +02:00
Dotty Dotter
c241d329aa feat(#173): Vote-Orphans-Auto-Bewertung als Cron-Job + Tracking
Phase 3 (Vote-Orphans-Auto-Bewertung):

- Neue Tabelle `auto_rate_runs` (additiv) mit started_at, source,
  bundesland, limit_requested, n_attempted/succeeded/failed/skipped,
  error_summary.
- Neue DB-Helper: record_auto_rate_run, list_auto_rate_runs,
  auto_rate_today_total.
- POST /api/auswertungen/vote-orphans/auto-rate erweitert um source,
  daily_cap und Run-Persistenz. Throttled gegen Tagessumme.
- Neuer Endpoint GET /api/auto-rate-runs (admin) — letzte N Runs +
  Tagessumme.
- scripts/auto-rate-orphans.sh: Cron-Wrapper (analog auto-fetch-news.sh)
  mit MAX_PER_RUN=30 / MAX_PER_DAY=200 Defaults, BUNDESLAND-Filter
  optional, ruft direkt die Python-Worker-Funktion via docker exec.
- Admin-Stand-Dashboard: KPI-Zeile "heute X Runs / Y versucht" + Tabelle
  der letzten 5 Runs mit BL/Counts/Notiz.

Refs: #173, ADR 0010

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 16:02:33 +02:00
Dotty Dotter
1a94b27a22 ux(#171): Mobile-Polish fuer Tab-Bars + Drilldown-Modal
- auswertungen.html: .auswert-tabs scrollt jetzt horizontal (overflow-x:auto)
  + nowrap-Buttons + kleinere Padding/Font auf <600px.
- aktuelle-themen.html: .at-tab Buttons whitespace:nowrap, Tab-Container
  ebenfalls scrollbar.
- Drilldown-Modal: 8px statt 20px Padding aussen, Tabelle in
  overflow-x-Wrapper, max-height 90vh statt 80vh.

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:57:40 +02:00
Dotty Dotter
7a1c37afe4 feat(#170 Phase 2.2): Drilldown-Modal von Heuchelei-Bar zu Antragsliste
Klick auf eine Heuchelei-Bar oeffnet ein Modal mit der konkreten
Liste der Antraege wo die Fraktion mit Nein gestimmt hat, obwohl
der Antrag inhaltlich zum eigenen Wahlprogramm passt.

- Backend: app.auswertungen.get_heuchelei_cases() + Endpoint
  GET /api/auswertungen/heuchelei-cases?partei=X[&bundesland=Y].
- Backend: _load_assessments_with_votes liefert jetzt zusaetzlich
  das ergebnis-Feld (additiv im SELECT).
- Frontend: onClick-Handler im Heuchelei-Bar-Chart, Modal-Markup
  wird lazy injiziert, Tabellen-Drilldown mit Drucksachen-Link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:54:51 +02:00
Dotty Dotter
741faae8ff feat(#169): eigene /stimmverhalten-View als linker Nav-Eintrag
Neue Route /stimmverhalten rendert dieselbe auswertungen.html, aber
mit default_tab='stimmverhalten' und v2_active_nav='stimmverhalten'.
Linker Nav-Eintrag 'Stimmverhalten' (Icon scales) zwischen
Auswertungen und Aktuelle Themen.

Beim Page-Load aktiviert das DOMContentLoaded-Handler den im Context
gesetzten Tab — fuer /auswertungen ist es 'bl-partei' (Default), fuer
/stimmverhalten direkt 'stimmverhalten'. Kein Code-Duplikat im
Tab-Inhalt.

Refs: #169

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:52:09 +02:00
Dotty Dotter
5823828fec feat: Opportunismus-Marker bei JA-Stimmen mit WP-Score < 3
Symmetrisch zur Heuchelei-Logik: bei JA-Fraktionen, deren eigener
Wahlprogramm-Score < 3 ist, erscheint ein dezenter italic '!' mit
Tooltip. 11 echte Cases gefunden auf dev (NRW + BB).

app/marker.py: opportunismus_score() — neun neue Tests (test_marker.py
jetzt 44 grün).

Refs: ADR 0010, Phase 2.4

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

Refs: ADR 0010

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:45:59 +02:00
Dotty Dotter
9498ca4b97 refactor + tests: marker.py + pm_render.py mit 56 Unit-Tests
Logik aus dem Jinja-Template (Heuchelei-Marker, Konsistenz-Block,
decisive-Outcome-Selection) in app/marker.py extrahiert. Template
ruft die drei Helper als Jinja-Globals auf. Damit ist die Logik
testbar ohne Render-Kontext.

Plus: app/pm_render.py als Python-Spiegelbild des JS-Mini-Markdown-
Renderers in aktuelle-themen.html — fuer Tests und potenzielle
Server-side-Render-Optionen (z.B. PM-Mail).

Tests:
- tests/test_marker.py (35 Cases): heuchelei_score, decisive_outcome,
  consistency_state inkl. Multi-Vote, ambivalente Empfehlung,
  Edge-Cases.
- tests/test_pm_render.py (21 Cases): Bold, Italic, Listen,
  HTML-Escape, Paragraph-Splitting, snake_case-Schutz.

Refs: ADR 0010, ADR 0011

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 15:44:12 +02:00