Commit Graph

100 Commits

Author SHA1 Message Date
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
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
2b2a363127 #62 Phase 2: Pivot nginx + docs.toppyr.de/gwoe-antragspruefer/
caddy-gitea-pages verworfen — dessen URL-Schema ({owner}.{domain}/{repo})
passt nicht für Single-Project-Hosting ohne DNS-Wildcards auf Sub-Sub-
Domains. Stattdessen simples nginx:alpine mit statischem Volume-Mount.

URL: https://docs.toppyr.de/gwoe-antragspruefer/

Der nginx-Container mounted docs-site/ nach
/usr/share/nginx/html/gwoe-antragspruefer/ — Traefik routet alles auf
Host docs.toppyr.de an den Container, nginx served den Pfad 1:1.
Skaliert für weitere Repos: einfach ein zweites Volume-Mount für
/usr/share/nginx/html/anderes-repo/ einrichten.

SSL: Traefik, nicht nginx/caddy.
DNS: *.toppyr.de Wildcard deckt docs.toppyr.de ab.

Update-Workflow:
  cd webapp && mkdocs build
  scp -r site/* vserver:/opt/gwoe-antragspruefer/docs-site/

Caddyfile.docs entfernt (war caddy-gitea-pages-spezifisch).

Refs: #62
2026-04-10 09:47:06 +02:00
Dotty Dotter
c26c2e7e94 caddy-gitea-pages: Caddyfile mit gitea-Modul + default_owner/repo/branch 2026-04-10 09:45:12 +02:00
Dotty Dotter
52e55e9cca Fix docs domain: gwoe-docs.toppyr.de (Wildcard *.toppyr.de matcht nur 2nd-Level) 2026-04-10 09:43:47 +02:00
Dotty Dotter
1e438a7baa #62 Phase 2: mkdocs + caddy-gitea-pages Hosting auf docs.gwoe.toppyr.de
mkdocs Material-Theme konfiguriert (mkdocs.yml). Build-Output wird in
den gh-pages-Branch gepusht, von dort served caddy-gitea-pages den
statischen Content als separater Container unter docs.gwoe.toppyr.de.

Neuer docker-compose-Service gwoe-docs:
- Image: ghcr.io/d7z-project/caddy-gitea-pages:nightly
- Liest automatisch aus dem gh-pages-Branch via Gitea-API
- Traefik-Labels für docs.gwoe.toppyr.de (SSL via Let's Encrypt)
- Token via GITEA_TOKEN in .env (bereits auf dem Server hinterlegt)

Wildcard-DNS *.toppyr.de zeigt bereits auf den VServer — kein
DNS-Eingriff nötig, Traefik + Let's Encrypt erledigen den Rest.

Doku-Update-Workflow:
  1. ADR oder docs/ editieren
  2. `mkdocs build` lokal
  3. `git checkout gh-pages && cp -r site/* . && git add -A && git commit && git push`
  4. caddy-gitea-pages refreshed automatisch

.gitignore: site/ ausgeschlossen (Build-Artefakt).

Refs: #62 (Phase 2)
2026-04-10 09:42:44 +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
45379a2639 #62 Phase 1+3: ADRs + Doku-Struktur in webapp/docs/
Architektur-Entscheidung aus Issue #62: Diátaxis-Framework für Doku-
Pflege ohne Drift. Pflege im Repo, ADRs immutable, Stale-Snapshots
explizit als Archiv markiert.

Phase 1 — Architecture Decision Records:

- docs/README.md — Diátaxis-Index, Erklärung was wo dokumentiert wird
- docs/adr/README.md — ADR-Workflow + Index
- docs/adr/template.md — Vorlage für neue ADRs
- docs/adr/0001-llm-citation-binding.md — Issue #60 Doppel-Fix-Story
  (A=ENUM-Anker, B=server-seitige Rekonstruktion, warum Option C verworfen)
- docs/adr/0002-adapter-architecture.md — ParlamentAdapter-Basisklasse
  + Registry, Klassen vs. Strategy vs. Modul-pro-Adapter
- docs/adr/0003-citation-property-tests.md — Sub-D Strategie, warum
  Property-Test gegen echte PDFs statt Schema-Tests oder Online-Verify
- docs/adr/0004-deployment-workflow.md — Docker-Compose + Volumes
  Standard-Workflow + SN-XML-Sonderpfad + Container-UTC-Gotcha

Phase 3 — Stale Doku archiviert:

- DOKUMENTATION.md (24.März, Skript-Architektur vor Webapp-Migrate)
  → docs/archive/DOKUMENTATION-2026-03-24.md
- STATUS-2026-03-28.md (Tagesstand-Snapshot)
  → docs/archive/STATUS-2026-03-28.md
- README.md (28.März, listet nur NRW-Adapter, vor 16 weiteren BLs)
  → docs/archive/README-2026-03-28.md
- docs/archive/README.md erklärt warum die Files da sind und warum
  niemand sie überschreiben oder ersetzen sollte

Plus neue Top-Level-README.md im Project-Root (außerhalb git, da
project-root kein Repo ist) als Folder-Index für den User.

CLAUDE.md ergänzt um Doku-Sektion mit Verweis auf docs/adr/.

Phase 2 (mkdocs Setup) folgt separat — braucht eine Docker-Image-
Erweiterung, die ich nicht autark einrollen will ohne Decision.

Tests: 194/194 grün (keine Code-Änderung).

Refs: #62
2026-04-10 01:38:03 +02:00
Dotty Dotter
4ec6190416 #47 PDF Zitat-Highlighting via PyMuPDF Single-Page-Render
Klick auf eine Zitat-Quelle im Report öffnet jetzt eine 1-Seiten-PDF-
Variante des Wahlprogramms mit gelb markiertem Snippet, statt nur zum
Page-Anchor zu springen und den Leser selbst suchen zu lassen.

Implementation:

embeddings.render_highlighted_page(programm_id, seite, query)
- Validiert programm_id gegen PROGRAMME (Path-Traversal-Schutz)
- Lädt das volle Wahlprogramm-PDF, extrahiert via insert_pdf nur die
  angeforderte Seite in einen neuen Document → kleinere Response
- search_for(query[:200]) → Bounding-Boxes aller Treffer
- Fallback: 5-Wort-Anker wenn Volltext-Match leer (LLM-Truncation,
  identisch zu find_chunk_for_text/Sub-D-Logik)
- add_highlight_annot mit gelber stroke-Color (1.0, 0.93, 0.0)
- Returns serialisierte PDF-Bytes oder None

embeddings._chunk_pdf_url
- Wenn chunk["text"] vorhanden: emittiert /api/wahlprogramm-cite-URL
  mit pid=, seite=, q=urlencoded(text[:200])
- Sonst: alter statischer /static/referenzen/X.pdf#page=N (Pre-#47
  rückwärts-kompatibel)
- text wird auf 200 Zeichen abgeschnitten, sonst blasen
  500-Zeichen-Snippets jedes Assessment-JSON auf

main.py /api/wahlprogramm-cite Endpoint
- Validiert pid gegen PROGRAMME registry
- seite: 1 ≤ n ≤ 2000
- Response: application/pdf, Cache-Control max-age=86400
- 404 bei unknown pid oder fehlendem PDF, 400 bei seite out of range

Reconstruct-Pipeline (Issue #60 Option B) zieht das automatisch durch:
reconstruct_zitate ruft _chunk_pdf_url(matched_chunk) auf, der jetzt
bevorzugt die Cite-URL emittiert. Keine Änderung an reconstruct_zitate
selbst nötig.

Tests: 194/194 grün (185 + 9 neue):

- TestChunkPdfUrl: 4 Cases (cite vs static, unknown prog, 200-char-truncate)
- TestRenderHighlightedPage: 5 Cases (unknown pid, invalid seite, valid
  render, empty query, query-not-found-falls-back-zu-leerem-Highlight)
- Plus Bridge im Test-Stub: pymupdf-as-fitz Shim falls eine
  third-party "fitz" das Pkg shadowt (kommt auf älteren Dev-Setups vor)

Refs: #47
2026-04-10 01:09:45 +02:00