Englische README (README.en.md), beide Sprachen untereinander verlinkt
This commit is contained in:
226
README.en.md
Normal file
226
README.en.md
Normal file
@@ -0,0 +1,226 @@
|
||||
# Board Game Editorial Desk (Spiele-Redaktion)
|
||||
|
||||
> **Deutsch:** [README.md](README.md) · **English:** [README.en.md](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](https://docs.astral.sh/uv/) + pytest
|
||||
|
||||
## Setup
|
||||
|
||||
```bash
|
||||
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](https://boardgamegeek.com/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
|
||||
|
||||
```bash
|
||||
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`.
|
||||
|
||||
```python
|
||||
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](LICENSE).
|
||||
Reference in New Issue
Block a user