Monitoring: täglicher Scan aller Landtags-Adapter + Mail-Digest (kein Auto-Fetch) #135

Closed
opened 2026-04-20 01:07:19 +02:00 by tobias · 2 comments
Owner

Kontext

Aus Gespräch dotty 2026-04-20: wir wollen täglich einen Überblick, was sich in
allen aktiven Landtags-Datenquellen seit gestern getan hat — ohne dass
dabei Anträge automatisch gezogen oder bewertet werden. Reine Monitoring-/
"Was wäre wenn"-Phase, um zu sehen

  • wie viel Volumen täglich überhaupt durchläuft,
  • wo wir Rückstand aufbauen,
  • was eine Voll-Bewertung kosten würde,
  • ob Adapter still kaputt gehen (0-Treffer-Drift).

Strikt kein Auto-Fetch, kein Auto-Analyze. Die Entscheidung, einen Antrag
zu bewerten, bleibt beim User.

Scope

In-scope:

  • Täglicher Cron-Job, der alle aktiven Adapter nach neuen Drucksachen fragt
  • Persistenz der Scan-Ergebnisse (Metadaten, kein PDF-Download)
  • Aggregierte Statistiken pro Tag/BL
  • Mail-Digest an User mit Übersicht + Kosten-Schätzung

Out-of-scope (explizit):

  • Kein PDF-Download
  • Kein LLM-Call
  • Kein automatisches Einreihen in die Queue
  • Keine Antrag-Volltext-Indexierung

Datenmodell

CREATE TABLE monitoring_scans (
    bundesland TEXT NOT NULL,
    drucksache TEXT NOT NULL,
    title TEXT,
    typ TEXT,
    fraktionen TEXT,              -- JSON-Array
    datum_antrag DATE,
    pdf_url TEXT,
    seen_first_at TIMESTAMP NOT NULL,
    last_seen_at TIMESTAMP NOT NULL,
    is_assessed BOOLEAN DEFAULT 0, -- JOIN-Flag gegen assessments
    PRIMARY KEY (bundesland, drucksache)
);

CREATE TABLE monitoring_daily_summary (
    scan_date DATE NOT NULL,
    bundesland TEXT NOT NULL,
    total_found INTEGER,
    new_since_last_scan INTEGER,
    errors TEXT,
    adapter_latency_ms INTEGER,
    PRIMARY KEY (scan_date, bundesland)
);

CREATE INDEX idx_scans_seen ON monitoring_scans(seen_first_at);
CREATE INDEX idx_summary_date ON monitoring_daily_summary(scan_date DESC);

Primary-Key-Begründung: (bundesland, drucksache) ist eindeutig innerhalb
einer Wahlperiode. WP-Wechsel ist Randfall und wird mit datum_antrag als
Tiebreaker erkannt (falls derselbe String in neuer WP auftaucht, entscheidet
das Datum).

Arbeitsschritte

Core (Must-have):

  • Migration: Tabellen monitoring_scans + monitoring_daily_summary in app/database.py
  • app/monitoring.py: async def daily_scan() iteriert aktive_bundeslaender(), ruft adapter.search(limit=N), UPSERT in monitoring_scans, aggregiert in monitoring_daily_summary
  • Kosten-Schätzung: estimate_cost(new_count, avg_input_tokens, avg_output_tokens, model) — pro Modell ein statischer Preissatz in app/config.py (Qwen-Plus: ~$0.0005/1K in, $0.0015/1K out; Qwen-Max höher)
  • app/mail.py: neues Template monitoring_digest.html/.txt, empfängt MonitoringDigestData
  • scripts/run-monitoring-scan.sh analog zu run-digest.sh
  • Host-Cron 0 6 * * * (vor dem E-Mail-Digest um 7)

