Commit Graph

108 Commits

Author SHA1 Message Date
Dotty Dotter
5d2a0338ee Kommentar-Sichtbarkeit: Öffentlich/Angemeldete/Nur ich + Badges + Server-Filter 2026-04-10 22:40:27 +02:00
Dotty Dotter
ad97a76824 Hamburger-Menü: Auswertungen/Quellen/Methodik/Auth als Dropdown, primäre Tabs bleiben 2026-04-10 22:29:55 +02:00
Dotty Dotter
e5d4ce2553 Merkliste-Tab + Kopfzeile einheitliche Schriftgröße (0.9rem) 2026-04-10 22:25:52 +02:00
Dotty Dotter
e1deec8b53 Merkliste: eigener Tab mit Bookmark-Übersicht, klickbar zum Detail 2026-04-10 22:24:43 +02:00
Dotty Dotter
4b40de4e93 #94 Bookmarks + Kommentare: DB-Schema, API, UI
DB (database.py):
- bookmarks-Tabelle (user_id + drucksache, toggle)
- comments-Tabelle (user_id, user_name, drucksache, text, visibility)
- Functions: toggle_bookmark, get_bookmarks, add_comment, get_comments, delete_comment

API (main.py):
- POST /api/bookmark (toggle, Auth-pflichtig)
- GET /api/bookmarks (User-Bookmarks)
- POST /api/comment (Auth-pflichtig, max 2000 Zeichen)
- GET /api/comments?drucksache= (öffentlich)
- DELETE /api/comment/{id} (nur eigene, Auth-pflichtig)

UI (index.html):
- Bookmark-Button ("🔖 Merken" / " Gemerkt") im Detail-Footer
- Kommentar-Bereich: Liste + Eingabefeld + Senden-Button
- Kommentare laden automatisch beim Detail-Öffnen
- Eigene Kommentare löschbar (✕ Button)
- Ohne Login: "Anmelden um zu kommentieren"

Gruppen-Sichtbarkeit (visibility) ist vorbereitet aber noch nicht
im UI exponiert — kommt als separater Schritt wenn Keycloak-Gruppen
konfiguriert sind.

Tests: 206 passed.

Refs: #94
2026-04-10 22:19:46 +02:00
Dotty Dotter
5ec0b08648 Fix: normalizePartei als globale Funktion (war in updateStats scoped → ReferenceError in showDetail) 2026-04-10 22:15:13 +02:00
Dotty Dotter
b851173e6d UI-Polish: 6 Fixes aus visuellem Review
1. AfD/AFD Duplikat in Partei-Stats: normalizePartei() client-seitig
2. Antragsteller:in Labels: aus item.fraktionen ableiten wenn
   istAntragsteller null (LLM liefert es inconsistent)
3. Überlange Titel in Liste: auf 80 Zeichen + Ellipsis gekürzt
4. Methodik-Text: "verworfen" → "verifiziert / nicht wörtlich markiert"
5. Bewertungsdatum im Header (neben Drucksache-Nr statt nur im Footer)
6. Index-Button: Schloss-Icon + Tooltip "Erfordert Anmeldung"
2026-04-10 22:13:30 +02:00
Dotty Dotter
f1a7da8544 Hybrid-Zitate: verified/unverified statt drop + UI-Labels
reconstruct_zitate droppt Zitate nicht mehr bei No-Match, sondern
markiert sie als verified=false. Das ist ehrlicher: paraphrasierte
Zitate sind wertvoller Kontext, sie brauchen nur ein visuelles
Unterscheidungsmerkmal.

UI:
- Verifizierte Zitate: grüner solid Border, "✓ verifiziert"
- Paraphrasierte Zitate: gelber dashed Border, "~ paraphrasiert
  (nicht wörtlich im Programm)"
- Warning-Text: "Zu diesem Themenkomplex konnten keine konkreten
  Formulierungen im Wahlprogramm gefunden werden"
- Antragsteller:in / Landesregierung als farbige Badges

Zitat-Model: neues Optional[bool] Feld "verified".

