2026-08-23 01:13:02 +00:00
2026-08-23 01:13:02 +00:00

Board Game Editorial Desk (Spiele-Redaktion)

Deutsch: README.md · English: README.en.md

AI assistance for board game magazine editorial teams — multi-user web application with a modular plugin architecture. A lean core with a plugin system, authentication/roles and migrations; fully implemented plugins: Audit Log, Notifications, New Releases (BGG sync), Dedup Checks, Planning List, Archive (12-month autopilot), Reminders, and Export (CSV/PDF).

Stack

  • Backend: Python 3.11+, FastAPI, SQLAlchemy 2 (SQLite, Postgres-ready)
  • Frontend: HTMX + Jinja2 + Tailwind (CDN) + Alpine.js — no build step
  • Auth: signed session cookies, Argon2id password hashing
  • Plugins: entry points (importlib.metadata, group spiele_redaktion.plugins) plus a local plugins/ directory
  • Background jobs: APScheduler (BGG new-release sync)
  • PDF export: WeasyPrint (system libraries: Pango/fontconfig)
  • Dependencies/tests: uv + pytest

Setup

uv sync                                   # venv + dependencies
uv run uvicorn main:app --reload          # dev server on http://127.0.0.1:8000

An initial admin account is created automatically on first start:

Username Password Role
admin admin admin

⚠️ For anything beyond local development, set SPIELE_INITIAL_ADMIN_PASSWORD before starting and change the password after the first login.

Environment variables

