docs+ops: ADRs 0006/0008, DDD-Bewertung, Zugriffsrechte, Smoke-Test, Cron-Scripts
ADRs: - 0006 Embedding-Modell-Migration v3->v4 (#123) - 0008 DDD-Lightweight-Migration (#136) Analysen: - ddd-bewertung.md (1237 Zeilen) — vollstaendige DDD-Analyse mit Tages-Roadmap - protokoll-parser-v6-machbarkeit.md (418 Zeilen) — #106 Phase 2 Vorbereitung Reference: - zugriffsrechte.md — 63 Routes x 3 User-Status, UI-Sichtbarkeits-Matrix Ops: - scripts/deploy.sh — mit Uptime-Kuma-Wartungsmodus (#149) - scripts/run-digest.sh — taeglicher Mail-Digest-Cron - scripts/run-monitoring-scan.sh — Monitoring-Scan-Cron (noch nicht aktiv) - scripts/smoke-test.sh — Gesamt-Funktionspruefung - pytest.ini: integration/slow/e2e Markers, addopts not-integration Tests/integration/: Live-Adapter-Tests + Frontend-XRef + Citation-Substring + Wahlprogramm-Indexed (4 Live-Test-Suites, marker-opt-in) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
2902164eff
commit
2dec009b5c
104
docs/adr/0006-embedding-model-migration-v3-to-v4.md
Normal file
104
docs/adr/0006-embedding-model-migration-v3-to-v4.md
Normal file
@ -0,0 +1,104 @@
|
||||
# ADR 0006: Embedding-Modell-Migration text-embedding-v3 → v4
|
||||
|
||||
**Status:** accepted
|
||||
**Datum:** 2026-04-11
|
||||
**Issue:** #123
|
||||
**Kontext:** #105 Clustering, #108 Empfehlungen
|
||||
|
||||
## Kontext
|
||||
|
||||
Bis 2026-04-11 wurden alle Wahlprogramm-Chunks (69 Programme, ~18k Chunks) mit
|
||||
`text-embedding-v3` (1024 Dimensionen) indexiert. Für die geplanten Features
|
||||
**#105 Antrag-Clustering** und **#108 Empfehlungen ähnlicher Anträge** brauchen
|
||||
wir Embeddings für die Assessments selbst, nicht nur für Programm-Chunks.
|
||||
|
||||
Bei der Modellwahl stellte sich die Frage: dasselbe Modell für beide Seiten
|
||||
oder darf eine Seite neuer sein?
|
||||
|
||||
**Kritisch:** v3- und v4-Embeddings liegen in unterschiedlichen Vektorräumen.
|
||||
Cosine-Distance zwischen einem v3- und einem v4-Vektor ist mathematisch
|
||||
gültig, aber semantisch Unsinn. Eine Vermischung macht die Citation-Binding-
|
||||
Logik aus ADR 0001 kaputt und blockiert jede zukünftige Feature, die
|
||||
Antrag-zu-Programm-Ähnlichkeit berechnet.
|
||||
|
||||
## Entscheidung
|
||||
|
||||
Vollständige Migration **beider Seiten** (Wahlprogramm-Chunks + Assessments)
|
||||
auf `text-embedding-v4` (1024 Dimensionen, gleiches Default wie v3).
|
||||
|
||||
**Begründung:**
|
||||
|
||||
- Alibaba DashScope preist v3 und v4 **identisch** ($0.07 / 1M Tokens)
|
||||
- v4 ist strikt besser auf MTEB (68.36 vs 63.39 overall, 59.30 vs 55.41 retrieval)
|
||||
- v4 unterstützt 100+ Sprachen (v3: 50+), relevant für Zitate mit Fremdwörtern
|
||||
- v4 bietet flexible Dimensionen 64–2048, wir bleiben aber auf 1024 (v3-kompatibel)
|
||||
- Die einmaligen Reindex-Kosten belaufen sich auf **~$0.50** für alle Programme
|
||||
- v3 wird vermutlich mittelfristig deprecated; wir vermeiden eine zweite Migration später
|
||||
|
||||
Die Alternative "v3 für Programme, v4 für Assessments" wurde verworfen, weil
|
||||
sie die zukünftige Kompatibilität für Antrag-zu-Programm-Ähnlichkeit
|
||||
permanent blockiert.
|
||||
|
||||
## Migrations-Strategie (Zero-Downtime)
|
||||
|
||||
**Zwei getrennte Settings** (`settings.embedding_model_write`,
|
||||
`settings.embedding_model_read`) ermöglichen einen stufenweisen Switch ohne
|
||||
Prod-Downtime:
|
||||
|
||||
| Phase | WRITE | READ | Zustand |
|
||||
|---|---|---|---|
|
||||
| 0 | v3 | v3 | Pre-Migration (alter Zustand) |
|
||||
| 1 | **v4** | v3 | Code deployed, Reindex läuft im Hintergrund, Prod läuft weiter auf v3-Rows |
|
||||
| 2 | v4 | **v4** | Reindex fertig, Flag geflippt, neue Queries nutzen v4-Rows |
|
||||
| 3 | v4 | v4 | Cleanup: alte v3-Rows gelöscht |
|
||||
|
||||
**Schema-Änderungen:**
|
||||
|
||||
- `chunks`: neue Spalte `model TEXT NOT NULL DEFAULT 'text-embedding-v3'`
|
||||
+ Index `idx_chunks_model`
|
||||
- `assessments`: neue Spalten `summary_embedding BLOB`, `embedding_model TEXT`
|
||||
|
||||
**Reindex-Skript:** `app/reindex_embeddings.py` (Ausführung via
|
||||
`docker exec gwoe-antragspruefer python -m app.reindex_embeddings`).
|
||||
Schreibt v4-Rows parallel zu den v3-Rows, mit 100ms Rate-Limit zwischen
|
||||
Calls (= max 10 req/sec). Bereits mit v4 indexierte Programme werden
|
||||
übersprungen, damit das Skript idempotent ist und nach Abbruch nahtlos
|
||||
fortgesetzt werden kann.
|
||||
|
||||
**Query-Pfad:** `find_relevant_chunks` filtert jetzt explizit
|
||||
`WHERE model = ?` mit `EMBEDDING_MODEL_READ`. Query-Embeddings werden mit
|
||||
demselben READ-Modell erzeugt (via neuer `model`-Parameter in
|
||||
`create_embedding`), damit Query und Chunks im selben Vektorraum liegen.
|
||||
|
||||
## Konsequenzen
|
||||
|
||||
**Positiv:**
|
||||
|
||||
- Einheitlicher Vektorraum für Chunks und Assessments → #105, #108, und künftige
|
||||
Ähnlichkeits-Features funktionieren out-of-the-box
|
||||
- Bessere Retrieval-Qualität (MTEB +3.9 Punkte)
|
||||
- Einmaliger Schritt, danach kein Mental-Load für Modell-Drift
|
||||
|
||||
**Negativ:**
|
||||
|
||||
- Reindex-Zeit: ~1–2h Wall-Time für alle 69 Programme (rate-limited)
|
||||
- Kurzzeitig doppelter Storage (v3 + v4 Chunks parallel) bis Phase 3
|
||||
- Assessment-Embedding-Generation adds ~100ms Latenz pro neuer Analyse (ein
|
||||
zusätzlicher API-Call), der aber non-blocking fehlertolerant ist — Backfill
|
||||
zieht später nach
|
||||
|
||||
**Neutral:**
|
||||
|
||||
- Die beiden Settings (`write` / `read`) bleiben langfristig im Code bestehen
|
||||
als Infrastruktur für zukünftige Modell-Migrationen (v5, v6, …). Das Pattern
|
||||
ist wiederverwendbar.
|
||||
|
||||
## Alternative: "v3 einfrieren"
|
||||
|
||||
Verworfen, weil v3 kein aktuell unterstütztes Flaggschiff mehr ist und
|
||||
Deprecation wahrscheinlich ist. Besser jetzt migrieren, wenn noch beide
|
||||
Modelle verfügbar sind, als später unter Zeitdruck.
|
||||
|
||||
## Alternative: "nur Assessments auf v4"
|
||||
|
||||
Verworfen wegen der Vektorraum-Fragmentierung (siehe Kontext).
|
||||
@ -21,6 +21,9 @@ und Konsequenzen. Format inspiriert von [Michael Nygard](https://cognitect.com/b
|
||||
| [0002](0002-adapter-architecture.md) | Adapter-Pattern mit ParlamentAdapter-Basisklasse + Registry | accepted | 2026-04-10 |
|
||||
| [0003](0003-citation-property-tests.md) | Sub-D Property-Verification: Zitate als Substring der zitierten PDF-Seite | accepted | 2026-04-10 |
|
||||
| [0004](0004-deployment-workflow.md) | Docker Compose Deploy mit DB-/Reports-Volume und SN-XML-Sonderpfad | accepted | 2026-04-10 |
|
||||
| [0005](0005-keycloak-sso-with-dev-bypass.md) | Keycloak SSO mit Dev-Bypass-Fallback | accepted | 2026-04-10 |
|
||||
| [0006](0006-embedding-model-migration-v3-to-v4.md) | Embedding-Modell-Migration text-embedding-v3 → v4 | accepted | 2026-04-11 |
|
||||
| [0008](0008-ddd-lightweight-migration.md) | DDD-Lightweight-Migration (Repository, LLM-Port, Domain-Verhalten) | accepted | 2026-04-20 |
|
||||
|
||||
## Wann ADR, wann nicht
|
||||
|
||||
|
||||
1237
docs/analysen/ddd-bewertung.md
Normal file
1237
docs/analysen/ddd-bewertung.md
Normal file
File diff suppressed because it is too large
Load Diff
418
docs/analysen/protokoll-parser-v6-machbarkeit.md
Normal file
418
docs/analysen/protokoll-parser-v6-machbarkeit.md
Normal file
@ -0,0 +1,418 @@
|
||||
# Plenarprotokoll-Parser v6 — Machbarkeits-Analyse
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| **Status** | Exploration |
|
||||
| **Datum** | 2026-04-20 |
|
||||
| **Refs** | Issue #106 (Abstimmungsverhalten), #126 (BL-übergreifender Parser), MEMORY `reference_nrw_protokoll_parser.md` |
|
||||
| **Autor** | Claude Opus 4.7 (1M) — strukturelle Exploration, kein Code |
|
||||
|
||||
## Zweck
|
||||
|
||||
Strukturierte Bewertung, ob ein Parser v6 für das Abstimmungsverhalten in
|
||||
Plenarprotokollen sinnvoll ist — und wenn ja, in welcher Architektur.
|
||||
Entscheidungsgrundlage, **keine** Implementierungs-Vorgabe.
|
||||
|
||||
## 1. Bestandsaufnahme: v5 heute
|
||||
|
||||
### 1.1 Wo lebt der Parser?
|
||||
|
||||
**Befund:** Im Produktionscode (`webapp/app/`) existiert **kein**
|
||||
`protokoll_parser.py` oder vergleichbares Modul. Grep über alle
|
||||
`app/*.py` auf `parse_protokoll`, `Plenarprotokoll`, `MMP`, `Wer stimmt`,
|
||||
`Damit ist der Antrag` liefert **null** Treffer in Produktions-Python —
|
||||
nur ein inzidentelles `Plenarprotokoll` im Kommentar zu
|
||||
`parlamente.py:986`.
|
||||
|
||||
Der v5-Parser ist also **POC-Code aus einer früheren Session**, der
|
||||
nicht ins Repo gelangt ist. Die einzige belastbare Referenz ist die
|
||||
Memory-Datei
|
||||
`~/.claude/projects/…gwoe…/memory/reference_nrw_protokoll_parser.md`
|
||||
(95 Zeilen, 12 Apr. 2026).
|
||||
|
||||
**Konsequenz:** v6 ist **kein Refactor**, sondern ein Neuaufbau. Das
|
||||
reduziert Altlast-Risiko, bedeutet aber auch: es gibt keine
|
||||
Baseline-Test-Fixtures im Repo, keine bestehenden Call-Sites, die
|
||||
zu brechen wären. Saubere grüne Wiese.
|
||||
|
||||
### 1.2 Was v5 laut Memory kann
|
||||
|
||||
Aus `reference_nrw_protokoll_parser.md` (zur Orientierung, nicht als
|
||||
Autorität — Memory ist 8 Tage alt):
|
||||
|
||||
**Input:** PDF-Text von `landtag.nrw.de/.../MMP{WP}-{N}.pdf`, nach
|
||||
Worttrennungs-Auflösung (`-\s*\n\s*` → `""`) und
|
||||
Whitespace-Normalisierung.
|
||||
|
||||
**Anchor-Phrasen für Ergebnis-Klassifikation:**
|
||||
|
||||
```
|
||||
Damit ist der Antrag [Drucksache X] angenommen|abgelehnt
|
||||
Damit ist der Gesetzentwurf [Drucksache X] angenommen|abgelehnt
|
||||
Damit ist dieser Antrag Drucksache X angenommen|abgelehnt
|
||||
Damit ist (diese|die) Überweisungsempfehlung … angenommen → überwiesen
|
||||
Somit ist dieser Antrag Drucksache X abgelehnt
|
||||
```
|
||||
|
||||
**Segment-Boundaries** (trennen Abstimmungen im selben TOP):
|
||||
|
||||
```
|
||||
Damit kommen wir zur Abstimmung
|
||||
Wir kommen (somit )?zur Abstimmung
|
||||
Wir stimmen (?!zu)
|
||||
Somit kommen wir (direkt )?zu den Abstimmungen
|
||||
```
|
||||
|
||||
**Vote-Pattern** (Ja/Nein/Enthaltung):
|
||||
|
||||
- `Wer stimmt … zu? – Das (ist|sind) [Fraktionen]` → Ja-Stimmer
|
||||
- `Wer stimmt dagegen?` / `Wer lehnt … ab?` → Nein-Stimmer
|
||||
- `Wer enthält sich?` / `Gibt es Enthaltungen?` → Enthaltungen
|
||||
|
||||
**Negations-Antworten** (= leere Liste): `niemand`, `Keine
|
||||
Gegenstimmen`, `nicht der Fall`, `Auch nicht`.
|
||||
|
||||
**Sonderfälle:**
|
||||
- Re-Vote (Präsident unterbricht): „Vielleicht sind sich dann alle
|
||||
einig." → letzte Instanz im Segment zählt.
|
||||
- Beschlussempfehlung-vs-Gesetzentwurf: abgestimmt wird über den
|
||||
Gesetzentwurf, die Empfehlungs-DS ist `nicht_gesondert_abgestimmt`.
|
||||
- Protokoll-Typo (MMP18-115): Anchor nennt falsche DS → Segment-Entry-DS
|
||||
hat Vorrang (v6-Forderung).
|
||||
- Petitions-Sammelüberweisung (Drucksache 18/33 = Übersicht) →
|
||||
Einzelpetitionen haben **kein** Ergebnis.
|
||||
|
||||
### 1.3 Validierungs-Stand v5
|
||||
|
||||
| Protokoll | Treffer | Bemerkung |
|
||||
|---|---|---|
|
||||
| MMP18-119 (Training) | 19/19 | 100 % Precision **und** Recall |
|
||||
| MMP18-115 | 10/32 | fehlende Anchor-Varianten |
|
||||
| MMP18-110 | 17/6 | Sammel-Petitions-Einzelansprache (False Positives) |
|
||||
| MMP18-100 | 8/25 | ähnlich wie MMP18-115 |
|
||||
|
||||
**Interpretation:** v5 ist **overfit auf MMP18-119**. Präzision bleibt
|
||||
hoch (keine falschen Abstimmungs-Ergebnisse), aber der Recall-Einbruch
|
||||
auf ~30-50 % schon **innerhalb** NRW zeigt: die Anchor-Liste deckt nicht
|
||||
einmal NRW-interne Variation ab. Cross-BL wird drastisch schlechter.
|
||||
|
||||
## 2. Cross-BL-Strukturanalyse
|
||||
|
||||
### 2.1 Sample-Verfügbarkeit im Repo
|
||||
|
||||
**Befund:** **Keine** Plenarprotokoll-Samples im Repo.
|
||||
|
||||
Geprüfte Pfade:
|
||||
- `webapp/tests/fixtures/` → leer (nur Sub-Dir-Marker, keine Dateien)
|
||||
- `webapp/app/static/` → nur Wahlprogramm-PDFs
|
||||
- `webapp/data/` → nur `gwoe-antraege.db` (SQLite)
|
||||
- `antraege/` (Projekt-Root) → 80+ PDFs, aber alle Drucksachen
|
||||
(Anträge), keine MMP-Protokolle
|
||||
- `TEMP/` → HAR-Captures der Dokumentations-Frontends, keine PDFs
|
||||
|
||||
Keine einzige `MMP*.pdf` oder `PlPr*.pdf` im gesamten Projekt-Tree.
|
||||
|
||||
### 2.2 Strukturelle Annahmen aus Doku-Systemen
|
||||
|
||||
Ohne Samples ist eine echte Struktur-Analyse nicht möglich. Aus
|
||||
`app/bundeslaender.py` und `app/parlamente.py` lassen sich nur
|
||||
**Plausibilitäts-Hypothesen** ableiten:
|
||||
|
||||
| Familie | BL | Protokoll-Quirk (Hypothese) |
|
||||
|---|---|---|
|
||||
| OPAL | NRW | MMP-PDF, stereotyp (v5-Referenz) |
|
||||
| StarWeb/portala | HE, SH, BB, RP, LSA, BE | Plenarprotokolle typischerweise als PDF mit Zweispalten-Layout |
|
||||
| ParlDok | MV, HH, TH | eigene Redaktionsrichtlinie pro LT |
|
||||
| PARiS | HB | Bürgerschaft, Kleinst-Parlament |
|
||||
| Eigensystem | BY, SL, SN | je eigene Typografie |
|
||||
| DIP (BUND) | BUND | Plenarprotokolle als **strukturiertes XML** verfügbar — Ausnahme! |
|
||||
|
||||
**Wichtig:** Die Adapter in `parlamente.py` liefern bisher **Drucksachen**
|
||||
(Anträge), nicht Plenarprotokolle. Für Protokoll-Download müssten die
|
||||
Adapter erweitert werden — das ist Teil des Scopes von #106.
|
||||
|
||||
### 2.3 Fraktions-Namen-Normalisierung — vorhanden
|
||||
|
||||
`app/parteien.py:187` bietet `normalize_partei(raw, *, bundesland=None)`,
|
||||
das laut Docstring die vier Adapter-eigenen `_normalize_fraktion()`
|
||||
ersetzt. Das ist **entscheidend**, weil Protokolle Fraktionen in ihrer
|
||||
LT-spezifischen Schreibweise nennen (`BÜNDNIS 90/DIE GRÜNEN`,
|
||||
`GRÜNE`, `B'90/Grüne`). Die Komponente ist da und getestet — v6 muss
|
||||
sie nicht neu bauen.
|
||||
|
||||
## 3. Lösungs-Ansätze
|
||||
|
||||
### 3.1 Option A — Rule-based expand (v5 pro BL)
|
||||
|
||||
Pro Bundesland eine `abstimmung_rules_<bl>.yaml` mit BL-spezifischen
|
||||
Anchors, Segment-Boundaries, Vote-Patterns. Ein generischer Engine-
|
||||
Kern lädt die Rules und parst.
|
||||
|
||||
**Pro:**
|
||||
- Deterministisch, reproduzierbar, offline.
|
||||
- Null Laufzeit-Kosten, Millisekunden pro Protokoll.
|
||||
- Präzision bleibt bei 100 % (nur bekannte Anchors matchen).
|
||||
|
||||
**Contra:**
|
||||
- **Recall-Problem überträgt sich** linear auf 17 BL. v5 schafft in NRW
|
||||
schon nur 30-50 % auf unbekannten Protokollen — 17-fach skaliert
|
||||
heißt: pro BL eigene Overfit-Runde.
|
||||
- Format-Drift zwischen Wahlperioden (neues Redaktionsteam, neuer
|
||||
Stilführer) bricht still.
|
||||
- Wartungs-Aufwand: geschätzt 2-3 Tage Reverse-Engineering pro BL, plus
|
||||
~0,5 Tage Nachpflege pro WP-Wechsel.
|
||||
|
||||
**Aufwand:** 2-3 Tage pro BL × 17 = **34-51 Personentage** bis
|
||||
Vollabdeckung. Pro BL-Nachpflege alle 4-5 Jahre.
|
||||
|
||||
**Risiko:** Stille Recall-Einbrüche, die nur durch Ground-Truth-Tests
|
||||
entdeckt werden. Ohne kontinuierliches Fixture-Update degradiert die
|
||||
Datenqualität.
|
||||
|
||||
### 3.2 Option B — LLM-Extraction pro Seite
|
||||
|
||||
Ein strukturiertes Prompt nimmt eine Protokoll-Seite (oder einen
|
||||
10-Seiten-Block) und liefert JSON:
|
||||
|
||||
```json
|
||||
{
|
||||
"abstimmungen": [
|
||||
{
|
||||
"drucksache": "18/17492",
|
||||
"titel": "…",
|
||||
"ergebnis": "angenommen|abgelehnt|überwiesen",
|
||||
"ja": ["CDU", "GRÜNE"],
|
||||
"nein": ["SPD", "AfD", "FDP"],
|
||||
"enthaltung": [],
|
||||
"namentlich": false
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**Pro:**
|
||||
- Ein Prompt für alle 17 BL, Format-Drift-robust.
|
||||
- Nutzt bestehende LLM-Pipeline (`analyzer.py`, Qwen-Plus).
|
||||
- Neue BL kosten **null** Entwicklungs-Zeit nach Grund-Integration.
|
||||
- Zitat-Binding-Pattern aus ADR 0001 direkt übertragbar (Post-LLM-Match
|
||||
gegen Protokoll-Text, genau wie bei Wahlprogramm-Zitaten).
|
||||
|
||||
**Contra:**
|
||||
- **Kosten:** ein Protokoll hat 120-200 Seiten. Bei 10 Seiten pro
|
||||
Prompt-Call: ~15-20 Calls × ~0,03 €/Call bei Qwen-Plus = **0,45-0,60 €
|
||||
pro Protokoll**. NRW-WP18 hat ~170 Plenarsitzungen → ~85 € allein für
|
||||
NRW, ~1500 € für alle 17 BL × 1 WP. Erträglich, aber nicht trivial.
|
||||
- **Laufzeit:** 15-20 Calls × 10 s = 2,5-3 min pro Protokoll.
|
||||
- **LLM-Halluzinationen:** ADR 0001 zeigt, dass Zitate cross-mixen.
|
||||
Hier: Gefahr, dass LLM ein Abstimmungs-Ergebnis halluziniert, das im
|
||||
Protokoll nicht vorkommt. Citation-Binding gegen Protokoll-Text ist
|
||||
Pflicht.
|
||||
|
||||
**Aufwand:** Grund-Integration 3-5 Tage (Prompt-Engineering + Citation-
|
||||
Binding analog ADR 0001 + Cache-Layer, damit dasselbe Protokoll nicht
|
||||
zweimal abgefragt wird). Pro BL **null** zusätzlich.
|
||||
|
||||
**Risiko:** LLM-Halluzinationen bei stillem Citation-Binding-Bug;
|
||||
Kosten-Explosion wenn alle WPs voll nachgefahren werden.
|
||||
|
||||
### 3.3 Option C — Hybrid: Rules-Pre-Filter + LLM-Strukturierung
|
||||
|
||||
**Stufe 1 (Rules):** Ein universelles Regex-Set (wenige, stabile
|
||||
Anchors) segmentiert das Protokoll in **Abstimmungs-Kandidat-Blöcke**.
|
||||
Kandidat: Text zwischen zwei `Abstimmung|stimmen ab|stimmen … zu`-
|
||||
Vorkommen.
|
||||
|
||||
**Stufe 2 (LLM):** Jeder Kandidat-Block (~500-2000 Tokens) wird an den
|
||||
LLM gegeben mit dem Prompt „Extrahiere Drucksache, Ergebnis, Ja/Nein/
|
||||
Enthaltung-Fraktionen. Nur was im Text steht."
|
||||
|
||||
**Stufe 3 (Verifier):** Jede extrahierte Fraktion wird gegen
|
||||
`normalize_partei()` geprüft und gegen `BUNDESLAENDER[bl]
|
||||
.landtagsfraktionen` gefiltert. Halluzinierte Fraktionen (nicht im LT
|
||||
vertreten) werden gedroppt oder loggen einen Warn.
|
||||
|
||||
**Pro:**
|
||||
- Kosten ~80 % niedriger als Option B (nur Kandidat-Blöcke, nicht
|
||||
ganze Seiten).
|
||||
- BL-übergreifend, weil Stufe 1 nur grobe Anchors braucht (`stimmen`,
|
||||
`Abstimmung` als Lemma-Formen sind in allen dt. Parlamenten üblich).
|
||||
- Verifier fängt die meisten Halluzinationen ab.
|
||||
- Skaliert auf neue BL mit nahezu null Zusatz-Aufwand.
|
||||
|
||||
**Contra:**
|
||||
- Drei Stufen = drei Stellen, an denen Bugs sein können.
|
||||
- Stufe 1 kann Kandidaten übersehen (stille Recall-Lücke). Debuggable
|
||||
über „kein Kandidat gefunden"-Log.
|
||||
- Komplexer zu testen als reine Rules oder reines LLM.
|
||||
|
||||
**Aufwand:** 5-7 Tage Grund-Integration. Pro BL: 0,5 Tag (einmal
|
||||
Kalibrierung der Stufe-1-Anchors mit einem Protokoll-Sample).
|
||||
|
||||
**Risiko:** Mittel. Komplexität ist höher, aber jede Stufe isoliert
|
||||
testbar. Analog zur LLM-Citation-Binding-Architektur aus ADR 0001.
|
||||
|
||||
### 3.4 Bewertungs-Matrix
|
||||
|
||||
| Kriterium | A (Rules) | B (LLM-voll) | C (Hybrid) |
|
||||
|---|---|---|---|
|
||||
| Precision (erwartet) | 95-100 % | 85-95 % | 90-98 % |
|
||||
| Recall (erwartet) | 30-60 % pro BL | 85-95 % | 80-92 % |
|
||||
| Entwicklung (Tage) | 34-51 | 3-5 | 5-7 |
|
||||
| Laufzeit-Kosten/Prot. | ~0 € | ~0,50 € | ~0,10 € |
|
||||
| Skaliert auf neue BL | nein (linear) | ja (gratis) | ja (halbgratis) |
|
||||
| Format-Drift-Robustheit | niedrig | hoch | mittel-hoch |
|
||||
| Debug-Transparenz | hoch | niedrig | mittel |
|
||||
| Offline-fähig | ja | nein | nein |
|
||||
|
||||
## 4. Entscheidungs-Baum
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start([Issue #106: Abstimmungsverhalten]) --> Samples{Plenarprotokoll-<br/>Samples<br/>beschaffbar?}
|
||||
Samples -->|nein| Stop1[STOP:<br/>Sample-Beschaffung zuerst]
|
||||
Samples -->|ja, ≥3 BL| Scope{Scope<br/>klar?}
|
||||
Scope -->|nur NRW, kurzfristig| OptA[Option A<br/>v5 härten auf NRW]
|
||||
Scope -->|alle 17 BL| OptFamily{Kosten-Bereitschaft<br/>für LLM-Calls?}
|
||||
OptFamily -->|nein, offline nötig| OptA_scaled[Option A:<br/>Rule-Engine pro BL<br/>34-51 Tage]
|
||||
OptFamily -->|ja, 1500 €/WP OK| OptC[Option C Hybrid:<br/>5-7 Tage Grund<br/>+ 0,5 Tag/BL]
|
||||
OptC --> POC[POC auf 2 BL:<br/>NRW + HE<br/>Ground-Truth-Set je 3 Prot.]
|
||||
POC --> Eval{Recall<br/>≥ 80 % auf HE?}
|
||||
Eval -->|ja| Rollout[Rollout auf 17 BL]
|
||||
Eval -->|nein| Rethink[LLM-Prompt iterieren<br/>oder auf B eskalieren]
|
||||
OptA_scaled --> NRW_first[NRW zuerst,<br/>dann nach Output-Ranking]
|
||||
```
|
||||
|
||||
## 5. Empfehlung
|
||||
|
||||
### 5.1 Familie
|
||||
|
||||
**Option C (Hybrid)** als Primärweg. Begründung:
|
||||
|
||||
1. Die bestehende Architektur passt: `analyzer.py` nutzt schon LLM,
|
||||
ADR 0001 liefert das Binding-Pattern, `parteien.py:normalize_partei`
|
||||
gibt den Verifier.
|
||||
2. Option A skaliert nicht auf 17 BL ohne Personen-Aufwand, den das
|
||||
Projekt realistisch nicht hat (siehe Issue-Backlog).
|
||||
3. Option B hat das Halluzinations-Risiko und Kosten ohne Gewinn
|
||||
gegenüber C.
|
||||
|
||||
### 5.2 Minimum-Viable-Scope
|
||||
|
||||
1. **Protokoll-Download-Erweiterung der Adapter:** `get_plenarprotokoll
|
||||
(wp, sitzung)` als optionale Methode. Zuerst nur NRW + 1 weiteres BL
|
||||
(Vorschlag: BUND, weil dort XML statt PDF → erleichtert
|
||||
Text-Extraktion).
|
||||
2. **Stufe-1-Anchors** als einzige YAML-Datei (nicht pro BL), mit den
|
||||
~5 stabilsten Formulierungen (`stimmen ab`, `Abstimmung über`,
|
||||
`wer stimmt zu`, `wer ist dagegen`, `wer enthält sich`).
|
||||
3. **Stufe-2-Prompt** mit Zitat-Binding-Anforderung (jede Fraktion
|
||||
muss als Exakt-Substring im Kandidat-Block vorkommen).
|
||||
4. **Stufe-3-Verifier** via `normalize_partei` und
|
||||
`BUNDESLAENDER[bl].landtagsfraktionen`.
|
||||
5. **Persistenz:** neue Tabelle `abstimmungen (drucksache, wp, sitzung,
|
||||
ergebnis, ja_fraktionen, nein_fraktionen, enthaltung_fraktionen,
|
||||
source_url, extracted_at, extractor_version)`.
|
||||
|
||||
### 5.3 Abbruch-Kriterien
|
||||
|
||||
- **Stop 1:** Wenn POC auf 2 BL nach 7 Tagen unter 70 % Recall bleibt,
|
||||
ist C nicht erreicht. Fallback: B (voller Seiten-Scan) oder Parken.
|
||||
- **Stop 2:** Wenn Nutzer-Interview zeigt, dass Abstimmungsverhalten
|
||||
pro Drucksache **keinen Dashboard-Mehrwert** gegenüber dem bereits
|
||||
aktiven Score hat, ist v6 unpriorisiert. Issue #106 hängt seit April
|
||||
offen — die Frage „braucht das jemand?" ist vor Implementierung zu
|
||||
klären.
|
||||
- **Stop 3:** Wenn eine offene Datenquelle (z.B. `abgeordnetenwatch.de
|
||||
/api`) Abstimmungsverhalten bereits strukturiert ausliefert, ist ein
|
||||
eigener Parser obsolet → **Evaluations-Pflicht vor Bau**.
|
||||
|
||||
### 5.4 BL-Priorisierung
|
||||
|
||||
Nach CSV-Export `/api/auswertungen/export.csv` und Wichtigkeit:
|
||||
|
||||
1. **NRW** — größter Output, v5 vorhanden
|
||||
2. **BUND** — XML statt PDF, einfachste Implementierung, hoher Impact
|
||||
3. **HE, BY, BW** — große Flächenländer, hohe Drucksachen-Zahl
|
||||
4. **BE, HH, HB** — Stadtstaaten, geringer Protokoll-Umfang, guter
|
||||
Cross-Check für den Hybrid
|
||||
5. Rest nach Output-Ranking aus Dashboard
|
||||
|
||||
## 6. Sample-Beschaffungs-Plan
|
||||
|
||||
Da im Repo keine Plenarprotokolle liegen, für den POC zuerst manuell
|
||||
ziehen. Pro BL 1 aktuelles Protokoll:
|
||||
|
||||
| BL | Quelle | Format |
|
||||
|---|---|---|
|
||||
| NRW | `landtag.nrw.de/portal/WWW/dokumentenarchiv/Dokument/MMP{WP}-{N}.pdf` | PDF |
|
||||
| BUND | `bundestag.de/services/opendata` (XML-Feed) | **XML** |
|
||||
| BY | `bayern.landtag.de/parlament/dokumente/plenarprotokolle` | PDF |
|
||||
| BW | `landtag-bw.de/home/dokumente/plenarprotokolle.html` | PDF |
|
||||
| BE | `parlament-berlin.de/dokumente/open-data` (XML verfügbar) | XML/PDF |
|
||||
| BB | `parlamentsdokumentation.brandenburg.de` (portala) | PDF |
|
||||
| HB | `paris.bremische-buergerschaft.de/starweb/paris` | PDF |
|
||||
| HH | `buergerschaft-hh.de/parldok` | PDF |
|
||||
| HE | `starweb.hessen.de/portal` | PDF |
|
||||
| MV | `dokumentation.landtag-mv.de` (ParlDok) | PDF |
|
||||
| NI | `nilas.niedersachsen.de` (HAR-Capture nötig, siehe MEMORY) | PDF |
|
||||
| RP | `opal.rlp.de` (portala) | PDF |
|
||||
| SL | `landtag-saar.de` (Umbraco) | PDF |
|
||||
| SN | `edas.landtag.sachsen.de` (XML-Export, siehe `project_sn_xml_export.md`) | **XML** |
|
||||
| LSA | `padoka.landtag.sachsen-anhalt.de` (PARDOK) | PDF |
|
||||
| SH | `lissh.lvn.parlanet.de/cgi-bin/starfinder/0` | PDF |
|
||||
| TH | `parldok.thueringer-landtag.de` | PDF |
|
||||
|
||||
Speicherung unter `webapp/tests/fixtures/protokolle/<bl>/` (git-lfs
|
||||
oder außerhalb des Repos, je nach Dateigröße). Pro Protokoll eine
|
||||
`<protokoll>.expected.json` mit Ground-Truth-Abstimmungen als
|
||||
Fixture.
|
||||
|
||||
**Minimum für POC:** NRW (MMP18-119 als Regression), BUND (XML als
|
||||
Gegenprobe), HE (StarWeb als Cross-BL). Drei Protokolle × ~20-30
|
||||
Abstimmungen = ~60-90 Ground-Truth-Items für Precision/Recall.
|
||||
|
||||
## 7. Offene Fragen für Tobias
|
||||
|
||||
1. **Bedarfs-Validierung:** Wer konsumiert das Abstimmungsverhalten
|
||||
konkret im Dashboard? Gibt es eine UI-Spec dafür, oder ist das
|
||||
gedachte Feature? Issue #106 nennt keine konkreten Konsumenten.
|
||||
2. **LLM-Kosten:** Ist ~1500 € Budget für die 17-BL-Erstbefüllung einer
|
||||
WP vertretbar? Oder muss der Rollout auf eine WP pro Jahr gedeckelt
|
||||
werden?
|
||||
3. **Alternative Datenquelle:** Wurde `abgeordnetenwatch.de/api`
|
||||
geprüft? Die liefern auf Bundesebene strukturierte
|
||||
Abstimmungsdaten und haben teilweise Landesabdeckung. Wenn ja, ist
|
||||
v6 für diese BL obsolet.
|
||||
4. **v5-POC-Code:** Existiert der Code noch auf der Festplatte (in
|
||||
einem früheren Session-Worktree)? Ein Fundstück würde 1-2 Tage
|
||||
Re-Implementations-Arbeit sparen.
|
||||
|
||||
## 8. Nächster Schritt (empfohlen)
|
||||
|
||||
1. **Vor jeder Zeile Code:** Beantworte Fragen 1 und 3 oben. Das
|
||||
entscheidet, ob v6 überhaupt startet.
|
||||
2. **Wenn ja:** POC auf NRW + BUND + HE mit Option C, Sample-Zug
|
||||
manuell. 7 Tage Timebox.
|
||||
3. **Wenn POC erfolgreich (Recall ≥ 80 %):** ADR für v6 schreiben
|
||||
(analog 0001/0002), Rollout-Plan.
|
||||
4. **Wenn POC scheitert:** Zurück zu Option A nur für NRW, v5-Recall-Lücken schließen, #106 auf NRW-only beschränken und #126 schließen
|
||||
als „nicht wirtschaftlich".
|
||||
|
||||
## 9. Verifikations-Checkliste
|
||||
|
||||
- [x] v5-Code lokal gesucht → nicht im Repo
|
||||
- [x] MEMORY-Referenz gelesen (95 Zeilen) — v5-Pattern dokumentiert
|
||||
- [x] Sample-Inventar: **null Protokoll-Samples im Repo**
|
||||
- [x] BL-Adapter-Architektur (ADR 0002) verstanden — `ParlamentAdapter`
|
||||
hat aktuell **keine** `get_plenarprotokoll`-Methode
|
||||
- [x] Fraktions-Normalisierung existiert (`parteien.py:187
|
||||
normalize_partei`)
|
||||
- [x] Kosten-Schätzung für Option B/C kalkuliert
|
||||
- [x] Drei Optionen bewertet, Matrix + Mermaid-Diagramm
|
||||
- [x] Abbruch-Kriterien formuliert
|
||||
- [x] Sample-Beschaffungs-Plan für alle 17 BL
|
||||
|
||||
---
|
||||
|
||||
**Nicht in diesem Dokument:** Konkreter Code, Prompt-Vorlagen, ADR-Text
|
||||
für v6. Alles nächste Phase nach Go/No-Go-Entscheidung von Tobias.
|
||||
140
docs/reference/zugriffsrechte.md
Normal file
140
docs/reference/zugriffsrechte.md
Normal file
@ -0,0 +1,140 @@
|
||||
# Zugriffsrechte & User-Status
|
||||
|
||||
Abgeleitet direkt aus dem Code (`app/main.py`, `app/auth.py`, Templates). Stand des Generats: 2026-04-20.
|
||||
|
||||
## Drei User-Status
|
||||
|
||||
| Status | Signal | Guard im Code |
|
||||
|---|---|---|
|
||||
| **Gast** (`anonymous`) | Kein gültiger `access_token`-Cookie | — |
|
||||
| **Registriert** (`authenticated`) | Gültiger Keycloak-JWT, Rolle beliebig | `Depends(require_auth)` |
|
||||
| **Admin** | Keycloak-JWT mit Rolle `admin` oder `gwoe-admin` | `Depends(require_admin)` |
|
||||
|
||||
Zwei Sonder-Modi:
|
||||
|
||||
- **Dev-Modus** (`AUTH_ENABLED=false` in .env): jede Anfrage wird als Anonymous+Admin ausgegeben, sämtliche Guards fallen. Nur lokal, nie in Prod. Siehe ADR 0005.
|
||||
- **Approval-pending**: Nutzer:innen, die über `/api/auth/register` angelegt sind aber noch nicht von einem Admin via `/api/auth/approve-user` freigeschaltet wurden, können sich einloggen, aber keine `require_auth`-Features nutzen (Keycloak verweigert Token-Ausgabe). Siehe `docs/how-to/keycloak-setup.md`.
|
||||
|
||||
## Endpoint × Status-Matrix (63 Routes)
|
||||
|
||||
### Admin-only (5)
|
||||
|
||||
Nur Nutzer:innen mit Rolle `admin`/`gwoe-admin` erreichen diese:
|
||||
|
||||
| Methode | Pfad | Zweck |
|
||||
|---|---|---|
|
||||
| POST | `/api/batch-analyze` | Batch-Bewertung starten |
|
||||
| POST | `/api/programme/index` | Wahlprogramm indexieren |
|
||||
| POST | `/api/auth/approve-user` | User-Freischaltung |
|
||||
| GET | `/api/auth/pending-users` | Liste offener Freischaltungs-Anträge |
|
||||
| DELETE | `/api/assessment/delete` | Bewertung löschen (für Re-Analyse) |
|
||||
|
||||
### Authenticated (8)
|
||||
|
||||
Jede:r eingeloggte:r Nutzer:in:
|
||||
|
||||
| Methode | Pfad | Zweck |
|
||||
|---|---|---|
|
||||
| POST | `/analyze` | Freitext-Upload bewerten |
|
||||
| POST | `/api/analyze-drucksache` | Antrag aus Landtag bewerten |
|
||||
| POST | `/api/bookmark` | Merklisten-Eintrag toggeln |
|
||||
| POST | `/api/comment` | Kommentar anlegen |
|
||||
| DELETE | `/api/comment/{id}` | Eigenen Kommentar löschen |
|
||||
| POST | `/api/subscriptions` | E-Mail-Abo anlegen |
|
||||
| DELETE | `/api/subscriptions/{id}` | E-Mail-Abo löschen |
|
||||
| POST | `/api/vote` | Antrag-Bewertung votieren |
|
||||
|
||||
### Optional-User (5)
|
||||
|
||||
Funktionieren auch ohne Login, aber personalisieren mit User-Daten wenn eingeloggt:
|
||||
|
||||
| Methode | Pfad | Verhalten |
|
||||
|---|---|---|
|
||||
| GET | `/api/auth/me` | Gibt Auth-Status zurück |
|
||||
| GET | `/api/bookmarks` | Liste eigener Bookmarks (wenn eingeloggt), sonst `[]` |
|
||||
| GET | `/api/comments?drucksache=X` | Öffentliche + eigene Kommentare |
|
||||
| GET | `/api/subscriptions` | Eigene Abos |
|
||||
| GET | `/api/votes?drucksache=X` | Alle Votes + eigenes Vote-Flag |
|
||||
|
||||
### Public (45)
|
||||
|
||||
Alle Lese-Endpoints und statische Seiten sind offen. Darunter u.a.:
|
||||
|
||||
- **Seiten:** `/`, `/antrag/{ds}`, `/classic`, `/auswertungen`, `/methodik`, `/quellen`, `/impressum`, `/datenschutz`, `/health`, `/v2/{merkliste,tags,cluster,neu,batch}`
|
||||
- **API-Listen:** `/api/assessments`, `/api/assessment`, `/api/clusters`, `/api/bundeslaender`, `/api/programme`, `/api/search`, `/api/search-landtag`, `/api/feed.xml`, `/api/wahlprogramm-cite`
|
||||
- **Auswertungen:** `/api/auswertungen/{matrix,zeitreihe,themen-matrix,export.csv,export.json}`
|
||||
- **Auth-Flow:** `/api/auth/login`, `/api/auth/register`, `/api/auth/callback`, `/api/auth/login-url`, `/api/auth/refresh`, `/unsubscribe/{sub}/{token}`
|
||||
- **Jobs:** `/api/analyze-drucksache`-Ergebnisse via `/status/{job_id}`, `/result/{job_id}`, `/result/{job_id}/pdf`, `/api/queue/status`
|
||||
|
||||
Das heißt: **Lesen und Navigieren braucht keinen Account**. Erst Aktionen (Merken, Kommentieren, Bewerten, neue Analyse starten) erfordern Login.
|
||||
|
||||
## UI-Sichtbarkeit — was sieht wer
|
||||
|
||||
### v2-Frontend (`/`, `/antrag/*`, `/v2/*`)
|
||||
|
||||
| UI-Element | Gast | Registriert | Admin |
|
||||
|---|:-:|:-:|:-:|
|
||||
| Sidebar-Gruppe „Lesen" (Durchsuchen / Merkliste / Tags / Cluster) | ✓ | ✓ | ✓ |
|
||||
| Sidebar-Gruppe „Prüfen" (Neuer Antrag / Batch-Analyse) | ✓ Links | ✓ funktional | ✓ funktional |
|
||||
| Sidebar-Gruppe „Daten" (Auswertungen / Export / Feed) | ✓ | ✓ | ✓ |
|
||||
| Sidebar-Gruppe „Administration" (Freischaltungen / Queue / Abos) | — | — | ✓ (via `{% if is_admin %}` in `base.html:61`) |
|
||||
| Topbar „Klassische Ansicht" + Theme-Toggle | ✓ | ✓ | ✓ |
|
||||
| `/v2/merkliste` Bookmark-Liste | Login-CTA | eigene Liste | eigene Liste |
|
||||
| Bookmark-Stern auf Antragsdetail | Login-CTA | ✓ | ✓ |
|
||||
| Kommentar-Form | Login-CTA | ✓ | ✓ |
|
||||
| Vote-Buttons | Login-CTA | ✓ | ✓ |
|
||||
| Re-Analyze-Button | — | ✓ (nach Ablauf) | ✓ jederzeit |
|
||||
| Delete-Assessment-Button | — | — | ✓ |
|
||||
|
||||
### Classic-Frontend (`/classic`)
|
||||
|
||||
| UI-Element | Gast | Registriert | Admin |
|
||||
|---|:-:|:-:|:-:|
|
||||
| Listenansicht + Detail | ✓ | ✓ | ✓ |
|
||||
| Hamburger → Anmelden/Registrieren | ✓ (öffnet Modal) | — | — |
|
||||
| Hamburger → Auswertungen / Quellen / Methodik | ✓ | ✓ | ✓ |
|
||||
| Merkliste-Tab | ✓ (localStorage, gerätegebunden) | ✓ (synced mit Server) | ✓ |
|
||||
| Kommentare anlegen | Login-CTA | ✓ | ✓ |
|
||||
| Admin-Tab | — | — | ✓ (Freischaltungen, Queue, Batch) |
|
||||
|
||||
## Login-/Auth-Flows
|
||||
|
||||
Zwei Varianten koexistieren:
|
||||
|
||||
### Direct Access Grant (Default in v2 + `/classic`-Modal)
|
||||
|
||||
1. User klickt „Anmelden" → Modal öffnet
|
||||
2. POST `/api/auth/login` mit `username`+`password`
|
||||
3. Server ruft Keycloak `grant_type=password` gegen Client `gwoe-antragspruefer`
|
||||
4. Setzt `access_token` (HttpOnly-Cookie) + `rt` (Refresh-Token, Path `/api/auth/refresh`)
|
||||
5. Modal schließt, UI refreshed via `/api/auth/me`
|
||||
|
||||
**Voraussetzung:** Keycloak-Client `gwoe-antragspruefer` hat `directAccessGrantsEnabled: true` (ist gesetzt seit 2026-04-20).
|
||||
|
||||
### OIDC Redirect (Fallback)
|
||||
|
||||
`/api/auth/login-url` → Keycloak-Login-Seite → `/api/auth/callback` → Cookie gesetzt → Redirect auf `/`.
|
||||
|
||||
Wurde mit der Direct-Access-Variante überflüssig, bleibt für Notfall erhalten.
|
||||
|
||||
## Admin-Rollen definieren
|
||||
|
||||
Im Keycloak-Admin:
|
||||
|
||||
1. Realm `collaboration` → **Roles** → Rolle `admin` anlegen (oder `gwoe-admin`)
|
||||
2. User → dem jeweiligen Nutzer die Rolle zuweisen
|
||||
3. Code prüft in `auth.py:220`: `"admin" in roles or "gwoe-admin" in roles`
|
||||
|
||||
Kein Fein-Granular-Rollen-Modell vorgesehen — `admin` ist alles-oder-nichts.
|
||||
|
||||
## Dev-Bypass
|
||||
|
||||
In `.env`: `AUTH_ENABLED=false` setzt alle Guards auf Bypass. `require_auth` gibt `Dev-Modus`-User zurück (`sub=anonymous`, `roles=[]`), `require_admin` gibt `roles=["admin"]`. Nur für lokale Entwicklung ohne Keycloak-Stack. In Prod immer auf `true`.
|
||||
|
||||
## Änderung gegenüber alter Doku
|
||||
|
||||
`docs/reference/api.md` hatte eine stale „Auth"-Spalte mit „Keycloak (geplant)". Diese Doku ersetzt sie inhaltlich — das api.md könnte zusammengelegt oder auf diese Seite verlinken.
|
||||
|
||||
## Wartungshinweis
|
||||
|
||||
Diese Doku wird nicht automatisch generiert. Bei neuen Routes / Guards manuell nachpflegen oder via `docs/reference/scan-zugriffsrechte.py` (TODO: hat bisher keiner geschrieben) regenerieren.
|
||||
@ -10,4 +10,5 @@ filterwarnings =
|
||||
markers =
|
||||
integration: live HTTP/PDF/LLM/DB tests, slow, may flake on backend issues
|
||||
slow: tests that take > 5s, opt out via -m "integration and not slow"
|
||||
addopts = -m "not integration"
|
||||
e2e: Playwright browser tests against live site, requires chromium
|
||||
addopts = -m "not integration and not e2e"
|
||||
|
||||
85
scripts/deploy.sh
Executable file
85
scripts/deploy.sh
Executable file
@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
# Deploy-Script mit Uptime-Kuma-Wartungsmodus
|
||||
# Usage: ./scripts/deploy.sh [files...]
|
||||
# Ohne Argumente: alles deployen
|
||||
#
|
||||
# Setzt den GWÖ-Monitor in Uptime Kuma auf Wartung,
|
||||
# deployed, und aktiviert den Monitor wieder.
|
||||
#
|
||||
# Benötigt: UPTIME_KUMA_USER + UPTIME_KUMA_PASS in ~/.env oder als ENV
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
SERVER="vserver"
|
||||
REMOTE_DIR="/opt/gwoe-antragspruefer"
|
||||
UPTIME_KUMA_URL="https://status.toppyr.de"
|
||||
MONITOR_ID=9 # GWÖ-Antragsprüfer
|
||||
|
||||
# Credentials laden
|
||||
if [ -f ~/.env ]; then
|
||||
source ~/.env
|
||||
fi
|
||||
|
||||
cd "$PROJECT_DIR"
|
||||
|
||||
echo "=== GWÖ-Antragsprüfer Deploy ==="
|
||||
|
||||
# 1. Uptime Kuma auf Wartung setzen
|
||||
if [ -n "${UPTIME_KUMA_USER:-}" ] && [ -n "${UPTIME_KUMA_PASS:-}" ]; then
|
||||
echo "⏸ Setze Monitor auf Wartung..."
|
||||
python3 -c "
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
api = UptimeKumaApi('$UPTIME_KUMA_URL')
|
||||
api.login('$UPTIME_KUMA_USER', '$UPTIME_KUMA_PASS')
|
||||
api.pause_monitor($MONITOR_ID)
|
||||
api.disconnect()
|
||||
print(' Monitor pausiert')
|
||||
" 2>/dev/null || echo " (Uptime Kuma nicht erreichbar, überspringe)"
|
||||
else
|
||||
echo "⚠ UPTIME_KUMA_USER/PASS nicht gesetzt, überspringe Wartungsmodus"
|
||||
fi
|
||||
|
||||
# 2. Build + Deploy
|
||||
if [ $# -gt 0 ]; then
|
||||
# Spezifische Files
|
||||
echo "📦 Packe: $@"
|
||||
tar czf /tmp/gwoe-deploy.tar.gz "$@"
|
||||
else
|
||||
# Alles
|
||||
echo "📦 Packe gesamtes Projekt (ohne venv/data/reports)..."
|
||||
tar czf /tmp/gwoe-deploy.tar.gz \
|
||||
--exclude='venv' --exclude='__pycache__' \
|
||||
--exclude='data' --exclude='reports' --exclude='.env' .
|
||||
fi
|
||||
|
||||
echo "🚀 Upload + Build..."
|
||||
scp /tmp/gwoe-deploy.tar.gz "$SERVER:/tmp/"
|
||||
ssh "$SERVER" "cd $REMOTE_DIR && tar xzf /tmp/gwoe-deploy.tar.gz && docker compose up -d --build" 2>&1 | tail -5
|
||||
|
||||
# 3. Warte auf Health
|
||||
echo "⏳ Warte auf Health-Check..."
|
||||
for i in $(seq 1 30); do
|
||||
code=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 3 "https://gwoe.toppyr.de/health" 2>/dev/null || echo "000")
|
||||
if [ "$code" = "200" ]; then
|
||||
echo "✅ Health OK nach ${i}s"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 4. Uptime Kuma wieder aktivieren
|
||||
if [ -n "${UPTIME_KUMA_USER:-}" ] && [ -n "${UPTIME_KUMA_PASS:-}" ]; then
|
||||
echo "▶ Reaktiviere Monitor..."
|
||||
python3 -c "
|
||||
from uptime_kuma_api import UptimeKumaApi
|
||||
api = UptimeKumaApi('$UPTIME_KUMA_URL')
|
||||
api.login('$UPTIME_KUMA_USER', '$UPTIME_KUMA_PASS')
|
||||
api.resume_monitor($MONITOR_ID)
|
||||
api.disconnect()
|
||||
print(' Monitor aktiv')
|
||||
" 2>/dev/null || echo " (Uptime Kuma nicht erreichbar)"
|
||||
fi
|
||||
|
||||
echo "=== Deploy abgeschlossen ==="
|
||||
19
scripts/run-digest.sh
Executable file
19
scripts/run-digest.sh
Executable file
@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
# Runs the daily email digest (Issue #124).
|
||||
# Install as host cron:
|
||||
# crontab -e
|
||||
# 0 7 * * * /opt/gwoe-antragspruefer/scripts/run-digest.sh >> /var/log/gwoe-digest.log 2>&1
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER=gwoe-antragspruefer
|
||||
|
||||
# Nur ausführen wenn Container läuft
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
|
||||
echo "$(date -Iseconds) SKIP — ${CONTAINER} is not running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$(date -Iseconds) START daily digest"
|
||||
docker exec "$CONTAINER" python -m app.mail
|
||||
echo "$(date -Iseconds) END"
|
||||
27
scripts/run-monitoring-scan.sh
Executable file
27
scripts/run-monitoring-scan.sh
Executable file
@ -0,0 +1,27 @@
|
||||
#!/bin/bash
|
||||
# Runs the daily monitoring scan for new Drucksachen (Issue #135).
|
||||
# Scannt alle aktiven Bundesländer auf neue Drucksachen (nur Metadaten,
|
||||
# kein PDF-Download, kein LLM-Call) und verschickt einen Mail-Digest.
|
||||
#
|
||||
# Install as host cron (nach Review durch Parent — Issue #135):
|
||||
# crontab -e
|
||||
# 30 6 * * * /opt/gwoe-antragspruefer/scripts/run-monitoring-scan.sh >> /var/log/gwoe-monitoring.log 2>&1
|
||||
#
|
||||
# Empfänger-Adresse kann als erstes Argument übergeben werden:
|
||||
# run-monitoring-scan.sh mail@tobiasroedel.de
|
||||
# Default: mail@tobiasroedel.de
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER=gwoe-antragspruefer
|
||||
RECIPIENT="${1:-mail@tobiasroedel.de}"
|
||||
|
||||
# Nur ausführen wenn Container läuft
|
||||
if ! docker ps --format '{{.Names}}' | grep -q "^${CONTAINER}$"; then
|
||||
echo "$(date -Iseconds) SKIP — ${CONTAINER} is not running"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "$(date -Iseconds) START monitoring scan (recipient: ${RECIPIENT})"
|
||||
docker exec "$CONTAINER" python -m app.monitoring "$RECIPIENT"
|
||||
echo "$(date -Iseconds) END"
|
||||
112
scripts/smoke-test.sh
Executable file
112
scripts/smoke-test.sh
Executable file
@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# Gesamt-Funktionsprüfung — Smoke-Test für GWÖ-Antragsprüfer Live-System.
|
||||
# Usage: ./scripts/smoke-test.sh [base-url]
|
||||
# Default: https://gwoe.toppyr.de
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
BASE="${1:-https://gwoe.toppyr.de}"
|
||||
PASS=0; FAIL=0; TOTAL=0
|
||||
|
||||
check() {
|
||||
local name="$1" expected="$2" url="$3" extra="${4:-}"
|
||||
TOTAL=$((TOTAL+1))
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" $extra "$BASE$url" 2>&1 || echo "000")
|
||||
if [[ "$code" == "$expected" ]]; then
|
||||
printf " ✓ %-40s %s\n" "$name" "$code"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
printf " ✗ %-40s expected=%s got=%s\n" "$name" "$expected" "$code"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
contains() {
|
||||
local name="$1" pattern="$2" url="$3"
|
||||
TOTAL=$((TOTAL+1))
|
||||
local body
|
||||
body=$(curl -s "$BASE$url" 2>&1)
|
||||
if echo "$body" | grep -qE "$pattern"; then
|
||||
printf " ✓ %-40s pattern=%s\n" "$name" "$pattern"
|
||||
PASS=$((PASS+1))
|
||||
else
|
||||
printf " ✗ %-40s pattern=%s NOT FOUND\n" "$name" "$pattern"
|
||||
FAIL=$((FAIL+1))
|
||||
fi
|
||||
}
|
||||
|
||||
echo "================================================================"
|
||||
echo " GWÖ-Antragsprüfer Smoke-Test gegen $BASE"
|
||||
echo " $(date -Iseconds)"
|
||||
echo "================================================================"
|
||||
|
||||
echo
|
||||
echo "[1] Hauptseiten erreichbar (alle 200)"
|
||||
check "v2 Default /" "200" "/"
|
||||
check "v2 Detail (echte DS)" "200" "/antrag/21/754S"
|
||||
check "Classic /classic" "200" "/classic"
|
||||
check "/auswertungen" "200" "/auswertungen"
|
||||
check "/methodik" "200" "/methodik"
|
||||
check "/quellen" "200" "/quellen"
|
||||
check "/impressum" "200" "/impressum"
|
||||
check "/datenschutz" "200" "/datenschutz"
|
||||
check "/v2/merkliste" "200" "/v2/merkliste"
|
||||
check "/v2/tags" "200" "/v2/tags"
|
||||
check "/v2/cluster" "200" "/v2/cluster"
|
||||
check "/v2/landtag-suche" "200" "/v2/landtag-suche"
|
||||
check "/v2/neu" "200" "/v2/neu"
|
||||
check "/v2/batch" "200" "/v2/batch"
|
||||
check "/health" "200" "/health"
|
||||
|
||||
echo
|
||||
echo "[2] API-Endpoints (öffentlich)"
|
||||
check "/api/assessments" "200" "/api/assessments"
|
||||
check "/api/bundeslaender" "200" "/api/bundeslaender"
|
||||
check "/api/clusters" "200" "/api/clusters"
|
||||
check "/api/queue/status" "200" "/api/queue/status"
|
||||
check "/api/programme" "200" "/api/programme"
|
||||
check "/api/feed.xml" "200" "/api/feed.xml"
|
||||
check "/api/search?q=klima" "200" "/api/search?q=klima"
|
||||
check "/api/auswertungen/matrix" "200" "/api/auswertungen/matrix"
|
||||
check "/api/auswertungen/export.csv" "200" "/api/auswertungen/export.csv"
|
||||
check "/api/auth/me (unauth)" "200" "/api/auth/me"
|
||||
|
||||
echo
|
||||
echo "[3] API-Endpoints (Auth-Schutz)"
|
||||
check "POST analyze-drucksache (no auth)" "401" "/api/analyze-drucksache" "-X POST -d ''"
|
||||
check "POST bookmark (no auth)" "401" "/api/bookmark" "-X POST"
|
||||
check "POST comment (no auth)" "401" "/api/comment" "-X POST"
|
||||
check "POST vote (no auth)" "401" "/api/vote" "-X POST"
|
||||
|
||||
echo
|
||||
echo "[4] Statics (CSS, Fonts, Icons)"
|
||||
check "v2 tokens.css" "200" "/static/v2/tokens.css"
|
||||
check "v2 v2.css" "200" "/static/v2/v2.css"
|
||||
check "Nunito-Sans woff2" "200" "/static/v2/fonts/nunito-sans-latin-variable.woff2"
|
||||
check "Phosphor magnifying-glass" "200" "/static/v2/icons/phosphor/magnifying-glass.svg"
|
||||
|
||||
echo
|
||||
echo "[5] Inhalts-Checks"
|
||||
contains "v2 rendert AppShell" 'v2-shell' "/"
|
||||
contains "v2 Sidebar vorhanden" 'v2-sidebar' "/"
|
||||
contains "v2 hat Login-Button" 'v2-auth-control' "/"
|
||||
contains "Detail rendert ScoreHero" 'big-num' "/antrag/21/754S"
|
||||
contains "Detail rendert MatrixMini" 'matrix-mini' "/antrag/21/754S"
|
||||
contains "Detail rendert Programm-Treue" 'Programm-Treue' "/antrag/21/754S"
|
||||
contains "Detail rendert Voting" 'castVote|v2DetailCastVote' "/antrag/21/754S"
|
||||
contains "Detail rendert Kommentare" 'v2-comments|loadComments' "/antrag/21/754S"
|
||||
contains "Detail OG-Meta-Tags" 'og:image' "/antrag/21/754S"
|
||||
contains "Cluster-API liefert JSON" 'clusters' "/api/clusters"
|
||||
contains "Auswertungen-Matrix JSON" 'bundeslaender' "/api/auswertungen/matrix"
|
||||
contains "Feed ist Atom" '<feed|application' "/api/feed.xml"
|
||||
|
||||
echo
|
||||
echo "[6] Live-Suche NRW (durchlauf)"
|
||||
check "/api/search-landtag (NRW)" "200" "/api/search-landtag?q=schule&bundesland=NRW"
|
||||
|
||||
echo
|
||||
echo "================================================================"
|
||||
echo " Ergebnis: $PASS/$TOTAL bestanden, $FAIL Fehler"
|
||||
echo "================================================================"
|
||||
exit $FAIL
|
||||
Loading…
Reference in New Issue
Block a user