Tests: 206 passed (test_drops angepasst auf neues Verhalten).
2026-04-10 21:45:36 +02:00
Dotty Dotter
9c162d14ac UI: Warning-Text verbessert + Antragsteller:in/Landesregierung Labels als Badges 2026-04-10 21:41:15 +02:00
Dotty Dotter
49c1b92753 Fix: JWT aud=account bei Keycloak Public Clients — prüfe azp statt aud 2026-04-10 21:32:08 +02:00
Dotty Dotter
f56c2af5cd Fix: Auth-Callback setzt Cookie via HTML-Response statt RedirectResponse 2026-04-10 21:27:32 +02:00
Dotty Dotter
0d0c06106a Auth-UI: Logout-Button + Re-Analyze-Feedback + Uhrzeit beim Bewertungsdatum 2026-04-10 21:24:07 +02:00
Dotty Dotter
9195d976bc Fix: httpx import in auth callback 2026-04-10 21:19:31 +02:00
Dotty Dotter
c3bcf1501d Auth: OIDC Code→Token Exchange Callback + Cookie-basiertes Login 2026-04-10 21:18:10 +02:00
Dotty Dotter
4c8b180383 Fix: Keycloak redirect_uri http→https (Traefik TLS-Termination) 2026-04-10 21:16:15 +02:00
Dotty Dotter
f728388286 #97 Neu bewerten: manueller Re-Analyse-Button + Bewertungsdatum
Fußzeile unter jedem Assessment-Detail jetzt mit:
- Bewertungsdatum ("Bewertet am DD.MM.YYYY") aus updated_at
- Quelle + Modell (batch-reanalyze / webapp, qwen-plus)
- "Neu bewerten"-Button (Auth-pflichtig, ausgegraut ohne Login)

Flow: Klick → DELETE /api/assessment/delete → POST /api/analyze-drucksache
→ Queue → pollAnalysis → Detail neu laden

Neuer DELETE-Endpoint /api/assessment/delete mit require_auth.

API-Response erweitert um updatedAt, source, model für beide
Endpoints (list + single assessment).

Tests: 206 passed.

Refs: #97
2026-04-10 21:10:33 +02:00
Dotty Dotter
790fe1a121 CDU Grundsatzprogramm: korruptes 2007er ersetzt durch echtes 2024er (82 Seiten) 2026-04-10 20:25:56 +02:00
Dotty Dotter
660498e8e3 LINKE Bremen (78p via Wayback) + CDU Hessen Langfassung (164p) + AfD SL registriert 2026-04-10 20:22:50 +02:00
Dotty Dotter
78f3e4e9f0 Wahlprogramme HB/HE/SN + AfD SL: 15 neue Programme registriert
Bremen WP 21 (2023): SPD, CDU, GRÜNE — 3 PDFs
  (AfD Bremen + LINKE Bremen nicht als PDF downloadbar)
Hessen WP 21 (2023): CDU, AfD, SPD, GRÜNE, FDP — 5 PDFs
Sachsen WP 8 (2024): CDU, AfD, BSW, SPD, LINKE, GRÜNE — 6 PDFs
Saarland: AfD SL 2022 ("Heimat ist wählbar") — aus real3d-flipbook
  extrahiert (pdfUrl in data-flipbook-options). 102 Seiten.

Total: 84 Programme registriert. Indexierung erfolgt nach Deploy.
2026-04-10 20:14:22 +02:00
Dotty Dotter
7ed2cca15f Tests: 8 Endpoint-Smoke-Tests (queue, auth, programme, health) 2026-04-10 20:09:34 +02:00
Dotty Dotter
3b6ecacc1e Tuning: min_similarity 0.45→0.35 + Anker 5→4 Wörter — mehr Chunks + weniger Drops 2026-04-10 20:06:35 +02:00
Dotty Dotter
14140571d8 Fix: CDU-PDF AssertionError Fallback + Kopfzeile vereinheitlicht + Fehler-Debug 2026-04-10 20:05:28 +02:00
Dotty Dotter
916b0ca643 Debug: JS-Fehler anzeigen + docker-compose version entfernt 2026-04-10 19:55:08 +02:00
Dotty Dotter
d75e9441a3 Quellen-Seite: Programme nach Bundesland gruppiert statt einer langen Liste 2026-04-10 19:10:18 +02:00
Dotty Dotter
9e341a695f Tests: 5 queue-Tests (enqueue, position, overflow, status) 2026-04-10 19:08:59 +02:00
Dotty Dotter
ee08cb0c29 Quellen-Seite: PDF-Thumbnails der ersten Seite + Thumbnail-API-Endpoint 2026-04-10 18:40:13 +02:00
Dotty Dotter
11e4da0bf3 Wahlprogramme BY/NI/SL: 11 PDFs registriert + Linke-Grundsatzprogramm
Bayern WP 19 (2023): CSU, GRÜNE, FW, AfD, SPD — 5 PDFs
Niedersachsen WP 19 (2022): SPD, CDU, GRÜNE, AfD — 4 PDFs
Saarland WP 17 (2022): SPD, CDU — 2 PDFs (AfD SL nicht auffindbar)

