Commit Graph

303 Commits

Author SHA1 Message Date
Dotty Dotter
d470e03caf feat: Scorecard Portrait redesign — Matrix mit Achsen-Labels, flex-grow gegen Slack
User-Feedback nach Browser-Inspektion: 'Bei mir sieht das immer noch
nicht so aus' — und tatsächlich, das vorherige Layout hatte zwei
sichtbare Probleme:

1. justify-content: space-between auf portrait-body verteilte den
   Slack-Raum nicht symmetrisch, sondern haeufte ihn unten zwischen
   Matrix-Block und Begruendung an. Folge: ~270 px Luecke zwischen
   diesen Sektionen.

2. Die Matrix war 'stilisiert' nur in Form (5×5 Farb-Grid) — aber
   ohne Achsen-Beschriftungen muessten Buerger:innen wissen was A1,
   B2 etc. bedeuten. Kommt nicht an.

Redesign:
- Layout-Strategie: portrait-matrix-block bekommt flex-grow:1 und
  absorbiert allen verbleibenden vertikalen Platz; Matrix bleibt
  zentriert. Andere Sektionen sitzen in natuerlicher Hoehe oben/
  unten. Kein space-between.

- Matrix stilisiert mit Achsen:
  · Spalten-Header: Wuerde / Solidaritaet / Nachhaltigkeit /
    Gerechtigkeit / Transparenz (Brand-Color, Mono-Caps)
  · Zeilen-Header: A·Lieferant:innen, B·Finanzen, C·Verwaltung,
    D·Buerger:innen, E·Gesellschaft & Natur
  · Cells in 88×88 Quadraten, gap 4 px
  · Legende horizontal unter der Matrix statt seitlich

- Begruendung: line-clamp 5, sitzt am Boden, mit Trennlinie und Sublabel.

- Cache-Control: no-store auf /v2/scorecard, damit Browser nach
  Layout-Aenderungen nicht die alte HTML-Variante zeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:32:44 +02:00
Dotty Dotter
099fbd0fb0 fix: Scorecard-Matrix — Neutral-Cells sichtbar, weisser Frame raus
Im rebalanced Render war die Matrix immer noch loechrig: r-0 (#d8d8d2)
hatte zu wenig Kontrast gegen den weissen Matrix-Frame, sodass die
neutralen Cells visuell wie Luecken wirkten.

Zwei Fixes:
- r-0 dunkler: #b8b8b2 statt #d8d8d2 (deutliche Grau-Praesenz)
- Matrix-Frame raus: kein weisser Hintergrund mehr, der Karten-Gradient
  wird durch die 4px-Gaps sichtbar — Cells stehen als klare Farbflaechen
  heraus, kein konkurrierender weisser Untergrund.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:22:36 +02:00
Dotty Dotter
c6278d8453 feat: Scorecard Portrait neu komponiert — Score-Hero-Band, sichtbare Neutral-Cells, Title gekuerzt
Visuelle Probleme im vorigen Render: Title dominierte oben (5+ Zeilen,
36pt), Matrix wirkte loechrig (Neutral-Cells #f0f0f0 verschwanden im
Karten-Hintergrund), Score-Row war optisch zu zart fuer den Anker.

Neue Komposition:
- Title kompakt: 26pt, line-height 1.18, line-clamp 3 — beschreibt aber
  dominiert nicht mehr.
- Score-Hero-Block: tonierter Hintergrund passend zur Score-Farbe
  (gruen/orange/rot) plus dicker Border-left, full-width — wird zum
  visuellen Anker statt nur zwischen Trennlinien zu sitzen. Score 132pt,
  Verdict 30pt.
- Matrix: 480x480 mit weissem Frame + zarter Border, 6px gap. Neutral-
  Cells (r-0) jetzt #d8d8d2 statt #f0f0f0 → klar sichtbar im Grid,
  Loch-Look weg.
- Legende: Swatches 22px statt 18px, gleiche Sichtbarkeit wie Cells.
- Begruendung: line-clamp 4 (statt 9), eigene 'Begruendung'-Sublabel,
  obere Trennlinie — bewusst schmal als Ausklang.

Hierarchie: Score-Hero ist optisch dominant (Anker), Matrix das datenreiche
Zentrum, Title deskriptiv aber zurueckhaltend, Begruendung ergaenzt ohne
zu konkurrieren.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:21:03 +02:00
Dotty Dotter
2f9be62649 fix: Scorecard-Button-JS hatte noch format=og hardcodiert
Route-Default ist seit letztem Commit portrait, der Button-Aufruf
ueberschrieb das aber explizit mit ?format=og. Param raus, jetzt
laeuft alles ueber den Server-Default (portrait, 1080×1350).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:14:21 +02:00
Dotty Dotter
5b2930b844 fix: Scorecard-Whitespace + Instagram-Button ehrlicher
Whitespace-Problem (User: 'Da ist immer noch viel Rand'):
Inspektion am gerenderten PNG zeigte: massive Slack-Zone unten zwischen
Summary-Text und Footer. Ursache: portrait-body hatte flex:1 ohne
justify-content, alle Items stapelten sich am oberen Rand und der
Bottom war leer. Plus: Summary war auf 360 Zeichen + line-clamp:6
beschraenkt — Text wurde regelmaessig vor Ende abgeschnitten und
fuellte selbst die wenigen Zeilen nicht voll.

Fix:
- portrait-body bekommt justify-content: space-between und
  padding-bottom: 26px
- Summary truncate 360 → 700, line-clamp 6 → 9
- gap 16 → 14, margin-top 16 → 14

Effekt: Slack wird zwischen Sektionen gleichverteilt UND der Begruendungs-
text fuellt jetzt seinen Bereich, sodass kaum noch Slack uebrig ist.

Instagram-Button (User: 'funktioniert weiter nicht'):
Realitaet ist: Instagram hat keine Web-Publishing-API. Auf Desktop
ist 'Direkt-Posten' physikalisch nicht moeglich. Vorher: Fallback
oeffnete das Bild im neuen Tab — fuehlte sich nicht wie 'Sharing' an.

Jetzt zwei klar getrennte Pfade:
A) Mobile mit Web-Share-Files: navigator.share({files:[png]}) oeffnet
   OS-Share-Sheet, Instagram als Ziel; AbortError (User-Cancel) wird
   STILL gehandelt (vorher fiel das in den Fallback).
