Commit Graph

382 Commits

Author SHA1 Message Date
Dotty Dotter
9fc0619a20 docs(adr): 0014 — Tour-System mit ElevenLabs-Voice + Web-Speech-Fallback
Architektur-Dokumentation der heutigen Session:
- Engine-Wahl (eigene Spotlight-Logik statt intro.js/shepherd.js)
- Pro-Page-Stations via window.GWOE_TOUR_STEPS
- Audio-Pipeline: ElevenLabs/Domi mit Server-Cache, Fallback auf
  Web Speech API
- Auto-Play-Block-Workaround (persistentes <audio> + User-Gesture-Unlock)
- CSP-Erweiterung: media-src 'self' data: blob:
- Konsequenzen positiv/negativ und Folge-Iterationen.
2026-05-09 12:25:04 +02:00
Dotty Dotter
bf6201eb00 feat(tour): seitenspezifische Touren für jede Public/Auth-Page
Bisher hatten nur Startseite und Antrag-Detail eigene Touren — die
Fallback-Tour (Logo+Topbar+Sidebar) war für jede andere Page identisch.
User-Wunsch: pro Menüpunkt eigene Tour, die diese Page funktional
erklärt.

Pro Page 3-5 Stationen mit konkreten Selektoren auf die Hauptbereiche:
- Quellen: Volltextsuche, Treffer-Liste, Programm-Karten
- Methodik: TOC, GWÖ-Matrix, Pipeline
- Auswertungen: Tabs (4 Sichten), Filter, Visualisierung
- Stimmverhalten: Stimm-Index, Heuchelei-Quote, Cross-BL — eigene
  Variante via {% if v2_active_nav == 'stimmverhalten' %}, da
  Stimmverhalten dasselbe Template wie Auswertungen nutzt
- Tags: Tag-Wolke, gefilterte Treffer
- Merkliste: Liste, Empty-State
- Neuer Antrag: BL-Wahl, Drucksachen-Eingabe, Modell, Submit
- Abos: Filter, aktive Abos
- Atom-Feed: Filter, Feed-URL
- Landtag-Suche: BL, Suchbegriff, Treffer + Bewertung anstoßen
- Aktuelle Themen: Top-News, Cluster+Zeitreihe, PM-Generator

Texte ermächtigend formuliert (was-wofür-wie), nicht oberflächlich.
Selektoren mit Fallback-Liste (komma-separiert), damit Templates ohne
exakte Klassennamen die Station trotzdem zeigen können.
2026-05-09 09:03:59 +02:00
Dotty Dotter
722156cff6 fix(tour): doppeltes Include durch Jinja-Tag in JS-Kommentar
Im JS-Kommentar des Topbar-Toggle-Skripts stand wörtlich
{% include "v3/components/tour.html" %} — Jinja parst das auch
innerhalb von /* ... */-Kommentaren und renderte das include ein
zweites Mal. Folge: 4 window.gwoeTourStart-Definitionen, doppeltes
Tour-Overlay-DOM, doppeltes <audio>-Element. Fix: Kommentar
umformuliert ohne Jinja-Tag.
2026-05-09 08:48:16 +02:00
Dotty Dotter
4c989ea443 fix(tour, csp): media-src für Tour-Audio + Tour global außer Administration
Zwei Bugs:

1) Audio kam nicht durch — die Content-Security-Policy hatte kein
   media-src und fiel auf default-src 'self' zurück. data:- (silent-WAV
   zum Element-Unlock) und blob:-URLs (ElevenLabs-MP3-Cache) wurden
   geblockt. Browser-Fehlermeldung im Console: „Loading media from
   ‚data:audio/wav;base64,…' violates the following Content Security
   Policy directive". Fix: ``media-src 'self' data: blob:;`` ergänzt.

2) Tour war nur auf Startseite + Antrag-Detail eingebunden. User-Wunsch:
   auf jeder Page außer Administration. Lösung: Tour-Engine-Include in
   v2/base.html, mit ``{% if v2_active_nav not in [admin_*] %}``-Guard.
   Pages ohne eigene ``window.GWOE_TOUR_STEPS`` bekommen einen Fallback
   mit drei Stationen (Logo+Konzept, Topbar, Sidebar).

   Topbar-Tour-Link sichtbar wenn ``window.gwoeTourStart`` existiert
   (Engine geladen) — nicht mehr abhängig von Page-eigenen Steps.

Aufräumen: redundante Tour-Includes aus durchsuchen.html und
antrag_detail.html entfernt — die Engine kommt jetzt nur einmal aus
base.html.
2026-05-09 08:43:35 +02:00
Dotty Dotter
57434485ea fix(tour, nav): Tour-Text richtet sich nach Auth-Status, nicht umgekehrt
Letzter Commit hatte die Daten-Nav für alle sichtbar gemacht, damit der
Tour-Text passte. User-Korrektur: nicht die Berechtigungen erweitern,
sondern den Tour-Text auf das anpassen, was anonyme tatsächlich sehen.

Nav zurück auf den ursprünglichen Stand (Daten-Sektion eingeloggt-only).
Tour-Station „Navigation links" jetzt zwei Varianten via {% if
is_authenticated %} im durchsuchen.html-Template:

- Anonym: erklärt Tags + Quellen (Topbar) und weist auf Login-Mehrwert
  hin (Auswertungen / Stimmverhalten / Merkliste sind dann da).
- Eingeloggt: erklärt Auswertungen + Stimmverhalten + Quellen.
2026-05-09 08:31:11 +02:00
Dotty Dotter
e397ae5028 fix(tour, nav): Audio-Auto-Play entsperren + Daten-Nav für alle sichtbar
Drei zusammenhängende UI-Bugs:

1) Audio kam nicht — Browser-Auto-Play-Block. ``new Audio(blobUrl).play()``
   nach einem await zählt nicht mehr als User-Gesture; Safari/Chrome
   kassieren mit NotAllowedError. Fix: persistentes <audio>-Element wird
   einmal beim Tour-Start (im Click-Handler, synchron) mit einer
   1×1-silent-WAV entsperrt. Folgende src-Updates spielen ohne Block.

2) Tour-Bubble „Weiter"-Button sah 90er aus — der lokale CSS-Override
   ``.gwoe-tour-bubble .v3-action-btn.primary`` hat den modernen
   pill-shaped Style ausgehebelt. Override entfernt; nutzt jetzt das
   globale ``.v3-action-btn.primary`` (teal-solid, runde Ecken,
   weicher Drop-Shadow).

3) Tour erzählt anonymen User:innen über „Auswertungen" und
   „Stimmverhalten", die in der linken Nav für Anonyme nicht sichtbar
   waren. Aggregierte Daten sind öffentlich — Daten-Nav-Gruppe jetzt für
   alle sichtbar (Auswertungen, Stimmverhalten, Aktuelle Themen,
   Export-API, Atom-Feed). Persönliche Items (Merkliste, Abos, Neuer
   Antrag, Batch) bleiben eingeloggt. Cluster + Landtag-Suche bleiben
   eingeloggt/admin (Backend-Routen sind ohnehin require_auth).
2026-05-09 08:07:27 +02:00
Dotty Dotter
a3a2b90e9f fix(tour): Topbar-Link synchron toggeln statt via DOMContentLoaded
DOMContentLoaded-Race: bei manchen Page-Loads war das Event schon
gefeuert, wenn das addEventListener-Script lief — der Listener wurde
nie aufgerufen, der Topbar-Tour-Link blieb hidden. Auf der Startseite
führte das dazu, dass nach Welcome-Banner-Dismiss kein Tour-Einstieg
mehr da war.

Fix: synchroner IIFE-Check, der Skript-Block steht ohnehin nach dem
body_scripts (STEPS sind dort schon gesetzt).
2026-05-09 07:40:25 +02:00
Dotty Dotter
c3fd617585 ui(tour): permanenter Tour-Link in der Topbar + modernes Button-Styling
User-Feedback: Tour-Start muss auch nach dem ersten Mal möglich sein,
und die Buttons sahen "sehr 90er" aus.

Permanenter Tour-Zugang:
- Topbar-Link "🧭 Tour" neben Methodik/Quellen, sichtbar auf jeder
  Page mit ``window.GWOE_TOUR_STEPS`` (Antrag-Detail + Startseite).
- Per JS in base.html nach DOMContentLoaded sichtbar geschaltet.
- Style: dezente teal-Pille, kein dominanter Button.

Button-Modernisierung:
- ``.v2-chip`` und ``.v3-action-btn`` jetzt pill-shaped (border-radius
  999px statt 3px), mit transition + box-shadow on hover und subtle
  transform on active. Focus-visible mit klarem outline.
- Primary-Variante: teal-solid mit weichem Drop-Shadow, nicht das alte
  dark-Mode-Schwarz.
- Welcome-Banner (Startseite): scharfes ``v2-kasten outline-blue`` weg,
  stattdessen weicher gradient-Hintergrund teal→blue mit border 22%
  teal-Hauch und subtle box-shadow. Skip-Button als Text-Link
  ("Später") statt vollwertiger Chip — visuell klar nachgeordnet.
