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>
106 lines
5.8 KiB
Markdown
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 |
|