Plus: DIE LINKE Erfurter Programm 2011 (111 Chunks indexiert)
Plus: AfD Grundsatzprogramm 2016 (128 Chunks, vorheriger Commit)

Alle PDFs verifiziert: korrekte Seitenzahlen, keine HTML-Wrapper,
Parteiname und Wahljahr im Titel korrekt. Quellen: offizielle
Partei-Websites, Wayback Machine, originalsozial.de.

Indexierung erfolgt nach Deploy im Container.
2026-04-10 18:27:38 +02:00
Dotty Dotter
1f53ca5a25 #63: Linke Erfurter Programm 2011 + AfD registriert — alle 6 Grundsatzprogramme komplett 2026-04-10 18:23:20 +02:00
Dotty Dotter
b6160cc6cb #31/#34/#35: BY, NI, SL auf aktiv=True — alle 17 Parlamente jetzt im UI 2026-04-10 17:43:32 +02:00
Dotty Dotter
521d940611 #22 NI: Deduplizierung (Server liefert manche Treffer doppelt) 2026-04-10 17:40:46 +02:00
Dotty Dotter
edcb4e9c76 #22 NI-Adapter: PortalaAdapter mit JSON-in-Comment-Parsing
Niedersachsen (NILAS) nutzt denselben portala/eUI-Stack wie LSA/BE/BB/RP,
aber mit einem dritten Hit-Format: JSON-Objekte in HTML-Kommentaren
(statt Perl-Dumps oder HTML-Card-Elements). Reverse-engineered aus
HAR-Capture www.nilas.niedersachsen.de.har.

Neuer dritter Parsing-Pfad in PortalaAdapter._parse_hit_list_html:
Auto-Detection via "<!-- {" + "WEV" im HTML → _parse_hit_list_json_comments.

Feld-Mapping (NI JSON-in-Comment):
- WEV01[0].main → Titel
- WEV03[0].main → Typ
- WEV05[0].main → Metadata (Urheber + DD.MM.YYYY + "Drucksache XX/YYYY")
- WEV05[0].1 oder WEV08[0].1 → PDF-URL

ADAPTERS-Eintrag:
- bundesland="NI", db_id="lns.lissh", wahlperiode=19,
  portala_path="/portala", document_type="Antrag"

Tests: 201 passed.

