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>
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>
- **X (Twitter) raus** — Button + Logik entfernt.
- **Copy & Paste**: vollständiger Body ohne Länge-Cut, mehrzeilig
strukturiert (Score, Titel, Drucksache, Beschreibung, Permalink,
Hashtags). Statt 240-Zeichen-Twitter-Variante.
- **Threads**: encodeURIComponent kümmert sich um UTF-8 — keine
Sonderzeichen-Probleme.
- **Mastodon**: gleicher Body wie Threads, Limit auf 420 Zeichen
(mit Permalink-Reserve), Instance-Prompt bleibt.
- **LinkedIn**: Composer öffnet nur den Permalink (LinkedIn-API-
Limitation), aber der vollständige Body landet parallel in der
Zwischenablage. Toast informiert User.
- **E-Mail**: strukturierter Body mit Umbrüchen — Score-Zeile, Titel,
Drucksache, Beschreibung, Permalink, Footer. Statt der knappen
Threads-Variante.
- **Magnific**: korrekte URL mit `last_filter=selection&last_value=1
&selection=1` — license-Filter vorausgewählt.
Refs: #178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DIP-Drucksachen mit `herausgeber: 'BR'` (Bundesrat) haben Bundesländer
als Antragsteller (z.B. SN, HE) statt Fraktionen. Variante b — explizite
Behandlung statt nur ausschließen:
- Drucksache-dataclass: neue Felder `is_bundesrat: bool`,
`urheber_bundeslaender: list[str]`. Existierende Pfade unberührt.
- BundestagAdapter._doc_to_drucksache: liest herausgeber + urheber-Liste,
setzt Bundesländer-Codes (bezeichnung wie "SN") in
urheber_bundeslaender. fraktionen bleibt leer fuer BR — verhindert
dass Stimmverhalten-Aggregate verwirrt werden.
- /api/search-landtag liefert is_bundesrat + urheber_bundeslaender im
Response.
- /api/analyze-drucksache (POST) lehnt BR-Drucksachen mit HTTP 400 +
klarer Meldung ab statt crashen.
- v2-Search-UI rendert grayen Bundesrat-Sticker mit BL-Codes statt
Fraktionen, "Analysieren"-Button durch "nicht unterstuetzt" ersetzt.
is_bundesrat_drucksache() in drucksache_typen.py als Format-Helper
(N/M/JJ-Pattern) bleibt fuer Cases wo nur die Drucksache-ID ohne
Adapter-Metadaten verfuegbar ist.
Refs: #6
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Row-ID wird via escAttr gebildet ([^a-zA-Z0-9_-] → '_'), z.B. '18/18089'
landet als id='merkliste-row-18_18089'. Der getElementById-Lookup nutzte
aber CSS.escape, das 18/18089 zu 18\\/18089 escaped — zwei verschiedene
Strings, getElementById lieferte null, el.remove() lief nicht.
Plus: getElementById akzeptiert ohnehin keinen CSS-Selektor — der
CSS.escape-Lookup war doppelt falsch.
Fix: gleiche Sanitization-Regex wie escAttr im Lookup nutzen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
JSON.stringify(a.drucksache) lieferte einen JSON-String mit Doublequotes
(z.B. "18/18089"). Eingesetzt in onclick="merkliste_remove("18/18089")"
brach das das HTML-Attribut beim ersten inneren Doublequote, der Browser
warf 'Unexpected end of input' beim Click und der DELETE-Request kam nie
beim Server an.
Fix: escHtml() um den JSON-String, sodass Quotes als " gerendert
werden — onclick-Attribut bleibt valide.
Bug headless mit Playwright + DEBUG_AUTH_TOKEN gefunden (commit f8cfa42).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In panel-themen wurde <div class='controls-bar'> geöffnet, aber nie
geschlossen. Folge: alle nachfolgenden Panels (Stimmverhalten,
Score-Verteilung, Cluster-Link) rutschten als Kinder in panel-themen
rein und erbten dessen display:none.
Bei aktiviertem Tab-Switch wurde das richtige Panel zwar mit class
'active' versehen, aber sein PARENT (panel-themen) blieb display:none
— daher 0×0 Bounding-Box auf allen Charts.
Ohne Debug-Bypass headless gefunden — Diagnose-Skript zeigte panel-
stimmverhalten als Kind von panel-themen im DOM-Stack.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend liefert edges als {a: 0, b: 1, sim: ...} mit Indizes in der
nodes-Liste. d3.forceLink mappt per id-Lookup und fand 'drucksache' als
Lookup-Key nicht. Folge: keine Links, Force-Sim degeneriert ohne Layout.
Fix: Index-Strings als id verwenden.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beim Klick auf eine Cluster-Card setzte showCluster() detail.style.display = ''.
Da #cluster-detail per CSS aber 'display:none' hat, fiel der Style auf
'none' zurueck — Detail-View blieb unsichtbar, Force-Graph wurde nie gesehen.
Fix: explizit 'block' setzen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Beim Klick auf einen Cluster wird jetzt zusätzlich zur Antragsliste
ein d3-Force-Graph eingeblendet. Knoten = Drucksachen, Kantendicke =
Cosine-Similarity, Knotenfarbe = dominante Fraktion. Klick auf einen
Knoten oeffnet das Antrag-Detail.
Daten kommen aus dem bereits vorhandenen /api/clusters-Response
(nodes/edges-Felder, vorher ungenutzt). Layout: forceSimulation mit
link/charge/center/collide. d3.v7.min.js wird im head_extra geladen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Backend liefert seit ueber den Refactor fields drucksachen/
avg_gwoe_score; Frontend-Template las members/avg_score → leere
Cluster-Cards. Beide Schluessel akzeptieren (Backwards-Compat).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stacked Bar (Ja gruen / Enth grau / Nein rot) zeigt die Fraktions-
Mehrheit pro Plenum-Vote. Caveat-Tooltip ⓘ stellt klar: Anzahl
Fraktionen, nicht Sitz-/Stimm-Anteile (Plenarprotokoll liefert
keine Sitz-Counts).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- get_empfehlungs_konsistenz_cases() liefert Antraege wo `partei` mit
NEIN gestimmt hat, obwohl die GWÖ-Empfehlung "Unterstuetzen" lautete.
- Endpoint GET /api/auswertungen/empfehlungs-konsistenz-cases
- Frontend: Konsistenz-Bar bekommt onClick → Modal-Tabelle mit Drucksache,
BL, Datum, GWÖ-Score, Empfehlung, Beschluss. Drucksachen-Link ins Detail.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Schema additiv: presse_drafts.style TEXT NOT NULL DEFAULT 'pm' via
ALTER TABLE (idempotent in init_db).
- presse_generator.generate_draft(style='pm'|'thread') nutzt eigenen
SYSTEM_PROMPT_THREAD (3-5 Posts à ≤280 Zeichen, Hook + Lebenslagen +
Forderung, Hashtags am Schluss; keine **fett**-Markdown).
- _find_existing_draft, list_drafts, list_drafts_for, get_draft liefern
jetzt auch das style-Feld zurueck.
- Endpoint /api/aktuelle-themen/generate-presse?style=thread baut den
Switch ein. Ohne Param weiterhin 'pm'.
- Frontend: PM-Modal zeigt den style-Tag (📰 PM / 🐦 Thread) im Banner
und bietet einen Knopf "Auch als Thread / Auch als PM" generieren.
Idempotenz pro (drucksache, news_url, style)-Tripel.
Refs: #170, #178
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neuer Endpoint GET /api/aktuelle-themen/drafts/{id}.pdf rendert den
gespeicherten PM-Body inkl. **fett**-Markdown als A4-PDF mit Header
(Drucksache-Link, GWÖ-Markup) und Footer-Quellenangabe.
PM-Modal in /aktuelle-themen bekommt zusätzlich einen 📄 PDF-Button
neben Mail/Clipboard. Dateiname `PM-DRUCKSACHE-DRAFTID.pdf`.
Refs: #170, #177
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3 (Vote-Orphans-Auto-Bewertung):
- Neue Tabelle `auto_rate_runs` (additiv) mit started_at, source,
bundesland, limit_requested, n_attempted/succeeded/failed/skipped,
error_summary.
- Neue DB-Helper: record_auto_rate_run, list_auto_rate_runs,
auto_rate_today_total.
- POST /api/auswertungen/vote-orphans/auto-rate erweitert um source,
daily_cap und Run-Persistenz. Throttled gegen Tagessumme.
- Neuer Endpoint GET /api/auto-rate-runs (admin) — letzte N Runs +
Tagessumme.
- scripts/auto-rate-orphans.sh: Cron-Wrapper (analog auto-fetch-news.sh)
mit MAX_PER_RUN=30 / MAX_PER_DAY=200 Defaults, BUNDESLAND-Filter
optional, ruft direkt die Python-Worker-Funktion via docker exec.
- Admin-Stand-Dashboard: KPI-Zeile "heute X Runs / Y versucht" + Tabelle
der letzten 5 Runs mit BL/Counts/Notiz.
Refs: #173, ADR 0010
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- auswertungen.html: .auswert-tabs scrollt jetzt horizontal (overflow-x:auto)
+ nowrap-Buttons + kleinere Padding/Font auf <600px.
- aktuelle-themen.html: .at-tab Buttons whitespace:nowrap, Tab-Container
ebenfalls scrollbar.
- Drilldown-Modal: 8px statt 20px Padding aussen, Tabelle in
overflow-x-Wrapper, max-height 90vh statt 80vh.
Visueller Test auf 375px steht aus (kein Browser im Build-Setup),
diese Aenderungen folgen aus statischer CSS-Audit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Klick auf eine Heuchelei-Bar oeffnet ein Modal mit der konkreten
Liste der Antraege wo die Fraktion mit Nein gestimmt hat, obwohl
der Antrag inhaltlich zum eigenen Wahlprogramm passt.
- Backend: app.auswertungen.get_heuchelei_cases() + Endpoint
GET /api/auswertungen/heuchelei-cases?partei=X[&bundesland=Y].
- Backend: _load_assessments_with_votes liefert jetzt zusaetzlich
das ergebnis-Feld (additiv im SELECT).
- Frontend: onClick-Handler im Heuchelei-Bar-Chart, Modal-Markup
wird lazy injiziert, Tabellen-Drilldown mit Drucksachen-Link.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neue Route /stimmverhalten rendert dieselbe auswertungen.html, aber
mit default_tab='stimmverhalten' und v2_active_nav='stimmverhalten'.
Linker Nav-Eintrag 'Stimmverhalten' (Icon scales) zwischen
Auswertungen und Aktuelle Themen.
Beim Page-Load aktiviert das DOMContentLoaded-Handler den im Context
gesetzten Tab — fuer /auswertungen ist es 'bl-partei' (Default), fuer
/stimmverhalten direkt 'stimmverhalten'. Kein Code-Duplikat im
Tab-Inhalt.
Refs: #169
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Symmetrisch zur Heuchelei-Logik: bei JA-Fraktionen, deren eigener
Wahlprogramm-Score < 3 ist, erscheint ein dezenter italic '!' mit
Tooltip. 11 echte Cases gefunden auf dev (NRW + BB).
app/marker.py: opportunismus_score() — neun neue Tests (test_marker.py
jetzt 44 grün).
Refs: ADR 0010, Phase 2.4
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Logik aus dem Jinja-Template (Heuchelei-Marker, Konsistenz-Block,
decisive-Outcome-Selection) in app/marker.py extrahiert. Template
ruft die drei Helper als Jinja-Globals auf. Damit ist die Logik
testbar ohne Render-Kontext.
Plus: app/pm_render.py als Python-Spiegelbild des JS-Mini-Markdown-
Renderers in aktuelle-themen.html — fuer Tests und potenzielle
Server-side-Render-Optionen (z.B. PM-Mail).
Tests:
- tests/test_marker.py (35 Cases): heuchelei_score, decisive_outcome,
consistency_state inkl. Multi-Vote, ambivalente Empfehlung,
Edge-Cases.
- tests/test_pm_render.py (21 Cases): Bold, Italic, Listen,
HTML-Escape, Paragraph-Splitting, snake_case-Schutz.
Refs: ADR 0010, ADR 0011
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wenn ein Antrag mehrere Plenum-Votes hat (Überweisung → Endabstimmung),
nimmt der Konsistenz-Block jetzt das erste mit angenommen/abgelehnt/
bestätigt. Vorher wurde stur [0] verwendet — das war oft "überwiesen"
und der Block blieb leer trotz vorhandenem Endbeschluss.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Direkt unter "Abstimmungsergebnis" steht ein Hinweis-Block:
- "Mehrheit kontra GWÖ-Empfehlung" (rot) wenn Empfehlung "unterstützen"
und Beschluss "abgelehnt" oder umgekehrt.
- "Mehrheit deckt sich mit GWÖ-Empfehlung" (grün) bei aligniertem Fall.
- Bei "überwiesen" oder ambivalenter Empfehlung kein Block.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Neben jeder NEIN-Fraktion erscheint ein dezentes ⚠ wenn der eigene
Wahlprogramm-Score >= 7 lag. Tooltip nennt den Score. Macht im Detail
sichtbar wer gegen das eigene Programm stimmt — gleicher Befund wie im
Stimmverhalten-Tab, aber pro Antrag punktgenau.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>