Kennzahlen im Digest:

  • Neue Anträge pro BL pro Tag (Tabelle + Summe)
  • Typ-Verteilung (Antrag / Gesetzentwurf / Große Anfrage / …)
  • Fraktions-Verteilung der Neuen
  • Publikations-Delay: datum_antragseen_first_at (Median pro BL)
  • Adapter-Health: rote Zeile wenn BL ≥ 3 Tage 0 Treffer
  • 7-Tage-Moving-Average pro BL
  • Kosten-Schätzung für heutige Neue: Σ (est_cost_per_antrag)
  • Backlog: ∑ monitoring_scans WHERE NOT is_assessed pro BL
  • Neu-Aufgelegte-Flag (Drucksache mit (neu)-Suffix)

Nice-to-have (später, separate Tickets wenn umfangreich):

  • Trend-Sparklines im Mail-HTML (inline SVG, 7 Tage pro BL)
  • "Was wäre wenn"-Szenarien: was würde die Voll-Bewertung nach Modell-Wahl kosten (Qwen-Plus vs Max)
  • Schwellwert-Alerts: Mail nur wenn ≥ X neue Anträge oder ≥ N ct Kosten
  • Fraktions-Highlight: "AfD hat heute Y Anträge gestellt" (ungewöhnliche Aktivität)
  • Thema-Vorhersage ohne LLM: Keyword-Heuristik auf Titel (klimaschutz, digital, …) — reine Text-Match, kein LLM
  • Duplikat-Detektion inter-BL: gleicher Titel in mehreren BL am selben Tag (koordinierte Kampagne)
  • Dashboard-Seite /monitoring mit Zeitreihen-Charts (Chart.js), Mail verlinkt darauf
  • CSV-Export GET /api/monitoring/export.csv für Long-Format-Analyse
  • RSS-Feed GET /monitoring.rss mit neuen Anträgen (read-only, kein Assessment)
  • Seasonality-Detect: Wochenende/Ferien-Muster automatisch erkennen und im Digest neutralisieren
  • "Ignore-Liste" pro User: Typen/Fraktionen die nie im Digest erscheinen sollen

Akzeptanzkriterien

  • Cron läuft täglich 06:00, Ergebnis in /var/log/gwoe-monitoring.log
  • Mail geht an dotty mit Übersicht aller aktiven BL
  • Keine Queue-Einträge, kein LLM-Call während des Scans
  • monitoring_scans wächst monoton, monitoring_daily_summary hat einen Eintrag pro (Tag, BL)
  • Kosten-Schätzung im Digest stimmt ±20% mit tatsächlichen qwen-plus-Tarifen überein
  • Wenn ein Adapter ≥ 3 Tage 0 Treffer liefert, wird das rot markiert im Digest

Offene Entscheidungen

  • Mail-Frequenz: täglich (default) oder nur an Werktagen?
  • Limit pro Adapter: search(limit=50) oder mehr? Balance zwischen Vollständigkeit und HTTP-Last
  • Retention: monitoring_scans unbegrenzt behalten oder nach 1 Jahr pruneeen?

Bezug

  • #124 E-Mail-Benachrichtigung (Infrastruktur wiederverwendbar: SMTP, Template-Rendering)
  • #127 Typ-Erkennung (dieselbe drucksache_typen.py für Typ-Verteilung)
  • ADR 0002 Adapter-Architektur
