Commit Graph

187 Commits

Author SHA1 Message Date
Dotty Dotter
16ecd31e50 test(#134): report.py Coverage 44.3% → 52.7%
- TestGetScoreColor: alle 5 Branches (>=7 blue, >=4 green, >=2 yellow,
  >=1 orange, sonst red)
- TestGetRatingSymbol: alle 5 Symbole (++, +, ○, −, −−)

Verbleibend (Lines 487-641): WeasyPrint-PDF-Render-Pfade — brauchen
echtes WeasyPrint-Setup, gehoeren in tests/integration/.

Total: 53.2% → 53.4%, 777 → 787 Tests.
2026-04-28 11:13:20 +02:00
Dotty Dotter
ccff2e3e8e test(#134): NRW Protokoll-Parser Coverage 51.7% → 85.1%
parse_protocol mit fitz-Mock (FakeDoc/FakePage):
- simple_angenommen mit ja/nein-Block
- einstimmig direct_broad → ja-Liste fallback
- ueber + so beschlossen → einstimmig-Fallback fuellt ja-Liste mit
  ALLE_FRAKTIONEN_NRW
- skips_anchor_without_drucksache: kein vorheriges 'Drucksache' → skip

compare_to_fixture:
- perfect_match → 1/1
- not_found → 0/1 mit 'NOT FOUND'-Error
- nicht_gesondert_abgestimmt: korrekt nicht-gefunden zaehlt als match
- wrong_ergebnis → error 'ergebnis X != Y'

Total Coverage: 52.1% → 53.2%, 769 → 777 Tests.
2026-04-28 11:11:52 +02:00
Dotty Dotter
58bfc84c41 test(#134): auth.py Coverage 47.1% → 86%
Security-kritisch — jetzt mit umfassender Test-Abdeckung:

- TestKeycloakUrls: issuer + jwks-URL-Konstruktion
- TestGetJwks: Cache-Hit (frisch), Fetch bei leerem Cache, Stale-Cache
  bei HTTP-Fehler (statt komplettem Crash)
- TestValidateToken: kein JWKS → None
- TestGetCurrentUser: Auth-disabled → None, kein Token → None
- TestRequireAuth: Dev-Modus, 401 ohne Token, 401 ungueltig, 200 mit
  validem Token
- TestRequireAdmin: Dev-admin, admin-Rolle, gwoe-admin-Rolle, 403 ohne
  Admin-Rolle
- TestKeycloakAdminToken: keine Credentials → 500, Erfolg → access_token,
  Keycloak-Fehler → 500

Verbleibend: kid-not-found-Pfad, ExpiredSignature/JWTError/ImportError-
Branches im _validate_token-Inneren — wuerden voll gemockten jose-Stack
brauchen.

Total Coverage: 51.2% → 52.1%, 750 → 769 Tests.
2026-04-28 11:10:08 +02:00
Dotty Dotter
3edb1e7501 test(#134): queue Coverage 26.6% → 43.4%
- TestStartWorker: erzeugt CONCURRENCY Tasks, ersetzt aktive nicht
- TestGracefulShutdown:
  - leerer Status → sofortiger Return
  - 'processing'-Job laesst shutdown warten bis er fertig ist
  - Timeout loggt ERROR
- TestEnqueueShuttingDown: enqueue blockiert mit QueueFullError waehrend
  Shutdown

Verbleibend: _worker-Hauptloop (while True, hart zu testen) und
re_enqueue_pending (DB+Adapter-I/O, eigenes Setup noetig).

Total Coverage: 50.8% → 51.2%, 744 → 750 Tests.
2026-04-28 11:08:04 +02:00
Dotty Dotter
8e6f435b94 test(#134): analyzer Coverage 70.1% → 83.1%
- TestContentFingerprint: empty/non-empty cases (Lines 45-48)
- TestGetDefaultBewerter: lazy-Import liefert QwenBewerter (Lines 58-60)
- TestLoadContextFile: existierende + fehlende Datei (Line 71)
- TestGetUserPromptTemplate: alle 4 Platzhalter im Template
- TestGetBundeslandContext:
  - unbekanntes BL → ValueError 'Unbekanntes Bundesland' (Line 263)
  - inaktives BL → ValueError 'nicht aktiv' (Line 265)

Verbleibend (alles im analyze_text LLM-Pfad): Embeddings-Fallback,
reconstruct_zitate-Branch, missing-Programme-Logging — wuerde End-to-End
Mock-Setup brauchen, Aufwand vs. Nutzen unguenstig.

Total: 50.6% → 50.8%, 736 → 744 Tests.
2026-04-28 11:06:24 +02:00
Dotty Dotter
98f7e610b4 test(#134): drucksache_typen Coverage 72.5% → 100%
likely_kleine_anfrage_titel-Heuristik (#149-Folge):
- empty/None Titel false
- 'Welche', 'Warum', 'Was' und andere Frage-Praefixe true
- Frage am Ende mit '?' true
- Nummern-Praefix (NRW '1Welche...', '12. Wie viele...') wird weg-gestrippt
- pure Digits-only Titel: nach Strippen leer → false
- case-insensitive Praefix-Match
- normaler Antrag-Titel ohne Frage → false

Coverage 50.4% → 50.6%, 724 → 736 Tests.
2026-04-28 11:04:31 +02:00
Dotty Dotter
581d1591b8 test(#134): clustering.py Coverage 82.3% → 99.3%
- TestUnionFindRankSwap: rank-Asymmetrie-Branch (Line 69)
- TestLoadAssessmentItems: tmp-DB mit korrekten + kaputten Embeddings,
  bundesland-Filter, vollstaendiges Item-Schema
- TestBuildHierarchySubclusters:
  - max_cluster_size=3 zwingt grossen Cluster zu sub-clustern
  - kleiner Cluster bekommt subclusters=None

Total Coverage: 49.9% → 50.4% (50%-Marke ueberschritten),
718 → 724 Tests.
2026-04-28 11:02:58 +02:00
Dotty Dotter
999926b5f3 test(#134): monitoring.py Coverage 83.2% → 99.3%
- TestSearchAdapterFallbackLogging: erster Query-Versuch failt mit
  Debug-Log, dritter klappt
- TestDailyScanDbUpsertFailure: erster upsert_monitoring_scan crasht,
  zweiter klappt → der Rest des Protokolls wird nicht blockiert,
  ERROR-Log ist da
- TestSendMonitoringDigest:
  - mail_sent=True bei erfolgreichem send_mail
  - mail_sent=False bei SMTP-Fehler, aber kein Crash

Verbleibend: Line 122 (return [] nach drei Fallback-Misses ohne
Exception — schwer ohne Adapter-Mock zu provozieren).

Total Coverage: 49.5% → 49.9%, 714 → 718 Tests.
2026-04-28 11:01:19 +02:00
Dotty Dotter
e69ca1c29d test(#134): mail.py Coverage 88.2% → 100%
- TestSendSync.test_raises_when_smtp_not_configured: leerer host/user
  fuehrt zu RuntimeError
- TestSendSync.test_calls_smtp_ssl_with_settings: smtplib.SMTP_SSL wird
  mit host/port instanziiert, login + send_message aufgerufen
- TestSendMailAsync.test_runs_send_sync_in_executor: send_mail()
  delegiert per loop.run_in_executor an _send_sync
2026-04-28 10:58:03 +02:00
Dotty Dotter
9af74b1a05 test(#134): qwen_bewerter Coverage 86% → 94%
- TestContentFingerprint: leerer/None content → 'len=0', sha1-Praefix
- TestStripMarkdownJsonFences: explizite ```json-Sprache-Erkennung
- TestLazyClientInstantiation:
  - injected client umgeht Lazy-Import
  - kein injected client triggert openai.AsyncOpenAI-Aufruf
    (sys.modules-Stub fuer Lazy-Import-Branch)

Verbleibend uncovered: Line 46 (json-Fence ohne Newline, defensiv aber
unerreichbar weil split('\n', 1) vorher crashen wuerde) und 110-111
(assert/raise-Pfad, im Code als 'unreachable' markiert).
2026-04-28 10:56:56 +02:00
Dotty Dotter
698562b1f5 test(#134): Coverage-Backfill auswertungen + Repositories
- app/auswertungen.py 87.4% → 97.9%
  - TestLoadAssessmentsRobustness: ungueltiges JSON in fraktionen-Spalte
    fallback to []
  - TestAggregateMatrixSkipsBlanks: bundesland-NULL-Eintrag wird ignoriert
  - TestGetWahlperioden: sortierte Liste

- app/repositories/abonnement_repository.py 85.2% → 100%
- app/repositories/antrag_repository.py 87.0% → 98.1%
- app/repositories/bewertung_repository.py 90% → 100%

Pattern fuer Sqlite-Repos: AsyncMock auf database.X-Funktion, dann
pruefen dass die Methode korrekt delegiert (Argumente, Return-Wert).
Trivial wrappers, aber jetzt auditierbar.

Total: 48.7% → 49.2%, 686 → 705 Tests.
2026-04-28 10:54:28 +02:00
Dotty Dotter
b13b46a444 test(#134): Coverage-Backfill drei Module
- app/ingest_votes.py 39.2% → 100%
  - TestDownloadPdf: schreibt Bytes, propagiert HTTP-Fehler
  - TestCli: --supported, kein-arg-error, fehlender PDF-Pfad,
    pdf-Pfad-Run, --url-Download-Pfad, exit-Code 2 bei null Resultaten,
    Errors-Liste im Output
  - DB-Error-Collection in ingest_pdf

- app/wahlprogramme.py 90.7% → 100%
  - TestLoadWahlprogrammText: paged-Datei, Normal-Datei-Fallback,
    fehlende Datei
  - TestSearchWahlprogramm: leere Returns
  - TestFindRelevantQuotes: ValueError bei unbekanntem BL
  - TestFormatQuoteForPrompt: leeres Dict

- app/abgeordnetenwatch.py 95.2% → 97.6%
  - test_rp_pattern_nr_wp_swap: '/538-18.pdf' → '18/538'
  - test_sn_pattern_dok_nr_leg_per_swap: 'dok_nr=2150&leg_per=8' → '8/2150'

Total: 47.59% → 48.69%, 666 → 686 Tests, 0 Failures.
2026-04-28 10:50:26 +02:00
Dotty Dotter
145ad1e8d4 docs(methodik): klarstellen wie System- und User-Prompt zusammenwirken
User-Frage zur Transparenz-Seite: 'Welcher Prompt wird ausgefuehrt?
Der System-Prompt ist deutlich umfangreicher.' Antwort: keiner allein —
beide werden in einem API-Call zusammen gesendet und gemeinsam
ausgewertet.

Auf /methodik#prompts neu vor den details-Bloecken:
- Erklaerung 'in einem einzigen API-Call', beide ins Kontextfenster
- 2-Spalten-Tabelle 'System (Wer/wie)' vs. 'User (Was)'
- Begruendung der Trennung (Caching, Compliance, Wartbarkeit)
- Code-Referenz zu qwen_bewerter.py:83-85 mit messages-Aufbau

Reine UI-Aenderung, keine Code-Logik betroffen.
2026-04-28 09:14:22 +02:00
Dotty Dotter
eb0669d6ac feat(#147): Hover-Tooltips fuer Abkuerzungen auf Antrag-Detail
User-Feedback: '(A)' hinter Partei, 'WP', 'PP' brauchen Erklaerung
fuer Erstleser:innen. Loesung: ausfuehrliche title-Tooltips plus
visuelle Affordanz (cursor:help).

Geaendert:
- v2-badge-antragsteller / -regierung: cursor:help
- v2-score-chip[title]: cursor:help
- (A) → 'A — Antragstellende Fraktion: hat den Antrag eingereicht.'
- (R) → 'R — Regierungsfraktion: traegt die aktuelle Mehrheit im Landtag.'
- WP-Chip: 'WP — Wahlprogramm-Treue (0–10): wie gut passt der Antrag
  zum aktuellen Wahlprogramm? + Begruendung'
- PP-Chip: analog fuer Parteiprogramm-Treue
- Score-Hero: Tooltip mit GWÖ-Score-Definition + Methodik-Verweis
- 'Enth.:' im Abstimmungs-Block: dotted underline + Tooltip 'Enth. —
  Enthaltung: weder Zustimmung noch Ablehnung'

Closes #147
2026-04-28 08:46:27 +02:00
Dotty Dotter
722b073bbd test(#134): wahlprogramm_fetch Coverage 42.8% → 54.4%
8 zusaetzliche Tests:
- TestLockFileRobustness: kaputtes JSON, fehlende Datei, _save_lock-Roundtrip
- TestLoadLinks: missing yaml + empty yaml (gestubbed)
- TestGetMissingProgrammes: leere/gefuellte Eintraege, Bundesland-Filter

yaml ist im Unit-Setup gestubbed; Tests patchen _load_links direkt
statt echte YAML-Parsing zu erzwingen — die echte Datei-Validierung
gehoert in die integration-Suite gegen die produktive links.yaml.
2026-04-28 08:42:29 +02:00
Dotty Dotter
8f3a811a83 test(#134): app/og_card.py Coverage 44% → 100%
10 Tests in test_og_card.py:
- TestCacheKey: deterministisch, aenderungs-empfindlich, 16 Zeichen lang
- TestGetCached: Pfad-Lookup mit/ohne Datei
- TestRenderOgCard: Cache-Hit vs Cache-Miss, URL-Encoding der DS,
  Playwright-Exception → None, cache_dir wird angelegt

Playwright wird ueber sys.modules-Stub eingehaengt, sync_playwright()
liefert einen ContextManager mit gemocktem Browser/Page-Stack — keine
echte Chromium-Installation noetig fuer den lokalen Run.

cache_key/get_cached-Tests waren bisher in test_wahlprogramm_fetch.py
verstreut; bleiben dort als Smoke, das eigentliche Modul-Test-File ist
jetzt test_og_card.py.
2026-04-28 08:40:20 +02:00
Dotty Dotter
50442f203a test(#134): build_pdf_href Coverage 50% → 100%
6 neue Tests in TestBuildPdfHref:
- explizite url wird unveraendert durchgereicht
- ohne url: WAHLPROGRAMME-Lookup ueber quelle-Feld
- ohne Seitenzahl in quelle → leerer href
- Quelle ohne WAHLPROGRAMME-Match → leerer href
- Query nutzt nur die ersten 5 Worte des Zitats
- Komma-Separator 'Titel, S. 17' parst genauso wie ' · S. 17'

app/redline_utils.py jetzt bei 100% Branch-Coverage.
2026-04-28 08:39:05 +02:00
Dotty Dotter
7de4df1fef feat(#126): protokoll_parsers/-Sub-Package + Registry-Pattern + ADR 0009
Architektur-Refactor zur Vorbereitung BL-uebergreifender Parser:

- app/protokoll_parser_nrw.py → app/protokoll_parsers/nrw.py
- app/ingest_votes_nrw.py → app/ingest_votes.py (BL-uebergreifend)
- Neue app/protokoll_parsers/__init__.py mit:
  - PROTOKOLL_PARSERS-Dict (BL-Code → Parser-Funktion, derzeit nur NRW)
  - parse_protocol(bundesland, pdf_path) als BL-uebergreifender Einstieg
  - supported_bundeslaender()-Helper
  - NotImplementedError mit hilfreicher Message bei unbekanntem BL

CLI bekommt --supported-Flag fuer BL-Discovery:
  python -m app.ingest_votes --supported  → 'NRW'

ADR 0009 dokumentiert das Muster (Sub-Package + Funktions-Registry,
analog zu ADR 0002 fuer ParlamentAdapter). Folge-BL bekommen je
eine eigene Datei und einen Eintrag in PROTOKOLL_PARSERS — kein
Refactoring der Bestands-Logik.

Tests:
- 7 neue Tests in test_protokoll_parsers.py fuer Registry und Dispatch
- Bestehende NRW-Tests umbenannt zu test_protokoll_parsers_nrw.py,
  Imports angepasst — keine Verhaltens-Aenderung
- Bestehende Ingest-Tests umbenannt zu test_ingest_votes.py

642 Tests gruen, kein Verhaltens-Drift.
2026-04-28 08:37:31 +02:00
Dotty Dotter
a9f0b61c75 build(#134): Coverage-Schwelle auf realistische Baseline 45%
Vorheriger Wert 60 unerreichbar mit reinen Unit-Tests, weil drei
grosse Bereiche bewusst nicht in der Default-Suite getestet werden:

- app/main.py — FastAPI-Endpoints, lokal via TestClient nur skipped;
  echte Smoke-Tests laufen in Docker-Suite oder integration/.
- app/parlamente.py — 16 Adapter, ~3400 LOC HTTP-Code; tests/integration/
  deckt das via Live-Calls.
- app/queue.py, app/report.py — Async-Worker und PDF-Renderer.

45% spiegelt das tatsaechliche Default-Suite-Coverage wider (46.21% am
2026-04-28), Schwelle steigt mit ergaenzenden Tests automatisch.
2026-04-28 08:07:53 +02:00
Dotty Dotter
7e0f0117e6 feat(#106): UI-Block 'Abstimmungsergebnis' auf Antrag-Detail
Antrag-Detail-Endpoint liest plenum_votes via get_plenum_votes() und
reicht sie an antrag_detail.html durch.

Block rendert pro Plenum-Abstimmung eine Karte:
- Ergebnis (angenommen/abgelehnt/...) farb-kodiert
- 'einstimmig'-Annotation falls gesetzt
- Quelle (Protokoll-ID, mit URL als Tooltip)
- Fraktions-Chips fuer Ja/Nein/Enthaltung

Mehrfach-Abstimmungen einer Drucksache (Ueberweisung + finale
Beschlussfassung) erzeugen mehrere Karten — chronologisch via
parsed_at DESC im Repository sortiert.

Block erscheint nur, wenn Eintraege existieren (kein leerer Header).
2026-04-28 08:04:32 +02:00
Dotty Dotter
e26607854f feat(#106): Ingest-CLI fuer NRW-Plenarprotokolle
app/ingest_votes_nrw.py: Pipeline PDF → protokoll_parser_nrw → DB.

CLI:
  python -m app.ingest_votes_nrw --pdf /pfad/MMP18-119.pdf
  python -m app.ingest_votes_nrw --url https://landtag.nrw.de/.../MMP18-119.pdf
  python -m app.ingest_votes_nrw --pdf x.pdf --protokoll-id MMP18-119 --bundesland NRW

Protokoll-ID wird default aus Datei-Stem abgeleitet (MMP18-119.pdf →
MMP18-119), URL-Mode parst sie aus dem letzten Pfadsegment.

ingest_pdf() ist die programmatische API (auch fuer Folge-Cron, falls
spaeter automatisch Plenarprotokoll-Sammelinges nachgeruestet wird).
Statistik-Dict: parsed/written/skipped_no_drucksache/errors.

6 Tests: Roundtrip, skip-bei-fehlender-Drucksache, default + override
fuer Protokoll-ID, BL-Override (fuer #126-Folge), idempotenter Re-Ingest.
2026-04-28 08:03:18 +02:00
Dotty Dotter
ae3f48be41 feat(#106): plenum_vote_results-Tabelle + Repository
DB-Schema fuer fraktions-aggregierte Plenum-Abstimmungsergebnisse:
- bundesland, drucksache, quelle_protokoll als Compound-PK
  (eine Drucksache kann mehrfach abgestimmt werden — Ausschuss-Empfehlung
  und finale Beschlussfassung leben nebeneinander)
- ergebnis (angenommen/abgelehnt/ueberwiesen/...), einstimmig-Flag
- fraktionen_ja/_nein/_enthaltung als JSON-Arrays
- quelle_protokoll (z.B. 'MMP18-119') + optional quelle_url
- Index auf (bundesland, drucksache) fuer Lookup-Path

Repository-API:
- upsert_plenum_vote(...) idempotent ueber Compound-PK
- get_plenum_votes(bl, drucksache) → Liste, neueste zuerst

7 Tests fuer Roundtrip, einstimmig-Flag, Idempotenz, Multi-Protokoll-Erhalt,
leere Queries, Unicode-Handling von 'GRÜNE'.

Refs #106 — naechster Schritt: Ingest-CLI gegen NRW-PDFs.
2026-04-28 08:01:26 +02:00
Dotty Dotter
d640734641 feat(#106,#134): NRW-Protokoll-Parser v5 ins Repo migriert
Vorher als parser_v5_iteration15.py nur auf Prod-Server, nicht
versionskontrolliert. Jetzt unter app/protokoll_parser_nrw.py
mit klarem Naming-Schema (BL-Suffix, damit Folge-Adapter analog
heissen koennen, vgl. ADR 0002).

Aenderungen am Code:
- from __future__ import annotations (Py3.9-kompatibel fuer 'str | None')
- fitz-Import optional (try/except), damit pure-string-Funktionen
  auch im Stub-conftest funktionieren

30 Tests in test_protokoll_parser_nrw.py (#134 Phase 2):
- normalize_fraktionen: F.D.P., GRÜNE-Aliase, Landesregierung
- _is_empty_phrase: Niemand/Keine/nicht-Mustern
- _parse_vote_block: ja/nein-Extraktion plus Negationen
- find_results: angenommen/abgelehnt, einstimmig (nur ueber-Kind!),
  (neu)-Suffix in Drucksachen-Nrn, Sortierung, Dedup
- resolve_drucksache_for_ueber: Backward-Search mit closest-match

Refs #106 (Abstimmungsverhalten verknuepfen — Vorbereitung fuer DB-Schema)
Refs #126 (BL-uebergreifender Parser — NRW als Referenz-Implementierung)
Refs #134 (Test-Suite Audit — Phase 2)
2026-04-28 02:08:03 +02:00
Dotty Dotter
3262f17458 build(#134): Coverage-Baseline (.coveragerc) + pytest-cov in dev-deps
Phase 3 von #134 / ADR 0007: 60%-Mindestschwelle pro Default-Lauf, mit
show_missing fuer schnelle Lueckenanalyse.

Konfiguration:
- source = app, omits Hilfs-Skripte (reindex_embeddings, sync_abgeordnetenwatch)
- exclude_lines: __repr__, NotImplementedError, __main__-Block,
  TYPE_CHECKING, Ellipsis-Stubs
- htmlcov-Ordner via .gitignore ausgeschlossen

Aufruf:
  pytest --cov=app --cov-report=term-missing
  pytest --cov=app --cov-report=html  # detaillierte HTML-Ansicht in htmlcov/

ADR 0007 (Test-Taxonomie) erklaert das Gesamtschema.
2026-04-28 02:05:39 +02:00
Dotty Dotter
7e20f910fe docs(#134): ADR 0007 — Test-Taxonomie
Phase 3 von #134: Klassifizierung Unit / Integration / E2E / Property / Smoke
mit Markern, Latenz-Budgets, Verzeichnis-Konventionen und Lauf-Befehlen.

Index aktualisiert (0007 zwischen 0006 und 0008 eingefuegt — ADRs sind
chronologisch, nicht numerisch sortiert).
2026-04-28 02:04:24 +02:00
Dotty Dotter
3a8c03db6c test(#134): test_wahlperioden.py — Datum→WP-Mapping
12 Tests fuer app/wahlperioden.py:
- aktuelle WP fuer Datum >= wahlperiode_start
- Vorgaenger-WP fuer Datum davor
- None bei unbekanntem BL
- Empty/None Datum → aktuelle WP (Default)
- Boundary-Tag (= start) gehoert zur neuen WP
- ISO-lexikographische Vergleichsannahme stimmt fuer alle BL
- all_wahlperioden() enthaelt aktuelle + Vorgaenger pro BL, keine Duplikate
2026-04-28 02:02:40 +02:00
Dotty Dotter
d2fc11f21b test(#134): test_rss.py — Atom-Feed-Validitaet, Filter, ETag, Limits
14 Tests fuer /api/feed.xml (#125):
- Atom-1.0 well-formed, Pflicht-Elemente vorhanden
- Entries nach updated_at DESC sortiert
- HTML-Escaping fuer Sonderzeichen (& in Titeln)
- Partei- und Bundesland-Filter wirken
- ETag-Header + 304 Not Modified
- Limit clamped auf [1, 200]
- Leere DB liefert gueltigen, aber leeren Feed
- CORS-Header gesetzt
- Self-URL enthaelt Filter-Parameter

Lokal skipped wenn app.main nicht importierbar (gleiche Konvention wie
test_endpoints_smoke.py); laeuft in Containern mit voller Deps.
2026-04-28 02:01:01 +02:00
Dotty Dotter
5559f42c92 feat(#138): SHA-Lock-File schuetzt vor stillem PDF-Tausch
Hintergrund: abgeordnetenwatch hatte das CDU-BE-2023-PDF unter dem alten
Slug-Namen gegen das CDU-BE-2026-Wahlprogramm ersetzt — ohne den
Datei-Namen zu aendern. Die Embedding-Indexierung haette das anachronistische
Programm uebernommen, ohne dass es jemand bemerkt.

Loesung: app/wahlprogramm-shas.lock.json pinnt nach erstem erfolgreichen
Download den SHA-256 jedes Programmes. Spaetere Aufrufe von
fetch_and_verify() vergleichen den Server-Inhalt gegen den Lock; bei
Abweichung wird abgebrochen mit klarer Fehlermeldung. Nur mit explizitem
Maintainer-Override (--accept-new-sha) wird der Lock aktualisiert.

CLI:
  python -m app.wahlprogramm_fetch --pin-existing
    seedet den Lock einmalig aus den vorhandenen PDFs (52 Eintraege).
  python -m app.wahlprogramm_fetch --fetch BL PARTEI [--accept-new-sha]
    laedt mit Lock-Pruefung; --accept-new-sha bei bewusstem Update.

6 neue Tests in test_wahlprogramm_fetch.py decken den Pferdetausch-
Block, das initiale Pinnen, das Migration-Szenario (PDF da, Lock leer)
und den --accept-new-sha-Override ab.

Closes #138
2026-04-28 01:58:42 +02:00
Dotty Dotter
d0d941444d feat(#144): Matrix-Ueberschriften ausschreiben + Hover-Tooltips
Statt Abkuerzungen (Wuerde, Solid., Liefer., Verwalt., Gesell.) jetzt
voll ausgeschrieben: Menschenwuerde, Solidaritaet, Lieferant:innen,
Verwaltung, Gesellschaft & Natur, etc.

Hover-Tooltip pro Spalte/Zeile mit Erklaerung + Staatsprinzip
(Rechtsstaatsprinzip, Gemeinnutz, Umwelt-Verantwortung, ...).
Matrix-Felder bekommen Tooltip mit Feldname als Vorschau, der
volle Erklaerungstext bleibt im Click-Modal (showField).

Layout: rhdr-Spalte 130/150px, line-height 1.25, min-height 36px,
damit lange Begriffe sauber umbrechen koennen.

Closes #144
2026-04-28 01:53:38 +02:00
Dotty Dotter
0d26cad549 feat(#145): LLM-Prompts auf /methodik als Transparenz-Block
System- und User-Prompt-Template stehen jetzt collapsed unter dem
neuen Abschnitt 'LLM-Prompts'. Der User-Prompt wird auf eine eigene
Konstante USER_PROMPT_TEMPLATE umgestellt und via .format(...) gerendert,
sodass das gleiche Template auf der Methodik-Seite gezeigt werden kann
ohne den f-string-Code zu duplizieren.

Closes #145
2026-04-28 01:50:25 +02:00
Dotty Dotter
5f6bcac282 feat(#146): Fraktionen je Treffer in Landtag-Suche anzeigen
Adapter liefert fraktionen schon mit, das Frontend ignorierte sie bisher.
Treffer-Zeile bekommt jetzt unter dem Titel kleine Teal-Chips fuer jede
einreichende Fraktion (Beispiel: 'CDU SPD' bei kollektiven Antraegen).

Stylistisch konsistent zum Score-Chip-System (color-mix mit ecg-teal),
mono Font, uppercase 10px — bleibt auch bei vielen Fraktionen lesbar.

Closes #146
2026-04-28 01:47:54 +02:00
Dotty Dotter
09c29cac69 fix(#142): SL HTTP 5xx als Fehler raisen statt return []
Symptom: Monitoring-Scan zeigte bei SL seen=0 errors=OK, obwohl der
Umbraco-Backend HTTP 500 zurueckgab. Im _post_search wurde 5xx via
'logger.error + return []' geschluckt, sodass der Monitoring-Layer
die Fehlerursache nicht in monitoring_daily_summary persistierte.

Fix: bei resp.status_code != 200 httpx.HTTPStatusError raisen — das
propagiert durch search() ueber _search_adapter ins outer except in
daily_scan, das den Fehlertext in summary.errors schreibt.

Regression-Test test_search_propagates_http_500.

Closes #142
2026-04-28 01:46:35 +02:00
Dotty Dotter
3921cb91a4 ops(dev): docker-compose.dev.yml + deploy.sh-Branch-Guard
Container-Duplikation fuer v1.x-Entwicklung:
- docker-compose.dev.yml: eigener Container gwoe-antragspruefer-dev,
  Traefik-Host gwoe-dev.toppyr.de, Keycloak-Client gwoe-antragspruefer-dev,
  ohne SMTP (Mail aus Dev = gar nicht), GITEA_FEEDBACK_LABELS=feedback,dev.
- scripts/deploy.sh: Branch-Guard verhindert Prod-Deploy aus main; Prod
  geht nur aus release/1.0 (oder mit --force).

Dev-Server zieht main per Cron alle 5 Minuten und baut neu.
2026-04-28 01:35:30 +02:00
Dotty Dotter
6d587c1f3a feat(feedback): konfigurierbare Issue-Labels via GITEA_FEEDBACK_LABELS
Dev-Container setzt GITEA_FEEDBACK_LABELS=feedback,dev, damit
Feedback-Issues aus gwoe-dev.toppyr.de unterscheidbar markiert werden.
Label-Farben: feedback rot, dev gelb, Sonst grau.

Teil der Container-Duplikation fuer v1.x-Entwicklung.
2026-04-28 01:31:25 +02:00
Dotty Dotter
4b03448e29 fix(feedback): Screenshot scharf + ohne Feedback-UI
- Auflösung: scale = window.devicePixelRatio (statt min:2 cap) — Retina-scharf
- Vor dem html2canvas-Capture werden v2-feedback-{modal,overlay,btn} auf
  display:none gesetzt; finally-Block stellt UI zurueck. Damit ist die
  ausgegraute Modal-Schicht nicht im Bild
- Capture nur des sichtbaren Viewports (width/height/x/y/windowWidth/Height
  explizit), spart Bandbreite + zeigt was der User wirklich sieht
- MAX_W 800 -> 1600, JPEG 0.7 -> 0.85, imageSmoothingQuality high
- requestAnimationFrame x2 vor capture, damit Browser den Reflow vor dem Snap fertig hat
- app_version 1.0.1 -> 1.0.2 (Cache-Buster)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:10:36 +02:00
Dotty Dotter
07bb832c35 ops: GITEA_TOKEN + GITEA_*-Settings im docker-compose.yml durchreichen
Container hatte kein GITEA_TOKEN trotz Eintrag in .env, weil docker-compose.yml
env-Vars explizit listet. 4 neue Eintraege fuer das Feedback-Widget (#149-Folge).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:06:57 +02:00
Dotty Dotter
a8d7b72702 feat(v2): Feedback-Widget mit Audit-Trail + Screenshot + direkter Gitea-Anbindung
- Component v2/components/feedback_widget.html: Button unten links oberhalb der
  Queue, Klick oeffnet Modal mit vorausgefuellten Kontext-Feldern (URL,
  Drucksache, Viewport, User-Agent, letzte 15 Klicks, letzte 10 Console-Errors,
  letzte 5 Page-Loads). Eingaben: Titel, Beschreibung, optional Screenshot
- Audit-Trail-Sammler in localStorage (Ringbuffer 30 Klicks, 10 Errors)
- Screenshot via self-hosted html2canvas 1.4.1 (194 KB unter app/static/v2/lib/)
- Backend POST /api/feedback (rate-limit 5/h):
  - validiert + html-strippt Inputs
  - erstellt Gitea-Issue per API mit Label 'feedback' (Label wird idempotent angelegt)
  - laedt Screenshot als Issue-Asset hoch (Gitea Issue-Attachment-API)
- 4 neue Settings: gitea_token, gitea_api_url, gitea_repo_owner, gitea_repo_name
- Server .env um GITEA_TOKEN ergaenzt
- 10 neue Unit-Tests (mit gemocktem httpx)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 01:00:44 +02:00
Dotty Dotter
fab1bddd3c fix(v2): Hamburger-Toggle wirklich ausblenden (Specificity-Konflikt + Cache)
Bug: .v2-topbar button {display:inline-flex} ueberschreibt .v2-menu-toggle{display:none}
wegen hoeherer Specificity. Fix: Selektor .v2-topbar .v2-menu-toggle + !important.

Plus app_version 1.0.0 -> 1.0.1 als Cache-Buster fuer alle CSS-Refs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:37:55 +02:00
Dotty Dotter
98787c8684 fix(v2): Cache-Buster fuer CSS via ?v=app_version
Browser-Cache zeigte alte v2.css ohne v2-menu-toggle-display:none-Regel.
Mit ?v=1.0.0 wird auf Versionsspruenge sauber neu geladen.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:33:18 +02:00
Dotty Dotter
b1ad2bd45d fix(v2): Hamburger-Menü-Toggle nur auf Mobile (< 900 px) sichtbar
Auf Desktop ist die Sidebar permanent — der Burger-Button hatte dort keine
Funktion. display: none default + @media max-width:900px → inline-flex.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:28:51 +02:00
Dotty Dotter
7a64335e64 feat(auth): 'Passwort vergessen?'-Link im v2-Login-Modal
Klick öffnet /api/auth/forgot-password → 302 zur Keycloak-Reset-Page mit
client_id + redirect_uri (auf eigene Domain). Keycloak schickt Mail mit
Reset-Link, User setzt neues Passwort, kommt zurück.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 00:21:02 +02:00
Dotty Dotter
c1926ada4f feat(#143): Registrierungs-Bestätigungsmail an User direkt nach Anmeldung
Vorher: User registriert -> Keycloak-User mit enabled=false angelegt -> KEINE
Mail bis Admin manuell freischaltet. UX-Luecke: User weiss zwischen Klick und
Admin-Freischaltung nicht, ob etwas passiert ist.

Jetzt: nach erfolgreichem Keycloak-User-Create wird sofort eine Bestaetigungs-
Mail an die angegebene Adresse geschickt mit Hinweis auf den 3-Schritt-Flow
(Anmeldung -> Admin-Freischaltung -> Passwort-Setzen-Mail). Plain-Text + HTML.
Fehler beim Mail-Versand wird geloggt aber nicht weitergereicht — User-Anlage
ist davon unabhaengig.

Response-Message angepasst: 'Wir haben dir eine Bestaetigung per E-Mail geschickt.'

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 23:40:04 +02:00
Dotty Dotter
6581acd28e ux(v2): Partei-Dropdown statt Freitext in /v2/abos und /v2/feed
Beide Routes liefern jetzt all_canonical_keys() (ohne Landesregierung) als Dropdown-
Optionen. Verhindert Tippfehler und gibt nur tatsaechlich erkannte Parteien zur Auswahl.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:37:31 +02:00
Dotty Dotter
7cbd46f88d feat(v2): Atom-Feed-Konfig-Seite + Eigene-Abos-Verwaltung
Backend (Filter sind seit jeher da):
- /api/feed.xml?bundesland=&partei=&limit=
- /api/subscriptions GET/POST/DELETE

UI:
- /v2/feed: Form mit BL/Partei/Limit, generiert Feed-URL live, Buttons Oeffnen/
  URL-Kopieren/In-Feedly. Default-BL aus Header-Selektor uebernommen
- /v2/abos: Liste eigener Abos + Form zum Anlegen/Loeschen, BL-Dropdown,
  Partei-Freitext, Frequenz daily/weekly
- Sidebar 'Daten'-Gruppe um beide Eintraege erweitert (statt Direkt-Link auf
  /api/feed.xml)
- Beide Routen mit Depends(require_auth) — Anonyme bekommen 401-Redirect

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:34:55 +02:00
Dotty Dotter
7f070b5e6c fix(v2): Topbar harte Hoehe 32px + Kleine-Anfragen-Heuristik in Landtag-Suche
Topbar:
- height: 32px (statt auto), line-height: 1, alle children max 24px
- Topbar-Icons explizit auf 12x12 (statt 14)
- selects/buttons/a mit fester Hoehe 22px, padding 2px 6px

Landtag-Suche:
- search_landtag filtert jetzt Drucksachen aus, deren Titel typische
  Frage-Praefixe haben (Welche/Wie viele/Wann/Was/Hat/Ist/...)  oder mit '?'
  enden — bei NRW-OPAL liefert der Adapter alle als 'sonstige', daher
  Title-Heuristik. Server-side, damit alle Adapter profitieren.
- Neuer Helper drucksache_typen.likely_kleine_anfrage_titel()

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:23:22 +02:00
Dotty Dotter
fa5a5b6026 ux(v2): Prüfen + Daten-Sidebar-Gruppen ganz ausblenden ohne Auth (statt nur leere Labels)
Vorher: '— Pruefen' + '— Daten'-Labels waren sichtbar, aber alle Eintraege darin
hidden — nur ein verlorener Header. Jetzt: ganzer Gruppen-Container hinter
{% if is_authenticated %} → Anonymous-User sieht nur 'Lesen'-Gruppe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:18:58 +02:00
Dotty Dotter
85a10b7fc3 ux(v2): bessere Anzeige für 'skipped' Drucksachen (Kleine Anfragen etc.)
Vorher: Button-Text 'Übersprungen', der Grund nur als Tooltip — User versteht
nicht warum. Jetzt: 'Nicht abstimmbar' + sichtbare Italic-Begruendung unter der
Zeile mit dem konkreten Reason-Text vom Server (Backend liefert reason, typ
und typ_normiert).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:17:11 +02:00
Dotty Dotter
997d59a9a5 fix(v2): Queue-Widget ist immer sichtbar (auch ohne aktive Jobs)
Vorher: filterte stale-Jobs raus, bei leerer aktiver Queue display:none → User sah nichts.
Jetzt: immer sichtbar mit 'Queue leer · N Worker bereit' wenn nichts aktiv.
Tooltip zeigt Stale-Jobs als 'letzter Lauf'-Liste, wenn keine aktiven Jobs da sind.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:13:30 +02:00
Dotty Dotter
273d45ea36 fix: PDF-Link mit #page=N-Hash — Browser-PDF-Viewer landet jetzt direkt auf der richtigen Seite
Browser-PDF-Reader (Chrome, Firefox) ignorieren das von /OpenAction-Eintrag im
PDF-Catalog (#88f9c7d) komplett. Der zuverlaessige Weg: URL-Hash-Anker '#page=N'.

Drei Stellen angepasst:
- redline_utils.build_pdf_href: haengt #page={seite} an die URL
- embeddings._build_zitat_url (rebind): analog
- v2/components/quote_card.html: bei alten DB-Eintraegen ohne Hash wird er
  on-the-fly aus dem 'seite='-Query-Param erzeugt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:09:46 +02:00
Dotty Dotter
88f9c7db6c fix: PDF-Endpoint setzt OpenAction auf gefundene Seite + Topbar weiter komprimiert
Vorher: /api/wahlprogramm-cite lieferte das gesamte PDF mit Highlight-Annot
auf der gefundenen Seite, aber der Browser-PDF-Viewer landete auf Seite 1.
Sieht User: 'PDF oeffnet, aber falsche Seite'.

Jetzt: doc.xref_set_key(catalog, 'OpenAction', '[<page-ref> 0 R /Fit]')
schreibt eine PDF-Open-Action ins Dokument-Catalog. Reader springt beim
Oeffnen direkt auf target_page_idx, ohne dass Browser-Hash-Anker noetig sind.

Plus: Topbar select/button padding-top/bottom 1px, links 0px (User: 'nur so
hoch wie noetig').

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 22:06:39 +02:00