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.
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.
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.
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.
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
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.
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.
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.
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).
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
User-Frage: 'Wo kann ich jetzt die Cards angucken? Vielleicht verbunden
mit einem Instagram Sharing Button?'
Endpoints existieren laengst (#179):
- /v2/scorecard?format=og → 1200×630 LinkedIn/Twitter-Card
- /v2/scorecard?format=square → 1080×1080 Instagram
- /api/assessment/scorecard.png?format=square&scale=2 → PNG
In der Share-Row jetzt drei neue Eintraege:
1. '📷 Instagram' — oeffnet Square-PNG (1080×1080) im neuen Tab,
legt Begleittext in die Zwischenablage. Instagram hat keinen
Web-Share-Endpoint, daher: Bild speichern + Text einfuegen.
2. '📊 Scorecard ansehen' — oeffnet die OG-Format-Vorschau (1200×630)
im neuen Tab, der User sieht wie die Card auf LinkedIn/Twitter
aussehen wird.
3. '🖼 Stock-Bild' — alter Magnific-Stockphoto-Knopf, jetzt klar
gelabelt damit er nicht mit der Scorecard verwechselt wird.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LinkedIn /sharing/share-offsite/ akzeptiert seit ~2024 keinen text-Param
mehr, der Composer oeffnet leer. Stattdessen /feed/?shareActive=true&text=
prefillt den Compose-Dialog mit Text + Permalink (Permalink rendert
LinkedIn als OG-Preview).
Plus: Text geht weiterhin in die Zwischenablage als Fallback (Strg/⌘-V
falls LinkedIn den Param mal wieder verschluckt). Pop-up-Blocker-
Hinweis wenn window.open null zurueckgibt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Suchergebnisse auf der Übersicht und in /durchsuchen oeffneten weiter-
hin den alten Profi-Modus, weil das result_row-Macro als Default
'/v2/antrag/'+drucksache eingebaut hatte. Jetzt zeigt der Default-
Pfad auf '/antrag/' = v3 Buerger:innen-Modus.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drei Korrekturen:
1. Mobile-Topbar: Auth-Widget + Bundesland-Selector + Theme-Toggle
pushten die rechte Kante über 390 px Phone-Viewport. Fix: Topbar
darf in @media (max-width: 900px) flex-wrappen, height auto,
row-gap fuer mehrzeilig.
2. Topbar-Link "Klassische Ansicht" → /classic entfernt (verlinkt auf
das alte v1-Frontend; v2 bzw. das neue v3 sind die aktiven Modi).
3. /tags-Seite hatte zwei Bugs:
- Titel wurde aus a.titel (existiert nicht) statt a.title gelesen
→ User sah nur Drucksachen-Nummern und dachte "kaum Daten".
- Kein Visual-Feedback welche Tag-Kombinationen leer wären.
Beide gefixt: title-Field korrekt, plus Tag-Greying via class
.tag-pill.disabled fuer Tags die zu 0 Treffern fuehren wuerden.
Ausserdem Score-Field gwoeScore-Fallback und HTML-Escape fuer alle
Strings (vorher XSS-anfaellig bei Title/Fraktion).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routen:
- /antrag/{drs} → v3 (Standard, Bürger:innen-Modus single column)
- /v2/antrag/{drs} → v2 (alter Profi-Modus, weiterhin erreichbar)
- /v3/antrag/{drs} → Alias auf v3 (für alte Bookmarks)
UI-Hinweise auf alternative Ansichten ausgeblendet:
- v3-Topbar-Pill "Bürger:innen-Modus · Beta" + "→ Profi-Modus"-Toggle raus
- v2-Topbar-Link "→ Bürger:innen-Modus · v3 Beta" raus
Im Admin-Bereich (/v2/admin/stand) neuer Block "Alternative Ansichten"
mit Beispiel-Drucksache, Live-Link auf v3 (Default) und v2 (Profi).
Nur Admins sehen die Hinweise auf v2.
Trennlinien-Cleanup im Rest-Block:
- Doppellinie unter Abstimmungsergebnis aufgelöst (.v3-rest hatte
border-top, das v3-section.border-bottom doppelt war).
- Neue Trennlinien via Klasse v3-rest-divider-top vor:
· Teilen-Block (zwischen Ähnliche und Teilen)
· Aktions-Links (zwischen Teilen und administrativem Bereich)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Der vorherige Refactor hatte die expliziten Sub-Headers im Body
abgeworfen. User wollte sie zurück (BELEGE-Layout-Spec):
- Header-Row: Programm-Label links, "Bewertung"-Tag + Score-Chip rechts
- Body: Sub-Label "Einschätzung" über der Begründung,
Sub-Label "Belege" über den Zitaten
Default bleibt <details open> — alles sofort lesbar, Falt-Toggle
optional. Chevron ▾ am Label rotiert beim Schließen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-Feedback: voriges Layout war "Usability-Alptraum" — alle 8 Blöcke
(4 Fraktionen × 2 Programme) waren default-closed, was 8 Klicks pro
Antrag bedeutete um Begründungen zu lesen.
Fix: <details open> als Default. Inhalt sofort vollständig sichtbar
beim Scrollen. Faltmechanismus bleibt für User, die die Liste nur
skimmen wollen — Chevron ▾ am Label rotiert beim Falten. Hover-Affordanz
über Label-Color-Change zum Brand-Blau (kein extra Caret-Element).
Layout-Spec bleibt: pro Partei zwei Treue-Blöcke (Wahlprogramm,
Parteiprogramm). Summary = Label + Score-Chip rechts. Body =
Einschätzung-Text + Belege via quote_card-Macro.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Skelett für Issues #184 + #185 — minimal, nicht-disruptiv:
- v3/base.html extendet v2/base.html (Topbar/Sidebar/Footer geteilt)
- v3/screens/antrag_detail.html extendet vorerst v2-Screen 1:1 und
injiziert nur Beta-Pill + Toggle "→ Profi-Modus"
- v2/screens/antrag_detail.html bekommt Topbar-Link "→ Bürger:innen-
Modus (v3 Beta)" → /v3/antrag/<drs>
- _render_antrag_detail() teilt DB-Reads/Context zwischen v2 + v3 —
Datenbasis garantiert in Sync, Unterschied ist nur template_name
- _MATRIX_EXPLANATIONS auf Modul-Ebene ausgelagert (war bisher
inline im v2-Route, jetzt von beiden Modi referenziert)
- v3.css als Add-On nach v2.css (lädt im v3/base head)
Was v3 noch NICHT tut: Score-Hero-Vereinfachung, Matrix→5-Werte,
Glossar-Tooltips, Default-Collapsing der Profi-Blöcke (Verbesserungen,
Kommentare). Diese Iterationen folgen pro PR — v2 bleibt unberührt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Matrix-Info-Modal:
- _row_to_detail liefert label+aspect je Matrix-Cell ans Frontend
- Modal zeigt antragsspezifische Bewertung (Label + ausformulierte
Begründung + Rating-Chip) UND die allgemeine Felderklärung
(was misst dieses Feld?). v1-Verhalten wiederhergestellt.
Abstimmungsergebnis (Vote-Block):
- Outlink-Pfeil ↗ ist jetzt ein klickbares <a> auf v.quelle_url
(target=_blank). Vorher: Span ohne Link, Pfeil tat nichts.
- Marker ⚠ (Heuchelei) und ! (Opportunismus) bekommen sichtbare
Hover-Affordanz (Hintergrund + dotted-border on focus). Native
title= bleibt fuer Screenreader-/Tooltip aktiv. tabindex=0+role=button
macht sie keyboard-erreichbar.
- Legende unter dem Vote-Pill-Block erklaert die Marker beim
ersten Auftreten in einer Liste — wird nur eingeblendet wenn auf
dem Block mindestens ein Marker tatsaechlich vorkommt.
- Vote-Pills via Klassen v2-vote-pill/-ja/-nein/-enth statt
Inline-Styles (CD-Annaeherung).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _row_to_detail liefert zitate inline pro Wahlprogramm/Parteiprogramm-Block
- Template rendert <details>: Summary mit Score-Chip, Body mit Einschätzung+Belege
- v2.css: neue Klassen v2-treue-block/-label/-body, v2-pill, v2-einschaetzung
- Separate "Belege — Partei"-Sektion entfernt (ist jetzt inline pro Programm)
Tests: tests/test_v2_pdf_consistency.py (#176 generalisiert) bleibt grün —
fraktions_scores trägt zusätzliche zitate-Felder, ändert aber keine
Score/Begründungs-Werte aus dem Vergleich.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 21 — Score-Hero auf Float-Layout:
- Score-Zahl floated links, Verdict-Text fließt rechts daneben UND
unter sie weiter (vorher: flex-baseline, Text bricht nicht um).
- overflow:hidden auf Container fuer clean float-clearing.
Phase 22 — Merken-Button oben rechts auf gleicher Höhe wie 'Bewertung'-Label:
- Visuelle Einheit: 'Bewertung'-Header links, Merken-Button rechts.
- Vorher: Merken-Button stand alleinig zwischen Score und Vote-Block,
optisch losgeloest.
Refs: #177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- **Zeitreihe-Modal-Chart**: vor Re-Render alte Chart.destroy(),
bei closeModal() + Escape ebenfalls. Vorher akkumulierten sich
Chart-Instanzen bei jedem Modal-Open (Listener bleiben hängen).
- **Cluster-Force-Sim**: `_currentForceSim` global gemerkt, beim
Verlassen Detail-View (showList) und vor neuem renderClusterGraph
per sim.stop() beendet. Vorher liefen tick-Listener weiter, hielten
nodes/links/SVG-Refs.
- **Polling-Pause auf visibilitychange** in queue_widget, admin_stand
und admin_queue. Wenn Tab versteckt → clearInterval, beim Show wieder
starten. Spart CPU/Akku + verhindert verwaiste Polls.
Refs: #183. Heap-Snapshot-Verifikation in Folge-Schritt.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vorher: `onclick="copyDraftToClipboard(this, ${JSON.stringify(...).replace(/"/g, '"')}, ...)"`
— funktional korrekt, aber Pattern-anfaellig (gleiche Klasse wie der
merkliste-bug aus Phase 17). Plus < und > waren nicht escaped.
Nachher: Button traegt nur eine numerische data-pm-id; der Handler
fetched den Draft per API und kopiert den Body. Robuster, weniger
Quote-Escaping, einheitlicher mit dem versionsHtml-Pattern oben in
derselben Datei.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit-Befund: alte UI sortierte _clusters nach (members || []).length —
Backend liefert aber size + drucksachen, members ist leer. Folge: alle
Cards hatten size 0 als Sort-Wert, Reihenfolge war effektiv random.
Backwards-compat-Lookup mit drucksachen → members → size-Fallback.
(Andere c.members-Lookups in antrag_detail.html + aktuelle-themen.html
betreffen News-Cluster, deren API tatsächlich 'members' liefert — kein Bug.)
Mockup im GWÖ-Stil mit:
- Drucksachen-Header (Kicker + Datum)
- Titel + antragstellende Fraktionen als Pills
- Empfehlungs-Verdict
- 420-Zeichen-Zusammenfassung
- Big Score-Zahl (farbcodiert nach 8/5/3-Schwellen)
- 5x5 Mini-Matrix mit korrekten 5 Klassen (rating-pp/-p/-0/-n/-nn)
- Footer mit Brand + Drucksachen-ID
Endpoints:
- GET /v2/scorecard?drucksache=&bundesland=&format=og|square (HTML)
- GET /api/assessment/scorecard.png?... (PNG via Playwright,
1200x630 für og, 1080x1080 für square)
Pattern entlehnt von app/og_card.py (Playwright-Headless-Render).
Refs: #179
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- **Matrix-Coloring konsistent**: Symbol wird jetzt aus rating
abgeleitet (rating_symbol-Macro) statt vom LLM übernommen. Bisher
wurde z.B. rating=4 + symbol="+" geliefert → Template zeigte "+"
aber mit m-pp-Klasse (kräftiges Grün) → "++/+ wirkten gleichfarbig".
Stichprobe: 7/30 Assessments hatten rating/symbol-Mismatch.
- **„Antragsteller:in" / „Regierungsfraktion"** als Pill ausgeschrieben
statt 1-Buchstabe-Badges A/R.
- **Programm-Treue mit Begründung sichtbar**: Wahlprogramm- und
Parteiprogramm-Begründung als Block direkt unter den Score-Chips.
Vorher nur Tooltip — auf Mobil schwer zugänglich.
- **„Redline" → „Verbesserungsvorschläge"** in beiden Heading-Pfaden.
Layout-Umstellung (Matrix↔Vote oben, Programm-Treue↔Verbesserung unten)
in #177 als Follow-up — braucht gerichtete Session mit Browser-Vorschau.
Refs: #177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>