B) Desktop / unsupported: PNG-Download via <a download> getriggert,
   Begleittext geht in die Zwischenablage. Toast erklaert klar:
   'Bild aufs Phone uebertragen, in der Instagram-App posten, Text
   einfuegen.' — keine falsche Erwartung mehr, dass Web allein das
   Posten erledigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:11:48 +02:00
Dotty Dotter
0e5b2180ab feat: Scorecard-Default = portrait, Instagram via Web-Share-API, Padding gestrafft
User-Feedback in drei Punkten:

1. 'Standard auch fuer die Scorecard sein' — /v2/scorecard und
   /api/assessment/scorecard.{png,pdf} default jetzt format=portrait
   statt og. Wer das alte OG-Layout will, muss explizit ?format=og
   setzen (oder square). Externe OG-Tags sind nicht betroffen, die
   nutzen ein anderes Template (v2/og_template.html).

2. 'Instagram-Button sollte den Teilen-Dialog aufrufen' — implementiert
   mit navigator.share() + File-Blob. Auf Mobile (Safari iOS / Chrome
   Android) oeffnet der native Share-Sheet und Instagram erscheint
   direkt als Ziel; Bild + Text gehen mit. Auf Desktop / Browsern
   ohne canShare({files:…}) falle auf den vorigen Fallback zurueck:
   Bild in neuem Tab + Text in Zwischenablage.

3. 'Card nutzt Platz besser, viel Rand' — alle Paddings reduziert:
   - Card-Padding portrait: 54/56/32 → 34/38/24
   - Body gap: 22 → 16, margin-top: 26 → 16
   - Title: 32pt → 36pt
   - Score-Number: 110pt → 130pt
   - Matrix: 380×380 → 460×460 (groesser, mehr Detail erkennbar)
   - Footer: enger an den Rand
   Inhalt nimmt jetzt mehr Platz ein, weniger Whitespace-Verschwendung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:06:46 +02:00