## Kontext Aus Gespräch dotty 2026-04-20: wir wollen täglich einen Überblick, was sich in allen aktiven Landtags-Datenquellen seit gestern getan hat — **ohne** dass dabei Anträge automatisch gezogen oder bewertet werden. Reine Monitoring-/ "Was wäre wenn"-Phase, um zu sehen - wie viel Volumen täglich überhaupt durchläuft, - wo wir Rückstand aufbauen, - was eine Voll-Bewertung kosten würde, - ob Adapter still kaputt gehen (0-Treffer-Drift). **Strikt kein Auto-Fetch, kein Auto-Analyze.** Die Entscheidung, einen Antrag zu bewerten, bleibt beim User. ## Scope **In-scope:** - Täglicher Cron-Job, der alle aktiven Adapter nach neuen Drucksachen fragt - Persistenz der Scan-Ergebnisse (Metadaten, kein PDF-Download) - Aggregierte Statistiken pro Tag/BL - Mail-Digest an User mit Übersicht + Kosten-Schätzung **Out-of-scope (explizit):** - Kein PDF-Download - Kein LLM-Call - Kein automatisches Einreihen in die Queue - Keine Antrag-Volltext-Indexierung ## Datenmodell ```sql CREATE TABLE monitoring_scans ( bundesland TEXT NOT NULL, drucksache TEXT NOT NULL, title TEXT, typ TEXT, fraktionen TEXT, -- JSON-Array datum_antrag DATE, pdf_url TEXT, seen_first_at TIMESTAMP NOT NULL, last_seen_at TIMESTAMP NOT NULL, is_assessed BOOLEAN DEFAULT 0, -- JOIN-Flag gegen assessments PRIMARY KEY (bundesland, drucksache) ); CREATE TABLE monitoring_daily_summary ( scan_date DATE NOT NULL, bundesland TEXT NOT NULL, total_found INTEGER, new_since_last_scan INTEGER, errors TEXT, adapter_latency_ms INTEGER, PRIMARY KEY (scan_date, bundesland) ); CREATE INDEX idx_scans_seen ON monitoring_scans(seen_first_at); CREATE INDEX idx_summary_date ON monitoring_daily_summary(scan_date DESC); ``` Primary-Key-Begründung: `(bundesland, drucksache)` ist eindeutig innerhalb einer Wahlperiode. WP-Wechsel ist Randfall und wird mit `datum_antrag` als Tiebreaker erkannt (falls derselbe String in neuer WP auftaucht, entscheidet das Datum). ## Arbeitsschritte **Core (Must-have):** - [ ] Migration: Tabellen `monitoring_scans` + `monitoring_daily_summary` in `app/database.py` - [ ] `app/monitoring.py`: `async def daily_scan()` iteriert `aktive_bundeslaender()`, ruft `adapter.search(limit=N)`, UPSERT in `monitoring_scans`, aggregiert in `monitoring_daily_summary` - [ ] Kosten-Schätzung: `estimate_cost(new_count, avg_input_tokens, avg_output_tokens, model)` — pro Modell ein statischer Preissatz in `app/config.py` (Qwen-Plus: ~$0.0005/1K in, $0.0015/1K out; Qwen-Max höher) - [ ] `app/mail.py`: neues Template `monitoring_digest.html/.txt`, empfängt `MonitoringDigestData` - [ ] `scripts/run-monitoring-scan.sh` analog zu `run-digest.sh` - [ ] Host-Cron `0 6 * * *` (vor dem E-Mail-Digest um 7) **Kennzahlen im Digest:** - [ ] Neue Anträge pro BL pro Tag (Tabelle + Summe) - [ ] Typ-Verteilung (Antrag / Gesetzentwurf / Große Anfrage / …) - [ ] Fraktions-Verteilung der Neuen - [ ] Publikations-Delay: `datum_antrag` → `seen_first_at` (Median pro BL) - [ ] Adapter-Health: rote Zeile wenn BL ≥ 3 Tage 0 Treffer - [ ] 7-Tage-Moving-Average pro BL - [ ] Kosten-Schätzung für heutige Neue: `Σ (est_cost_per_antrag)` - [ ] Backlog: `∑ monitoring_scans WHERE NOT is_assessed` pro BL - [ ] Neu-Aufgelegte-Flag (Drucksache mit `(neu)`-Suffix) **Nice-to-have (später, separate Tickets wenn umfangreich):** - [ ] Trend-Sparklines im Mail-HTML (inline SVG, 7 Tage pro BL) - [ ] "Was wäre wenn"-Szenarien: was würde die Voll-Bewertung nach Modell-Wahl kosten (Qwen-Plus vs Max) - [ ] Schwellwert-Alerts: Mail nur wenn ≥ X neue Anträge oder ≥ N ct Kosten - [ ] Fraktions-Highlight: "AfD hat heute Y Anträge gestellt" (ungewöhnliche Aktivität) - [ ] Thema-Vorhersage ohne LLM: Keyword-Heuristik auf Titel (klimaschutz, digital, …) — reine Text-Match, kein LLM - [ ] Duplikat-Detektion inter-BL: gleicher Titel in mehreren BL am selben Tag (koordinierte Kampagne) - [ ] Dashboard-Seite `/monitoring` mit Zeitreihen-Charts (Chart.js), Mail verlinkt darauf - [ ] CSV-Export `GET /api/monitoring/export.csv` für Long-Format-Analyse - [ ] RSS-Feed `GET /monitoring.rss` mit neuen Anträgen (read-only, kein Assessment) - [ ] Seasonality-Detect: Wochenende/Ferien-Muster automatisch erkennen und im Digest neutralisieren - [ ] "Ignore-Liste" pro User: Typen/Fraktionen die nie im Digest erscheinen sollen ## Akzeptanzkriterien - Cron läuft täglich 06:00, Ergebnis in `/var/log/gwoe-monitoring.log` - Mail geht an dotty mit Übersicht aller aktiven BL - **Keine** Queue-Einträge, **kein** LLM-Call während des Scans - `monitoring_scans` wächst monoton, `monitoring_daily_summary` hat einen Eintrag pro (Tag, BL) - Kosten-Schätzung im Digest stimmt ±20% mit tatsächlichen qwen-plus-Tarifen überein - Wenn ein Adapter ≥ 3 Tage 0 Treffer liefert, wird das rot markiert im Digest ## Offene Entscheidungen - **Mail-Frequenz:** täglich (default) oder nur an Werktagen? - **Limit pro Adapter:** `search(limit=50)` oder mehr? Balance zwischen Vollständigkeit und HTTP-Last - **Retention:** `monitoring_scans` unbegrenzt behalten oder nach 1 Jahr pruneeen? ## Bezug - #124 E-Mail-Benachrichtigung (Infrastruktur wiederverwendbar: SMTP, Template-Rendering) - #127 Typ-Erkennung (dieselbe `drucksache_typen.py` für Typ-Verteilung) - ADR 0002 Adapter-Architektur
Author
Owner

