Commit Graph

175 Commits

Author SHA1 Message Date
Dotty Dotter
b7256b6d24 fix(was-ist-neu): 'moeglichst erklaerend' als Wortwahl fuer die neue Ansicht 2026-05-10 13:12:08 +02:00
Dotty Dotter
f9a9c21fbd fix(was-ist-neu): keine Werte-Bars mehr, kein Buerger:innen-Modus-Etikett
- Werte-Bars sind aus dem UI raus, raus aus dem Text
- 'Buerger:innen-Modus' war nur intern als v3-Label gedacht; aussen heisst
  es einfach 'neu gestaltetes Antrag-Detail' mit dem Anspruch, sich Schritt
  fuer Schritt selbst zu erklaeren
- Programm-Treue-Grid + Glossar + klickbare Matrix-Zellen als Umsetzung
2026-05-10 13:10:38 +02:00
Dotty Dotter
fabe3c6514 feat(was-ist-neu): Ankuendigungs-Seite zur 2.0 mit Intention pro Block
Erklaert die acht groessten Veraenderungen seit 1.0 — Buerger:innen-Modus,
Tour mit Sprachausgabe, Stimmverhalten, Aktuelle-Themen-Dashboard, Programme
mit zeitpunkt-genauer Bewertung, Scorecards, Auswertungen und Quellen-Suche
— jeweils mit kurzer Intention und Umsetzungsbeschreibung.

Topbar-Link rechts oben (vor Methodik), Endpoint /was-ist-neu, Template
unter v2/screens/was-ist-neu.html. Eigene neu-* CSS-Klassen analog zu
methodik.html, daher kein neuer Inline-Style-Eintrag.
2026-05-10 12:54:17 +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
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
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
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
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
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
Dotty Dotter
4e122bceb0 fix: Score 9.0/10 nicht umbrechen (white-space:nowrap)
Im PNG-Export wurde 9.0/10 manchmal auf zwei Zeilen gerendert (9.0/
in einer Zeile, 10 in einer neuen). Ursache: WeasyPrint laedt Inter
ueber das Google-Fonts-CDN nicht zuverlaessig (CSP-/Font-Loading-
Issue), Fallback auf System-Sans hat andere Metriken → Score-Inhalt
wird breiter als 320px Score-Box → Umbruch.

Fix: white-space: nowrap auf .score .num und .score .num small.
Erzwingt 1-zeilige Darstellung egal welcher Font-Fallback geladen
wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:31:34 +02:00
Dotty Dotter
4569f3335f feat: Scorecard Multi-Format (Cloud-Design ZIP-2) — 4 Layouts mit Switcher
User hat die zweite ZIP geliefert: 'Scorecard Formate.html' mit
Spec fuer drei zusaetzliche Formate. Plus Anmerkung: 'doppelte Borders'
und 'Export viel zu gross'.

Vier Formate jetzt im selben Template scorecard_portrait.html:
- format=portrait (DEFAULT) → 1080×1350 · 4:5 · IG-Feed
- format=square           → 1080×1080 · 1:1 · IG/LinkedIn
- format=story            → 1080×1920 · 9:16 · Story/Reels
- format=wide             → 1920×1080 · 16:9 · OG/Slide/Twitter

Wide hat 2-spaltigen Body-Aufbau (Story-Spalte links, Daten-Spalte
rechts, Header+Footer ueber volle Breite), die anderen drei nutzen
das gemeinsame 1-spaltige Body-Markup. Aller Formate teilen sich die
Daten-Aggregation (Chips, Fraktions-Bars, Beschluss).

Bug-Fixes aus dem User-Feedback:

1. 'Doppelte Borders um die Partei und Field-Chips' — die 1.5-px-
   Borders im Cloud-Design wurden von WeasyPrint als zwei einzelne
   1-px-Linien gerendert (Subpixel-Bug bei fractional border-widths).
   Alle 1.5px → 2px (integer).

2. 'Export viel zu gross' — der Download-Button hatte scale=2 als
   Default → 2160×2700 PNG-Pixel. Fuer IG-Upload reicht 1080×1350
   exakt (Instagram skaliert hochgeladene Bilder ohnehin). Default
   jetzt scale=1, der ?scale=2-Param bleibt verfuegbar fuer Retina.

