718 lines
38 KiB
Markdown
718 lines
38 KiB
Markdown
# Spiele-Redaktion
|
||
|
||
> **Deutsch:** [README.md](README.md) · **English:** [README.en.md](README.en.md)
|
||
|
||
**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**, **Archiv (12-Monats-Autopilot)**,
|
||
**Erinnerungen** und **Export (CSV/PDF)**.
|
||
|
||
## 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)
|
||
- **PDF-Export:** WeasyPrint (System-Bibliotheken: Pango/fontconfig)
|
||
- **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` | `<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, gesellschaftsspiel, familienspiel, strategiespiel, kartenspiel, würfelspiel` | Komma-getrennte Suchbegriffe für den regelmäßigen Sync (deutsche Spielbegriffe) |
|
||
| `SPIELE_BGG_MAX_TREFFER_PRO_SUCHE` | `50` | Obergrenze Treffer je Suchbegriff (schont das BGG-Rate-Limit) |
|
||
| `SPIELE_BGG_JAHR_FILTER` | `1` | Nur Spiele mit Erscheinungsjahr ≥ aktuelles Jahr übernehmen (`1`) oder alle (`0`) |
|
||
| `SPIELE_BGG_DEUTSCHE_TITEL_FILTER` | `1` | Nur Spiele mit deutsch klingendem Titel übernehmen (`1`) oder alle (`0`) |
|
||
| `SPIELE_BGG_TOKEN` | *(leer)* | API-Token für die BGG-XML-API2, wird als `Authorization: Bearer *** gesendet; seit der Token-Pflicht von BGG erforderlich (siehe [BGG-Thread 3602374](https://boardgamegeek.com/thread/3602374)) — ohne Token wird der Sync übersprungen |
|
||
| `SPIELE_DEDUP_BGG_AKTIV` | `1` | BGG-Zusatzdaten für die Dedup-Prüfung an (`1`) oder aus (`0`): Alternate-Names und Erweiterungs-Relationen |
|
||
| `SPIELE_DEDUP_LLM_AKTIV` | `0` | LLM-Zweitprüfung der Dedup-Grenzfälle an (`1`) oder aus (`0`) |
|
||
| `SPIELE_DEDUP_LLM_BASIS_URL` | *(leer)* | Basis-URL einer OpenAI-kompatiblen Chat-Completions-API, z. B. `https://api.openai.com/v1` |
|
||
| `SPIELE_DEDUP_LLM_API_KEY` | *(leer)* | API-Key, wird als `Authorization: Bearer …` gesendet |
|
||
| `SPIELE_DEDUP_LLM_MODELL` | *(leer)* | Modellname, z. B. `gpt-4o-mini` |
|
||
| `SPIELE_DEDUP_LLM_KONFIDENZ_MIN` | `0.7` | Mindest-Konfidenz; darunter wird das LLM-Ergebnis verworfen und nur der Regelbefund angezeigt |
|
||
| `SPIELE_DEDUP_LLM_TIMEOUT_SEKUNDEN` | `20` | Timeout je LLM-Aufruf; Netzwerk-/Server-Fehler werden genau einmal wiederholt |
|
||
| `SPIELE_NEUHEITEN_KI_AKTIV` | `0` | Optionale LLM-Hilfsstufe des Web-Quellen-Crawlers an (`1`) oder aus (`0`) |
|
||
| `SPIELE_NEUHEITEN_KI_BASIS_URL` | *(leer)* | Basis-URL einer OpenAI-kompatiblen Chat-Completions-API, z. B. `https://api.openai.com/v1` |
|
||
| `SPIELE_NEUHEITEN_KI_API_KEY` | *(leer)* | API-Key, wird als `Authorization: Bearer …` gesendet |
|
||
| `SPIELE_NEUHEITEN_KI_MODELL` | *(leer)* | Modellname, z. B. `gpt-4o-mini` |
|
||
| `SPIELE_NEUHEITEN_KI_KONFIDENZ_MIN` | `0.7` | Mindest-Konfidenz; darunter wird das KI-Ergebnis verworfen und der klassische Parser-Befund behalten |
|
||
| `SPIELE_NEUHEITEN_KI_TIMEOUT_SEKUNDEN` | `20` | Timeout je LLM-Aufruf; Netzwerk-/Server-Fehler werden genau einmal wiederholt |
|
||
| `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` |
|
||
| `SPIELE_ERINNERUNG_JOB_AKTIV` | `1` | Täglicher Erinnerungs-Check an (`1`) oder aus (`0`) |
|
||
| `SPIELE_ERINNERUNG_JOB_UHRZEIT` | `08:00` | Uhrzeit (HH:MM) des täglichen Erinnerungs-Checks |
|
||
|
||
|
||
## 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) 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 die **Coverbilder-Erweiterung**: Migrationen für `bild_url` in allen
|
||
vier Tabellen inkl. Daten-Erhalt beim ALTER TABLE, BGG-Bildextraktion
|
||
(`<image>` bevorzugt, `<thumbnail>` als Rückfallebene, gemockte Antworten),
|
||
Bildübernahme aus Web-Quellen, Update-Pflege des Bildes im Sync-Upsert sowie
|
||
Template-Darstellung (`loading="lazy"`/`decoding="async"` bzw. Platzhalter).
|
||
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.
|
||
|
||
Das Plugin **erinnerung** ist abgedeckt mit: Ausgaben-CRUD (nur Admin, mit
|
||
Audit), Erinnerungslogik mit eingefrorener Uhr (freezegun) — 4-Wochen-Grenze
|
||
(28 Tage exakt), kein Versand vor der Frist und nach dem Redaktionsschluss,
|
||
Empfängerregeln (offene oder fehlende Planung, Rollen-Filter), Doppelschutz
|
||
pro Ausgabe+Benutzer, manuelle Prüfung, Scheduler-Lifecycle/Uhrzeit-Konfiguration
|
||
— wiederum plus ein Integrationstest mit den echten Plugins.
|
||
Dazu das Plugin **export** (CSV-Inhalt mit Semikolon/BOM/Umlauten inkl. Quoting, gültige
|
||
PDF-Dateien via Kopf-/Struktur-Check, Rollen-Zugriff je Liste und Format,
|
||
Audit-Protokollierung der Downloads; PDF-Tests skippen sauber, wenn die
|
||
WeasyPrint-System-Bibliotheken fehlen — ein Installationsversuch läuft
|
||
zuvor automatisch) plus ein Integrationstest mit den echten Plugins., Bearbeiten/Löschen, Rechte pro Rolle), das Plugin
|
||
**export** (CSV-Inhalt mit Semikolon/BOM/Umlauten inkl. Quoting, gültige
|
||
PDF-Dateien via Kopf-/Struktur-Check, Rollen-Zugriff je Liste und Format,
|
||
Audit-Protokollierung der Downloads; PDF-Tests skippen sauber, wenn die
|
||
WeasyPrint-System-Bibliotheken fehlen — ein Installationsversuch läuft
|
||
zuvor automatisch) plus ein Integrationstest mit den echten Plugins
|
||
(In-App-Nachricht + Audit-Eintrag).
|
||
|
||
## 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/ VOLL IMPLEMENTIERT: Ausgaben, 4-Wochen-Erinnerung, Tages-Job
|
||
├── export/ VOLL IMPLEMENTIERT: CSV/PDF-Downloads, Rollen, Audit
|
||
└── … 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`.
|
||
|
||
```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/.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:
|
||
|
||
```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.
|
||
|
||
## 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 Migrationen `0001_neuheiten_tabelle`, `0002_bgg_id_nullable`,
|
||
`0003_quellen_status` und `0004_bild_url`; 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 |
|
||
| `bild_url` | URL des Coverbildes (nullable; BGG-Image/-Thumbnail oder Bild der Web-Quelle) |
|
||
| `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.
|
||
|
||
**Coverbilder:** Aus der Thing-Antwort wird die Bild-URL extrahiert
|
||
(bevorzugt das große `<image>`, sonst das kleinere `<thumbnail>`;
|
||
protokoll-relative URLs werden auf https normalisiert) und in
|
||
`bild_url` gespeichert. Beim Update bestehender Einträge wird das Bild
|
||
mitgepflegt; fehlt es in einer Antwort, bleibt ein bereits gespeichertes
|
||
Bild erhalten. Bilder werden direkt von BGG eingebettet (Hotlinking,
|
||
kein Download) — im UI immer mit `loading="lazy"` und `decoding="async"`.
|
||
|
||
- **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).
|
||
|
||
**BGG-API-Token (Pflicht):** BoardGameGeek verlangt seit seiner Umstellung
|
||
für die XML API2 eine Bearer-Authentifizierung ([Thread
|
||
3602374](https://boardgamegeek.com/thread/3602374)). Alle Requests des
|
||
Plugins senden deshalb den Header `Authorization: Bearer <Token>`; der Token
|
||
kommt aus `SPIELE_BGG_TOKEN`. Ist kein Token gesetzt, wird der Sync gar nicht
|
||
erst gestartet und mit der Meldung „Kein BGG-API-Token konfiguriert
|
||
(SPIELE_BGG_TOKEN) — Sync übersprungen.“ abgebrochen. Lehnt BGG eine Anfrage
|
||
mit HTTP 401 ab (fehlender oder ungültiger Token), erscheint statt der
|
||
generischen Fehlermeldung der Hinweis, einen gültigen API-Token in
|
||
`SPIELE_BGG_TOKEN` zu hinterlegen.
|
||
|
||
### 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.
|
||
|
||
### Web-Quellen-Crawler mit optionaler KI-Hilfsstufe
|
||
|
||
Neben BGG crawlt das Plugin zusätzlich redaktionell gepflegte Web-Quellen
|
||
(spielbox.de, brettspielbox.de, spiel-essen.de, cliquenabend.de) über je einen
|
||
Adapter in `plugins/neuheiten/quellen/` — mit Rate-Limit (1 Request/s),
|
||
Retry/Backoff, konditionalen Requests (ETag/304), Fehler-Isolation pro Quelle
|
||
und Duplikat-Gate gegen die BGG-Liste. Der Crawler arbeitet standardmäßig rein
|
||
regelbasiert. Liefert eine Quelle im Listenelement ein Bild (`img` mit `src`
|
||
bzw. `data-src`), wird dessen URL in `bild_url` übernommen — sonst bleibt das
|
||
Feld leer.
|
||
|
||
Optional kann eine **LLM-Hilfsstufe** (`plugins/neuheiten/quellen/ki_hilfe.py`)
|
||
zugeschaltet werden — konsistent zur Dedup-KI über die
|
||
`SPIELE_NEUHEITEN_KI_*`-Variablen (siehe Tabelle oben). Das LLM wird nur in
|
||
zwei Situationen genutzt:
|
||
|
||
- **Struktur-Erkennung:** Liefert der Regel-Parser keine oder verdächtig wenige
|
||
Einträge (0 Treffer; 1–2 Treffer auf einer sehr linkreichen Seite), darf das
|
||
LLM aus dem gelieferten HTML Pagination-/Filter-Folge-URLs vorschlagen.
|
||
Der Vorschlag wird im Code hart gefiltert: nur URLs derselben Domain wie die
|
||
geparste Seite, maximal 20 je Seite, bereits besuchte/geplante URLs werden
|
||
übersprungen, nicht erreichbare Vorschläge brechen den Lauf nicht ab —
|
||
Schutz vor Rate-Limit-Überlastung und Domain-Ausbruch.
|
||
- **Feld-Extraktion:** Einzelne Treffer sind unsicher (fehlender Verlag oder
|
||
verdächtiger/generischer Titel). Nur solche Treffer gehen ans LLM, das
|
||
höchstens `titel`, `verlag` und `autor` korrigiert — die URL ist strukturell
|
||
nicht änderbar.
|
||
|
||
Unter der Mindest-Konfidenz (`SPIELE_NEUHEITEN_KI_KONFIDENZ_MIN`,
|
||
Standard 0.7) wird jedes KI-Ergebnis verworfen; der klassische Parser-Befund
|
||
bleibt dann unverändert bestehen.
|
||
|
||
- **Fallback-Pflicht:** Standardmäßig aus (`SPIELE_NEUHEITEN_KI_AKTIV=0`).
|
||
Ohne vollständige Konfiguration oder bei jedem Fehler (Timeout,
|
||
Server-Störung nach genau einem Retry, kaputtes JSON) läuft EXAKT der
|
||
klassische Crawler — ein KI-Fehler kann den Sync nie blockieren.
|
||
- **Audit:** KI-Eingriffe eines Sync-Laufs landen gebündelt im audit-log
|
||
(`korrigiert` / `neuheiten_ki_crawler`) mit Quelle, Modell, Konfidenz und
|
||
den jeweils korrigierten Feldern sowie den verfolgten KI-Folge-URLs.
|
||
|
||
## 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
|
||
|
||
```python
|
||
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).
|
||
|
||
### LLM-Zweitprüfung für Grenzfälle
|
||
|
||
Regeln stoßen bei Editionen, Big Boxes, Übersetzungen und Verlags-Aliasen an
|
||
Grenzen („El Grande Big Box“ vs. „El Grande: 25 Jahre“, „Catan – Das schnelle
|
||
Spiel“ vs. „Catan Fast Edition“). Deshalb kann optional ein LLM als **zweite
|
||
Stufe** eingeschaltet werden — die Regelprüfung bleibt maßgeblich:
|
||
|
||
- **Nur Grenzfälle gehen ans LLM:** Titel mit gemeinsamen Wort-Tokens, aber
|
||
Fuzzy-Score unter der Schwelle (max. 5 Paarungen je Prüfung), sowie ein
|
||
ergänzender Call zum Verlags-/Titel-Hinweis bei einem regelbasierten
|
||
Verlags-Konflikt. Klare Treffer und klare Non-Treffer kosten kein Token.
|
||
- **Kontext:** beide Titel inkl. Alternate-Names (BGG-Sync), Verlage und ggf.
|
||
BGG-Expansion-Relationen.
|
||
- **Strukturierte Antwort** (JSON): `ist_gleiches_spiel`, `konfidenz`,
|
||
`begruendung`, `empfohlener_verlag`, `deutscher_titel`. Bestätigte Treffer
|
||
laufen in die üblichen Prüfungen a)–d) zurück; ein übernommener deutscher
|
||
Titel füllt die Titel-Empfehlung, ein Verlags-Hinweis landet im Detail des
|
||
Verlags-Konflikts. Unter der Mindest-Konfidenz
|
||
(`SPIELE_DEDUP_LLM_KONFIDENZ_MIN`, Standard 0.7) wird das Ergebnis
|
||
verworfen und nur der Regelbefund angezeigt.
|
||
- **Fallback-Pflicht:** Standardmäßig aus (`SPIELE_DEDUP_LLM_AKTIV=0`). Ohne
|
||
Key oder bei jedem Fehler (Timeout, Server-Störung nach genau einem Retry,
|
||
kaputtes JSON) verhält sich die Prüfung exakt wie rein regelbasiert — das
|
||
LLM kann die Prüfung nie blockieren.
|
||
- **Audit:** Jede Prüfung mit LLM-Befunden schreibt einen Eintrag ins
|
||
audit-log (`geprüft` / `dedup_ki`) mit Modell, Konfidenz und Entscheidung
|
||
je bewertetem Grenzfall.
|
||
|
||
Schnittstelle: OpenAI-kompatible Chat-Completions-API via httpx, konfiguriert
|
||
über die `SPIELE_DEDUP_LLM_*`-Variablen (siehe Tabelle oben).
|
||
|
||
### 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 Migrationen `0001_planungsliste` und `0002_bild_url`,
|
||
Tabelle `planungsliste`:
|
||
|
||
| Spalte | Inhalt |
|
||
|--------|--------|
|
||
| `titel` / `verlag` / `autor` | Spieldaten |
|
||
| `bgg_id` | BoardGameGeek-ID (Match-Kriterium der dedup-Prüfung) |
|
||
| `bild_url` | URL des Coverbildes (nullable; wird beim Verschieben aus den Neuheiten übernommen) |
|
||
| `ausgabe` | Magazin-Ausgabe, z. B. „3/2025“ (mehrere parallel möglich) |
|
||
| `rezensent_id` | Zuordnung (FK auf die Benutzer-Tabelle des Kerns) |
|
||
| `status` | `offen` → `in_bearbeitung` → `abgeschlossen` |
|
||
| `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 Migrationen `0001_archiv_tabellen` und `0002_bild_url`
|
||
(Spiegel-Tabellen, SQLite ↔ Postgres portabel, ohne Fremdschlüssel —
|
||
archivierte Zeilen überleben auch das Löschen eines Benutzers;
|
||
`bild_url` wird mit archiviert und beim Wiederherstellen zurückgeschrieben):
|
||
|
||
| 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.
|
||
## Plugin „erinnerung“ (implementiert)
|
||
|
||
Redaktionsschluss pro Ausgabe mit automatischer 4-Wochen-Erinnerung an alle
|
||
Rezensenten — mehrere Ausgaben laufen parallel.
|
||
|
||
### Datentabellen
|
||
|
||
Eigene Migration `0001_ausgaben_und_protokoll`, zwei Tabellen
|
||
(SQLite ↔ Postgres portabel):
|
||
|
||
| Tabelle | Spalten | Zweck |
|
||
|---------|---------|-------|
|
||
| `erinnerung_ausgabe` | `name` (z. B. „3/2025“), `redaktionsschluss` (Datum), Zeitstempel | Eine Magazin-Ausgabe; Anlegen/Bearbeiten/Löschen nur für Rolle **Admin** |
|
||
| `erinnerung_protokoll` | `ausgabe_id`, `user_id`, `kanaele`, `erstellt_am` | Jede versendete Erinnerung; **eindeutig** über (Ausgabe, Benutzer) → keine Doppelerinnerung |
|
||
|
||
### Erinnerungslogik
|
||
|
||
- **Fenster:** 28 Tage (4 Wochen) vor dem Redaktionsschluss bis zum
|
||
Redaktionsschluss selbst (jeweils inklusive). Davor und danach wird nicht
|
||
erinnert.
|
||
- **Empfänger:** alle aktiven Rezensenten, die noch offene Planungseinträge
|
||
haben (Status ≠ „abgeschlossen“, also „offen“ oder „in Bearbeitung“) oder
|
||
gar keine Einträge. Wer alles abgeschlossen hat, wird nicht erinnert;
|
||
Admins und Redakteure nie.
|
||
- **Versand:** über `send_notification` des benachrichtigung-Plugins
|
||
(Kategorie `erinnerung`) — die Kanäle richten sich nach der Präferenz des
|
||
Benutzers. Ohne Partner-Plugin wird kein Protokoll geschrieben, damit der
|
||
nächste Lauf es erneut versucht.
|
||
- **Doppelschutz:** pro (Ausgabe, Benutzer) wird genau einmal erinnert; der
|
||
Versand steht in `erinnerung_protokoll`. Auch ein späteres Bearbeiten des
|
||
Redaktionsschlusses löst keine zweite Erinnerung aus.
|
||
- **Audit:** jede Erinnerung als `System`/`benachrichtigt` (Objekttyp
|
||
`erinnerung`), jede Ausgaben-Änderung als `erstellt`/`geaendert`/
|
||
`geloescht` (Objekttyp `ausgabe`).
|
||
|
||
### Geplanter Job & manuelle Prüfung
|
||
|
||
APScheduler prüft **täglich** (Standard 08:00 Uhr, konfigurierbar über
|
||
`SPIELE_ERINNERUNG_JOB_UHRZEIT`, abschaltbar über
|
||
`SPIELE_ERINNERUNG_JOB_AKTIV=0`). Unter **Erinnerungen → „Jetzt prüfen und
|
||
erinnern“** führt ein Admin denselben Check sofort aus; das Ergebnis
|
||
erscheint als Banner. Die Uhr liegt zentral in `dienst.heute()` — Tests
|
||
frieren sie mit freezegun ein.
|
||
|
||
### UI
|
||
|
||
Unter **Erinnerungen** (`/erinnerung`) sehen alle angemeldeten Benutzer die
|
||
Ausgaben mit Redaktionsschluss und Restzeit-Badge. Admins verwalten zusätzlich
|
||
die Ausgaben (anlegen/bearbeiten/löschen), starten den Sofort-Check und
|
||
sehen den **Erinnerungsstatus**: wer wurde für welche Ausgabe wann über
|
||
welche Kanäle erinnert.
|
||
## Plugin „export“ (implementiert)
|
||
|
||
Redaktionslisten als Datei: die drei Listen **Neuheiten**, **Planung** und
|
||
**Archiv** sind je als CSV und PDF unter **Export** (`/export`) herunterladbar.
|
||
Keine eigene Datentabelle — das Plugin liest die Tabellen der Partner-Plugins
|
||
über die gemeinsame SQLAlchemy-Metadata (ohne Plugin-Importe); fehlt ein
|
||
Partner, erscheint die Liste als „nicht verfügbar“, statt zu brechen.
|
||
|
||
### Formate
|
||
|
||
| Format | Details |
|
||
|--------|---------|
|
||
| **CSV** | stdlib `csv`, Semikolon-getrennt (Excel mit deutschem Gebietsschema), UTF-8 mit Byte-Order-Mark — Umlaute/ß bleiben erhalten, Felder mit `;` werden korrekt gequotet; Auslieferung als `StreamingResponse` (`text/csv; charset=utf-8`) |
|
||
| **PDF** | WeasyPrint aus einem generierten HTML: A4-Tabelle mit deutscher Kopfzeile (wird auf jeder Seite wiederholt), Erstell-Datum und Anzahl Einträge im Kopf, Fußzeile „Seite X von Y“; Auslieferung als `StreamingResponse` (`application/pdf`) |
|
||
|
||
WeasyPrint wird erst beim PDF-Download importiert: Ohne installierte
|
||
System-Bibliotheken (Pango & Co.) bleiben App und CSV-Export voll nutzbar,
|
||
der PDF-Download liefert eine verständliche Fehlermeldung.
|
||
|
||
### Spalten
|
||
|
||
- **Neuheiten:** Titel · Verlag · Autor · Erscheinungsjahr · Status („Neuheit“, „In Planung“, „Archiviert“) · Aktualisiert am
|
||
- **Planung:** Titel · Verlag · Autor · Ausgabe · Rezensent (aufgelöster Name) · Status (Offen / In Bearbeitung / Abgeschlossen) · Notizen · Aktualisiert am
|
||
- **Archiv** = Neuheiten-Tabelle gefiltert auf Status „archiviert“ (gefüllt durch den Autopilot des archiv-Plugins)
|
||
|
||
### Rollen & Protokollierung
|
||
|
||
Admins und Redakteure exportieren alle Listen; **Rezensenten nur die
|
||
Planungsliste**. Die Prüfung passiert serverseitig an jedem Download-Endpunkt
|
||
(`403` bei Zugriff ohne Berechtigung, `404` bei unbekannter Liste); auf der
|
||
Export-Seite sind nicht erlaubte Karten ausgegraut. Jeder Download wird best
|
||
effort ins Audit-Log geschrieben (Aktion „exportiert“, Format und Zeilenzahl
|
||
in den Details).
|
||
|
||
## 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.
|
||
|
||
## Oberfläche: Coverbilder & responsives Layout
|
||
|
||
**Coverbilder:** Die Listen (Neuheiten, Planung, Archiv) zeigen je Eintrag
|
||
eine kleine Cover-Vorschau (40×56 px, `loading="lazy"`, `decoding="async"`,
|
||
Alt-Text = Spieltitel); die Detail-/Prüf-Ansichten der Planung zeigen das
|
||
Cover großformatig (max. 200 px Breite). Fehlt ein Bild, erscheint ein
|
||
dezenter CSS-Platzhalter mit Spielicon-Zeichen. Eingebettet wird per
|
||
Hotlink direkt von BGG bzw. der jeweiligen Quelle — es werden keine Bilder
|
||
heruntergeladen oder lokal gespeichert.
|
||
|
||
**Responsive Verhalten:** Das Basis-Layout ist mobil-first:
|
||
|
||
- Navigation: Ab Tablet (`md:`) horizontale Leiste; auf Mobilgeräten ein
|
||
Burger-Menü (Alpine.js) mit vollhöhen Touch-Zielen.
|
||
- Listen: Auf schmalen Screens (<768 px) kollabieren Neuheiten, Planung und
|
||
Archiv zu Karten-Ansichten — Cover links, Titel + Metadaten rechts,
|
||
Status-Badge oben. Desktop behält die Tabellenform.
|
||
- Formulare (Planung, Benutzerverwaltung, Einstellungen): vollbreite
|
||
Eingabefelder mit Labels oberhalb auf Mobil.
|
||
- Sync-Übersicht und Audit-Log bleiben Tabellen und scrollen horizontal
|
||
(`overflow-x-auto`).
|
||
- Buttons/Links haben auf Mobil mindestens 44 px Höhe (Touch-Ziel).
|
||
|
||
Getestet gegen 375 px (Smartphone), 768 px (Tablet) und Desktop.
|
||
|
||
## 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 |
|
||
| 8 | Plugin `erinnerung` (Redaktionsschluss pro Ausgabe, 4-Wochen-Erinnerung, Tages-Job) | ✅ fertig |
|
||
| 9 | Plugin `export` (CSV + PDF via WeasyPrint) | ✅ fertig |
|
||
| 10 | Integrationstests über alle Plugins | ✅ 316 Tests grün |
|
||
| 11 | Deployment auf swen.henry.insight-it.de (Compose + Traefik, Live-Check) | ✅ live |
|
||
|
||
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)
|
||
- WeasyPrint-Systemabhängigkeiten im Container prüfen
|
||
- Tailwind via CDN nur für Dev; für Produktion lokal gehostete Assets
|
||
|
||
## Lizenz
|
||
|
||
Dieses Projekt steht unter der [MIT-Lizenz](LICENSE).
|