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>
5.8 KiB
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-getestetOutboxJournal: 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 nachrejected/Signer,UuidV7,Backoff,EventJson,StatsDiff,HealthWindow,FileChangeTracker
mc/: dünne NeoForge-SchichtGameHooks: Event-Handler auf dem Server-Thread. Sie erzeugen nur Records und legen sie in eineConcurrentLinkedQueueStatsCollector: kopiert alle 5 min die Zählertabelle online Spieler (max. 2 Spieler pro Tick; Zugriff über Access Transformer aufStatsCounter.stats); Umrechnung und Diff im Worker-ThreadHealthMonitor: misst Tickdauer perServerTickEvent.Pre/PostFileWatcher: reicht geänderte FTB-Dateien unverändert alsfile_syncweiter (Parsing im Backend)WorkerThread: einziger Hintergrund-Thread für Serialisierung, Datei- und Netzwerk-I/OServerInfoCollector: Versionen und Modliste überModList
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 |