Initial Scan (limit=500) abgeschlossen

Erfassung: 2543 Drucksachen in monitoring_scans
Deltalogik: 2. Scan-Durchlauf zeigte new=0 für alle bereits erfassten BL — Delta-Erkennung arbeitet korrekt

Befunde pro BL (limit=500)

BL Gesehen Bemerkung
BUND, BW, HE 500 limit erreicht — haben mehr
BB 240
HB 177
BE 154
HH 131
NI 50 Adapter cappt bei 50 trotz limit=500 → eigenes Folge-Ticket
SN, LSA, SH 50 evtl. auch cap — unklar
RP, MV, BY, TH 15-49 normales Volumen, unterhalb limit
NRW 0 Bug — OPAL-Adapter returns [] bei leerem Query
SL 0 Bug — ReadTimeout silent swallowed

Bugs zum Fixen vor Cron-Scharfschaltung

  1. NRW OPAL leerer Query → 0 Treffer — Adapter needs alternative Query-Strategie (z.B. Datum-basiert statt Keyword)
  2. SL silent swallow — ReadTimeout sollte in monitoring_daily_summary.errors landen, nicht verschwiegen werden. Root-Cause liegt im SL-Adapter (swallowt Exception und returned [])
  3. NI cap bei 50 — Adapter ignoriert limit>50 oder hat eigene Pagination-Grenze