3. Statusleiste mit Format-Switcher: vier Pills (4:5 Feed / 1:1 Square
   / 9:16 Story / 16:9 Wide), aktuelles Format hervorgehoben. Klick
   wechselt URL-format-Param. Plus PNG- und PDF-Download-Buttons,
   die das aktuelle Format mitfuehren.

main.py: dimensions-Mapping um story+wide erweitert in
scorecard_template UND _render_scorecard_pdf. Format-Validation
ebenfalls erweitert. format-Variable an's Template durchgeschleift
(damit der Template-Switch fuer card-portrait/square/story/wide
funktioniert).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:29:24 +02:00
Dotty Dotter
443c9b0874 feat: Scorecard-Browser-Ansicht — Statusleiste mit PNG-Download-Knopf
User-Wunsch: 'Fuege auf der Webseite zur Ansicht der Scorecard unten
eine Statusleiste hinzu mit einem Download-Knopf, der dann das Dokument
als PNG oder JPEG herunterlaedt.'

Statusleiste am unteren Viewport-Rand (position: fixed) mit:
- Label links: 'Scorecard · NRW · Drs. 18/17449 · 1080×1350 (Instagram 4:5)'
- Buttons rechts:
  · primaerer 'PNG herunterladen' (Akzent-gruen, scale=2 = 2160×2700 px)
  · sekundaerer 'PDF' (Outline-Style, format=portrait)

Nutzt das bestehende /api/assessment/scorecard.png-Endpoint und das
download-Attribut sorgt fuer den richtigen Dateinamen
('gwoe-scorecard-18-17449.png').

JPEG-Variante ist nicht implementiert — der Endpoint liefert PNG, was
fuer Scorecards mit Text/scharfen Kanten qualitativ besser ist als
JPEG. Falls explizit JPEG gewollt: separater Endpoint noetig.

Body-Padding bottom 80 px auf @media screen, damit die fixed Toolbar
keinen Inhalt verdeckt. PDF-Render ist unbeeinflusst (Toolbar via
@media print { display:none }).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:16:06 +02:00
Dotty Dotter
0b5dcba8f9 feat: Scorecard-Browser — skaliert auf 90 % Viewport-Hoehe (mit Width-Fallback)
User: 'Skaliere die Scorecard so, dass sie die Hoehe des Browser
Viewports Minus 10% einnimmt'

Vorherige Logik nahm scaleByWidth, capped auf 1.0. Jetzt zwei Faktoren
und das kleinere gewinnt:
- scaleByHeight = window.innerHeight * 0.9 / 1350
- scaleByWidth  = (window.innerWidth - 40) / 1080
- scale = min(beiden, max 1.0)

Auf einem 1440×900-Desktop ergibt das scale 0.6 (= 90% × 900 / 1350),
Card 648×810. Auf einem 390-px-Phone gewinnt die Breite-Begrenzung,
Card skaliert kleiner damit horizontal nichts abgeschnitten wird.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:11:38 +02:00
Dotty Dotter
2b5062df08 fix: Scorecard-Browser-Skalierung via JS (CSS calc-scale-Unit-Mismatch behoben)
Mein vorheriger CSS-Versuch transform: scale(calc(min(1080px, 100vw-40px)
/ 1080)) liefert eine px-Ausgabe — scale() braucht aber unitless. Folge:
keine Skalierung, Card wird nur abgeschnitten (auf Phone sah man nur
linkes Ecke).

Fix: JS-basiert. adjustScale() berechnet das Verhaeltnis
window.innerWidth/1080 (max 1.0 bei Desktop), setzt transform: scale()
auf .card und passt die .card-viewport-Dimensionen an. Re-fired bei
window.resize + load.

WeasyPrint fuehrt kein JS aus → PDF unbeeinflusst (Card bleibt 1:1
1080×1350).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:06:37 +02:00
Dotty Dotter
d2c9a805b2 feat: Scorecard-Browser-Preview skaliert auf Viewport-Breite
User: 'Kannst du den Viewport so veraendern, dass ich da scrollen kann?
Oder im Idealfall, dass sie ihn klein angezeigt wird.'

Bisher hatte die Scorecard-Page body { width:1080px; height:1350px;
overflow:hidden } — fest fuer den PDF-Render. Im Browser wurde die
Card deshalb nicht skaliert oder scrollbar gemacht; auf kleinerem
Viewport einfach abgeschnitten.