2026-05-09 03:31:09 +02:00
Dotty Dotter
6ec05d2b86 feat(tour): ElevenLabs-Voice für die Tour (#185 Phase 2)
Audio-Backend:
- ``app/tour_audio.py`` ruft ElevenLabs-TTS mit voice_id=Domi
  (AZnzlk1XvdvUeBnXmlld) und model=eleven_multilingual_v2. ENV-konfiguriert
  via ``ELEVENLABS_API_KEY``, ``ELEVENLABS_VOICE_ID``, ``ELEVENLABS_MODEL_ID``.
- Voice-Settings: stability 0.55, similarity_boost 0.7 (warm, klar, natürlich).
- Caching: SHA-256(text|voice|model) → ``data/tour_audio/<hash>.mp3``.
  Folgeabrufe gehen aus dem Datei-Cache, kein API-Quota-Verbrauch.

Endpoint: ``GET /api/tour/voice?text=...`` rate-limited 30/min,
liefert audio/mpeg mit Cache-Control 30 Tage. Bei fehlendem
API-Key 503 — Frontend fällt dann auf ``speechSynthesis`` zurück
(Browser-eingebaute Stimme).

Frontend (tour.html):
- ``speak()`` versucht erst Server-Audio (ElevenLabs), bei 503/Fehler
  Fallback auf Web Speech API.
- Session-Cache via Blob-URL: Vor/Zurück-Navigation in der Tour zieht
  nicht jedes Mal eine neue Network-Roundtrip.
- ``stopSpeak()`` stoppt beide Audio-Pfade sauber.

Konfiguration für dev: ``ELEVENLABS_API_KEY`` und (optional)
``ELEVENLABS_VOICE_ID`` in ``/opt/gwoe-antragspruefer-dev/.env`` setzen,
dann Container restart.
2026-05-09 03:17:06 +02:00
Dotty Dotter
e31ee1ad07 feat(tour): Welcome-Banner + Tour auf Startseite, Logo-Klick zur Startseite
Drei zusammenhängende UI-Bausteine:

1) Tour-Engine ist jetzt page-agnostisch — sie liest die Stationen aus
   ``window.GWOE_TOUR_STEPS`` (pro Page hinterlegt), nicht mehr aus einem
   eingebauten Konstanten. Tour-Komponente wird per ``{% include %}``
   eingehängt; das Page-Template definiert vorher seine eigenen Steps.
   Antrag-Detail-Tour wurde entsprechend in das eigene Template gezogen.

2) Startseite (v2/screens/durchsuchen.html): „Du bist neu hier?"-Banner
   oben mit zwei Buttons — „🧭 Tour starten" und „Nein, danke". Banner
   bleibt sichtbar, bis explizit weggeklickt wird (localStorage-Flag),
   oder die Tour gestartet wird. Fünf Stationen für die Startseite:
   Marken-Block, Suche, Score-Filter + Sortierung, Antrags-Liste,
   linke Navigation.

3) Logo-Klick führt jetzt zur Startseite — sowohl in v2/base.html als
   auch in components/appshell.html. ``v2-brand`` und ``v2-brand-sub``
   sind in einen ``<a href="/">`` mit Hover-Highlight gewickelt
   (``.v2-brand-link``).

