Zum Inhalt springen

Architektur

Ein Scraper für das Oldenburger Ratsinformationssystem speist eine SQLite-Datenbank. Ein Web-Frontend (FastAPI + Next.js) sitzt auf denselben Daten und liefert personalisierte Benachrichtigungen per E-Mail und Web-Push.

flowchart TD
  RIS["Oldenburger Ratsinformationssystem
  (SessionNet)"]
  SCRAPER["Scraper (council/)
  Sitzungen · Protokolle · Vorlagen · Anlagen"]
  DB[("SQLite
  Ratsdaten · Konten & Themen")]
  LLM["LLM via OpenRouter
  zusammenfassen · matchen · klassifizieren"]
  API["FastAPI-Backend"]
  WEB["Next.js-Frontend
  ratslotse.de · iOS-App"]
  NOTIFY["Benachrichtigungen
  E-Mail (Resend) · Web-Push (APNs/FCM)"]

  RIS -->|"HTML & PDFs — ohne LLM"| SCRAPER
  SCRAPER --> DB
  DB <--> LLM
  DB --> API
  API --> WEB
  DB --> NOTIFY

Welche Dokumente im Einzelnen ausgewertet werden und was daraus entsteht, zeigt die Übersichtsgrafik unter Ratsdokumente & Beschlüsse.


Das Ratsinformationssystem (buergerinfo.oldenburg.de) läuft auf SessionNet (Somacos GmbH). Es gibt keine öffentliche API — die Seiten werden mit BeautifulSoup4 gescrapt:

  • die Kalenderseite liefert alle Sitzungen eines Monats,
  • die Sitzungs-Detailseite Kopfdaten (Datum/Zeit/Ort) und die Tagesordnungstabelle (TOPs mit Ö sind öffentlich, N nicht-öffentlich),
  • die Vorlagen-Seite Vorlagen- und Anlagen-PDFs (council/vorlagen.py, ausgewertet ganz ohne LLM).

Der Store merkt sich, welche Sitzungen bereits gesehen und welche Alerts schon versendet wurden — das verhindert Doppel-Benachrichtigungen.

Die Klassifikation läuft über OpenRouter (OpenAI-SDK) und ist bewusst zustandslos — kein Fine-Tuning, strukturierte Prompts mit JSON-Mode. Die Modelle sind je Aufgabe über Env-Variablen konfigurierbar; Defaults greifen ohne Konfiguration.

Details: siehe KI-Pipeline.

Jeder Nutzer wählt pro Konto einen Zustellkanal (email / push / both — oder off für gar nicht) und zusätzlich, wofür er Post bekommt — sechs Anlässe, einzeln abschaltbar. Zustellung: E-Mail via Resend, Web-Push auf die registrierten App-Geräte via APNs/FCM. Fehlt das Secret eines Kanals, wird dieser still übersprungen.

Kein Anlass sendet selbst: Alle reihen in notification_queue ein, und nwz.notify.zustellen() liefert zentral aus — unter zwei harten Grenzen (höchstens zwei Zustellungen pro Person und Tag, nichts zwischen 21 und 7 Uhr). Termingebundene Anlässe — die Vorabend-Erinnerung — zählen dabei in einem eigenen Topf, sonst kämen sie am Sitzungstag statt am Vortag. Details in App & Konten.


Zwei SQLite-Dateien: eine für Konten & Themen, eine für die Ratsdaten (siehe ADR 0005).

Die Trennlinie ist: alles Nutzerbezogene liegt in nwz.sqlite, alle Ratsdaten in council.sqlite. Deshalb liegen z. B. Themen-Treffer und Quiz-Antworten in der Konten-DB, obwohl sie auf Beschlüsse bzw. Fragen zeigen — ein Join über beide Dateien ist nicht möglich, solche Zeilen denormalisieren die nötigen Felder.

Konten & Nutzerdaten (nwz.sqlite)

  • web_users — Konten (E-Mail, Passwort-Hash, Rolle, Status, display_name, delivery_channel, badges, onboarding, apple_sub, token_version)
  • topics, committee_subscriptions — Themen-Watchlist und Ausschuss-Abos
  • council_topic_matches, topic_hits_seen — Beschluss-Treffer je Thema und was davon schon gesehen wurde („Neu“-Zähler)
  • council_agenda_matches, council_agenda_classified — Tagesordnungs-Treffer und bereits klassifizierte Agenden
  • push_tokens, email_verification_tokens, password_reset_tokens — Push und E-Mail-/Passwort-Flows
  • quiz_answers, quiz_ratings, quiz_daily, user_quiz_questions — Quiz: Antworten/Punkte, 👍/👎, tägliche Challenge, eigene Fragen
  • user_activity, llm_usage — Aktivitätssignale und LLM-Kostenerfassung
  • prompts — live editierbare Prompt-Overrides (Admin-UI)

Ratsdaten (council.sqlite)

  • council_sessions, council_agenda_items, council_scheduled_sessions — Sitzungen, Tagesordnungen und terminierte Sitzungen ohne Tagesordnung
  • council_alerts_sent, committee_notifications — Deduplizierung der Benachrichtigungen
  • committee_summaries — gecachte Agenda-Zusammenfassungen
  • council_protocols, council_decisions, council_attendance, council_vorlagen, council_anlagen, council_beratungen — Dokument-Auswertung und Beratungsfolge (siehe Beschlüsse)
  • council_persons, council_memberships — Mandatsträger:innen und Gremien
  • council_entities (+ _links, _meta, _obs, _scanned) — erkannte Orte und Projekte samt Geokoordinaten für die Stadtkarte
  • council_embeddings, council_similar — Vektoren und vorberechnete Nachbarn („Ähnliche Beschlüsse“, siehe ADR 0003)
  • council_field_recaps, council_goal_links — Themenfeld-Rückblicke und Ziele-Tracking
  • council_fundstuecke — kuratierte „Fundstück des Tages“-Karten (siehe Bewertungs-Scores)
  • council_quiz_questions, council_haushalt — Quizfragen und Haushaltszahlen
  • council_decisions_fts — FTS5-Volltextindex über die Beschlüsse

Alle Auswertungen laufen als geplante Jobs auf dem Server — jeder Lauf ist idempotent und meldet Fehler per E-Mail an die Betreiber-Adresse:

Rhythmus Aufgabe
täglich Datenbank-Backup (rotierend, optional off-site gespiegelt)
täglich Ausschuss-Tagesordnungen zusammenfassen
mehrmals täglich Sitzungen auf Themen-Treffer prüfen (inkl. geänderter Tagesordnungen)
täglich neue Protokolle parsen — und in einem Rutsch: Vorlagen & Anlagen nachladen, Beschlüsse klassifizieren, Beträge erkennen, „Lotti erklärt’s einfach“ schreiben, Tragweite und Gesprächswert bewerten, Wichtigkeit neu berechnen
wöchentlich Backfills als Auffangnetz: Entitäten & Beschreibungen, Geocoding, Embeddings/Ähnliche, Themen↔Beschlüsse, Rückblicke, Stammdaten, Scores in Tranchen, Quizfragen, Fundstücke

Der tägliche Protokoll-Lauf deckt neue Beschlüsse tagesaktuell ab (mit Obergrenzen je Lauf), der wöchentliche Lauf holt in Tranchen den Bestand nach. Details und Zeitpläne: Betrieb.


  • Hosting: ein App-Server hinter einer Edge-VM; Caddy auf der Edge terminiert TLS und proxyt auf das Next.js-Frontend (siehe ADR 0004).
  • Prozesse: Backend (FastAPI/uvicorn) und Frontend (Next.js) laufen als systemd-Services.
  • CI/CD: GitHub Actions. Nur ein gemergter Pull Request nach main löst den Deploy aus (rsync + Service-Restart + Frontend-Build) — ein direkter Push auf main läuft nur durch die Tests (siehe ADR 0008).
  • Secrets: .env liegt nur auf dem Server, nie im Git; der Deploy-Key ist ein GitHub-Actions-Secret.