Loesung: @media screen-Block, den nur Browser sehen (WeasyPrint
rendert mit media=print, ignoriert ihn). Wrapper-Div .card-viewport
bekommt aspect-ratio 1080/1350 und max-width 1080px (oder
viewport-40px). Die Card wird per CSS-transform scale() proportional
zur Viewport-Breite verkleinert. Auf einem normalen Desktop
(1920x1080) erscheint die Card jetzt in Originalgroesse zentriert
mit dunklem Rahmen drumherum, auf Mobile skaliert sie sich passend
auf die Bildschirmbreite.

PDF-Generierung unbeeinflusst: WeasyPrint sieht nur die Default-CSS
(body 1080x1350, card 1080x1350, kein transform).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 14:05:08 +02:00
Dotty Dotter
5fb326e8bc fix: Scorecard Header Wahlperiode-String — 'NRW-WP18' wurde als 'WPNRW-WP18' gerendert
wahlperiode_for() liefert 'NRW-WP18' (Bundesland-Praefix + WPnn). Ich
hatte im Template nochmal 'WP' davor gehaengt → 'WPNRW-WP18'. Fix:
Suffix-Teil nach Bindestrich nehmen ('WP18'), oder fallback 'WP'+Wert
falls kein Bindestrich.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:56:26 +02:00
Dotty Dotter
1350fc7f52 feat: Scorecard Portrait — Claude-Design 1:1 übernommen (1080×1350)
User hat eine Design-Vorgabe als ZIP geliefert (GWÖ Antrag Score Card.zip,
Claude Design Output, Stand 2026-05-07). Komplett anderes Konzept als
mein vorheriges Layout — nicht eine 5×5-Matrix als Datenviz, sondern eine
editorial-magazin-mässige Card mit klaren Modulen.

Übernommen 1:1:
- Inter + JetBrains Mono via Google Fonts
- 3-Zonen-Grid: Header 88px / Body / Footer 96px
- Paper-BG #f5f1ea, Ink #1a1a1a, GWÖ-Grün gedeckt #0a5d3f
- Header-Strip: Brand-Dot + 'GWÖ-Antragsprüfer' / 'Matrix 2.0' / 'NRW · WP18'
- Topline: Drs.-ID + Antragsteller-Pill (Partei-Farbpunkt)
- H1 Antragstitel 78pt, weight 800, line-height 0.95
- Lede: 1-Satz-Zusammenfassung, max 180 Zeichen
- Score-Block: 320px breite gruene Kachel mit 9.0/10 + 'Empfehlung' kicker
  + Verdict-Text + 3 Schwerpunkt-Chips (Code + Wert-kurz + Symbol)
- Fraktions-Grid: bis zu 5 Spalten, pro Fraktion Name + WP-Bar + Score
  + Vote-Label (Ja ✓ / Nein ✗); weak-Klasse (rot) bei Score<5 oder Nein
- Decision-Bar: invertiert schwarz, Beschluss-Text mit ✓/✗-Akzent
- Footer: URL + /antrag/-Pfad + CC BY 4.0 + QR-Pattern

Datenaggregation in main.py erweitert:
- matrix_chips: Top-3 positive Felder (rating > 0) mit Code+Wert+Symbol
- fraktionen_bars: aus wahlprogramm_scores + plenum_votes-Lookup
  (WP-Bar + Vote-Label pro Fraktion)
- beschluss: aus erstem plenum_vote (ergebnis + Mehrheits-Verhältnis)
- wahlperiode: via wahlperioden.wahlperiode_for(datum, BL)

Routing:
- /v2/scorecard?format=portrait → scorecard_portrait.html (NEU)
- /v2/scorecard?format=square|og → scorecard.html (alt, unveraendert)
- /api/assessment/scorecard.png?format=portrait nutzt ebenfalls das neue
  Portrait-Template fuer die PNG-Generierung.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:55:16 +02:00
Dotty Dotter
79e7937d51 feat: PDF-Generierung auf v3-Layout, Werkstatt-Link im Admin, Share nur bei Login
Drei Aufgaben in einem Schwung:

