Flo Hartmann 72fe3b7eda Plugin archiv: 12-Monats-Autopilot mit Sicherungsdatei-Prinzip
- Eigene Migration 0001_archiv_tabellen: Spiegel-Tabellen archiv_neuheiten
  und archiv_planung (ohne FK, damit Archiv-Zeilen Benutzerlöschung überleben)
- Service mit eingefrierbarer Uhr: Stichtag „jetzt minus 12 Monate“ in UTC,
  strenger Vergleich (genau 12 Monate bleibt aktiv); Neuheiten nach
  Erscheinungsjahr (großzügig Jahresende) sonst Eintragsdatum, Planung nach
  Eintragsdatum; Monatsarithmetik mit Klemmung (29. Februar)
- Täglicher APScheduler-Cron-Job (Standard 03:00, konfigurierbar über
  SPIELE_ARCHIV_JOB_UHRZEIT / SPIELE_ARCHIV_JOB_AKTIV); je archiviertem
  Titel ein Audit-Log-Eintrag als System
- Admin-Ansicht /archiv: vereinigte Liste beider Tabellen, Suche über
  Titel/Verlag/Autor, Herkunftsfilter, Wiederherstellen in die aktive Liste
  (BGG-Konflikt wird abgelehnt, verwaiste Rezensentenzuordnung übernimmt der
  Admin — gemeldet und auditiert); Rollen: nur Admin
- Kern minimal erweitert: PluginContext.registry stellt Hintergrund-Jobs die
  Plugin-Registry bereit (keine Fachlogik im Kern)
- 30 neue Tests mit eingefrorener Uhr (Grenzfälle, Zeitzonen, Schaltjahr,
  Job-Lauf, Wiederherstellen, Rollen, Suche/Filter); 171 Tests grün
- README aktualisiert: neues Plugin-Kapitel, Env-Variablen, Fortschritt #7 
2026-08-21 20:26:00 +00:00

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; vollständig implementiert sind bisher Audit-Log, Benachrichtigung, Neuheiten (BGG-Sync), Dedup-Prüfung, Planungsliste und Archiv (12-Monats-Autopilot).

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
  • Hintergrund-Jobs: APScheduler (BGG-Neuheiten-Sync)
  • Dependencies/Tests: uv + pytest

Setup

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 <Projekt>/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)
SPIELE_BGG_SYNC_AKTIV 1 Hintergrund-Sync des Neuheiten-Plugins an (1) oder aus (0)
SPIELE_BGG_SYNC_INTERVALL_STUNDEN 24 Intervall des BGG-Syncs in Stunden (min. 1)
SPIELE_BGG_SUCHBEGRIFFE brettspiel Komma-getrennte Suchbegriffe für den regelmäßigen Sync
SPIELE_BGG_MAX_TREFFER_PRO_SUCHE 25 Obergrenze Treffer je Suchbegriff (schont das BGG-Rate-Limit)
SPIELE_DEDUP_BGG_AKTIV 1 BGG-Zusatzdaten für die Dedup-Prüfung an (1) oder aus (0): Alternate-Names und Erweiterungs-Relationen
SPIELE_ARCHIV_JOB_AKTIV 1 Täglicher Archivierungs-Job an (1) oder aus (0)
SPIELE_ARCHIV_JOB_UHRZEIT 03:00 Tageszeit des täglichen Archiv-Laufs im Format HH:MM

