# Spiele-Redaktion **KI-Assistenz für Spielemagazin-Redaktionen** — Multi-User-Webanwendung mit modularer Plugin-Architektur. Lauffähiger Kern mit Plugin-System, Authentifizierung/Rollen und Migrationen; als erstes Fachplugin ist das **Audit-Log** vollständig implementiert, weitere Plugins folgen. ## Stack - **Backend:** Python 3.11+, FastAPI, SQLAlchemy 2 (SQLite, Postgres-fähig) - **Frontend:** HTMX + Jinja2 + Tailwind (CDN) + Alpine.js — kein Build-Step - **Auth:** Session-Cookies (signiert), Passwort-Hashing mit Argon2id - **Plugins:** Entry-Points (`importlib.metadata`, Gruppe `spiele_redaktion.plugins`) plus lokales `plugins/`-Verzeichnis - **Dependencies/Tests:** [uv](https://docs.astral.sh/uv/) + pytest ## Setup ```bash uv sync # venv + Dependencies uv run uvicorn main:app --reload # Dev-Server auf http://127.0.0.1:8000 ``` Beim ersten Start wird automatisch ein Admin-Konto angelegt: | Benutzername | Passwort | Rolle | |--------------|----------|--------| | `admin` | `admin` | admin | > ⚠️ Für alles außer der lokalen Entwicklung vor dem Start > `SPIELE_INITIAL_ADMIN_PASSWORD` setzen und das Passwort nach dem ersten > Login ändern. ### Umgebungsvariablen | Variable | Default | Bedeutung | |----------|---------|-----------| | `SPIELE_DATABASE_URL` | `sqlite:///./data/spiele-redaktion.db` | SQLAlchemy-URL (Postgres: z. B. `postgresql+psycopg://…`) | | `SPIELE_SESSION_SECRET` | Dev-Wert | Secret für signierte Session-Cookies | | `SPIELE_SESSION_DAUER` | `43200` | Session-Dauer in Sekunden (12 h) | | `SPIELE_INITIAL_ADMIN_PASSWORD` | `admin` | Passwort des initialen Admins | | `SPIELE_PLUGINS_DIR` | `/plugins` | Pfad zum lokalen Plugin-Verzeichnis | | `SPIELE_SMTP_HOST` | *(leer)* | SMTP-Server für den E-Mail-Kanal; leer = Dev-Fallback (nur Protokoll) | | `SPIELE_SMTP_PORT` | `587` | SMTP-Port | | `SPIELE_SMTP_BENUTZER` | *(leer)* | SMTP-Login (optional) | | `SPIELE_SMTP_PASSWORT` | *(leer)* | SMTP-Passwort (optional) | | `SPIELE_SMTP_ABSENDER` | Benutzer bzw. `spiele-redaktion@localhost` | From-Adresse | | `SPIELE_SMTP_TLS` | `starttls` | `starttls`, `ssl` oder `keine` | | `SPIELE_TELEGRAM_BOT_TOKEN` | *(leer)* | Bot-Token für den Telegram-Kanal; leer = Dev-Fallback (nur Protokoll) | ## Tests ```bash uv run pytest ``` Abgedeckt: Plugin-Loader lädt alle Plugins (inkl. Lifecycle-Hooks und Entry-Point-Pfad), Migrations-Laufzeit inkl. Idempotenz, Login/Logout, Rollen-Zugriff (admin/redakteur vs. rezensent), Benutzerverwaltung sowie das Audit-Log-Plugin (Logging-Funktion, Migration, Filter, Paginierung, Nur-Admin-Zugriff). ## Architektur ``` src/redaktionskern/ schlanker Kern — KEINE Fachlogik ├── app.py App-Fabrik: Discovery → Migrations → on_load → Routen ├── config.py Einstellungen (Umgebungsvariablen) ├── db.py Engine/Session (SQLite ↔ Postgres portabel) ├── migrationen.py Migrations-Laufzeit (Tabelle schema_migrations pro Plugin) ├── plugin_loader.py Entry-Points + plugins/-Verzeichnis, eindeutige Namen ├── contracts.py Plugin-Vertrag (BasePlugin, Migration, NavEntry, PluginContext) └── auth/ Login/Logout, Rollen, Benutzerverwaltung plugins/ ein Ordner pro Funktion, ladbar über den Plugin-Loader ├── audit-log/ VOLL IMPLEMENTIERT: Model, Migration, API, Admin-Ansicht ├── neuheiten/ benachrichtigung/ (in Arbeit) ├── dedup/ planung/ archiv/ erinnerung/ export/ (Stubs, ladbar) └── … je __init__.py + templates// ``` ### Der Plugin-Vertrag Ein Plugin ist ein Paketordner unter `plugins/` mit einem `__init__.py`, das ein Modulattribut `plugin` (Instanz einer `BasePlugin`-Unterklasse) bereitstellt. Alternativ: installiertes Paket mit Entry-Point in der Gruppe `spiele_redaktion.plugins`. ```python from fastapi import Depends, Request from redaktionskern.auth.deps import require_user from redaktionskern.contracts import BasePlugin, Migration, NavEntry class MeinPlugin(BasePlugin): name = "meinplugin" # eindeutig, = Ordnername title = "Mein Plugin" # deutscher Anzeigename description = "Was es tut." def migrations(self): # eigene Schema-Migrationen (optional) return [] def navigation(self): # Einträge in der Hauptnavigation (optional) return [NavEntry(label=self.title, url="/meinplugin")] def on_load(self, context): # Lifecycle-Hook beim Start super().on_load(context) # context.engine/.session_factory/.templates/.settings def on_unload(self): # Lifecycle-Hook beim Herunterfahren super().on_unload() plugin = MeinPlugin() ``` Der Kern stellt jedem Plugin über `context.templates` eine gemeinsame Jinja- Umgebung bereit (Plugin-Templates erben `base.html`), dazu Engine und Session-Fabrik. Migrationen laufen transaktional und werden pro Plugin in `schema_migrations` protokolliert; die `up`-Funktionen bekommen eine SQLAlchemy-Connection und können bei Dialekt-Unterschieden verzweigen. ## Plugin „Audit-Log“ (implementiert) Protokolliert, wer was wann verschoben, eingetragen oder geändert hat. ### Datentabelle Eigene Migration `0001_audit_eintraege`, Tabelle `audit_eintraege` (SQLite ↔ Postgres portabel): | Spalte | Inhalt | |--------|--------| | `actor_id` / `actor_name` | Wer (Benutzer-ID/-Name; `System` bei automatischen Ereignissen) | | `action` | Was (`erstellt`, `verschoben`, `geaendert`, `geloescht`, `benachrichtigt`) | | `objekt_typ` / `objekt_id` | Betroffenes Objekt (ID als Text) | | `alt` / `neu` | Alt-/Neustand als JSON (optional) | | `details` | Freie Zusatzinformationen als JSON (optional) | | `ip_adresse` | Herkunfts-IP (optional) | | `erstellt_am` | Zeitstempel | ### Öffentliche API für andere Plugins Andere Plugins rufen das Audit-Log über die Plugin-Registry bei jedem relevanten Ereignis auf: ```python audit = request.app.state.registry.get("audit-log") if audit is not None: await audit.log( # async-Variante user, # User-Objekt, Benutzername oder None (= System) "geaendert", # Aktion (Konstanten: AKTIONEN_ANZEIGE im Plugin) "planungseintrag", # Objekttyp eintrag.id, # Objekt-ID details={"alt": alt, "neu": neu}, # „alt“/„neu“ → JSON-Spalten, Rest → details ip_adresse=request.client.host, # optional ) ``` In synchronen Routen (`def`, FastAPI-Threadpool) steht `audit.log_sync(...)` mit denselben Argumenten bereit. ### Admin-Ansicht Unter `/audit-log` (nur Rolle **Admin**; Rezensenten/Redakteure erhalten 403, Anonyme werden zum Login umgeleitet): filterbar nach Benutzer, Aktionstyp und Zeitraum (Von/Bis), paginiert (25 Einträge pro Seite), neueste zuerst. ## Plugin „benachrichtigung“ Erstes fachlich umgesetztes Plugin (Adapter-Muster, drei Kanäle): | Kanal | Zustellung | Empfängerdaten | |-------|------------|----------------| | **E-Mail** | SMTP (`smtplib`, TLS: STARTTLS/SSL), konfigurierbar per Env | E-Mail-Adresse pro Benutzer | | **Telegram** | Bot-API (`sendMessage`) | Chat-ID pro Benutzer, Token zentral per Env | | **In-App** | Persistente Nachrichten im Portal mit Unread-Counter und „Alle als gelesen markieren“ | — | Ist ein Kanal installationsweit nicht konfiguriert (kein SMTP-Host bzw. kein Bot-Token — der Normalfall in der Entwicklung), protokolliert ein **Dev-Log-Adapter** die Nachricht auf dem Server, statt sie zu versenden; in der Entwicklung geht so keine Benachrichtigung verloren. Jeder Benutzer wählt unter **Benachrichtigungen → Kanäle & Einstellungen** seine Kanäle (mehrere gleichzeitig möglich) und hinterlegt E-Mail-Adresse bzw. Telegram-Chat-ID. Ohne gespeicherte Präferenz wird In-App zugestellt. ### Öffentliche Plugin-API für andere Plugins ```python plugin = request.app.state.registry.get("benachrichtigung") bericht = await plugin.send_notification(user, "Titel", "Nachrichtentext", "kategorie") # bericht.zugestellt -> z. B. ["email", "inapp"] # bericht.fehlgeschlagen -> Kanäle ohne Adresse/Fehler ``` `send_notification(user, titel, text, kategorie="allgemein")` wirft nicht; einzelne Kanalausfälle werden protokolliert und im Bericht gemeldet. Die In-App-Nachricht landet im Posteingang des Benutzers. ## Deployment (später) Docker/Podman Compose ist vorgesehen (Henry-Lab, danach Kundenhardware). Für Postgres genügt `SPIELE_DATABASE_URL`; die Migrationen sind portabel geschrieben. ## Projekt-Fortschritt > Diese Tabelle ist der Live-Status. Sie wird bei jedem Push aktualisiert. | # | Baustein | Status | |---|----------|--------| | 1 | Kern: FastAPI, Plugin-System, DB/Migrationen, Auth/Rollen, HTMX-Layout | ✅ fertig (28 Tests grün) | | 2 | Plugin `benachrichtigung` (E-Mail/Telegram/In-App, User-Präferenzen) | 🔄 in Arbeit | | 3 | Plugin `audit-log` (Wer/Was/Wann, Admin-Ansicht, öffentliche API) | ✅ fertig | | 4 | Plugin `neuheiten` (BGG-Sync, APScheduler, Filter) | 🔄 in Arbeit | | 5 | Plugin `dedup` (Verlags-Konflikt, deutsche Version, Vorgänger-/Planungs-Check) | ⏳ offen | | 6 | Plugin `planung` (Verschiebung, händischer Eintrag + Prüfungen + Benachrichtigung) | ⏳ offen | | 7 | Plugin `archiv` (12-Monats-Autopilot) | ⏳ offen | | 8 | Plugin `erinnerung` (Redaktionsschluss pro Ausgabe, 4-Wochen-Erinnerung) | ⏳ offen | | 9 | Plugin `export` (CSV + PDF via WeasyPrint) | ⏳ offen | | 10 | Integrationstests über alle Plugins | ⏳ offen | | 11 | Deployment auf swen.henry.insight-it.de (Compose + Traefik, Live-Check) | ⏳ offen | Legende: ✅ fertig · 🔄 in Arbeit · ⏳ offen · ⚠️ fertig mit offenen Punkten ## Offene Punkte (nicht Teil von Phase 1) - CSRF-Schutz für Formulare (aktuell SameSite=Lax-Cookie als Basisschutz) - Hintergrund-Jobs (APScheduler) für BGG-Sync und Erinnerungen - WeasyPrint für PDF-Export (Systemabhängigkeiten im Container) - Tailwind via CDN nur für Dev; für Produktion lokal gehostete Assets