Dotty Dotter
8c8dfbe625 feat: Scorecard im Instagram 4:5 Hochformat (1080×1350) — Title + Score + Matrix prominenter
User-Frage: 'Mach die Scorecard im Instagram-Format hochkant. Was genau
ist das nochmal? Vier zu fünf?' — Ja, 1080×1350 ist das Instagram-Feed-
Format mit dem groessten vertikalen Real-Estate. Stories sind 9:16,
quadratisch ist 1:1.

Neuer format=portrait (1080×1350) in /v2/scorecard und
/api/assessment/scorecard.png. Layout speziell vertikal:

- Title gross (32 pt) mit reichlich Leading
- Fraktions-Pills in eigener Zeile
- Score-Row: HUGE Zahl (110 pt) links + Empfehlung-Wort rechts,
  oben/unten zarte Trennlinien — klare Bewertungs-Anker
- Matrix 5×5 gross (380×380) mit Legende daneben — User sieht
  sofort welche Felder + - + - sind
- gwoe_begruendung als 5-zeilige Zusammenfassung darunter
- Footer am Boden

Instagram-Button im Share-Block stellt von square auf portrait um.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:02:04 +02:00
Dotty Dotter
17dd5911d0 fix: PM-Generieren-Button nur fuer angemeldete User
User-Feedback: 'PM generieren sollte gar nicht angezeigt werden, wenn
ich nicht angemeldet bin.' Der Endpoint erfordert auth und verbraucht
qwen-max-Credits — der Button ist fuer Gaeste sinnlos.

Render-Logik in loadNewsMatches() gated auf currentUser. Plus
DOMContentLoaded-Init wartet jetzt async auf initAuth(), bevor
loadNewsMatches() laeuft — sonst wuerde der Button bei langsamer
auth-Antwort fuer angemeldete User auch fehlen (Race).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:57:43 +02:00
Dotty Dotter
2821b8566e feat: Scorecards in Share-Block sichtbar + Instagram-Button
User-Frage: 'Wo kann ich jetzt die Cards angucken? Vielleicht verbunden
mit einem Instagram Sharing Button?'