Empfehlung: Cron NICHT scharf schalten, bis mindestens NRW und SL sauber scannen. Sonst wird der Digest irreführend („0 neue NRW-Anträge heute" — in Wahrheit Adapter-Bug).

Delta-Metrik in der Kosten-Schätzung

Bei Cron-Scharfschaltung wird new_total realistisch sein (Delta seit gestern). Im initialen Seed sind es 2543 × 4 ct = ca. 102 € fiktive Kosten — das darf einmalig im ersten Mail stehen, dann normalisiert sich das.

Nächste Schritte

  • Folge-Issue: NRW OPAL leerer-Query-Bug
  • Folge-Issue: SL silent-swallow fix (und generelles Adapter-Error-Propagation-Pattern)
  • Optional: NI Adapter-Limit-Drift
  • Dann Cron scharf schalten
## Initial Scan (limit=500) abgeschlossen **Erfassung:** 2543 Drucksachen in `monitoring_scans` **Deltalogik:** 2. Scan-Durchlauf zeigte `new=0` für alle bereits erfassten BL — Delta-Erkennung arbeitet korrekt ### Befunde pro BL (limit=500) | BL | Gesehen | Bemerkung | |----|--------:|-----------| | BUND, BW, HE | 500 | limit erreicht — haben mehr | | BB | 240 | | | HB | 177 | | | BE | 154 | | | HH | 131 | | | NI | 50 | **Adapter cappt bei 50 trotz limit=500** → eigenes Folge-Ticket | | SN, LSA, SH | 50 | evtl. auch cap — unklar | | RP, MV, BY, TH | 15-49 | normales Volumen, unterhalb limit | | **NRW** | **0** | **Bug** — OPAL-Adapter returns [] bei leerem Query | | **SL** | **0** | **Bug** — ReadTimeout silent swallowed | ### Bugs zum Fixen vor Cron-Scharfschaltung 1. **NRW OPAL leerer Query → 0 Treffer** — Adapter needs alternative Query-Strategie (z.B. Datum-basiert statt Keyword) 2. **SL silent swallow** — ReadTimeout sollte in `monitoring_daily_summary.errors` landen, nicht verschwiegen werden. Root-Cause liegt im SL-Adapter (swallowt Exception und returned `[]`) 3. **NI cap bei 50** — Adapter ignoriert `limit>50` oder hat eigene Pagination-Grenze **Empfehlung:** Cron NICHT scharf schalten, bis mindestens NRW und SL sauber scannen. Sonst wird der Digest irreführend („0 neue NRW-Anträge heute" — in Wahrheit Adapter-Bug). ### Delta-Metrik in der Kosten-Schätzung Bei Cron-Scharfschaltung wird `new_total` realistisch sein (Delta seit gestern). Im initialen Seed sind es 2543 × 4 ct = ca. 102 € fiktive Kosten — das darf einmalig im ersten Mail stehen, dann normalisiert sich das. ### Nächste Schritte - [ ] Folge-Issue: NRW OPAL leerer-Query-Bug - [ ] Folge-Issue: SL silent-swallow fix (und generelles Adapter-Error-Propagation-Pattern) - [ ] Optional: NI Adapter-Limit-Drift - [ ] Dann Cron scharf schalten
tobias added this to the 1.0 milestone 2026-04-25 20:59:55 +02:00
Author
Owner

Cron scharf:

30 6 * * * /opt/gwoe-antragspruefer/scripts/run-monitoring-scan.sh >> /var/log/gwoe-monitoring.log 2>&1

Läuft täglich um 06:30 (vor dem Mail-Digest 07:00). Logfile /var/log/gwoe-monitoring.log chmod 666. Test-Lauf gestartet (~1-2 Min, scant 17 BL).

Gesamte Cron-Landschaft jetzt:

  • 0 3 DB-Backup (#1.0-Release)
  • 30 6 Monitoring-Scan (dieses Issue)
  • 0 7 Mail-Digest (#124)

Schließe.

Cron scharf: ``` 30 6 * * * /opt/gwoe-antragspruefer/scripts/run-monitoring-scan.sh >> /var/log/gwoe-monitoring.log 2>&1 ``` Läuft täglich um 06:30 (vor dem Mail-Digest 07:00). Logfile `/var/log/gwoe-monitoring.log` chmod 666. Test-Lauf gestartet (~1-2 Min, scant 17 BL). Gesamte Cron-Landschaft jetzt: - `0 3` DB-Backup (#1.0-Release) - `30 6` Monitoring-Scan (dieses Issue) - `0 7` Mail-Digest (#124) Schließe.
Sign in to join this conversation.
No description provided.