# 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-.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 X-ATM-Server: X-ATM-Timestamp: 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 |