Variable Default Meaning
SPIELE_DATABASE_URL sqlite:///./data/spiele-redaktion.db SQLAlchemy URL (Postgres: e.g. postgresql+psycopg://…)
SPIELE_SESSION_SECRET dev value Secret for signed session cookies
SPIELE_SESSION_DAUER 43200 Session duration in seconds (12 h)
SPIELE_INITIAL_ADMIN_PASSWORD admin Password of the initial admin account
SPIELE_PLUGINS_DIR <project>/plugins Path to the local plugin directory
SPIELE_SMTP_HOST (empty) SMTP server for the e-mail channel; empty = dev fallback (log only)
SPIELE_SMTP_PORT 587 SMTP port
SPIELE_SMTP_BENUTZER (empty) SMTP login (optional)
SPIELE_SMTP_PASSWORT (empty) SMTP password (optional)
SPIELE_SMTP_ABSENDER username or spiele-redaktion@localhost From address
SPIELE_SMTP_TLS starttls starttls, ssl or keine/none
SPIELE_TELEGRAM_BOT_TOKEN (empty) Bot token for the Telegram channel; empty = dev fallback (log only)
SPIELE_BGG_SYNC_AKTIV 1 New-releases background sync on (1) or off (0)
SPIELE_BGG_SYNC_INTERVALL_STUNDEN 24 BGG sync interval in hours (min. 1)
SPIELE_BGG_SUCHBEGRIFFE brettspiel Comma-separated search terms for the recurring sync
SPIELE_BGG_MAX_TREFFER_PRO_SUCHE 25 Max. results per search term (protects BGG rate limits)
SPIELE_BGG_TOKEN (empty) API token for the BGG XML API2, sent as `Authorization: Bearer *** required since BGG introduced token auth (thread 3602374) — without a token the sync is skipped
SPIELE_DEDUP_BGG_AKTIV 1 BGG auxiliary data for dedup checks on (1) or off (0): alternate names and expansion relations
SPIELE_ARCHIV_JOB_AKTIV 1 Daily archiving job on (1) or off (0)
SPIELE_ARCHIV_JOB_UHRZEIT 03:00 Time of day for the daily archive run, format HH:MM
SPIELE_ERINNERUNG_JOB_AKTIV 1 Daily reminder check on (1) or off (0)
SPIELE_ERINNERUNG_JOB_UHRZEIT 08:00 Time of day for the daily reminder check

Tests

uv run pytest

224 tests cover: plugin loader (all plugins incl. lifecycle hooks and entry point path), migration runtime incl. idempotency, login/logout, role access (admin/editor vs. reviewer), user management, the audit log plugin (logging, migration, filters, pagination, admin-only access), the new releases plugin (BGG parsing, expansion/prototype filters, rate limit & retry backoff against mocked HTTP responses — no real API calls —, update-instead-of-duplicate logic, UI search/filter/sort, role protection on the sync endpoint, scheduler lifecycle), dedup (title heuristics, fuzzy matching, all four checks with a fake BGG client, check log), planning (both entry paths, publisher selection dialog, per-role permissions), archive (12-month rule with a frozen clock incl. leap-year and timezone edge cases, restore incl. conflicts), reminders (issue CRUD, exact 28-day window, duplicate protection), and export (CSV content with semicolon/BOM/quoting, valid PDFs, role access; PDF tests skip cleanly if WeasyPrint system libraries are missing). Integration tests run the real plugins end to end (in-app message + audit row in the database).

Architecture

src/redaktionskern/        lean core — NO domain logic
├── app.py                 app factory: discovery → migrations → on_load → routes
├── config.py              settings (environment variables)
├── db.py                  engine/session (SQLite ↔ Postgres portable)
├── migrationen.py         migration runtime (schema_migrations table per plugin)
├── plugin_loader.py       entry points + plugins/ directory, unique names
├── contracts.py           plugin contract (BasePlugin, Migration, NavEntry, PluginContext)
└── auth/                  login/logout, roles, user management

plugins/                   one folder per feature, loaded by the plugin loader
├── audit-log/             FULLY IMPLEMENTED: model, migration, API, admin view
├── neuheiten/             FULLY IMPLEMENTED: BGG client, sync job, migration, UI
├── benachrichtigung/      FULLY IMPLEMENTED: e-mail/Telegram/in-app
├── dedup/                 FULLY IMPLEMENTED: check_titel API, heuristics, log
├── planung/               FULLY IMPLEMENTED: planning list, moves, dialogs
├── archiv/                FULLY IMPLEMENTED: 12-month autopilot, job, UI
├── erinnerung/            FULLY IMPLEMENTED: issues, 4-week reminders, daily job
├── export/                FULLY IMPLEMENTED: CSV/PDF downloads, roles, audit
└── …                      each with __init__.py + templates/<name>/

The plugin contract

A plugin is a package folder under plugins/ whose __init__.py exposes a module attribute plugin (an instance of a BasePlugin subclass). Alternatively: an installed package with an entry point in the group spiele_redaktion.plugins.

from fastapi import Depends, Request
from redaktionskern.auth.deps import require_user
from redaktionskern.contracts import BasePlugin, Migration, NavEntry

class MyPlugin(BasePlugin):
    name = "myplugin"             # unique, = folder name
    title = "My Plugin"           # display name
    description = "What it does."

    def migrations(self):         # own schema migrations (optional)
        return []

    def navigation(self):         # entries in the main navigation (optional)
        return [NavEntry(label=self.title, url="/myplugin")]

    def on_load(self, context):   # lifecycle hook at startup
        super().on_load(context)  # context.engine/.session_factory/.templates/
                                  # .settings/.registry (plugin registry for
                                  # background jobs without a request)

    def on_unload(self):          # lifecycle hook at shutdown
        super().on_unload()

plugin = MyPlugin()

The core provides each plugin with a shared Jinja environment via context.templates (plugin templates extend base.html), plus the engine and session factory. Migrations run transactionally and are recorded per plugin in schema_migrations; the up functions receive a SQLAlchemy connection and can branch on dialect differences.

The plugins

  • audit-log — records who moved/created/changed what and when; public log() / log_sync() API; admin view at /audit-log with filters and pagination.
  • benachrichtigung (notifications) — adapter pattern with three channels: e-mail (SMTP), Telegram (bot API), in-app inbox with unread counter. Per-user channel preferences; public send_notification(user, title, text, category) API that never raises. Unconfigured channels fall back to a dev-log adapter so nothing is lost in development.
  • neuheiten (new releases) — builds and maintains the new-releases list from the BoardGameGeek XML API2: search + thing queries in batches, rate-limited (min. 1 s between requests) with exponential backoff and HTTP 202 queue handling. Expansions and prototypes are filtered out; existing entries are updated by unique bgg_id instead of duplicated. Requires a BGG bearer token (SPIELE_BGG_TOKEN); without it the sync is skipped with a clear message.
  • dedup — four checks before any title enters the pipeline: (a) same game published under different publishers → publisher selection conflict; (b) same game under different titles → prefer the German version (heuristics
    • BGG alternate names); (c) game or predecessor already reviewed (BGG expansion relations + rapidfuzz fuzzy matching); (d) title already being reviewed by another editor. Public check_titel() API with structured results; every check is logged.
  • planung (planning) — reviewers pick titles from the new-releases list ("→ move to planning") or add them manually; both paths run through the dedup checks. On a hit, the entering reviewer is notified and an audit log entry written. Reviewers may only edit their own entries.
  • archiv (archive) — titles older than 12 months are moved automatically into archive tables (backup-copy principle: everything is preserved, restorable). Daily APScheduler job, admin view with search/filters and restore including conflict handling.
  • erinnerung (reminders) — configurable issue deadlines; four weeks before press date, every reviewer with open (or no) planning entries receives a reminder through their preferred channels. Exactly once per issue and user (unique constraint), audited, daily job plus manual "check now" button.
  • export — all lists (new releases, planning, archive) as CSV (semicolon-separated, UTF-8 BOM — Excel-friendly) and PDF (WeasyPrint, repeated table headers, page numbers). Admins/editors export everything, reviewers only the planning list; every download is audited.

Deployment

Docker/Podman Compose with Traefik labels is included (docker-compose.yml, Dockerfile). The Dockerfile installs the WeasyPrint system libraries (Pango etc.). For Postgres simply set SPIELE_DATABASE_URL; the migrations are portable.

Project status

All planned components are implemented and deployed:

# Component Status
1 Core: FastAPI, plugin system, DB/migrations, auth/roles, HTMX layout done
2 Plugin notifications (e-mail/Telegram/in-app, per-user preferences) done
3 Plugin audit-log done
4 Plugin new releases (BGG sync, APScheduler, filters) done
5 Plugin dedup (publisher conflict, German title, predecessor/planning checks) done
6 Plugin planning (move + manual entry, both checked, notifications) done
7 Plugin archive (12-month autopilot, restore, daily job) done
8 Plugin reminders (per-issue deadlines, 4-week reminder, daily job) done
9 Plugin export (CSV + PDF via WeasyPrint) done
10 Integration tests across all plugins 224 tests green
11 Deployment (Compose + Traefik behind HTTPS) live

Open items

  • CSRF protection for forms (currently SameSite=Lax cookies as a baseline)
  • Self-hosted Tailwind assets instead of CDN for production

License

This project is licensed under the MIT License.

Description
No description provided
Readme MIT 1.6 MiB
Languages
Python 87%
HTML 12.9%
Dockerfile 0.1%