Tests

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) sowie das Neuheiten-Plugin: BGG-Parsing, Erweiterungs-/ Prototyp-Filter, Rate-Limit & Retry-Backoff (gemockte HTTP-Antworten, keine echten API-Calls), Update-statt-Duplikat-Logik, UI-Suche/Filter/Sortierung, Rollen am Sync-Endpunkt und Scheduler-Lifecycle. Dazu die beiden neuen Plugins: dedup (Titel-Heuristik, Fuzzy-Match, alle vier Prüfungen mit Fake-BGG-Client, Prüfprotokoll, Prüfseite) und planung (beide Eintragswege mit Stub-gemockter dedup-/Benachrichtigungs-/Audit-API, Verlags-Auswahl- Dialog, Statuswechsel, Bearbeiten/Löschen, Rechte pro Rolle) plus ein Integrationstest mit den echten Plugins (In-App-Nachricht + Audit-Eintrag). Dazu das archiv-Plugin: 12-Monats-Regel mit eingefrorener Uhr (Grenzfälle „genau 12 Monate“, Schaltjahr/29. Februar, UTC-Konsistenz bei Zeitzonen-Unterschieden), Job-Lauf mit Audit-Einträgen, Wiederherstellen inkl. BGG-Konflikt und Ersatz-Rezensent, Rollen sowie Suche/Filter in der Admin-Ansicht.

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/              VOLL IMPLEMENTIERT: BGG-Client, Sync-Job, Migration, UI
├── benachrichtigung/       VOLL IMPLEMENTIERT: E-Mail/Telegram/In-App
├── dedup/                  VOLL IMPLEMENTIERT: check_titel-API, Heuristik, Protokoll
├── planung/                VOLL IMPLEMENTIERT: Planungsliste, Verschiebung, Dialoge
├── archiv/                 VOLL IMPLEMENTIERT: 12-Monats-Autopilot, Job, UI
├── erinnerung/ export/     (Stubs, ladbar)
└── …                       je __init__.py + templates/<name>/

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.

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/.registry (Plugin-Registry für
                                 # Hintergrund-Jobs ohne Request)

    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:

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

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.

Plugin „neuheiten“ (implementiert)

Neuheitenliste auf Basis der BoardGameGeek XML API2 (https://boardgamegeek.com/xmlapi2). Regelmäßiger Sync als Hintergrund-Job, manuelle Synchronisation für die Redaktion, deutsche sortier-/filterbare Liste.

Datentabelle

Eigene Migration 0001_neuheiten_tabelle, Tabelle neuheiten (SQLite ↔ Postgres portabel):

Spalte Inhalt
titel Spieltitel
verlag / autor Verlag bzw. Autor(en), kommagetrennt
erscheinungsjahr Erscheinungsjahr — BGG liefert nur das Jahr, kein genaues Datum
bgg_id BoardGameGeek-ID, eindeutig → Merge-Kriterium des Syncs
status neuheit (spätere Plugins: Planung/Archiv)
quelle Datenquelle (boardgamegeek)
erstellt_am / aktualisiert_am Zeitstempel

BGG-Client & Sync-Ablauf

Pro Suchbegriff: /search?type=boardgame&query=… → IDs sammeln → /thing?id=…&type=boardgame in Batches à 20 IDs.

  • Rate-Limit: mindestens 1 Sekunde zwischen zwei Requests.
  • Retry mit Backoff: exponentielles Zurückhalten bei 5xx, 429 und Netzwerkfehlern; HTTP 202 (BGG-Warteschlange) wird gemäß Retry-After erneut versucht; kaputtes XML erzeugt eine klare Fehlermeldung.
  • Filter:
    • Erweiterungen (type=boardgameexpansion) werden auf Request-Ebene (type=boardgame) und zusätzlich pro Element ausgeschlossen.
    • Prototypen: Die XML API2 liefert keinen Prototyp-Marker; als Heuristik werden Titel mit Prototyp-Schlüsselwörtern („Prototyp“, „Prototype“, …) gefiltert. Restrisiko verbleibt — redaktionelle Prüfung bleibt wichtig.
  • Update statt Duplikat: bestehende Einträge werden über die eindeutige bgg_id aktualisiert (Titel/Verlag/Autor/Jahr), der status bleibt erhalten; neu angelegt wird nur bei unbekannter BGG-ID.

Hintergrund-Job & manuelle Synchronisation

Beim Start registriert das Plugin einen APScheduler-Job (Standard: alle 24 h, konfigurierbar über SPIELE_BGG_SYNC_INTERVALL_STUNDEN, abschaltbar über SPIELE_BGG_SYNC_AKTIV=0). Die Suchbegriffe kommen aus SPIELE_BGG_SUCHBEGRIFFE (komma-getrennt). Läuft ein Sync noch, wird ein überlappender Durchlauf übersprungen.

Unter Neuheiten → „Jetzt synchronisieren“ lösen Admins und Redakteure einen sofortigen Sync aus — optional mit einem Sofort-Suchbegriff für eine gezielte Recherche. Das Ergebnis (neu/aktualisiert/gefiltert) erscheint als Banner; der Endpunkt ist rollengeschützt (Rezensenten erhalten 403).

UI

Die Seite /neuheiten zeigt alle Einträge als Tabelle (Spieltitel, Verlag, Autor, Erscheinungsjahr, Status, aktualisiert am):

  • Sortierung per Spaltenkopf, Suche über Titel/Verlag/Autor und Statusfilter — serverseitig umgesetzt, per HTMX ohne Seitenreload (ohne JavaScript als normaler Formular-GET nutzbar).
  • Jeder Titel verlinkt direkt auf den BGG-Eintrag.
  • Sync-Steuerung nur für Rolle Admin/Redakteur.

Plugin „dedup“ (implementiert)

Dedup-Prüfungen bei Import und händigem Eintrag. Die Prüf-Logik liegt komplett im Plugin; der Kern bleibt unberührt.

Öffentliche Prüf-API für andere Plugins

dedup = request.app.state.registry.get("dedup")
ergebnis = await dedup.check_titel(titel, verlag, bgg_id, user=user)
# ergebnis.hat_konflikte        -> bool
# ergebnis.konflikte            -> Liste TitelKonflikt (art, beschreibung, details)
# ergebnis.verlags_optionen     -> Verlage zur Auswahl bei Verlags-Konflikt
# ergebnis.titel_empfehlung     -> deutschester bekannter Titel
# ergebnis.als_text()           -> lesbare Zusammenfassung (deutsch)

Die vier Prüfungen

# Prüfung Mechanik
a Verlags-Konflikt Gleiches Spiel (BGG-ID oder Fuzzy-Match) bereits unter anderem Verlag/Vertrieb → Konflikt mit Verlagsauswahl
b Titel-Varianten Deutsche Version wird bevorzugt — Heuristik über deutsche Titel-Indikatoren (Umlaute/ß, Funktionswörter, typische Spielbegriffe) plus BGG Alternate-Names, falls verfügbar
c Bereits besprochen Abgeschlossene Planungseinträge bilden das Korpus; Vorgänger werden über die BGG boardgameexpansion-Relation erkannt und zusätzlich fuzzy auf Titel gematcht (rapidfuzz, Schwelle 85, token_set_ratio)
d Schon in Planung Titel steht bereits in der Planungsliste eines anderen Rezensenten (eigene Einträge zählen nicht)

Fehlt eine Partner-Tabelle (Plugin nicht geladen), entfällt die jeweilige Teilprüfung. BGG-Störungen blockieren nie: der Hilfsclient degradiert auf die reine Heuristik und protokolliert nur. Abschaltbar über SPIELE_DEDUP_BGG_AKTIV=0 (Standard: an, Rate-Limit 1 s).

Prüfprotokoll & Prüfseite

Jede check_titel-Prüfung landet in der eigenen Migration 0001_pruefprotokoll (Tabelle dedup_pruefungen: Titel, Verlag, BGG-ID, Befund als JSON, geprüft von, Zeitpunkt). Unter Dedup-Prüfung (/dedup) kann jeder angemeldete Benutzer einen Titel von Hand prüfen und sieht die letzten 20 Prüfungen.

Plugin „planung“ (implementiert)

Planungsliste mit Zuordnung an Rezensenten und Ausgaben. Beide Eintragswege laufen automatisch durch die dedup-Prüfung.

Datentabelle

Eigene Migration 0001_planungsliste, Tabelle planungsliste:

Spalte Inhalt
titel / verlag / autor Spieldaten
bgg_id BoardGameGeek-ID (Match-Kriterium der dedup-Prüfung)
ausgabe Magazin-Ausgabe, z. B. „3/2025“ (mehrere parallel möglich)
rezensent_id Zuordnung (FK auf die Benutzer-Tabelle des Kerns)
status offenin_bearbeitungabgeschlossen
notizen Freitext
quelle neuheiten (verschoben) oder manuell (nachgetragen)
pruefung Befund der dedup-Prüfung beim Anlegen (JSON)
erstellt_am / aktualisiert_am Zeitstempel

Die beiden Eintragswege

  1. Verschiebung aus der Neuheitenliste — Button „→ Zur Planung“ in der Neuheitenliste ruft POST /planung/uebernehmen/<id> auf; der Neuheiten- Eintrag wechselt in den Status planung, der Planungseintrag wird als Quelle neuheiten angelegt.
  2. Händisches Nachtragen — Formular auf der Planungsseite (POST /planung/neu, Quelle manuell).

Bei einem Prüftreffer erhält der eintragende Rezensent eine Benachrichtigung (send_notification des benachrichtigung-Plugins, Kategorie dedup), und es wird ein Audit-Log-Eintrag geschrieben (log des audit-log-Plugins, Befund in details.dedup_konflikte). Bei einem Verlags-Konflikt erscheint zuerst der Auswahl-Dialog „Welcher Verlag soll geführt werden?“ — erst die bestätigte Wahl legt den Eintrag an; die Entscheidung ist auditiert.

UI & Rechte

Unter Planung (/planung) sieht jeder angemeldete Benutzer die komplette Liste mit Zuordnung (Rezensent, Ausgabe, Status, Prüfhinweis-Badge). Rezensenten können nur ihre eigenen Einträge bearbeiten (Statuswechsel, Bearbeiten, Löschen); Admins und Redakteure alle. Jede Änderung wird im Audit-Log protokolliert (erstellt/verschoben/geaendert/geloescht).

Plugin „archiv“ (implementiert)

Automatische Archivierung nach dem Sicherungsdatei-Prinzip: Titel, deren Erscheinungs- bzw. Eintragsdatum länger als 12 Monate zurückliegt, werden vollständig in eigene Archiv-Tabellen verschoben und aus den aktiven Listen entfernt. Nichts wird gelöscht oder verändert — jeder Eintrag lässt sich unverändert zurück in die aktive Liste verschieben.

Datentabellen

Eigene Migration 0001_archiv_tabellen mit zwei Spiegel-Tabellen (SQLite ↔ Postgres portabel, ohne Fremdschlüssel — archivierte Zeilen überleben auch das Löschen eines Benutzers):

Tabelle Spiegel von Besondere Spalten
archiv_neuheiten neuheiten alle Originalspalten plus quell_id, archiviert_am, archiviert_von, grund, referenz_am
archiv_planung planungsliste alle Originalspalten (inkl. pruefung-JSON) plus dieselben Archiv-Metadaten

Die 12-Monats-Regel

  • Stichtag: „jetzt 12 Monate“ in UTC; archiviert wird, wer streng älter ist — genau 12 Monate gilt noch nicht als „älter als“.
  • Referenzdatum:
    • Neuheiten: das Erscheinungsdatum. BGG liefert nur das Jahr, daher großzügig das Jahresende (31.12., 23:59:59) — ein Titel von 2023 wird erst ab dem 01.01.2025 archiviert. Ohne Jahr zählt das Eintragsdatum.
    • Planungseinträge: das Eintragsdatum, unabhängig vom Status.
  • Zeitzonen: verglichen wird durchgängig in UTC; naive Datenbank- Zeitstempel gelten als UTC, zeitzonenbewusste Uhrzeiten werden nach UTC verschoben (per Tests mit eingefrorener Uhr abgedeckt, inkl. 29. Februar).

Täglicher Hintergrund-Job

Beim Start registriert das Plugin einen APScheduler-Cron-Job (Standard täglich 03:00, konfigurierbar über SPIELE_ARCHIV_JOB_UHRZEIT, abschaltbar über SPIELE_ARCHIV_JOB_AKTIV=0). Pro archiviertem Titel schreibt der Job einen Audit-Log-Eintrag mit dem Akteur „System“ (Aktion verschoben, Grund und Referenzdatum in den Details). Der Registry-Zugriff aus dem Hintergrund-Job läuft über den neuen context.registry-Hook des Plugin-Kontexts.

Admin-Ansicht mit Wiederherstellen

Unter Archiv (/archiv, nur Rolle Admin) werden beide Tabellen als vereinigte Liste angezeigt (neueste Archivierung zuerst):

  • Suche über Titel/Verlag/Autor und Filter nach Herkunft (Neuheiten/Planung).
  • Pro Eintrag: Herkunfts-Badge, Status, Rezensent, Archivierungszeitpunkt und der Grund (z. B. „älter als 12 Monate (Erscheinungsjahr 2023)“).
  • Wiederherstellen verschiebt den Eintrag vollständig zurück in die aktive Liste und protokolliert dies im Audit-Log (Akteur: der Admin). Konfliktfälle werden sicher abgelehnt: existiert in der Neuheitenliste bereits ein aktiver Eintrag mit derselben BGG-ID (erneuter Sync), wird nicht wiederhergestellt; existiert der zugeordnete Rezensent nicht mehr, übernimmt der wiederherstellende Admin die Zuordnung (gemeldet und auditiert). Rezensenten und Redakteure erhalten 403.

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) fertig
3 Plugin audit-log (Wer/Was/Wann, Admin-Ansicht, öffentliche API) fertig
4 Plugin neuheiten (BGG-Sync, APScheduler, Filter) fertig
5 Plugin dedup (Verlags-Konflikt, deutsche Version, Vorgänger-/Planungs-Check) fertig
6 Plugin planung (Verschiebung, händischer Eintrag + Prüfungen + Benachrichtigung) fertig
7 Plugin archiv (12-Monats-Autopilot, Wiederherstellen, täglicher Job) fertig (171 Tests grün)
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 Erinnerungen
  • WeasyPrint für PDF-Export (Systemabhängigkeiten im Container)
  • Tailwind via CDN nur für Dev; für Produktion lokal gehostete Assets
Description
No description provided
Readme MIT 1.6 MiB
Languages
Python 87%
HTML 12.9%
Dockerfile 0.1%