Commit Graph

59 Commits

Author SHA1 Message Date
Dotty Dotter
6f35efe4d7 #47: Volles PDF mit Highlight statt 1-Seiten-Extract
User-Feedback: "Kontext geht verloren wenn nur 1 Seite kommt".

Änderung: render_highlighted_page liefert jetzt das GESAMTE Wahlprogramm-
PDF mit gelber Highlight-Annotation auf der Fundstelle, statt eines
1-Seiten-Auszugs. Der Browser öffnet das vollständige Programm.

Frontend hängt #page=N an die URL → Browser scrollt direkt zur
Fundstelle. found_page wird als X-Found-Page Header mitgeliefert,
falls der Text auf einer anderen Seite als angefordert gefunden wurde
(Pre-#60 halluzinierte Seitennummern).

Return-Typ geändert: (bytes, int) statt bytes — zweiter Wert ist die
1-indexed Seitennummer wo der Treffer tatsächlich liegt.

Tests angepasst: Tuple-Unpacking, Size-Check entfernt (volles PDF ist
größer als 1-Seiten-Extract, der alte Vergleich war obsolet).

Refs: #47
2026-04-10 10:16:00 +02:00
Dotty Dotter
47897e13cd #47 Fix: Highlighting retroaktiv für alle bestehenden Assessments
Problem: Alle Assessments in der Prod-DB haben Pre-#47-URLs
(/static/referenzen/X.pdf#page=N). Die _chunk_pdf_url-Änderung wirkt
nur auf NEUE Analysen, die noch nicht stattgefunden haben.

Fix (zwei Seiten):

1. Endpoint /api/wahlprogramm-cite akzeptiert jetzt auch pdf=<filename>
   als Alternative zu pid=<programm_id>. Reverse-Lookup über PROGRAMME-
   Registry: pdf-Filename → programm_id. Damit können die statischen
   URLs aus Pre-#47-Assessments trotzdem an den Cite-Endpoint geleitet
   werden.

2. Frontend: neue JS-Funktion makeCiteUrl(z) die JEDE Zitat-URL on-the-
   fly umschreibt:
   - /static/referenzen/X.pdf#page=N + z.text
     → /api/wahlprogramm-cite?pdf=X.pdf&seite=N&q=<urlencoded text>
   - /api/wahlprogramm-cite?... → durchreichen (schon Cite-URL)
   - Fallback: URL unverändert

   Funktioniert retroaktiv für ALLE ~31 Assessments in der DB, ohne
   Re-Analyse. Sobald ein User auf ein Zitat klickt, wird die Seite
   des Wahlprogramms mit gelber Markierung gerendert.

Tests: 194/194 grün.

Refs: #47
2026-04-10 09:57:58 +02:00
Dotty Dotter
92dcd25f73 #63 B+C: Force-Honesty + UI-Warning bei Score ohne Zitate
Problem: BUND 21/3660 zeigt Score 10/10 für Linke und Grüne, aber null
Zitate — der Report sieht aus als sei die Bewertung fundiert, obwohl das
LLM mangels indexierter Quellen (linke-grundsatz fehlt) aus
Trainingswissen geraten hat. User-Feedback: "Da muss stehen warum."

Fix C — Force-Honesty im Prompt:

- format_quotes_for_prompt akzeptiert neuen Parameter searched_parties.
  Parteien, für die kein Chunk retrievt wurde, werden explizit als
  "KEINE QUELLEN VORHANDEN" markiert, mit der Anweisung "score: 0,
  zitate: [], Begründung: keine Quellen im Index".
- Neue ZITATEREGEL Punkt 5: "Wenn KEINE QUELLEN VORHANDEN → score 0."
  Das ist die strukturelle Lösung — das LLM darf nicht mehr raten.
- analyzer.py: fraktionen-Liste wird an format_quotes_for_prompt als
  searched_parties durchgereicht.

Fix B — UI-Transparenz:

- index.html: gelbe Warn-Box (amber, border-left #ffc107) wenn
  wp.wahlprogramm.score > 0 aber wp.wahlprogramm.zitate.length === 0:
  "Keine belegbaren Quellen im Index gefunden — Score basiert auf
  LLM-Einschätzung, nicht auf verifizierten Programm-Stellen."
- Wird für bestehende Assessments sofort sichtbar (JS-seitig berechnet),
  keine DB-Migration nötig. Neue Assessments nach Force-Honesty sollten
  idealerweise Score=0 haben, aber die Warning ist ein Fallback für
  den Fall dass das LLM die Prompt-Regel nicht immer 100% befolgt.

Fix A (Linke/AfD-Grundsatzprogramme) folgt als separater Commit —
sind öffentlich downloadbar, brauchen manuellen Sichtbarkeitscheck.

Tests: 194/194 grün (keine Schema-Änderung, nur Prompt + Template).

Refs: #63, ADR 0001
2026-04-10 09:32:31 +02:00
Dotty Dotter
7cf073122f Phase E (substituted): Auswertungen-Drilldown-Modal (#59)
Sachsen-Adapter (#26/#38) ist Eigensystem mit ASP.NET-Webforms-Postbacks
(__VIEWSTATE/__CALLBACKID, siehe bundeslaender.py:343-348) und braucht
HAR-Aufnahme → Blocker für autonome Bearbeitung. Phase E entsprechend
substituiert mit der Frontend-Erweiterung der Auswertungen.

- Matrix-Zellen sind jetzt klickbar (`cell-with-data`-Klasse +
  hover-outline mit Blue-Border)
- Klick öffnet ein Modal, das `/api/auswertungen/zeitreihe?
  bundesland=...&partei=...` aufruft und die Score-Entwicklung dieser
  (BL, Partei)-Kombination über alle bekannten WPs als Tabelle rendert
- ESC-Taste oder Backdrop-Klick schließt das Modal
- Schließt damit den Frontend-Loop für die in Phase C gebauten
  Backend-Endpoints

(CLAUDE.md-Sync separat — die Datei liegt im Projekt-Root außerhalb
des Webapp-Git-Repos.)

Refs: #59 (Phase E substituted)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:30:10 +02:00
Dotty Dotter
26f13bd29d Phase D: zwei getrennte Suchfelder + Auswertungen-Link (#16, #59)
Schließt #16 (UI: zwei klar getrennte Suchen) und ergänzt den Header
um den Link auf das neue Auswertungen-Dashboard aus Phase C.

- Search-Row in `index.html` aufgespalten in zwei untereinanderliegende
  Inputs: oben "Suche in geprüften Anträgen (DB)" mit Live-Debouncing
  (wie bisher), unten "Im Landtag suchen (live)" mit Enter-Trigger und
  expliziter Such-Button. Beide Felder schreiben in dieselbe Liste,
  sind aber visuell und semantisch klar getrennt.
- `searchLandtag()` zieht jetzt aus `landtag-search-input` statt aus
  dem DB-Suchfeld
- `changeBundesland()` resettet zusätzlich das Landtag-Feld
- Header: neuer `📈 Auswertungen`-Link neben `📚 Quellen`

Refs: #16, #59 (Phase D)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:27:29 +02:00
Dotty Dotter
3631e5418c Phase C: Auswertungen-Dashboard #58 + CSV-Export #45 (Roadmap #59)
Drei-dimensionale Aggregations-Sicht über Bundesland × Partei ×
Wahlperiode mit minimalem Frontend.

Backend (`app/auswertungen.py`):

- `aggregate_matrix(filter_wp=None)` — 2D-Matrix Bundesland × Partei mit
  (n, Ø-Score) pro Zelle, optional gefiltert nach Wahlperiode
- `aggregate_zeitreihe(bundesland, partei)` — Score-Verlauf einer
  (BL, Partei)-Kombination über alle bekannten WPs
- `export_long_format()` — Long-Format-CSV-Export für externe Tools
  (deckt #45 vollständig ab)
- Partei-Auflösung läuft strikt durch `normalize_partei()` aus #55 —
  damit wird BB-`FREIE WÄHLER` korrekt als `BVB-FW` aggregiert und
  NICHT mit dem RP-FW zusammengezählt

Wahlperioden-Helper (`app/wahlperioden.py`):

- `wahlperiode_for(datum, bundesland)` mappt ein ISO-Datum + BL auf eine
  Kennung wie `"NRW-WP18"` oder `"MV-WP7"` (Vorgänger-WP). Single Source
  of Truth ist `BUNDESLAENDER[bl].wahlperiode_start`
- `all_wahlperioden()` für UI-Filter-Dropdowns

Endpoints in `app/main.py`:

- `GET /auswertungen` — HTML-Seite (neues Template)
- `GET /api/auswertungen/matrix?wahlperiode=NRW-WP18` — JSON-Matrix
- `GET /api/auswertungen/zeitreihe?bundesland=MV&partei=CDU` — JSON-Verlauf
- `GET /api/auswertungen/export.csv` — CSV-Download

Frontend (`app/templates/auswertungen.html`):

- Statisches Template mit Vanilla-JS, kein Build-Step
- Wahlperioden-Dropdown + Reload-Button + CSV-Export-Button
- Matrix-Tabelle mit Score-Color-Coding (rot ≤ 3, gelb 3-6, grün > 6)
- Sticky-Bundesland-Spalte für horizontales Scrolling

Tests (`tests/test_auswertungen.py`):

- 19 Cases mit in-memory SQLite-Fixture
- Verifiziert WP-Mapping, Matrix-Aggregation, Koalitions-Counting,
  WP-Filter-Korrektheit, BVB-FW-Disambiguierung in der Matrix,
  CSV-Long-Format
- 176 Unit-Tests grün (157 alt + 19 neu)

Refs: #58, #45, #59 (Phase C)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 11:25:57 +02:00
Dotty Dotter
f1867d463c Bundesland filter & transparency: stringent split + visible source (#8)
Brings the Bundesland-Dropdown from a cosmetic header widget to a real
filter that propagates through every layer (Listing, internal search,
statistics, party/tag filters, upload mode), and at the same time makes
the source parliament visible in every place where assessments from
multiple bundesländer can be mixed.

Backend
- database.get_all_assessments(bundesland=None) — new optional filter,
  "ALL" treated as None.
- database.search_assessments — bug fix: previous `if bundesland:`
  branch incorrectly added a `WHERE bundesland='ALL'` clause; now
  guarded with `bundesland and bundesland != "ALL"`.
- main.list_assessments — accepts ?bundesland= query param, includes the
  bundesland field in the response so the frontend can render badges.
- main.get_single_assessment — also includes bundesland in the response
  so the detail header can show the source parlament.
- main.search_landtag — early HTTP 400 when bundesland is missing or
  "ALL"; the live Landtag adapter cannot serve a synthetic Bundesweit
  request.
- main.index() and main.list_bundeslaender — synthetic "🌍 Bundesweit"
  entry prepended to the bundesländer list (kept out of bundeslaender.py
  on purpose — ALL is not a real state). Both endpoints additionally
  expose a parlament_names map so the frontend can render the source
  parliament without an extra round-trip.

Report (PDF + HTML)
- generate_html_report / generate_pdf_report — new optional bundesland
  parameter. When set, the report header carries the parliament name
  ("Landtag von Sachsen-Anhalt", "Landtag Nordrhein-Westfalen", …)
  beside the title. Three call sites updated: run_analysis,
  run_drucksache_analysis, download_assessment_pdf.

Frontend (templates/index.html)
- Header dropdown gets the synthetic ALL entry as first option;
  initial currentBundesland is now 'ALL' (was 'NRW').
- localStorage persistence: changeBundesland writes, DOMContentLoaded
  reads and validates against the visible options.
- changeBundesland resets the score / party / tag filter state, syncs
  the upload-mode bundesland select, disables the Landtag-Suche button
  + tooltip when ALL, and toggles a data-mode attribute on
  .list-content (used by CSS to show/hide the per-item bundesland
  badge).
- loadAssessments now sends ?bundesland=… so the API does the actual
  filtering. updateStats renders an additional per-bundesland average
  block (Ø NRW: x · Ø LSA: y) when in ALL mode and the loaded list
  spans more than one bundesland.
- renderList prepends a small "bl-badge" beside the Drucksachen-Nummer.
  Hidden in single-bundesland mode via CSS selector to avoid clutter.
- showDetail header now shows the parliament name as its own line
  (.detail-parlament).
- searchLandtag has an early-out alert if currentBundesland === 'ALL',
  saving a network round-trip.
- Upload-Mode bundesland select now starts with a "— Bundesland wählen
  —" placeholder (no auto-default), and startAnalysis validates that a
  concrete bundesland was chosen.

CSS
- .bl-badge plus the .list-content[data-mode="single"] hide rule.
- .detail-parlament for the detail header line.
- .header-parlament for the PDF report header line.

Resolves #8.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 23:00:39 +02:00
Dotty Dotter
bcd532be89 Fix responsive layout for mobile viewports (#6)
Body becomes a flex column so the header takes its natural height and the
main container fills the rest via flex:1 — replaces the brittle
calc(100vh - 70px) that assumed a 70px header and broke as soon as the
header wrapped on mobile. Adds 100dvh fallback for iOS Safari address
bar quirks.

Mobile breakpoint (≤900px) reworked: list scrolls internally via
list-content max-height:50vh, detail-panel uses overflow:visible so the
whole document scrolls naturally instead of nesting scrollers. Tapping
an item auto-scrolls to the detail panel and a new "← Zur Liste" button
(mobile-only) jumps back. Adds a tighter ≤600px breakpoint that hides
the subtitle, collapses the matrix grid to one column and shrinks the
matrix table for phone screens.

Resolves issue #6.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 13:48:55 +02:00
Dotty Dotter
63de3ca20d Initial commit: GWÖ-Antragsprüfer v1.0
Features:
- GWÖ-Matrix 2.0 Analyse für NRW-Landtagsanträge
- Verbesserungsvorschläge im Redline-Format (Original/Vorschlag/Begründung)
- Wahlprogramm- und Parteiprogrammtreue-Bewertung
- Landtag-Suche via OPAL-API
- Tag-Wolke mit Multi-Select Filter
- Partei-Filter mit Durchschnittswerten
- PDF-Report-Generierung
- Security Headers (CSP, X-Frame-Options, etc.)
- Persistente SQLite-DB via Docker Volumes

Tech Stack:
- FastAPI + Jinja2
- Qwen LLM via DashScope API
- SQLite + aiosqlite
- WeasyPrint für PDF
- Docker Compose mit Traefik
2026-03-28 22:30:24 +01:00