Endpoints existieren laengst (#179):
- /v2/scorecard?format=og     → 1200×630 LinkedIn/Twitter-Card
- /v2/scorecard?format=square → 1080×1080 Instagram
- /api/assessment/scorecard.png?format=square&scale=2 → PNG

In der Share-Row jetzt drei neue Eintraege:

1. '📷 Instagram' — oeffnet Square-PNG (1080×1080) im neuen Tab,
   legt Begleittext in die Zwischenablage. Instagram hat keinen
   Web-Share-Endpoint, daher: Bild speichern + Text einfuegen.
2. '📊 Scorecard ansehen' — oeffnet die OG-Format-Vorschau (1200×630)
   im neuen Tab, der User sieht wie die Card auf LinkedIn/Twitter
   aussehen wird.
3. '🖼 Stock-Bild' — alter Magnific-Stockphoto-Knopf, jetzt klar
   gelabelt damit er nicht mit der Scorecard verwechselt wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:56:37 +02:00
Dotty Dotter
51f6afa029 fix: LinkedIn-Share — feed/?shareActive=true statt deprecated share-offsite
LinkedIn /sharing/share-offsite/ akzeptiert seit ~2024 keinen text-Param
mehr, der Composer oeffnet leer. Stattdessen /feed/?shareActive=true&text=
prefillt den Compose-Dialog mit Text + Permalink (Permalink rendert
LinkedIn als OG-Preview).

Plus: Text geht weiterhin in die Zwischenablage als Fallback (Strg/⌘-V
falls LinkedIn den Param mal wieder verschluckt). Pop-up-Blocker-
Hinweis wenn window.open null zurueckgibt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:54:46 +02:00
Dotty Dotter
a89334c6e7 fix: News — alle Items anzeigen, nur Summary-Absatz clampen (4 Zeilen)
User-Feedback: 'Es duerfen ruhig mehrere Items angezeigt werden. Aber
der Summary Text soll gekuerzt sein.'

Vorher: nur erstes Item sichtbar, Rest display:none.
Jetzt: alle Items sichtbar, Summary-<p> jedes Items auf 4 Zeilen
clamped. Titel/Meta/Tags/PM-Button bleiben pro Item komplett.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:53:42 +02:00
Dotty Dotter
7545d714f8 fix: News-Clamp auf Summary statt aufs ganze Item — PM-Button bleibt sichtbar
User-Feedback: 'Bei der Kuerzung der Beitraege geht der PM Generieren-
Button unter.' Mein 9-Zeilen-line-clamp lag auf dem ganzen Item-DIV
und schnitt deshalb den Button am Ende weg.

Fix: nur Summary-<p> wird auf 5 Zeilen geclampt; Meta, Title, Tags
und PM-Button bleiben unangetastet sichtbar. Item-Hoehe haengt damit
mehr von Title-Laenge ab, ist aber immer komplett.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:52:41 +02:00
Dotty Dotter
349af3749a fix: result_row default-href auf /antrag/ (= v3) statt /v2/antrag/
Suchergebnisse auf der Übersicht und in /durchsuchen oeffneten weiter-
hin den alten Profi-Modus, weil das result_row-Macro als Default
'/v2/antrag/'+drucksache eingebaut hatte. Jetzt zeigt der Default-
Pfad auf '/antrag/' = v3 Buerger:innen-Modus.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:46:40 +02:00
Dotty Dotter
c64f4df4da feat(methodik): Aktualisiert auf Stand Mai 2026 — neue Sektion Stimmverhalten, Versionsstand, partei-skopierte Verifikation
Methodik-Seite jetzt synchron zu Codebase + Features:

Was-macht-der-Pruefer-Liste:
- Empfehlung + Bewertungs-Sicherheit (Konfidenz im Tooltip) ergaenzt
- Schwerpunkt-Felder, Themen-Tags, Kernforderungen aufgenommen
- Abstimmungsergebnis + Wahlprogramm-Konflikt-Marker erwaehnt
- Aehnliche Antraege + News-Match aufgenommen

Pipeline-Step 4: ehrlicher dokumentiert — Verifikation ist
partei-skopiert (heutiger Bugfix), wortgleiche Treffer in fremdem
Programm fuehren zur Verwerfung statt zur Misattribution.

Neue Sektion "Stimmverhalten & Marker" mit
- Tabelle Heuchelei (⚠) / Opportunismus (!) inkl. Schwellen
- Hinweis auf v3-Sprache "Wahlprogramm-Konflikt"
- Konsistenz-Hinweis (Mehrheit deckt sich / gegen GWOE-Empfehlung)
- Code-Pointer zu app/marker.py

Einschraenkungen erweitert:
- Aktuelles vs. historisches Programm (Issue #186)
- Drucksachen-Eindeutigkeit-Limit

Qualitaetssicherung:
- Score-Cap-Invariante dokumentiert
- Neu-Analyse korrekt als manuell beschrieben (vorher "automatisch" -
  nicht ganz richtig)
- Property-/Fixture-Tests-Abschnitt referenziert ADR 0003 + 0008

Neue Sektion "Versionsstand": Live-Tabelle mit Sprachmodell, Embedding-
Modell, Programm-/Chunk-Counts, Adapter-Count, Matrix-Version, Skala.
Werte werden via Template-Context aus dem laufenden System gefuellt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:08:17 +02:00
Dotty Dotter
f0588714bf fix: Topbar-Wrap auf Mobile, Klassische-Ansicht raus, Tags-Greying + Title-Bug
Drei Korrekturen:

1. Mobile-Topbar: Auth-Widget + Bundesland-Selector + Theme-Toggle
   pushten die rechte Kante über 390 px Phone-Viewport. Fix: Topbar
   darf in @media (max-width: 900px) flex-wrappen, height auto,
   row-gap fuer mehrzeilig.

2. Topbar-Link "Klassische Ansicht" → /classic entfernt (verlinkt auf
   das alte v1-Frontend; v2 bzw. das neue v3 sind die aktiven Modi).

3. /tags-Seite hatte zwei Bugs:
   - Titel wurde aus a.titel (existiert nicht) statt a.title gelesen
     → User sah nur Drucksachen-Nummern und dachte "kaum Daten".
   - Kein Visual-Feedback welche Tag-Kombinationen leer wären.
   Beide gefixt: title-Field korrekt, plus Tag-Greying via class
   .tag-pill.disabled fuer Tags die zu 0 Treffern fuehren wuerden.
   Ausserdem Score-Field gwoeScore-Fallback und HTML-Escape fuer alle
   Strings (vorher XSS-anfaellig bei Title/Fraktion).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 12:05:10 +02:00
Dotty Dotter
848039627c fix: Mobile Overflow — minmax(0,1fr) + overflow-x:hidden
CSS-Grid 1fr-Track schrumpft per Spec NICHT unter min-content der Kinder.
Bei 390-px-Viewport bleibt der Track deshalb auf ~968px stehen (max-
content der weitesten Inhalte). Standard-Fix: minmax(0,1fr) hebt das
auf, body.overflow-x:hidden als Notbremse, overflow-wrap:anywhere
fuer lange unbreakable Strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:59:22 +02:00
Dotty Dotter
d8d8f99965 fix: Mobile-Overflow — v2-shell auf 100vw zwingen, sonst wuchs 1fr-Grid auf Content-Breite
Bug auf 390-px-Phone-Viewport: html/body waren korrekt 390 px, aber
.v2-main rendert mit 968 px Breite, alle Inhalte überlaufen horizontal
um 200-580 px. Ursache: .v2-shell hatte im mobile-Media-Query nur
grid-template-columns: 1fr — ohne explizite width:100% nimmt Grid die
max-content-Breite des 1fr-Tracks an, was bei .v3-page max-width:880
auf 880+padding wächst. Folge: Sidebar ausgeblendet, aber Layout
trotzdem auf Desktop-Breite.

Fix in @media (max-width: 900px):
- body.v2 .v2-shell { width: 100%; max-width: 100vw; min-width: 0; }
- body.v2 .v2-main  { max-width: 100%; min-width: 0; padding: 16px 12px; }
- body.v2 .v2-topbar/.v2-footer { max-width: 100%; }

body-Selector mit body.v2-Prefix für Spezifität gegen die Default-
Regel ohne body.v2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:55:03 +02:00
Dotty Dotter
535c2f15e4 fix: Citation-Binding partei-skopiert (Cross-Partei-Misattribution gestoppt)
Bug: AfD-Parteiprogramm-Block enthielt ein Zitat mit quelle "CDU
Grundsatzprogramm 2024, S. 33" (DRS 21/4939). Ursache: reconstruct_zitate
hatte alle Chunks aller Parteien in einen Pool gemischt. Wenn der LLM
unter AfD-Parteiprogramm einen Text emittierte, der zufaellig auch im
CDU-Programm vorkam, matched der Code den CDU-Chunk und ueberschrieb
quelle/url mit CDU-Werten.

Fix: Match strikt auf chunks_by_party[fraktion][kind]. Fallback auf
gleiche Partei/andere Kategorie (z.B. AfD hat nur Grundsatz-, kein
Wahlprogramm im Index). Wenn kein Match in der eigenen Partei → Zitat
verwerfen statt fremde quelle behalten. Lieber 0 Zitate als ein
Misattributions-Zitat.

Plus v3-UI:
- News-Box von ganz hinten nach oberhalb "Neu analysieren" verschoben
- News-Liste auf 1 Item gekuerzt + 9-Zeilen-Clamp via -webkit-line-clamp

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:51:03 +02:00
Dotty Dotter
1ef5578e02 feat(v3): v3 wird Default unter /antrag/{drs}, v2 zieht nach /v2/antrag/{drs}
Routen:
- /antrag/{drs}     → v3 (Standard, Bürger:innen-Modus single column)
- /v2/antrag/{drs}  → v2 (alter Profi-Modus, weiterhin erreichbar)
- /v3/antrag/{drs}  → Alias auf v3 (für alte Bookmarks)

UI-Hinweise auf alternative Ansichten ausgeblendet:
- v3-Topbar-Pill "Bürger:innen-Modus · Beta" + "→ Profi-Modus"-Toggle raus
- v2-Topbar-Link "→ Bürger:innen-Modus · v3 Beta" raus

Im Admin-Bereich (/v2/admin/stand) neuer Block "Alternative Ansichten"
mit Beispiel-Drucksache, Live-Link auf v3 (Default) und v2 (Profi).
Nur Admins sehen die Hinweise auf v2.

Trennlinien-Cleanup im Rest-Block:
- Doppellinie unter Abstimmungsergebnis aufgelöst (.v3-rest hatte
  border-top, das v3-section.border-bottom doppelt war).
- Neue Trennlinien via Klasse v3-rest-divider-top vor:
  · Teilen-Block (zwischen Ähnliche und Teilen)
  · Aktions-Links (zwischen Teilen und administrativem Bereich)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:29:06 +02:00
Dotty Dotter
4f4d9f3478 feat(v3): Merken+Vote-Zeile direkt unter Metadaten, Aktions-Links unter Teilen
User-Wünsche:
- Merken + Bewertung-treffend nach oben — direkt unter den Metadaten,
  noch vor der Zusammenfassung.
- PDF-Bericht / Original / JSON / Permalink-Zeile aus dem Kopf des
  Rest-Blocks weiter nach unten — zwischen Teilen und Neu-Analysieren.

Reihenfolge im Rest-Block jetzt: Ähnliche Anträge → Teilen → Aktions-
Links (PDF/…) → Neu analysieren → Historie → News → Kommentare.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:24:10 +02:00
Dotty Dotter
9eaa376fbe feat(v3): Wortwahl entjargonisiert, Layout-Reorganisation, Score mit Farb-Tint
Wortwahl (User-feedback: "vermeide Konfidenz, neutralere Formulierung
als Heuchelei"):
- "Konfidenz" → "Bewertungs-Sicherheit" (Tooltip behält wissenschaftl. Begriff)
- "Heuchelei-Marker"/"Opportunismus-Marker" → einheitlich
  "Wahlprogramm-Konflikt" (Tooltip behält wissenschaftl. Klassifikation)
- "Mehrheit kontra GWÖ-Empfehlung" → "Mehrheit gegen GWÖ-Empfehlung"
- Marker-Legende-Block entfernt (Tooltip pro Marker reicht)

Layout-Reorganisation:
- Stärkster/Schwächster Wert nach oben — zwischen Bewertung und
  User-Aktionen (vorher in "Rest"). Grid 2-spaltig, responsive.
- Konsistenz-Hinweis ("Mehrheit ... GWÖ-Empfehlung") an die Spitze
  des Abstimmungsergebnis-Blocks (vorher in "Rest").
- Marker-Legende komplett raus.

Visuelles:
- Score x/10 mit Farb-Tint hinterlegt: --score-{high,mid,low}-bg/-fg
  aus tokens.css. Farbe folgt der Score-Schwelle (≥7 grün, 4-6 mittel,
  <4 rot).
- Globale `body.v2 p { max-width: 72ch }` mit höherer Spezifität für
  .v3-page-Children überschrieben — alle Body-Texte nehmen volle Breite.

Programm-Treue Fallback:
- Wenn keine Zitate für ein Programm gefunden: erklärender
  Hinweistext im Body-Bereich, statt leerer Block.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:16:39 +02:00
Dotty Dotter
f471586f33 feat(v3): Themen, Kernpunkte, Schwerpunkt, Konfidenz, fehlende Programme, Wahlperiode + Ähnliche Anträge
DB-Felder die bisher in der UI fehlten, jetzt in v3 sichtbar:

- _row_to_detail() liefert themen, kernpunkte, schwerpunkt, link,
  konfidenz, fehlende_programme, wahlperiode an's Frontend.
- _wahlperiode_silent() leitet die WP aus datum+bundesland ab via
  wahlperioden.wahlperiode_for() — silent-fail bei Lookup-Fehler.

v3-Template:
- Wahlperiode in der Antrag-ID-Zeile ("18. Wahlperiode")
- Themen-Chips als Reihe unter byline
- Kernforderungen als Bullet-Liste in der Zusammenfassungs-Sektion
- Konfidenz-Pille (hoch/mittel/niedrig) neben der Empfehlung
- Schwerpunkt-Felder (Top-Matrix-Cells) als Chips über der Matrix
- Disclaimer "fehlende Programme" am Programm-Treue-Block
- Original-PDF-Link im Aktions-Block
- Ähnliche Anträge als eigener Block, geladen via JS aus
  /api/assessment/similar (Re-Use des bestehenden Endpoints aus #108)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 11:01:18 +02:00
Dotty Dotter
4de2df9cea feat(v3): single-column Restructure — neue Reihenfolge, CD-konforme Radien
User-Spec: 1a (single column überall), CD-Rundungen subtil 2-4 px,
keine 8px-Pills, keine Kreise. v3 wird nach Reife der neue Default.

Reihenfolge:
1. Metadaten/Titel
2. Zusammenfassung
3. Bewertung — Score xl + Empfehlung daneben + verdict_body voll breit
4. Merken + Bewertung treffend
5. Matrix 5×5 (volle Profi-Variante mit Klick-Modal)
6. Programm-Treue — pro Partei: WP-Zeile + PP-Zeile (default closed),
   Klick auf die Zeile expandiert Begründung + Belege. Score-Chip
   rechtsbündig.
7. Verbesserungsvorschläge — volle Breite, kein max-width
8. Abstimmungsergebnis — Plenum + namentliche Abstimmung
9. Rest — PDF/Teilen/Re-analyze/Historie + Stärken/Schwächen +
   Konsistenz-Hinweis + Marker-Legende + News-Box + Kommentare

v2-Templates unangetastet (Sub-Blocks vom letzten Commit beibehalten,
nur main-Block wird in v3 komplett überschrieben). v2-JS-Handler
bleiben funktionsfähig — gleiche DOM-IDs (v2-merkliste-btn, v2-vote-up,
v2-comments-list, v2-matrix-field-modal, …) im neuen Layout.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:51:33 +02:00
Dotty Dotter
895187ac36 feat(v3): Bürger:innen-Modus visuell ausgebaut — Wort-Etikett, 5-Werte-Bars, Glossar, Collapsibles
v2 unangetastet — v3 überschreibt nur Sub-Blocks. Neue v2-Blocks
für Override-Punkte: antrag_id_section, score_hero_section,
matrix_section, verbesserungen_section, aktions_section,
comments_section.

v3-Anpassungen:
1. Drucksache-ID: "Antrag im Landtag NRW · Drucksache 18/18246" —
   "Drucksache" als Glossar-Hinweis klickbar.
2. Score-Hero: großes Wort-Etikett ("Stark gemeinwohlfördernd" /
   "Gemischt" / "Widerspricht dem Gemeinwohl") aus verdict_title oder
   abgeleitet. Score-Zahl als kleiner Untertitel mit Glossar-Link.
   Akzentfarbe links (grün/blau/rot je nach Score).
3. Matrix: 5 Werte als Diverging-Bars (-5..+5) sofort sichtbar;
   volle 5×5-Matrix in <details>. Aggregation = Spalten-Mittel über
   die 5 Berührungsgruppen.
4. Verbesserungsvorschläge: <details> default zu, Hint-Text mit Anzahl.
5. Aktions-Links: JSON-Export raus — nur PDF + Permalink.
6. Kommentare: <details> default zu (für Erst-Leser:innen unwichtig).

Glossar-System: 6 Begriffe (Drucksache, Fraktion, GWÖ-Score,
GWÖ-Matrix, Heuchelei-Marker, Opportunismus-Marker) mit Klick/Focus-
Tooltip via Modal. Trigger: Element mit class="v3-glossar"
data-glossar="<key>".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:15:50 +02:00
Dotty Dotter
acda3cdb3a fix: Programm-Treue Sub-Labels (Einschätzung, Belege) zurück + Bewertung-Tag
Der vorherige Refactor hatte die expliziten Sub-Headers im Body
abgeworfen. User wollte sie zurück (BELEGE-Layout-Spec):
- Header-Row: Programm-Label links, "Bewertung"-Tag + Score-Chip rechts
- Body: Sub-Label "Einschätzung" über der Begründung,
        Sub-Label "Belege" über den Zitaten

Default bleibt <details open> — alles sofort lesbar, Falt-Toggle
optional. Chevron ▾ am Label rotiert beim Schließen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:11:12 +02:00
Dotty Dotter
bd0fe54559 fix: Programm-Treue im BELEGE-Layout, default-OPEN statt default-closed
User-Feedback: voriges Layout war "Usability-Alptraum" — alle 8 Blöcke
(4 Fraktionen × 2 Programme) waren default-closed, was 8 Klicks pro
Antrag bedeutete um Begründungen zu lesen.

Fix: <details open> als Default. Inhalt sofort vollständig sichtbar
beim Scrollen. Faltmechanismus bleibt für User, die die Liste nur
skimmen wollen — Chevron ▾ am Label rotiert beim Falten. Hover-Affordanz
über Label-Color-Change zum Brand-Blau (kein extra Caret-Element).

Layout-Spec bleibt: pro Partei zwei Treue-Blöcke (Wahlprogramm,
Parteiprogramm). Summary = Label + Score-Chip rechts. Body =
Einschätzung-Text + Belege via quote_card-Macro.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 10:03:04 +02:00
Dotty Dotter
c4750d3274 feat(v3): Bürger:innen-Modus-Sandbox unter /v3/antrag/{drs}
Skelett für Issues #184 + #185 — minimal, nicht-disruptiv:

- v3/base.html extendet v2/base.html (Topbar/Sidebar/Footer geteilt)
- v3/screens/antrag_detail.html extendet vorerst v2-Screen 1:1 und
  injiziert nur Beta-Pill + Toggle "→ Profi-Modus"
- v2/screens/antrag_detail.html bekommt Topbar-Link "→ Bürger:innen-
  Modus (v3 Beta)" → /v3/antrag/<drs>
- _render_antrag_detail() teilt DB-Reads/Context zwischen v2 + v3 —
  Datenbasis garantiert in Sync, Unterschied ist nur template_name
- _MATRIX_EXPLANATIONS auf Modul-Ebene ausgelagert (war bisher
  inline im v2-Route, jetzt von beiden Modi referenziert)
- v3.css als Add-On nach v2.css (lädt im v3/base head)

Was v3 noch NICHT tut: Score-Hero-Vereinfachung, Matrix→5-Werte,
Glossar-Tooltips, Default-Collapsing der Profi-Blöcke (Verbesserungen,
Kommentare). Diese Iterationen folgen pro PR — v2 bleibt unberührt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:55:06 +02:00
Dotty Dotter
c4f8ce398a feat: Matrix-Klick mit Antrags-Begründung + Vote-Block-Verbesserungen
Matrix-Info-Modal:
- _row_to_detail liefert label+aspect je Matrix-Cell ans Frontend
- Modal zeigt antragsspezifische Bewertung (Label + ausformulierte
  Begründung + Rating-Chip) UND die allgemeine Felderklärung
  (was misst dieses Feld?). v1-Verhalten wiederhergestellt.

Abstimmungsergebnis (Vote-Block):
- Outlink-Pfeil ↗ ist jetzt ein klickbares <a> auf v.quelle_url
  (target=_blank). Vorher: Span ohne Link, Pfeil tat nichts.
- Marker ⚠ (Heuchelei) und ! (Opportunismus) bekommen sichtbare
  Hover-Affordanz (Hintergrund + dotted-border on focus). Native
  title= bleibt fuer Screenreader-/Tooltip aktiv. tabindex=0+role=button
  macht sie keyboard-erreichbar.
- Legende unter dem Vote-Pill-Block erklaert die Marker beim
  ersten Auftreten in einer Liste — wird nur eingeblendet wenn auf
  dem Block mindestens ein Marker tatsaechlich vorkommt.
- Vote-Pills via Klassen v2-vote-pill/-ja/-nein/-enth statt
  Inline-Styles (CD-Annaeherung).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:33:53 +02:00
Dotty Dotter
70d9790b4b feat(#177): Programm-Treue im BELEGE-Layout — pro Partei zwei aufklappbare Blöcke (WP+PP)
- _row_to_detail liefert zitate inline pro Wahlprogramm/Parteiprogramm-Block
- Template rendert <details>: Summary mit Score-Chip, Body mit Einschätzung+Belege
- v2.css: neue Klassen v2-treue-block/-label/-body, v2-pill, v2-einschaetzung
- Separate "Belege — Partei"-Sektion entfernt (ist jetzt inline pro Programm)

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 09:21:42 +02:00
Dotty Dotter
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
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