Files
minecraft-atm10-stats/docs/ARCHITECTURE.md
T
lucashahmannandClaude Opus 5.5 e66f89f49e feat: ATM10-Statistikportal
Statistikportal für den All-the-Mods-10-Server (Minecraft 1.21.1, NeoForge 21.1):

- mod/: serverseitige NeoForge-Mod atmstats – Ereignisse, Live-Zähler, Servergesundheit,
  FTB-Dateien; Outbox auf der Festplatte, signierter gzip-Versand per HTTPS mit Backoff
- backend/: Fastify + PostgreSQL – idempotenter Ingest, Projektionen, Parser für SNBT,
  FTB Quests/Teams, world/stats, world/advancements und latest.log; Ranglisten, Snapshots,
  Retention, öffentliche API, SSE-Live-Updates, Admin mit argon2/CSRF, SFTP-Worker
- web/: Next.js-Dashboard – Übersicht, Ranglisten, Spielerprofile, ATM10, Server,
  Datenquellen, Verwaltung
- shared/: Protokoll v1 und API-Typen; tools/: Simulator, Log-Anonymisierer, RCON-Client
- deploy/: Docker Compose hinter Traefik, Backup; docs/: Architektur, Datenquellen, Betrieb

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:33:25 +02:00

106 lines
5.8 KiB
Markdown