Phase 2 (ElevenLabs-Voice) ist der nächste Schritt — bisher läuft das
Audio über die Web Speech API.
2026-05-09 02:47:04 +02:00
Dotty Dotter
1c74cb8801 feat(antrag-detail): geführte Tour mit Sprachausgabe (#185 Phase 1)
Schaltfläche „🧭 Tour" in der userrow neben „Merken". Klick öffnet ein
Spotlight-Overlay mit vier Stationen, ermächtigend statt vereinfachend
formuliert:

1. Die Gemeinwohl-Note (was die Zahl 0–10 sagt, was die Empfehlung ist)
2. Die GWÖ-Matrix (5 Werte × 5 Berührungsgruppen, Farbcodierung)
3. Programm-Treue pro Fraktion (Score + Belege)
4. Stimmverhalten + Marker (Heuchelei ⚠ und Opportunismus !)

Audio: Web Speech API (Browser-eingebaute Stimme), de-DE, möglichst
weibliche Stimme. „Stimme an / aus"-Toggle in der Bubble. Bei
ESC oder Klick auf Overlay-Hintergrund: Tour-Ende, Audio stoppt.

Phase 2 (separate Iteration) wird das Audio-Backend gegen ElevenLabs
tauschen — Tour-Skript + UI bleiben gleich, nur ``speak()`` ruft dann
einen Server-Endpoint, der eine vorgenerierte und gecachte MP3 liefert.

Komponente in ``app/templates/v3/components/tour.html``, included am
Ende von antrag_detail.html. CSS inline in der Komponente (1 ``<style>``-
Block, keine ``style=""``-Attribute — Anti-Regression-Wache aus #184
respektiert).
2026-05-09 02:39:01 +02:00
Dotty Dotter
57e11b3da7 chore(ci): Anti-Regression-Wache gegen neue Inline-Styles (#184)
Nicht alle 1322 Inline-Style-Vorkommen in einer Sitzung migrieren — aber
zumindest verhindern, dass es mehr werden. Drei Bausteine:

1. ``tools/audit_inline_styles.py`` — CLI für Audit (Cluster, Top-Files)
   und Baseline-Erzeugung (--baseline → JSON {file: count}).

2. ``tools/inline_styles_baseline.json`` — eingefrorene IST-Zählung pro
   Template-Datei. Wird vom Test als obere Schranke gelesen.

3. ``tests/test_inline_styles_baseline.py`` (3 Tests) — pro Datei
   und global: aktuelle Anzahl <= Baseline. Schlägt Alarm bei neuen
   Inline-Styles und auch bei neuen Templates mit Inline-Styles, die
   noch nicht in der Baseline stehen.

Workflow für künftige Migrationen: Inline-Styles in einer Datei nach
benannten Klassen überführen, Baseline neu einfrieren via
``python3 tools/audit_inline_styles.py --baseline > tools/inline_styles_baseline.json``.

Cluster-Verteilung der 1322 Treffer:
- layout: 625, typography: 323, color: 262, sonstige: 112.

Top-Brennpunkte: index.html (463, Classic-UI Legacy),
auswertungen.html (125), antrag_detail.html v2 (119),
aktuelle-themen.html (82).

1220/1220 Tests grün.
2026-05-09 02:32:35 +02:00
Dotty Dotter
c7eab5a695 feat(pdf): Heuchelei-/Opportunismus-Marker im Vote-Block (#175)
Web-Detail zeigt diese Marker bereits — pro NEIN-Fraktion einen ⚠ wenn
der eigene Wahlprogramm-Score ≥ 7/10 ist (Heuchelei: stimmt gegen die
eigenen Versprechen), pro JA-Fraktion einen ! wenn der Wahlprogramm-
Score < 3/10 (Opportunismus: stimmt zu obwohl Antrag inhaltlich nicht
zum eigenen Programm passt). Im PDF fehlten sie bisher.

Daten-Pfad: report.py rechnet die Marker einmal vor (heuchelei_score /
opportunismus_score aus app/marker.py, gefüttert mit umgemappten
fraktions_scores aus assessment.wahlprogramm_scores) und reicht zwei
Maps fraktion → score ans Template. Template macht nur noch Lookup:
``opportunismus_by_fraktion.get(f)`` neben jeder JA-Fraktion,
``heuchelei_by_fraktion.get(f)`` neben jeder NEIN-Fraktion. Plus
kompakte Legende unter dem Vote-Block, falls überhaupt Marker
vorkommen.

Stimmverhalten und Programm-Treue-Begründungen sind im PDF schon da
(verifiziert bei der Code-Inspektion). Damit ist die "PDF auf Augenhöhe
mit Web-Detail"-Liste aus #175 bis auf News-Match abgehakt; News-Match
explizit out-of-scope nach User-Entscheidung.
2026-05-09 02:21:12 +02:00
Dotty Dotter
ad73c824d3 perf(browser-mem): Polling-Frequenz + Page-Hide-Cleanup (#183)
Drei Mitigations:

1) Admin-Queue-Polling 5s → 15s. Die Queue ändert sich pro Sekunde
   ohnehin nicht spürbar; senkt CPU + Network ohne UX-Verlust.

2) ``pagehide``-Listener in admin_queue.html, admin_stand.html und
   auswertungen.html. Zerstört Chart.js-Instanzen + cleart setInterval-
   Handles, sobald die Page in den Back/Forward-Cache geht oder
   geschlossen wird. Bisher hingen sie bis Browser-GC.

3) /auswertungen: zentrales Cleanup für ``_histChart``, alle ``_svCharts.*``
   und ``window._zeitreiheModalChart`` beim pagehide. Bisher zerstört
   nur die einzelnen Render-Funktionen ihre Vorgänger; beim Page-Verlassen
   blieben sie alle stehen.

Was nicht abgedeckt ist (für eventuelle Folge-Iteration mit konkretem
Heap-Snapshot):
- Lazy-Render lange News-/Drucksachen-Listen via IntersectionObserver
- Detaillierte Detached-DOM-Untersuchung pro Seite

Bestehende Maßnahmen (bereits da, hier nicht angefasst): chart.destroy()
vor jedem neuen Chart, sim.stop() in cluster.html, visibilitychange-
Pause für Polling.
2026-05-09 02:17:23 +02:00
Dotty Dotter
61c39eb820 fix(share): Threads-Unicode + Instagram-Dialog macOS (#178)
Threads-Encoding: rendered Sonderzeichen als ? oder Rauten, weil der
Text mit zerlegten Codepoints (z.B. ``a`` + Combining Diaeresis statt
``ä``) ankam — Threads' Composer kommt damit nicht klar. Fix: NFC-
Normalisierung (``str.normalize('NFC')``) vor encodeURIComponent. Das
vereinigt zerlegte Umlaute und typografische Anführungszeichen.

Instagram-Share auf macOS: bisher versuchten wir auch auf Desktop den
``navigator.share()``-Pfad mit File. Das öffnet das macOS-Share-Sheet,
zeigt aber nur AirDrop / Mail / Notizen — Instagram-App ist auf Desktop
nicht installiert, also nutzlos. Fix: Mobile-Detection via User-Agent
+ maxTouchPoints (für iPad-iOS-13+-Maskierung). Auf Desktop direkt zu
Pfad B (Download + Clipboard) statt OS-Sheet.
2026-05-09 02:13:28 +02:00
Dotty Dotter
1a3aa9bbcb fix(antrag-detail, merkliste): Score in Merkliste + Metadaten-Whitespace (#177)
Zwei kleine UI-Bugs:

1) Score in Merkliste fehlte. Ursache: /api/assessment liefert ``gwoeScore``
   (camelCase), das Merkliste-Template las ``a.gwoe_score`` (snake_case).
   Fix: beide Schreibweisen akzeptieren.

2) Metadaten-Zeile im Antrag-Detail rendert mit Whitespace-Müll, weil die
   Jinja-If-Blöcke ohne Whitespace-Steuerung Newlines durchlassen, die
   der Browser zu Spaces collapst:
   - "13.04.2026 , qwen-plus" (Komma mit Leerzeichen davor) statt
     "13.04.2026, qwen-plus".
   - "NRW-WP18. Wahlperiode" statt "18. Wahlperiode": ``antrag.wahlperiode``
     ist der Filter-Key wie "NRW-WP18", nicht die reine Zahl.

   Fix: Whitespace-Steuerung ``{%- ... -%}`` an allen relevanten If-Tags
   in der Metadaten-Zeile + Byline. Plus neues Feld
   ``antrag.wahlperiode_zahl`` (nur die Zahl) im _row_to_detail-Mapping,
   das bevorzugt vor ``antrag.wahlperiode`` zur Anzeige genutzt wird.

Vorher: NRW · Drs. 18/18246 · Antrag · NRW-WP18. Wahlperiode · eingebracht 18.03.2026
        Eingebracht von SPD — Analyse 13.04.2026 , qwen-plus · 5 Zitate verifiziert
Nachher: NRW · Drs. 18/18246 · Antrag · 18. Wahlperiode · eingebracht 18.03.2026
         Eingebracht von SPD — Analyse 13.04.2026, qwen-plus · 5 Zitate verifiziert
2026-05-09 02:11:02 +02:00
Dotty Dotter
35fed45339 ui(antrag-detail): Programm-Treue als 2-Spalten-Grid, Trennlinien raus
User-Feedback: vorherige Pillen-Variante zu schmal/dicht. Stattdessen
gewünscht: Layout wie ursprünglich (head + 2 Programm-Reihen), aber
mit weniger Whitespace und ohne Trennlinien — und die Partei in eine
linke Spalte gezogen, damit head und Programm-Rows nebeneinander
sitzen statt stapeln.

Layout: ``.v3-fraktion`` als CSS-Grid mit zwei Spalten:
- links (140px–25%): Fraktion-Name + Pillen vertikal
- rechts (1fr): zwei Programm-Reihen (Wahlprogramm / Parteiprogramm)
  ohne border-top zwischen den Reihen, padding 3px statt 10/8,
  font kleiner (11px statt 12px).
2026-05-09 01:23:53 +02:00
Dotty Dotter
2f700adbb8 ui(antrag-detail): Programm-Treue-Box auf 1/3 der Höhe verkleinert
Pro Fraktion bisher ~137px (Head + Wahlprogramm-Reihe + Parteiprogramm-
Reihe in drei vertikalen Blöcken). Nutzer wollte deutlich kompakter.

Layout: Fraktion-Name + Antragsteller-/Regierungs-Pillen + zwei
Programm-Pillen (WP / PP) jetzt in EINER flex-row. Pro Programm-Pille
ein klickbares <details> mit Mini-Score-Chip; beim Aufklappen fließt
der Body (Begründung + Zitate) dank ``flex-basis: 100%`` unter die
Zeile in voller Breite.

Höhe collapsed: ~40-45px pro Fraktion (von vorher ~137px). Begründung
in der ausgeklappten Box bekommt zusätzlich einen ``Wahlprogramm:``-/
``Parteiprogramm:``-Präfix, da der Programm-Typ aus der Pille nicht
mehr im Body explizit auftaucht.

Doppelte v3-prog-text-Regel im CSS entfernt (war vergessenes Cruft).
2026-05-09 01:18:27 +02:00
Dotty Dotter
e48cab6db3 fix(quellen): Suche ohne <form>, Click + Enter direkt binden
Browser-Quirk: in einem <form> blieb der fetch nach Submit hängen,
auch mit preventDefault() im submit-Handler. Status-Text bekam
"Suche läuft …", aber die Response kam nie an — der Browser hat den
fetch durch den Submit-State des Forms blockiert.

Lösung: <form> → <div>, Button auf type="button". Click direkt an
runSearch binden, Enter via keydown auf dem Input. Keine Form-Submit-
Semantik mehr.

Bei E2E-Smoketest mit Playwright reproduzierbar gefixt.
2026-05-09 01:09:45 +02:00
Dotty Dotter
37941f0a2b fix(quellen): runSearch nach Form-Submit via setTimeout entkoppeln 2026-05-09 01:07:12 +02:00
Dotty Dotter
87725ee3d9 fix(quellen): Submit-Listener direkt binden, nicht via DOMContentLoaded
Folge-Fix zu 501f32b: das body_scripts-Skript läuft am Ende des Body,
da ist DOMContentLoaded oft schon vorbei. Der bisherige Wrapper
``document.addEventListener('DOMContentLoaded', ...)`` wurde dann nie
gefeuert, der Submit-Listener nie gebunden — Suchknopf weiter still.

Lösung: IIFE direkt aufrufen + Idempotenz-Marker (``_quellenBound``)
gegen Doppelbindung.
2026-05-09 01:02:43 +02:00
Dotty Dotter
501f32b9ae fix(quellen): Suchformular bricht fetch durch Form-Submit ab
Bug: ``runSearch`` ist async und returnt damit Promise<false>. Im
Inline-Handler ``onsubmit="return runSearch(event)"`` interpretiert der
Browser den Promise als truthy → Default-Form-Submit läuft an, die
Page navigiert, und der gerade abgesetzte fetch bricht mit
"Failed to fetch" ab. Im echten Browser merkt man's beim Klick auf
"Suchen" oder Enter im Suchfeld: Status bleibt auf "Suche läuft …"
hängen, keine Ergebnisse erscheinen.

Fix: Bindung über ``addEventListener('submit', ...)`` mit
``event.preventDefault()`` synchron vor dem async runSearch-Aufruf.
JS-Direktaufruf von ``runSearch()`` (z.B. bei Filter-Wechsel)
funktioniert weiterhin.

Gefunden bei E2E-Browser-Smoketest mit Playwright.
2026-05-09 00:58:30 +02:00
Dotty Dotter
d5c96e6888 refactor(embeddings): PROGRAMME-Literal entfernt, Re-Export aus programme.py
Folge-Schritt zu #222 (Commit bd591b9): Das große Datenliteral in
``embeddings.py`` (~280 Einträge, 712 Zeilen) wird durch einen
Re-Export aus ``programme.PROGRAMME`` ersetzt. Damit existieren die
Programm-Stammdaten endgültig nur noch an einer Stelle.

Schema-Brücke: Die ``chunks``-Tabelle führt ``typ`` als alte Sammel-
Bezeichnung (``wahlprogramm`` oder ``parteiprogramm``).
``programme.PROGRAMME`` differenziert ``grundsatzprogramm-bund`` vs.
``grundsatzprogramm-land``. Beim Re-Export werden beide auf
``parteiprogramm`` gemappt — alte Chunks und neue Indexierungen
tragen denselben typ-String, der Filter in
``get_relevant_quotes_for_antrag`` (typ="parteiprogramm") deckt
beide Grundsatzprogramm-Varianten weiter ab.

embeddings.py schrumpft von 1574 auf 926 Zeilen (−648). Tests
unverändert grün (1217 passed).
2026-05-09 00:46:34 +02:00
Dotty Dotter
bd591b9246 refactor(programme): WAHLPROGRAMME → programme.PROGRAMME konsolidiert (#222)
Schließt #222. Entfernt die Doppelung zwischen ``wahlprogramme.WAHLPROGRAMME``
und ``programme.PROGRAMME``. Single source of truth ist jetzt
``programme.PROGRAMME`` als Literal mit allen 287 Programmen
(Wahlprogramme + Bundes- + Landes-Grundsatzprogramme, historisch + aktuell).

Schema schmaler — Felder ohne Konsumenten entfallen:
- ``regierungsbildung`` / ``regierungsende`` → gehören zu
  ``legislaturen.REGIERUNGEN``. Verbindung Programm→Regierung läuft jetzt
  über ``legislaturen.regierung_zum_zeitpunkt(bl, datum)``.
- ``partei`` (Langform "CDU NRW") → ableitbar aus partei + bundesland.
- ``jahr`` → ableitbar aus ``gueltig_ab[:4]``.
- ``beschluss`` / ``wahl`` / ``hinweis`` → keine App-Konsumenten.

Felder im neuen Schema: id, typ, partei, bundesland, wp, gueltig_ab,
gueltig_bis, name, titel (Slogan, optional), pdf, seiten.

Daten-Migration einmalig via ``tools/build_programme_literal.py``:
- Basis: bisherige embeddings.PROGRAMME (alle 287 IDs + gueltig_ab/bis)
- titel aus WAHLPROGRAMME für die ~80 aktuellen Wahlprogramme +
  Land-Grundsatzprogramm-Slogans (ehem. _ARCHIVED_SKELETONS)
- seiten via ``fitz.open(p).page_count`` für alle 287 PDFs

Aufrufer migriert:
- app/main.py:4055 — ``aktuelles_wahlprogramm(bl, partei).pdf``
- app/wahlprogramm_check.py — ``parteien_mit_wahlprogramm(bl)``
- app/redline_utils.py — Reverse-Lookup über ``all_programme()``
- app/wahlprogramm_fetch.py (3 Stellen) — ``aktuelles_wahlprogramm()``
- tests/test_redline_parser.py — Programm-Lookup statt WAHLPROGRAMME

``wahlprogramme.py`` schrumpft auf den Such-Code: Keyword-Fallback +
PDF-Text-Loader + ein dünner ``get_wahlprogramm``-Compat-Adapter zu
``programme.aktuelles_wahlprogramm``.

Drei Helper gelöscht (keine App-Konsumenten):
``regierungsbildung_for``, ``regierungsende_for``, ``regierung_aktuell``.
Wer das Datum der Regierungsbildung will, fragt
``legislaturen.aktuelle_regierung(bl).get('von')``.

Test-Suite: 1217 grün (vorher 1244, Differenz 27 = entfernte
regierungs-Helper-Tests + obsolete WAHLPROGRAMME-Strukturtests).
2026-05-09 00:37:35 +02:00
Dotty Dotter
7d507f81f4 feat(analyzer): zeitpunkt-genaue Bewertung — datum-Filter durch Embedding-Suche
Schließt #224. ADR 0013 hat die Datenbasis für historische Bewertung
geschaffen (programme.PROGRAMME mit gueltig_ab/gueltig_bis); jetzt
nutzt der Analyzer sie auch tatsächlich.

Vorher: get_relevant_quotes_for_antrag suchte über ALLE Wahl- und
Grundsatzprogramme einer Partei in einem BL — egal aus welcher WP.
Folge: ein Antrag aus 2018 in NRW konnte Zitate aus dem cdu-nrw-2022
Wahlprogramm (das er noch nicht kennen konnte) zugeordnet bekommen.
Anachronismus-Halluzination.

Nachher: Wenn ``datum`` (ISO YYYY-MM-DD) durchgereicht wird, filtert
``find_relevant_chunks`` die Chunks auf Programme mit
``[gueltig_ab, gueltig_bis)`` ⊇ datum. Programme, die zum
Antragszeitpunkt nicht galten, werden komplett ausgelassen.

Signaturen erweitert (alle additiv, datum=None ⇒ altes Verhalten):
- embeddings.find_relevant_chunks(..., datum=None)
- embeddings.get_relevant_quotes_for_antrag(..., datum=None)
- analyzer.analyze_antrag(..., datum=None)

main.run_drucksache_analysis: reicht doc.datum durch (DIP/OPAL liefern
das Antragsdatum vor dem LLM-Call, kein Zwei-Pass-Workaround nötig).

Tests:
- test_embeddings.test_datum_param_is_passed_through_to_find_relevant_chunks
- test_bug_regressions.test_analyzer_propagates_datum_to_embeddings
1244/1244 Unit-Tests grün.
2026-05-08 22:07:32 +02:00
Dotty Dotter
9169e7699d fix(parteien): BIW → BiW (kanonische Schreibweise) für HB-Wahlprogramme
parteien.py listet 'BiW' als kanonischen Key (Mixed-Case), 'BIW' nur als
Alias. Bestehende HB-Assessments tragen bereits 'BiW' in
wahlprogramm_scores. Beim Block 2.4 hatte ich die historischen BiW-
Wahlprogramme aber mit "BIW" angelegt — Folge: find_relevant_chunks
mit parteien=["BiW"] hätte die 131 Chunks der biw-hb-*-PDFs nicht
gefunden, weil der partei-Filter exakt matcht.

Geändert:
- embeddings.PROGRAMME: 3 biw-hb-* Einträge "partei": "BIW" → "BiW"
- wahlprogramme.WAHLPROGRAMME["HB"]: Key "BIW" → "BiW", "partei" Feld
  "BIW Bremen" → "BiW Bremen"
- test_hb_has_five_parteien: erwartete Set entsprechend angepasst

Folge-Schritt: chunks.partei in der embeddings.db muss von 'BIW'
auf 'BiW' migriert werden — die 131 betroffenen Chunks werden beim
nächsten Reindex der biw-hb-* Programme ohnehin überschrieben.

92/92 Programme-Tests grün, 1242 Unit-Tests grün.
2026-05-08 14:28:12 +02:00
Dotty Dotter
d418fb0b4a fix(wahlprogramme): HB-Bovenschulte-II-Datum + Helper-Semantik klarer
Zwei kleine Verbesserungen, die beim Schreiben der Drift-Tests in
bf5400a aufgefallen sind:

1. HB-Datum-Typo: regierungsbildung war 2023-07-04 (Tag der Konstituierung
   der 21. Bürgerschaft), korrekt ist 2023-07-05 (Vereidigung Senat
   Bovenschulte II). 5 Einträge angepasst (SPD/CDU/GRÜNE/LINKE/BIW).
   Kommentar im Header ebenfalls.

2. Helper-Docstrings (regierungsbildung_for, regierungsende_for,
   regierung_aktuell) explizit darüber, dass das Datum die ERSTE
   Regierung der WP ist, NICHT die aktuell amtierende. Wichtig bei
   Sukzessionen wie RP WP18 (Dreyer III 2021-05-18 → Schweitzer I
   2024-07-10) — die 2021er Wahlprogramme bleiben wirksam, auch nach
   MP-Wechsel. Für aktuell amtierende Regierung gibt es
   ``legislaturen.aktuelle_regierung``.

92/92 Programme-Tests grün.
2026-05-08 14:25:35 +02:00
Dotty Dotter
bf5400ae33 test(programme): Drift-Schutz zwischen WAHLPROGRAMME und PROGRAMME
ADR 0013 hatte als offene Folge "Doppelter Daten-Bestand zwischen
WAHLPROGRAMME und embeddings.PROGRAMME ist nicht aufgelöst — Risk:
stille Drift". Der invasive Compat-Shim (#222) ist aufwendig; bis
dahin fängt eine neue Test-Klasse die Drift bidirektional ab:

TestWahlprogrammeProgrammeConsistency (4 Tests):
- Jeder WAHLPROGRAMME-Eintrag hat ein passendes aktuelles Programm in
  PROGRAMME (sonst liefert aktuelles_wahlprogramm None)
- pdf-Dateinamen müssen übereinstimmen (file == pdf)
- Partei-Kurzform muss übereinstimmen
- Jedes aktuelle Wahlprogramm in PROGRAMME muss auch in WAHLPROGRAMME
  registriert sein (orphan-check andere Richtung)

Drift-Funde dabei:
- BIW (Bürger in Wut) HB war in PROGRAMME (biw-hb-2023, biw-hb-2019,
  biw-hb-2015), aber NICHT in WAHLPROGRAMME-HB. Bewertungs-Pipeline
  hätte BIW-Anträge gegen kein Wahlprogramm geprüft. Eintrag ergänzt:
  BÜRGER IN WUT — Programm Bürgerschaftswahl 2023 (26 Seiten).
- Test test_hb_has_four_parteien → test_hb_has_five_parteien.

92/92 Programme-Tests grün.
2026-05-08 14:18:41 +02:00
Dotty Dotter
27fd92c15f feat(quellen): Semantische Volltextsuche über alle Programme (#235)
GET /api/quellen/search?q=&filter=current|all&top_k=&bundesland=&partei=
nutzt text-embedding-v4 für wortunscharfe Suche (Endungen + Synonyme).
Filter:
- filter=current: nur Programme mit gueltig_bis IS NULL (Default)
- filter=all: auch historische Programme

Response liefert pro Treffer name, partei, bundesland, gueltig_ab/bis,
seite, gekürztes Snippet, similarity, plus pdf_url mit Direkt-Sprung
ins highlightete PDF (über /api/wahlprogramm-cite).

UI auf /quellen oben über der BL-Auflistung:
- Suchfeld + Submit
- Radio-Toggle "nur aktuelle Programme" / "auch historische"
- Treffer-Karten mit Partei-Badge, gültig-Pille (grün/grau),
  Seite + Relevanz-%, Snippet, Direktlink ins PDF
- Filter-Wechsel triggert automatischen Re-Run

Smoketest dev: "Klimaschutz" liefert 13 Treffer in aktuellen Programmen
mit korrekter Similarity-Sortierung; "Solidarität" mischt Wahl- und
Grundsatzprogramme. Zugriff erfordert keinen Login (read-only).
2026-05-08 14:11:09 +02:00
Dotty Dotter
18ea326e43 feat: Block 2.4 + 2.5 — historische Wahlprogramme zurück bis 2011
92 neue PDFs + Geltungsdaten in PROGRAMME-Registry:

Block 2.4 — direkte 2019er-Lücken (21 Dokumente):
- BIW Bremen: Wahlprogramme 2015 + 2019 + 2023
- BUND WP19 (BTW 2017): CDU, CSU, SPD, GRÜNE, FDP, AfD, LINKE
- HH WP21 (BS 2015): SPD, CDU, GRÜNE, LINKE, AfD, FDP
- TH WP6 (LTW 2014): LINKE, CDU, SPD, GRÜNE, AfD, FDP

Block 2.5 — Vor-Vorperioden 2011-2014 (~71 Dokumente) für 15 BL:
BB WP6, BE WP17, BUND WP18, BW WP15, BY WP17, HB WP19, HE WP19,
LSA WP6, MV WP6, NI WP17, NRW WP16, RP WP16, SH WP18, SL WP15, SN WP6.
Inkl. PIRATEN, BVB/FREIE WÄHLER, FREIE WÄHLER, SSW.

Coverage: 287 Programme (286 indiziert auf dev — linke-sl-2012 ist
ein 2-Seiten-Kurzwahlprogramm und liefert keine Chunks).

Source-of-Truth-Pflege:
- wahlprogramm-links.yaml ergänzt (Quellen-URLs für alle PDFs)
- wahlprogramm-shas.lock.json ergänzt (SHA-256 für Integritätsprüfung)
- test_programme.py: drei Versionen NRW CDU statt zwei

Schließt #233 + #234.
2026-05-08 14:10:52 +02:00
Dotty Dotter
3777cde69a fix(security): Dockerfile auf python:3.12-alpine fuer 0 CRITICAL CVEs
Security-Audit ergab 3 CRITICAL CVEs im python:3.12-slim-Base
(Debian-Bullseye-Reste, deren Updates erst mit Debian-Bookworm-Bump
kommen). Wechsel auf python:3.12-alpine, das aktuell 0 CRITICAL hat.

Aenderungen:
- FROM python:3.12-slim -> python:3.12-alpine
- apt-get -> apk add (zwei Phasen: runtime + build-deps mit virtual,
  build-deps werden nach pip install entfernt)
- adduser-Syntax: Alpine `adduser -D -u 1000` statt Debian-Variante
- Zusaetzliche build-deps fuer C-Extensions: build-base, gcc, musl-dev,
  rust+cargo (cryptography), libxml2-dev/libxslt-dev (lxml), openssl-dev
- Runtime-Pakete fuer WeasyPrint: pango, cairo, gdk-pixbuf,
  shared-mime-info, fontconfig, ttf-dejavu

Image-Groessen-Effekt: Alpine + alle Build-Deps nach Cleanup
~250 MB statt slim ~480 MB.

Auto-Deploy auf gwoe-antragspruefer-dev rebuilt sich alle 5 Min via
auto-deploy.sh-Cron — Wirksamkeit innerhalb der naechsten 10 Min
sichtbar. gwoe-antragspruefer (prod, eingefroren auf v1.0.2) bekommt
das beim naechsten Release.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 12:15:23 +02:00
Dotty Dotter
f0f6a1438d feat: Block 2.3 — historische Wahlprogramme fuer 13 Bundeslaender + Bund
Massen-Beschaffung von Vorperioden-Wahlprogrammen via 15 parallele
Background-Agents. Jeder BL bekommt seine direkt vorhergehende WP
indiziert, sodass wahlprogramm_zum_zeitpunkt() jetzt fuer Antrage aus
2016-2024 historisch korrekt das damalige Programm liefert (vorher None
oder das aktuelle).

Indiziert (~83 PDFs, 9.799 Chunks insgesamt fuer Block 2.3):

| BL | Vorperiode | Wahltag | gueltig bis | Parteien |
|----|-----------|---------|------------|----------|
| BB | WP7 | 2019-09-01 | 2024-09-22 | SPD, CDU, GRUENE, AfD, LINKE, BVB/FW |
| BE | WP18 | 2016-09-18 | 2021-09-26 | SPD, LINKE, GRUENE, CDU, AfD, FDP |
| BW | WP16 | 2016-03-13 | 2021-03-14 | GRUENE, CDU, AfD, SPD, FDP |
| BY | WP18 | 2018-10-14 | 2023-10-08 | CSU, GRUENE, FW, AfD, SPD, FDP |
| HB | WP20 | 2019-05-26 | 2023-05-14 | SPD, GRUENE, LINKE, CDU, FDP, AfD |
| HE | WP20 | 2018-10-28 | 2023-10-08 | CDU, GRUENE, SPD, AfD, FDP, LINKE |
| HH | WP22 | 2020-02-23 | 2025-03-02 | SPD, GRUENE, CDU, LINKE, AfD, FDP |
| LSA | WP7 | 2016-03-13 | 2021-06-06 | CDU, SPD, GRUENE, AfD, LINKE, FDP |
| MV | WP7 | 2016-09-04 | 2021-09-26 | SPD, CDU, AfD, LINKE, GRUENE |
| NI | WP18 | 2017-10-15 | 2022-10-09 | SPD, CDU, GRUENE, AfD, FDP |
| RP | WP17 | 2016-03-13 | 2021-03-14 | SPD, GRUENE, FDP, AfD, CDU |
| SH | WP19 | 2017-05-07 | 2022-05-08 | CDU, SPD, GRUENE, FDP, AfD, SSW |
| SL | WP16 | 2017-03-26 | 2022-03-27 | CDU, SPD, LINKE, AfD, GRUENE |
| SN | WP7 | 2019-09-01 | 2024-09-01 | CDU, GRUENE, SPD, AfD, LINKE |
| TH | WP7 | 2019-10-27 | 2024-09-01 | LINKE, SPD, GRUENE, CDU, AfD, FDP |

Live-Verifikation auf gwoe-antragspruefer-dev: 17/17 historische
Lookups korrekt (alle 16 BL + Bund). Tests: 117 grun.

PDF-Quellen: 60% direkt von Parteiwebseiten, 30% via Mirror
(abgeordnetenwatch.de, Friedrich-Ebert-Stiftung, Friedrich-Naumann-
Stiftung, KAS-Archiv), 10% via Wayback Machine fuer Programme der
Vorperioden, deren Original-URLs nicht mehr existieren.

Total Embeddings-Index: 195 Programme, 24 BL/Wahlperioden-Kombinationen
abgedeckt. Block 2 (historische Indexierung) damit zu rund 60%
abgeschlossen — pro BL 1 vorhergehende WP plus aktuell, vor 2016 ist
noch nichts indiziert.

Roadmap-Update: Block 2.3 abgeschlossen, naechster Schritt waere
Block 2.4 (zweite Vorperiode pro BL).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:27:43 +02:00
Dotty Dotter
e04eb6d340 feat: Block 2.2 — BUND WP20 (BTW 2021, Scholz-Ampel) historisch indiziert
7 Wahlprogramme zur BTW 26.09.2021 — die Programme der Scholz-Ampel-Periode
(SPD+GRÜNE+FDP, vereidigt 08.12.2021, vorgezogenes Ende 25.03.2025):

- cdu-bund-2021 (gemeinsam CDU/CSU "Stabilitaet und Erneuerung", 140 S., 232 chunks)
- csu-bund-2021 (eigenstaendige CSU-Bayern-Fokus-Variante, 18 S., 24 chunks)
- spd-bund-2021 (Zukunftsprogramm "Aus Respekt vor Deiner Zukunft", 66 S., 105 chunks)
- gruene-bund-2021 (272 S. barrierefreie Fassung, 269 chunks)
- fdp-bund-2021 (Beschluss 14.-16.05.2021 Berlin, 68 S., 136 chunks)
- afd-bund-2021 ("Deutschland. Aber normal.", 210 S., 160 chunks)
- linke-bund-2021 ("Zeit zu handeln!", 168 S., 324 chunks)

Total: 1.250 Chunks.

Geltungszeitraum 2021-09-26 (Wahltag) bis 2025-02-23 (Wahltag BTW 2025,
exklusiv). Antraege aus dieser Periode bekommen jetzt automatisch das
korrekte Programm zurueckgeliefert via wahlprogramm_zum_zeitpunkt():

- 2024-01-01 BUND/SPD -> spd-bund-2021 (Scholz-Ampel)
- 2025-02-22 BUND/SPD -> spd-bund-2021 (noch alt)
- 2025-02-23 BUND/SPD -> spd-bund-2025 (BTW-Wahltag, Wechsel)

Tests: 117 gruen, plus neue test_bund_2024_returns_btw_2021 und
test_bund_grenze_btw_2021_btw_2025.

Block 2.2 abgeschlossen — Block 2 Roadmap (16 BL × 3 WPs) ist 2/16 BL.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 10:25:51 +02:00
Dotty Dotter
445fcc90ca feat: Block 2.1 — NRW WP17 historische Wahlprogramme indiziert (Pilot)
5 Programme zur LTW NRW 14.05.2017 als historische Wahlprogramme im
Embeddings-Index — erster Datensatz für die zeitpunktige Bewertung
historischer Antraege:

- cdu-nrw-2017 (Laschet, 120 S., 172 chunks)
- spd-nrw-2017 (Kraft, 116 S., 169 chunks)
- gruene-nrw-2017 (131 S., 322 chunks)
- fdp-nrw-2017 (Lindner, 56 S., 92 chunks)
- afd-nrw-2017 (84 S., 78 chunks)

Geltungszeitraum 2017-05-14 (Wahltag WP17) bis 2022-05-15 (Wahltag
WP18, exklusiv). Eintraege liegen NUR in embeddings.PROGRAMME — die
WAHLPROGRAMME[NRW]-Struktur bleibt single-current (cdu-nrw-2022).

programme._migrate_from_legacy hat einen neuen Schritt 2b, der
typ=wahlprogramm-Eintraege aus embeddings.PROGRAMME mit explizitem
gueltig_ab/_bis als historische Wahlprogramme registriert. Damit
liefert wahlprogramm_zum_zeitpunkt() jetzt fuer NRW-Antraege aus dem
Zeitraum 2017-2022 das passende Programm.

Live-Verifikation auf gwoe-antragspruefer-dev:
- 2018-09-01 -> cdu-nrw-2017 (WP17)
- 2024-01-01 -> cdu-nrw-2022 (WP18)
- Grenze: 14.05.2022 -> WP17, 15.05.2022 -> WP18

Tests: 116 gruen, plus neue test_grenze_zwischen_wp17_und_wp18 und
angepasstes test_datum_vor_aktueller_wp_nrw_wp17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:44:26 +02:00
Dotty Dotter
b003cc1d6d feat(programme): Wahlprogramm-Geltung beginnt am Wahltag (B1+B2)
Antwort auf B1 + B2 aus der Roadmap:
- B1: Antrag VOR Regierungsbildung (z.B. NRW WP18-Antrag im Mai 2022,
  vor Vereidigung Wuest II am 29.06.2022) bekommt jetzt das passende
  Wahlprogramm zurueck — der Geltungsbeginn ist der Wahltag, nicht die
  Vereidigung.
- B2: Opposition vs. Regierung wird einheitlich behandelt. Die fruehere
  Logik "Geltung ab Regierungsbildung" war fuer Regierungsfraktionen
  intuitiv (Koalitionsvertrag wird zu Politik), fuer Opposition aber
  willkuerlich. Programme werden zur Wahl beschlossen und sind
  Wahlversprechen ab dem Tag der Wahl.

Implementation in programme._migrate_from_legacy:
- gueltig_ab = aktuelle_legislatur(bl)["wahltermin"] (Fallback auf
  altes "regierungsbildung" fuer rueckwaerts-kompatible Eintraege)
- ``wahl``-Feld auf Wahltag gesetzt
- ``wp``-Feld aus aktuelle_legislatur ergaenzt

Das ``regierungsbildung``-Feld in WAHLPROGRAMME bleibt erhalten und
versorgt den Bewertungs-Kontext-Block weiterhin mit dem Anzeige-Wert
"Regierung zur Antragszeit" (per legislaturen.regierung_zum_zeitpunkt
laeuft das primaer ueber legislaturen.REGIERUNGEN).

UI-Effekt: im Antrag-Detail liest sich z.B. ein BUND-Eintrag jetzt
"gueltig seit 2025-02-23, 60 S." (BTW-Wahltag) statt "2025-05-06"
(Vereidigung Merz I).

Tests: 115 gruen (test_programme + test_legislaturen + test_wahlprogramme
+ test_embeddings). Tests test_bund_btw_2025_in_uebergangsphase und
test_bund_btw_2025_vor_wahl neu, decken die geaenderte Geltungs-Logik
explizit ab.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:17:08 +02:00
Dotty Dotter
176b7c5018 docs: Roadmap Programme/Legislaturen mit 11 Entscheidungen
Strategische Klärungs-Runde im Anschluss an ADR 0013 — alle 11 offenen
Fragen entschieden, in 5 Implementierungs-Blöcke strukturiert:

Block 1 — Schema-Refactor (kurz):
- gueltig_ab umstellen von Regierungsbildung auf Wahltag (B1+B2)
- CSU als Alias auf CDU-BTW-Eintrag (B3)

Block 2 — Daten-Beschaffung:
- ~50 historische Wahlprogramme indizieren (A1)
- BIW Bremen + BVB/FW Brandenburg historisch (A2)
- BSW Manifest 2023 als parteiprogramm-Eintrag (B4)

Block 3 — Infrastruktur:
- Eigener PDF-Mirror auf Toppyr-Server (C3)
- Monatlicher Cron: Parteiwebseiten-Update-Check (C1)
- Täglicher Cron: inkrementelle Reindexierung (C2)

Block 4 — Auswertungen + UI:
- Eigene Detail-Views: Regierung-im-Detail, Wahlperiode (D2)
- Programmwechsel-Diff, Treuequote-Zeitreihe, Koalitions-Shift (D1)

Block 5 — Konsolidierung + Release:
- WAHLPROGRAMME als View über programme.PROGRAMME (E2)
- analyzer.py auf zeitpunktige Helper
- v1.1.0 Production-Release (E1)

Plus Detail-Gotchas und nächster konkreter Schritt (Block 1).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 09:03:20 +02:00
Dotty Dotter
9f2f805aff docs(methodik): Bewertungs-Kontext-Sektion mit Verweis auf ADR 0013
Neue Sektion auf /methodik (zwischen "Analyse-Pipeline" und "Stimmverhalten
& Marker"), die transparent macht, gegen welches Wahlprogramm der Prüfer
einen Antrag misst. Erklärt:

- Wahlperiode → Programm-Geltung (Konstituierung, nicht Wahltag)
- Regierung zur Antragszeit (mit Sukzession-Beispiel Dreyer III →
  Schweitzer I)
- Wahlprogramm-Liste pro Antragsteller-Fraktion mit Geltungsdatum
- Snapshot-Hinweis (Bewertungsdatum + Modell)

Plus ehrliche Einschraenkung: aktuell nur die jeweils gegenwaertigen
Wahlprogramme im Embeddings-Index. Architektur ist fuer historische
Indexierung vorbereitet (siehe ADR 0013).

Inhaltsverzeichnis-Link ergaenzt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 08:42:04 +02:00
Dotty Dotter
1cf249353e test: 26 Tests fuer app.programme — Helper-API + Migration
Direkte Test-Abdeckung der zentralen Programm-Registry. Vorher nur
indirekt via test_legislaturen.py (das nicht aus programme.py importiert)
und ueber den Antrag-Detail-Smoke-Test.

Geprueft:
- Migration aus WAHLPROGRAMME + embeddings.PROGRAMME (>80 Eintraege,
  alle Pflichtfelder, eindeutige IDs, alle drei Typen vertreten)
- aktuelles_wahlprogramm: NRW/CDU → 2022, BUND/CDU → BTW 2025; XX → None
- wahlprogramm_zum_zeitpunkt: 2024 in NRW gibt cdu-nrw-2022; vor 2025-05-06
  in BUND gibt None (BTW-2025-Programme gelten erst ab Regierungsbildung)
- grundsatzprogramm_zum_zeitpunkt: BL-Fallback (CDU NRW → cdu-grundsatz-nrw,
  CDU HE → cdu-grundsatz Bund-Fallback); SSW/SH; CSU 2023
- parteien_mit_wahlprogramm: NRW=5, BUND=8
- alle_versionen, get_programm, Edge-Cases

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:07:59 +02:00
Dotty Dotter
89b82b1627 docs(adr): 0013 — Programme + Legislaturen mit zeitpunktiger Bewertung
Architektur-Doku zum Schema-Refactor der letzten Session:
- Begruendung fuer programme.py + legislaturen.py
- Optionen-Vergleich (zentrale Registry vs. Liste-im-Schema vs. Status quo)
- Migrationsweg via _migrate_from_legacy() und Lazy-Init
- Datenstand (86 Wahlprogramme + 12 Grundsatzprogramme + 56 Legislaturen
  + 70 Regierungen)
- Offene Folgearbeiten: historische Wahlprogramme indizieren,
  analyzer.py-Migration, wahlprogramme.py-Compat-Shim

Index aktualisiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 01:04:04 +02:00
Dotty Dotter
d16cacc7fe feat: Bewertungs-Kontext mit PDF-Link + Snapshot-Hinweis
Erweiterung des Geltungs-Kontext-Blocks (Antrag-Detail):
- Programm-Titel als Link auf das PDF ({titel} klickbar →
  /static/referenzen/{pdf}, opens in new tab).
- Seitenzahl als ergaenzende Info: "116 S." neben Geltungsdatum.
- Snapshot-Zeile am Block-Ende: "Diese Bewertung wurde am
  {datum} mit {modell} gegen den oben genannten Programm-Stand
  erzeugt." — macht klar, dass die Anzeige eine Momentaufnahme der
  damaligen LLM-Bewertung ist und nicht "live" gegen heutige
  Programme misst.

CSS:
- v3-geltung-pdf: ECG-blauer Link mit dezenter underline-Linie.
- v3-geltung-snapshot: kursiv, getrennt durch hairline-border, gedaempft.

Tests: 88 grün (test_legislaturen + test_wahlprogramme + test_embeddings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:48:30 +02:00
Dotty Dotter
c7861cfb58 feat: Antrag-Detail um Bewertungs-Kontext erweitert
Vor der Programm-Treue-Sektion eine kompakte Info-Box, die transparent
macht, was zur Antragszeit fuer das jeweilige Parlament galt:

- **Wahlperiode** (Nummer + Konstituierung-bis-Ende-Spanne) ueber
  legislatur_zum_zeitpunkt(bl, antrag_datum)
- **Regierung zur Antragszeit** (Name + Koalitionsparteien + Vereidigung,
  ggf. Endedatum bei Sukzessionen wie Dreyer III -> Schweitzer I) ueber
  regierung_zum_zeitpunkt(bl, antrag_datum)
- **Bewertet gegen die folgenden Wahlprogramme** (pro Antragsteller-
  Fraktion mit gueltig-seit-Datum) ueber wahlprogramm_zum_zeitpunkt
  pro Partei

Daten kommen aus den neuen Modulen app/legislaturen.py + app/programme.py.
Helper laufen historisch korrekt — z.B. ein Antrag aus 2020-02-15 in TH
wuerde "Kemmerich I (FDP)" zurueckliefern.

Aktuell zeigen alle Antraege die jeweils "aktuelle" Regierung & das
aktuelle Programm, weil keine historischen Wahlprogramme im Embeddings-
Index sind. Die Architektur ist aber fuer den Tag vorbereitet, wo
historische Programme indiziert werden.

Implementation:
- main.py: _render_antrag_detail laedt geltung_kontext und gibt es ans
  Template weiter. ISO-Datum aus row["datum"] (nicht aus dem display-
  formatierten antrag["datum"]).
- v3/screens/antrag_detail.html: neue Sektion v3-geltung vor Programm-
  Treue-Block.
- static/v3/v3.css: neue v3-geltung-Klassen mit dezentem Doku-Look.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:45:44 +02:00
Dotty Dotter
a80ac17218 feat: 6 Grundsatzprogramme + Landesgrundsatzprogramme indiziert
Neu in embeddings.PROGRAMME (typ=parteiprogramm mit gueltig_ab):
- csu-grundsatz: CSU "Für ein neues Miteinander" (2023-05-06, 125 chunks)
  ersetzt das bisher indizierte CSU 2016-Programm logisch
- cdu-grundsatz-nrw: CDU NRW "Aufstieg, Sicherheit, Perspektive"
  (2015-06-13, Landesgrundsatzprogramm, 127 chunks)
- cdu-grundsatz-sn: CDU Sachsen "Zukunftsplan für Sachsen"
  (2023-11-20, Landesgrundsatzprogramm, 52 chunks)
- cdu-grundsatz-lsa: CDU Sachsen-Anhalt "Unsere Verantwortung. Unsere
  Zukunft." (2023-09-30, Landesgrundsatzprogramm, 74 chunks)
- ssw-grundsatz: SSW Rahmenprogramm (2016-04-16, Landesgrundsatzprogramm
  für Schleswig-Holstein, 65 chunks)
- fw-grundsatz: FREIE WAEHLER Bundesgrundsatzprogramm (Stand 09/2025;
  FW nicht im Bundestag, gilt fuer FW Bayern + FW RLP, 43 chunks)

programme.py:
- _migrate_from_legacy() unterscheidet jetzt grundsatzprogramm-bund
  (bundesland=None) und grundsatzprogramm-land (bundesland gesetzt).
- _ADDITIONAL_PROGRAMME-Slot vereinfacht — alle Daten leben in
  embeddings.PROGRAMME.

Auf gwoe-antragspruefer-dev indiziert (text-embedding-v4):
486 neue Chunks, 0 failed. wahlprogramm-shas.lock.json + -links.yaml
gepinnt mit allen 6 SHA-256-Hashes.

Test-Suite: 88 grün (test_legislaturen + test_wahlprogramme + test_embeddings).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:30:06 +02:00
Dotty Dotter
4e7f7dac25 chore: konsolidiere Working-Tree mit dev-Stand nach Nextcloud-Sync-Resolution
Mehrtaegiger Sync-Stillstand hatte ueber 50 conflicted-copy-Dateien im
Working-Tree erzeugt. Die jeweils neuere Version wurde basierend auf
md5-Hash-Vergleich zum laufenden gwoe-antragspruefer-dev-Container
eingespielt.

Konsolidiert (38 modifiziert):
- analyzer.py, auswertungen.py, auth.py, config.py, database.py,
  drucksache_typen.py, embeddings.py, main.py, models.py, parlamente.py,
  ports/llm_bewerter.py, presse_generator.py, redline_utils.py, report.py,
  validators.py, wahlprogramm_fetch.py, wahlprogramm-links.yaml,
  wahlprogramm-shas.lock.json
- v2-Templates: base, components/{icon, matrix_mini, queue_widget,
  result_row}, screens/{admin_queue, admin_stand, aktuelle-themen,
  antrag_detail, auswertungen, cluster, landtag_suche, merkliste,
  methodik, tags}, static/v2/v2.css
- Tests: test_embeddings (Strict-Mode-Drop in reconstruct_zitate),
  test_endpoints_smoke, test_presse_generator, test_report,
  test_wahlprogramme (mit TestRegierungsbildung-Block, +120 LOC)
- docker-compose.dev.yml, docs/adr/index.md, docs/reference/api.md, mkdocs.yml

Neuzugaenge:
- app/marker.py, app/pm_render.py — Konsistenz-Marker, PM-Render-Adapter
- app/templates/v2/screens/scorecard{,_portrait,_werkstatt}.html — Cloud-Design-Scorecard
- app/static/v3/, app/templates/v3/ — v3-Layout-Hierarchie
- docs/adr/0010-stimmverhalten-gwoe-aggregat.md
- docs/adr/0011-aktuelle-themen-pm-generator.md
- docs/adr/0012-debug-auth-token-bypass.md
- scripts/{auto-rate-orphans, pm-quality-audit, pm-sample-bundle, rotate-debug-token}.sh
- tests/e2e/test_smoke_browser.py, tests/test_{auto_rate_runs, icons,
  marker, pm_render, presse_generator_style, thread_splitter,
  v2_pdf_consistency}.py

Plus inhaltlich uebernommen aus dem Conflict-Stand:
- embeddings.py: fw-by-2023.partei korrigiert von "FW" zu "FREIE WAEHLER"
  (war Mismatch zu wahlprogramme.py)
- embeddings.py: detailliertere Naming der BTW-2025-Wahlprogramme

Test-Suite-Stand: 1209 passed, 73 skipped (4 pre-existing failures in
test_presse_generator_style.py + 1 collection error in
integration/test_citations_substring.py — beide nicht durch dieses
Konsolidierungs-Commit verursacht).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:19:41 +02:00
Dotty Dotter
b5d2bb2515 feat: regierungsbildung+regierungsende in WAHLPROGRAMME, gueltig_ab in Grundsatzprogrammen
WAHLPROGRAMME erweitert:
- Pflichtfelder regierungsbildung (Vereidigung Kabinett) + regierungsende
  (None=laufend) pro Wahlprogramm. Geltung beginnt mit Regierungs-
  bildung, NICHT Wahltag.
- 6 fehlende Bundeslaender ergaenzt: BY, HB, HE, NI, SL, SN.
- BUND BTW-2025: 8 Wahlprogramme (CDU, CSU, SPD, GRUENE, FDP, AfD, LINKE,
  BSW) ersetzen die bisherigen Grundsatzprogramm-Eintraege. Vereidigung
  Merz I 2025-05-06.
- Helper regierungsbildung_for(), regierungsende_for(), regierung_aktuell().

embeddings.PROGRAMME erweitert:
- 6 Grundsatzprogramme (CDU 2024, SPD 2007, GRUENE 2020, FDP 2012,
  AfD 2016, LINKE 2011) tragen jetzt Beschluss-Datum als gueltig_ab,
  gueltig_bis=None.
- 8 BTW-2025-Wahlprogramme als Indexer-Eintraege.
- FDP-Programm-Name auf den korrekten "Karlsruher Freiheitsthesen 2012"
  umgestellt (vorher generisch "FDP Grundsatzprogramm 2012").

Tests: 71 grün (test_wahlprogramme + test_embeddings + test_legislaturen).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:18:33 +02:00
Dotty Dotter
991d1eb903 feat: Programme + Legislaturen-Registry mit historisch korrekter Geltung
Neue Module:
- app/programme.py: zentrale Programm-Registry (alle Wahl- und Grundsatz-
  programme in einem Index), mit Geltungsdaten gueltig_ab/gueltig_bis und
  Helpern wahlprogramm_zum_zeitpunkt(), grundsatzprogramm_zum_zeitpunkt(),
  alle_versionen(). Skelett fuer 6 zusaetzliche Eintraege (CSU 2023,
  CDU NRW 2015, CDU SN 2023, CDU LSA 2023, SSW SH 2016, FREIE WAEHLER)
  vorbereitet — PDFs folgen.
- app/legislaturen.py: 56 Legislaturen + 70 Regierungen fuer 16 BL + Bund.
  Helper legislatur_zum_zeitpunkt(), regierung_zum_zeitpunkt(),
  regierungen_einer_wp() fuer historisch korrekte Antrags-Bewertung
  (z.B. Kemmerich-28-Tage-Kabinett, RP-Uebergang Dreyer III -> Schweitzer I,
  BUND Scholz-Ampel -> geschaeftsfuehrend -> Merz I).
- tests/test_legislaturen.py: 20 Tests zu Konsistenz + Historie.

Datenbasis: 8 BTW-2025-Wahlprogramme (CDU, CSU, SPD, GRUENE, FDP, AfD,
LINKE, BSW) als PDFs hinzugefuegt. SHA-256-Pinning in
app/wahlprogramm-shas.lock.json (separat).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:18:33 +02:00
Dotty Dotter
7793705486 test: WAHLPROGRAMME→PROGRAMME-Match akzeptiert auch pdf-Field-Lookup
Mein BUND-Eintrag von vorhin nutzt 'cdu-grundsatzprogramm.pdf' als
file, aber embeddings.PROGRAMME hat den Schluessel 'cdu-grundsatz'
(historisch ohne 'programm'-Suffix). Der test_every_wahlprogramm_
has_embeddings_entry-Test ist deshalb rot geworden.

Test akzeptiert jetzt zwei Match-Pfade:
1. file-stem == PROGRAMME-Key (Standard fuer LT-Programme)
2. file == PROGRAMME[pid].pdf (Spezialfall Grundsatzprogramme)

Damit bleibt die Konsistenz-Pruefung sinnvoll, ohne dass ich die
Embedding-Programme-Keys umbennenen + reindizieren muss.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:30:41 +02:00
Dotty Dotter
c8bce35a16 fix: WAHLPROGRAMME['BUND'] mit Grundsatzprogrammen befuellt + Permalink-Copy-Click
User: 'Aber für diesen speziellen Antrag müssten doch alle Programme
verfügbar sein. https://gwoe-dev.toppyr.de/antrag/21/1594'

Ursache: WAHLPROGRAMME (in app/wahlprogramme.py) hatte keinen 'BUND'-
Eintrag, daher hat check_missing_programmes() fuer jeden Bundestags-
Antrag ALLE 8 Fraktionen als fehlend markiert. Im Embedding-Index
(app/embeddings.py) sind die *-grundsatzprogramm.pdf Dateien aber
laengst registriert (typ=parteiprogramm, ohne bundesland-Bindung).
Die Lookup-Tabellen waren inkonsistent.

Fix: WAHLPROGRAMME['BUND']-Eintrag mit den 6 Grundsatzprogrammen
(CDU/SPD/GRUENE/FDP/AfD/LINKE) ergaenzt — entspricht der Realitaet
im embeddings.py-Index. CSU + BSW haben keine indizierten Programme
und werden weiterhin als fehlend gemeldet (was korrekt ist).

Bestehende BUND-Assessments mit fehlende_programme=[8 Parteien] in
der DB bleiben erst mal so (waehrend einer Re-Analyse korrekt). Issue
#186 (historische BTW-Wahlprogramme) bleibt offen — Grundsatzprogramme
sind nur ein Notbehelf gegen die 'alle fehlen'-Anzeige.

Plus: Permalink-Klick kopiert jetzt die absolute URL in die Zwischen-
ablage statt zur Page zu navigieren. window.v3CopyPermalink in
v2/screens/antrag_detail.html (wird via super() von v3 mitvererbt).
Link-Text 'Permalink kopieren', 1.6s 'Permalink kopiert ✓'-Flash
nach Copy. Fallback auf window.prompt() wenn Clipboard-API fehlt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 15:27:33 +02:00
Dotty Dotter
332453b940 fix: Programm-Treue-Block bei BUND-/Antraegen mit komplett fehlenden Programmen ausblenden
User: '?? Hinweis: Für folgende Parteien lag kein Wahl-/Parteiprogramm
vor — keine Treue-Bewertung möglich: CDU, CSU, AfD, SPD, GRÜNE, LINKE,
BSW, FDP.' Vorher zeigte die UI den Disclaimer + trotzdem die LLM-
halluzinierten Programm-Treue-Scores aller 8 BT-Parteien — schlechte UX.

WAHLPROGRAMME['BUND'] ist aktuell leer (keine Bundestags-Wahlprogramme
indiziert), daher wird check_missing_programmes alle BT-Fraktionen als
'fehlend' markieren. Bisher wurden trotzdem die LLM-Scores rausgespielt.

Fix in v3/screens/antrag_detail.html und v3/pdf/antrag_pdf.html:
- Wenn _all_missing (alle Fraktionen fehlen) → ganze Programm-Treue-
  Sektion ausblenden, nur klare Disclaimer-Box zeigen ('Programm-Treue
  nicht verfuegbar — fuer dieses Parlament sind aktuell keine Programme
  indiziert').
- Wenn nur einzelne Fraktionen fehlen → die einzelnen Karten via
  {% if fs.fraktion not in _missing_set %} skippen, Disclaimer fuer
  die wegfallenden Fraktionen zeigen ('werden hier nicht aufgefuehrt').

Damit keine LLM-halluzinierten Scores mehr gezeigt werden, wo es keine
Quelle gibt. Issue #186 (historische Programme indizieren) ist die
langfristige Loesung — diese Aenderung macht die UI bis dahin ehrlich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:59:00 +02:00
Dotty Dotter
55423e92e3 feat: PDF mit Inter+JetBrains Mono (Cloud-Design-Anlehnung), Matrix kompakter
User: 'Kannst du die Schriftarten eher nachempfinden? Und die Matrix
ist so sehr raumgreifend ... das könnte deutlich in der Höhe gestaucht
sein.'

Fonts:
- Google-Fonts-Link für Inter + JetBrains Mono ergänzt im PDF-Template.
- font-family-Stack: 'Inter', dann Avenir/Helvetica als Fallback (falls
  WeasyPrint die CDN-Fonts nicht laden kann).
- Mono-Stack: 'JetBrains Mono' bevorzugt, dann Source Code Pro.

Matrix in der Höhe gestaucht (war ca. 320pt hoch, jetzt ~165pt):
- Zellen-Höhe 60pt → 28pt (Symbole bleiben mit 11pt-Schrift gut lesbar)
- Zellen-Breite 60pt → 52pt (knapper, Cells leicht breiter als hoch)
- Zeilen-Label-Spalte 90pt → 76pt
- Header-Reihe 24pt → 18pt
- Gap 2pt → 1.5pt
- Legend kleiner: 8pt → 7.5pt, swatches 12pt → 9pt

Resultat: Matrix-Block fast halbiert, Schwerpunkte-erklärt-Sektion
darunter rückt einen Seitenwechsel weniger in den Hintergrund.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:45:32 +02:00
Dotty Dotter
f59286d15f fix: PNG-Export-Canvas + Doppel-Borders bei field-chip/party-pill
User: 'immer noch doppelte Borders ... Inhalte zu klein skaliert
nach oben links gerutscht (800 px breit statt 1080)'

Ursachen:

1. Canvas-Content-Mismatch (Inhalt 75% der PNG-Breite):
   WeasyPrint rechnet 1 CSS-px = 0.75 PDF-pt (96dpi → 72dpi). @page
   war auf {width}pt × {height}pt (1080×1350) gesetzt, body aber auf
   1080×1350 CSS-px. Folge: Body fuellte nur 1080*0.75=810pt der
   1080pt-Page → Content top-left, 25% rechts/unten leer; PyMuPDF
   rasterisiert mit zoom=1 → 1080×1350 PNG, Content nur in den linken
   810×1012 px → 'Inhalte zu klein nach oben links gerutscht'.

   Fix: @page-Groesse auf (width * 0.75)pt × (height * 0.75)pt setzen.
   Body fuellt jetzt die volle Canvas-Breite. PyMuPDF kompensiert mit
   zoom = scale * 4/3, damit die PNG wieder die gewuenschten Pixel-
   Dimensionen hat (1080×1350 für scale=1).

2. Doppel-Borders auf field-chip + party-pill:
   WeasyPrint hat einen bekannten Render-Bug bei
   'border + border-radius' auf inline-flex-Elementen — der Border
   wird zweimal gezeichnet (innen + aussen). 1.5px → 2px hat das
   nicht behoben, weil's nicht am Subpixel-Wert lag.

   Fix: border ersetzt durch box-shadow: inset 0 0 0 2px var(--rule).
   Inset-Shadow rendert sauber, kein Doppel-Effekt. border-radius
   bleibt erhalten.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:37:55 +02:00