1. Werkstatt-Link im Admin
   admin_stand bekommt eine Sektion 'Design-Werkstaetten' mit Link auf
   /v2/scorecard-werkstatt — damit Admins den Live-Editor finden ohne
   die URL kennen zu muessen.

2. Share-Block nur fuer angemeldete User
   Der ganze Share-Block (Kopieren, Threads, Mastodon, LinkedIn,
   Instagram, E-Mail, Scorecard, Stock-Bild) bekommt id=v2-share-block
   und wird per initAuth() display:none/block geschaltet — analog zum
   Comment-Form. Default im Markup: display:none, damit Gaeste ihn
   nicht waehrend des Auth-Roundtrips kurz sehen. Funktioniert in v2
   und v3 (gleicher JS-Handler via super-Inheritance).

3. PDF-Layout = v3-Layout
   Neues Template v3/pdf/antrag_pdf.html (single column, A4) reused die
   Visuallogik aus der Online-Detailseite:
   - Score-Hero-Block mit Farb-Tint
   - Matrix 5×5 mit Achsen-Labels (Werte oben, Berührungsgruppen links)
   - Programm-Treue pro Fraktion mit Begruendung + Zitaten + Fallback-
     Hinweis bei fehlenden Zitaten
   - Verbesserungsvorschlaege mit Redline-Format
   - Abstimmungsergebnis (best-effort via get_plenum_votes) inkl.
     Konsistenz-Hinweis

   Online-spezifisches gestrichen: Merken-Button, Vote-treffend, Share,
   Kommentare, News-Box, Reanalyze, Historie, Modals.

   NEU im PDF: 'Schwerpunkte erklaert'-Sektion direkt unter der Matrix.
   Listet die Top-4 positiven und Top-4 negativen Matrix-Felder mit
   ihrem LLM-generierten label + aspect — Ersatz fuer den interaktiven
   Klick, der im PDF nicht funktioniert.

   report.generate_html_report_v3() neu, generate_pdf_report() ruft
   diese statt der alten Inline-HTML-Variante. Alte generate_html_report
   bleibt als Fallback erhalten.

   WeasyPrint rendert mit @page A4, Footer mit Drucksache + Branding +
   Seitenzahl 'Seite X von Y'.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:48:19 +02:00
Dotty Dotter
8ae2b92313 feat: Scorecard-Werkstatt — Live-Editor unter /v2/scorecard-werkstatt
User-Wunsch: 'Baue eine Entwicklungsseite, wo wir all das in CSS code
zusammenschreiben und länger daran arbeiten können ohne jedes mal png
erzeugen zu müssen. Können wir hinterher auch nutzen, um irgendwo
mal schnell eine Übersicht einzublenden.'

Neue Route /v2/scorecard-werkstatt mit Split-Layout:
- Links: Live-iframe-Vorschau der /v2/scorecard, mit Zoom-Toolbar
  (Fit / 40 / 50 / 65 / 80 / 100 %).
- Rechts: Drucksachen-Selector (Top-60 Anträge), Format-Pills
  (Portrait / Square / OG), CSS-Editor-Textarea + Apply-Button.
- Apply schreibt das User-CSS als <style>-Element in den iframe →
  keine Server-Roundtrips, kein PNG-Render, instantane Iteration.
- Strg/⌘+Enter im Editor wendet sofort an. Tab fuegt 2 Spaces ein.
- Direkt-Link + Iframe-Snippet werden generiert — die Card laesst sich
  also direkt embedden (z.B. Übersicht in einer anderen App).

Plus: Cache-Buster `&_=Date.now()` am Scorecard-Button im v3-Detail,
damit die Vorschau-Anzeige nach Layout-Aenderungen nicht weiter eine
gecachete Version zeigt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 13:39:45 +02:00
Dotty Dotter
4734e89522 fix: Matrix groesser (110px Cells statt 88px) — fuellt mehr Vertikalraum
Vorige Version hatte ~110 px Slack zwischen Matrix-Legende und Begruendung.
Matrix-Cells von 88×88 auf 110×110 hochgezogen, Label-Spalte 130→150 px,
Symbol-Schrift 19→24 pt, Zeilen-Header 36→40 px Hoehe.

Resultat: Matrix-Grid jetzt ca. 700×586 px (vorher 570×476). Slack im
matrix-block (flex-grow) deutlich kleiner, Card visueller dichter
gefuellt.

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