Refs: #22, #34 (UI-Aktivierung folgt separat)
2026-04-10 17:39:18 +02:00
Dotty Dotter
4565a5cf0c #63 teilweise: AfD-Grundsatzprogramm 2016 registriert + PDF (96 Seiten, via Wayback Machine) 2026-04-10 17:30:28 +02:00
Dotty Dotter
6a433e9217 #44 Batch-Analyse: POST /api/batch-analyze
Neuer Endpoint der die neuesten ungeprüften Drucksachen eines BL
automatisch sucht, herunterlädt und in die Queue (#95) einreiht:

POST /api/batch-analyze
  bundesland=NRW  (Pflicht)
  limit=10        (1-100, default 10)

Flow:
1. adapter.search("", limit=limit*3) holt neueste Drucksachen
2. Pro Drucksache: check ob schon bewertet → skip
3. download_text → enqueue(run_drucksache_analysis)
4. Queue verarbeitet seriell mit 10s Pause (DashScope-freundlich)

Response:
{
  "status": "batch_enqueued",
  "enqueued": 7,
  "skipped_existing": 3,
  "jobs": [{"drucksache": "18/...", "title": "...", "queue_position": 1}, ...]
}

Rate-limited auf 3/min. Erfordert Auth (#43).
Bei voller Queue: enqueued nur soweit Platz, kein Error.

Tests: 201 passed.

Refs: #44, #95 (Queue-Basis)
2026-04-10 17:26:05 +02:00
Dotty Dotter
289d37a84b #95 Job-Queue: SQLite-backed asyncio Worker mit Backpressure
FIFO-Queue für Analyse-Jobs — ersetzt FastAPI BackgroundTasks:

app/queue.py:
- asyncio.Queue mit MAX_QUEUE_SIZE=50
- Einzelner Worker-Coroutine (Concurrency=1, DashScope-freundlich)
- MIN_PAUSE_SECONDS=10 zwischen Jobs
- Exponentielles Backoff bei Serien-Fehlern (15s → 5min)
- get_queue_status() für den Status-Endpoint
- QueueFullError → HTTP 429 + Retry-After Header
- start_worker() als FastAPI-Startup-Task
- re_enqueue_pending() markiert Crash-Überlebende als 'stale'

main.py:
- POST /api/analyze-drucksache nutzt queue.enqueue() statt
  background_tasks.add_task()
- Response enthält queue_position
- GET /api/queue/status zeigt pending, max_size, processed,
  estimated_wait_seconds, worker_running
- Worker wird bei app.startup() gestartet

Tests: 201 passed, 5 skipped.

Refs: #95, #44 (Batch baut auf Queue auf)
2026-04-10 17:24:34 +02:00
Dotty Dotter
1a82f8294c #57 Security: print() → logger.exception für alle Module
Befund #4 aus dem Security-Audit (PII/LLM-Content im Container-Log):
Die letzten 10 print()-Aufrufe in app/{report,embeddings,parlamente}.py
durch strukturiertes Logging (logger.warning/exception/info) ersetzt.

Betroffen:
- report.py: 2× print in _append_original_antrag → logger.exception
- embeddings.py: 3× print in index_programm → logger.warning/info/exception
- parlamente.py: 5× print in NRWAdapter → logger.error/exception

logger.exception statt print+traceback: Stack-Trace wird automatisch
angehängt, ohne den LLM-Content oder Antrags-Details als Volltext zu
leaken (nur die Drucksache-ID als Kontext-Parameter).

Audit-Status nach diesem Commit: alle 7 adressierbaren Befunde aus #57
sind gefixt (1 Rate-Limit, 2/6 XSS/XXE, 3 Path-Traversal, 4 PII-Log,
5 CSRF via Auth, 7 Search-DoS). Befund 8 (Secrets als ENV) ist
akzeptiertes Risiko für Single-Server-Docker.

Tests: 201 passed, 5 skipped.
2026-04-10 17:05:12 +02:00
Dotty Dotter
0870e8a910 #96: Methodik-Seite um konkretes Bewertungsbeispiel ergänzt 2026-04-10 16:34:44 +02:00
Dotty Dotter
65d7dfeb88 Docs: Keycloak-Setup How-to + ADR-Index aktualisiert 2026-04-10 16:33:52 +02:00
Dotty Dotter
f0f1c39911 Docs: Feld-Mapping-Tabelle pro Adapter + ADR 0005 + Auth-Tests
Adapter-Capabilities-Matrix (#93) erweitert um detailliertes Feld-
Mapping: Pro Adapter welches API-/HTML-/JSON-Feld zu welchem
Drucksache-Feld wird (title, datum, fraktionen, drucksache, link, typ)
mit konkreten Beispielwerten. 12 Adapter-Sektionen.

ADR 0005: Keycloak SSO mit Dev-Bypass — dokumentiert die Entscheidung
für Read/Write-Trennung (GET offen, POST mit JWT) und den Dev-Modus
(Auth deaktiviert wenn KEYCLOAK_URL nicht gesetzt).

Auth-Tests: 7 neue Tests für Token-Extraction, Auth-Enabled-Detection,
_pick_best_title (letztere skipped wenn slowapi nicht installiert).

201 passed, 5 skipped.
2026-04-10 16:29:28 +02:00
Dotty Dotter
8bd311dbc8 Tests für auth.py: Token-Extraction, Auth-Enabled-Detection, _pick_best_title 2026-04-10 16:25:51 +02:00
Dotty Dotter
07507de24a #96 Methodik-/Transparenz-Seite unter /methodik
Neue Seite für Endnutzer-Transparenz über die Bewertungsmethodik:

- GWÖ-Matrix 2.0 Erklärung mit interaktivem 5×5-Grid
- Analyse-Pipeline als 5-Schritt-Visualisierung (Download → Embedding
  → LLM → Verifikation → Darstellung)
- Wahlprogramm-Vergleich: Erklärung des Retrieval + Top-K + Verifikation
- Qualitätssicherung: Sub-D Property-Tests, server-seitige Quellen-
  Rekonstruktion, automatische Neu-Analyse
- Einschränkungen: KI-Bias, keine juristische Bewertung, nur indexierte
  Programme, kein Abstimmungsverhalten
- Datenquellen: dynamische Tabelle aller angebundenen Parlamente aus
  ADAPTERS + bundeslaender.py
- Technische Details aufklappbar (details/summary) für Interessierte,
  Haupttext verständlich für Nicht-Techniker
- Links zu Quellen-Seite, Adapter-Matrix, ADRs

In Hauptnavigation verlinkt (neben Quellen + Auswertungen).
Template-Variablen: adapter_count, model_name, programme_count,
chunk_count, bundeslaender — alles dynamisch aus dem Backend.

Tests: 194/194 grün.

Refs: #96
2026-04-10 16:14:38 +02:00
Dotty Dotter
5ea507b771 Fix: PFLICHT-FRAKTIONEN = alle Landtagsfraktionen der WP, nicht nur Antragsteller+Regierung 2026-04-10 16:08:04 +02:00
Dotty Dotter
038ebd6447 Fix: NRW-Titel + Regierungsfraktionen-Pflicht im LLM-Prompt
Bug 1 — NRW-Titel "Drucksache XX/YYYYY":
NRW's get_document machte nur HEAD-Request auf die PDF-URL und gab
title="Drucksache 18/18085" zurück — keinen echten Titel. Fix: nutzt
jetzt search(drucksache) um den echten Eintrag von OPAL zu holen.
Fallback: leerer Titel statt generischer, damit der LLM-Titel nicht
überschrieben wird. Plus _pick_best_title Helper: doc.title nur
übernehmen wenn es ein echter Titel ist (nicht "Drucksache XX").

Bug 2 — Nur Antragsteller im Passungsprofil, keine Regierungsfraktionen:
Der LLM ignorierte die "UND Regierungsfraktionen"-Anweisung im Prompt.
Fix: explizite PFLICHT-FRAKTIONEN-Zeile im User-Prompt:
"Du MUSST folgende Fraktionen in wahlprogrammScores bewerten: SPD, CDU, GRÜNE"
(dedupliziert aus fraktionen + regierungsfraktionen).

Tests: 194/194 grün.
Batch-Re-Analyse muss nochmal laufen mit den Fixes (21 bereits fertig,
15 noch offen — werden alle erneut benötigt weil die Titel/Fraktionen
in den neuen Assessments falsch sind).
2026-04-10 16:05:57 +02:00
Dotty Dotter
303b30f6dd Fix SyntaxError: user=Depends nach Form-Params (Python positional-after-default) 2026-04-10 14:30:54 +02:00
Dotty Dotter
7159240f49 #43 Keycloak SSO: JWT-Middleware + UI-Guiding
Auth-Schicht vorbereitet — Dev-Modus (KEYCLOAK_URL leer) lässt alles
durch, Prod-Modus (ENV gesetzt) validiert JWT gegen Keycloak-JWKS.

Backend (app/auth.py):
- JWKS-Cache mit 1h TTL (async httpx fetch)
- get_current_user: Optional, gibt User-Dict oder None
- require_auth: Pflicht, gibt User-Dict oder HTTP 401
- keycloak_login_url: Baut die OIDC-Login-URL
- _is_auth_enabled: prüft ob alle 3 ENV-Vars gesetzt sind

Abgesicherte POST-Endpoints:
- POST /analyze → Depends(require_auth)
- POST /api/analyze-drucksache → Depends(require_auth)
- POST /api/programme/index → Depends(require_auth)

Neue Endpoints:
- GET /api/auth/me → {authenticated, sub, email, name, roles} oder {authenticated: false}
- GET /api/auth/login-url → {enabled, url} für Keycloak-Redirect

Frontend (index.html):
- initAuth() beim DOMContentLoaded → prüft /api/auth/me
- "Anmelden"-Button im Header (neben "Quellen")
- "Jetzt prüfen"-Button: disabled + Tooltip "Nur nach Anmeldung
  verfügbar" wenn nicht eingeloggt; aktiv wenn eingeloggt
- currentUser-State steuert Button-Zustände

Dev-Modus: Solange KEYCLOAK_URL nicht gesetzt ist (lokale Dev, aktueller
Prod-Stand), sind alle Endpoints offen wie bisher. Kein Breaking Change.

Dependency: python-jose[cryptography]>=3.3.0 in requirements.txt.

Tests: 194/194 grün (auth.py hat keine Seiteneffekte im Import).

Refs: #43
2026-04-10 14:28:57 +02:00
Dotty Dotter
ea9479dc81 #62: API-Reference + Datenmodelle + Embeddings-Pipeline (mkdocstrings) 2026-04-10 14:14:15 +02:00
Dotty Dotter
59994fc5e3 #93 Vergleichsmatrix: Adapter-Capabilities pro Bundesland 2026-04-10 14:09:42 +02:00
Dotty Dotter
a821c19202 #47: Auto-Re-Analyse bei nicht-verifizierbaren Zitaten
Statt eine Nachricht "Textstelle nicht auffindbar" zu zeigen (was User
zurecht als Quatsch bezeichnet hat), erkennt der Cite-Endpoint jetzt
halluzinierte Zitate und triggert automatisch eine Re-Analyse:

Flow:
1. User klickt auf Zitat-Link
2. render_highlighted_page gibt (pdf, page, highlighted=False) zurück
3. Endpoint prüft: ds+bl Parameter vorhanden? Assessment in DB?
4. → Löscht altes Assessment, startet Re-Analyse als Background-Task
5. → Zeigt HTML-Warte-Seite mit Spinner und "Wird neu analysiert..."
6. → Auto-Redirect nach 15s zurück zum Assessment

Das neue Assessment hat durch reconstruct_zitate verifizierte Zitate,
die dann beim nächsten Klick korrekt gehighlighted werden.

Änderungen:
- embeddings.render_highlighted_page: Return-Typ (bytes, int, bool) —
  drittes Element ist True wenn Highlight gesetzt wurde
- database.delete_assessment: neue Funktion für die Re-Analyse
- main.py cite-Endpoint: akzeptiert ds= und bl= als optionale Params,
  triggert Re-Analyse bei highlighted=False + ds vorhanden
- Frontend: makeCiteUrl reicht ds+bl aus dem Assessment-Kontext mit
  durch in die Cite-URL
- Cache-Control auf 1h reduziert (war 24h, zu aggressiv für
  Assessments die sich durch Re-Analyse ändern)

Tests: 194/194 grün.

Refs: #47, #60
2026-04-10 10:35:01 +02:00
Dotty Dotter
8c27c302f7 #47: Fallback-Notiz bei nicht-auffindbarem Zitat + Year-Suffix-Fix
Wenn search_for den Zitat-Text in keiner Seite findet (Pre-#60
halluzinierte Snippets die nie im PDF standen), wird jetzt statt
stilles Nicht-Highlighting eine sichtbare FreeText-Annotation am
Seitenkopf platziert: "Textstelle nicht im Dokument auffindbar —
das Zitat wurde möglicherweise vom LLM paraphrasiert."

Damit versteht der User sofort warum kein Gelb-Highlighting da ist.
Die echte Lösung ist Re-Analyse mit der neuen Pipeline (reconstruct_
zitate erzeugt verifizierte Zitate), aber bis dahin ist die Notiz
der ehrliche UX-Fallback.

Tests: 194/194 grün.

Refs: #47
2026-04-10 10:22:36 +02:00
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
5a035be20b #47 Fix: Highlighting für falsche Seitenzahlen + Year-Suffix-Matching
Zwei Bugs aus User-Test:

1. "Unbekanntes Wahlprogramm" bei Klick auf Grünes Grundsatzprogramm:
   Pre-#60 Assessments haben halluzinierte Dateinamen wie
   "gruene-grundsatzprogramm-2020.pdf" statt "gruene-grundsatzprogramm.pdf".
   Fix: Year-Suffix-Stripping im Reverse-Lookup (X-YYYY.pdf → X.pdf).

2. "Eine Seite, aber kein Highlighting": Pre-#60 Assessments haben oft
   falsche Seitennummern. search_for findet nichts auf der falschen Seite.
   Fix: wenn die angegebene Seite leer ist, ALLE Seiten durchsuchen und
   die erste mit einem Treffer nehmen. So funktioniert Highlighting auch
   bei halluzinierten Seitenzahlen retroaktiv. Performance: ~50ms pro PDF
   (Grundsatzprogramme haben ~100-160 Seiten), akzeptabel für on-demand.

Tests: 194/194 grün.

Refs: #47
2026-04-10 10:08:02 +02:00