# Architektur
Für Entwickler, die am Portal weiterarbeiten. Die vollständige Design-Begründung steht in
`docs/superpowers/specs/2026-10-13-atm10-stats-design.md`.
## Leitidee
Alle Datenquellen erzeugen **Ereignisse desselben Protokolls** (v1, `shared/src/protocol.ts`) und laufen durch
**eine** Pipeline (`backend/src/pipeline/process.ts`):
```
Mod (Push) ──┐
├─▶ processEvents(events, source)
SFTP (Pull) ─┘ 1. INSERT INTO events … ON CONFLICT (event_id) DO NOTHING ← Idempotenz
2. nur neue Ereignisse → Projektionen (sessions, stat_current, deaths, quest_progress …)
3. nach COMMIT → pg_notify('atm_live') → LiveHub → SSE-Clients
```
Lese-API und Frontend kennen nur die projizierten Tabellen, nie die Quelle.
## Komponenten
### Mod `atmstats` (`mod/`)
- `core/`: reines Java ohne Minecraft, vollständig unit-getestet
- `OutboxJournal`: Journal auf der Festplatte (`current.jsonl` → `seg-<n>.jsonl`; ein Segment = ein Batch,
gelöscht erst nach Bestätigung; Limits mit Vorrang für Ereignisse vor Snapshots; abgeschnittene letzte Zeile
nach Absturz wird verworfen)
- `Shipper`: Queue → Journal → HTTPS; Backoff 1 s … 5 min mit Jitter; 4xx-Batches nach `rejected/`
- `Signer`, `UuidV7`, `Backoff`, `EventJson`, `StatsDiff`, `HealthWindow`, `FileChangeTracker`
- `mc/`: dünne NeoForge-Schicht
- `GameHooks`: Event-Handler auf dem Server-Thread. Sie erzeugen nur Records und legen sie in eine `ConcurrentLinkedQueue`
- `StatsCollector`: kopiert alle 5 min die Zählertabelle online Spieler (max. 2 Spieler pro Tick; Zugriff
über Access Transformer auf `StatsCounter.stats`); Umrechnung und Diff im Worker-Thread
- `HealthMonitor`: misst Tickdauer per `ServerTickEvent.Pre/Post`
- `FileWatcher`: reicht geänderte FTB-Dateien unverändert als `file_sync` weiter (Parsing im Backend)
- `WorkerThread`: einziger Hintergrund-Thread für Serialisierung, Datei- und Netzwerk-I/O
- `ServerInfoCollector`: Versionen und Modliste über `ModList`
### Backend (`backend/`)
| Modul | Aufgabe |
|---|---|
| `ingest/` | `POST /api/ingest`: Bearer-Token (als SHA-256 gespeichert), Zeitfenster ±5 min, HMAC über die übertragenen Bytes, gzip mit 2-MB-Grenze, Validierung je Ereignis |
| `pipeline/` | `processEvents`, Projektionen je Ereignistyp |
| `parsers/` | SNBT (FTB-Format ohne Kommas), FTB-Quests, `world/stats`, `world/advancements`, `latest.log` |
| `stats/` | Ranglisten-Definitionen (`metrics.ts`), SQL je Board, Snapshots, Retention |
| `routes/` | öffentliche API, SSE (`/api/live`), Admin |
| `worker/` | SFTP-Abgleich (`FileSource`: SFTP oder lokal), Jobs mit `pg_try_advisory_lock` |
| `live/` | Notifier (Ereignis → Live-Meldung, ausgeblendete Spieler gefiltert) und LiveHub (LISTEN, Ringpuffer 500, Resume über `Last-Event-ID`) |
Zwei Prozesse aus einer Codebasis: `server.ts` (api) und `worker.ts`. Migrationen (`backend/migrations/*.sql`)
führt der eigene Migrator beim Start aus.
### Frontend (`web/`)
Next.js App Router, Server Components holen Daten direkt bei der API (`API_INTERNAL_URL`), der Browser nutzt
same-origin `/api`. Live-Teile (Ticker, Diagramm-Umschalter, Admin) sind Client-Komponenten. Design-Tokens in
`src/app/globals.css`: Deepslate-Flächen, ATM-Erzfarben als Akzente, validierte Diagrammpalette.
## Protokoll v1
```
POST /api/ingest
Authorization: Bearer <token>
X-ATM-Server: <serverId>
X-ATM-Timestamp: <unix ms>
X-ATM-Signature: hex(HMAC-SHA256(secret, timestamp + "." + hex(sha256(body))))
Content-Encoding: gzip
{ "protocol": 1, "events": [ … ] } → { accepted, duplicate, rejected }
```
Ereignistypen: `server_start`, `server_stop`, `player_join`, `player_leave`, `player_death`, `entity_kill`,
`advancement`, `dimension_change`, `stats_snapshot`, `file_sync`, `health`. Beispiele:
`shared/fixtures/protocol/`. Push-Ereignisse tragen UUIDv7, SFTP-Ereignisse deterministische UUIDv5 aus Datei und
Inhalt. Dadurch ist auch wiederholtes Einlesen idempotent.
**Zählerregel**: Werte sind absolut. Pro (Spieler, Kategorie, Schlüssel) gewinnt die Beobachtung mit dem neuesten
Zeitstempel, unabhängig von der Quelle und auch wenn der Wert kleiner ist.
## Datenmodell (Kern)
`players`, `name_history`, `sessions`, `events`, `stat_current`, `stat_snapshots`, `deaths`, `kills`,
`advancements`, `dimension_visits`, `teams`, `team_members`, `quest_progress`, `quest_defs`, `file_cache`,
`server_info`, `server_health`, `server_health_hourly`, `sync_state`, `sync_requests`, `app_errors`,
`admin_users`, `admin_sessions`, `ingest_tokens`. Ranglisten werden live aus `stat_current` berechnet
(Index `(category, key, value DESC)`), Zeiträume aus den Tages-Snapshots.
## Sicherheit
- Ingest: Token + HMAC + Zeitfenster + Rate-Limit + IP-Allowlist in Traefik; Payload-Grenzen; Pfad-Traversal-Schutz für `file_sync`
- Admin: argon2id, Session-Cookie `httpOnly; Secure; SameSite=Strict; Path=/api/admin`, CSRF-Token-Header, Login-Rate-Limit
- Keine Tokens, Signaturen oder Cookies in Logs (pino-Redaction); Spieler-IPs werden nirgends gespeichert
- Spielerköpfe lädt die API serverseitig (Cache 24 h), damit Besucher keine Drittanbieter kontaktieren
- Datenbank nur im internen Docker-Netz
## Tests
| Ebene | Was |
|---|---|
| `shared` | Fixtures gegen Schemas |
| `backend` (Vitest, echte Postgres) | Ingest-Auth und Idempotenz, alle Projektionen inkl. Absturz-Sitzungen und Namenswechsel, Parser an echten Serverdateien, Ranglisten, Snapshots/Retention, API inkl. ausgeblendeter Spieler, SSE, Admin/CSRF, Worker inkl. Log-Rotation |
| `mod` (JUnit) | Journal (Neustart, Absturz, Rotation, Limits), Shipper (Backoff, Ablehnung, Uhrabweichung), Serialisierung gegen `shared/fixtures`, Signatur-Testvektor |
| `web` (Vitest + Testing Library) | Formatierung, Ranglisten-Karte, Heatmap |
| E2E (Playwright) | Smoke-Test gegen den Compose-Stack, Desktop und Mobil |