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>
This commit is contained in:
lucashahmannandClaude Opus 5.5 committed 2026-10-13 09:33:25 +02:00
1 parent 56d2078e6a
commit e66f89f49e
214 files changed
+27077

No files matched your search

+105
View File
@@ -0,0 +1,105 @@
# 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 |
+72
View File
@@ -0,0 +1,72 @@
# Datenquellen und Einschränkungen
Jede Zahl im Portal stammt aus einer der unten genannten Quellen. Nichts wird geschätzt oder erfunden.
Wo eine Quelle fehlt, zeigt das Portal einen leeren Zustand oder den Hinweis, woher die Daten kommen werden.
## Grundlage: was am echten Server geprüft wurde
Stand 13.10.2026, aus einer Kopie des Servers (`server-snapshot/`, nicht im Repository, enthält Spieler-IPs):
| Fakt | Beleg |
|---|---|
| Minecraft 1.21.1 | `DataVersion 3955` in `world/stats/*.json` |
| 464 Mods, u. a. FTB Quests 2101.1.36, FTB Teams 2101.1.11, Mekanism 10.7.19, Apotheosis 8.8.0, Spark 1.10.124 | Modliste des Servers |
| FTB-Quest-Fortschritt je Team als SNBT (`completed`, `started`, `task_progress`, `claimed_rewards`) | `world/ftbquests/<team>.snbt` |
| Mod-eigene Zähler in den Vanilla-Statistiken: `apotheosis:world_tiers_activated`, `lootr:looted_stat`, `waystones:waystone_activated` | `world/stats/*.json` |
| Item-Statistiken enthalten Einträge aus Mekanism, Powah, Mystical Agriculture, Ars Nouveau, Iron's Spellbooks, Create, Modern Industrialization, Oritech u. v. m. | `world/stats/*.json` |
| Log enthält Beitritte (mit UUID), Verlassen, Advancements/Ziele/Herausforderungen im Klartext; Zeilenenden CRLF | `logs/latest.log` |
| NeoForge 21.1.219 und Java 21 (Testlauf der Mod auf einem lokalen NeoForge-Server) | `server_start`-Ereignis der Mod |
| Quest-Konfiguration: 66 Kapitel (`chapters/*.snbt`), 4 790 Quests, Texte über ftbquestslangsplitter in `lang/en_us/**/*.snbt_merged` (decken alle 3 103 Titel der 1,5 MB großen `lang/en_us.snbt` ab); Titel mit Farbcodes (`&a`, `&6&l`, `\&` für ein echtes &) | `config/ftbquests/quests/` |
| Von 585 IDs in `completed` (CDOGamer) sind nur 267 Quests, der Rest Aufgaben und Kapitel | Abgleich mit der Quest-Konfiguration |
| FTB-Teams: `id`, `type` (player/party/server), `ranks` (UUID → Rang), `properties."ftbteams:display_name"` | `world/ftbteams/player/*.snbt` |
Die exakte NeoForge- und Java-Version **des Produktionsservers** meldet die Mod beim ersten Start (Seite *Server*).
## Welche Kennzahl woher kommt
| Kennzahl | Quelle | Aktualität | Hinweis |
|---|---|---|---|
| Online-Status, Sitzungen, Heatmap „Wann gespielt wird“ | Mod: Beitritt/Verlassen; Fallback: Server-Log | sofort; Fallback ≤ 15 min | Log-Sitzungen werden nur genutzt, wenn die Mod seit 10 min nichts gesendet hat; liefert die Mod später nach, werden überlappende Sitzungen nicht doppelt gezählt |
| Spielzeit, Tode, besiegte Mobs, Sprünge, Schaden | Vanilla-Zähler: Mod liest sie live, SFTP liest `world/stats` | 5 min (Mod) bzw. beim Speichern der Welt (SFTP) | die neueste Beobachtung gewinnt |
| Strecken je Fortbewegungsart | Vanilla-Zähler `*_one_cm` | wie oben | „Weltenbummler“ ohne Fallstrecke |
| Abgebaut / hergestellt / benutzt / aufgesammelt je Item und Mod | Vanilla-Zähler `minecraft:mined` usw. | wie oben | Mod-Zuordnung über den Namespace der Item-ID |
| Technik / Magie / Farming | Summe hergestellter und benutzter Items der Mods einer Kategorie | wie oben | Zuordnung in `backend/config/mod-categories.json` |
| Diamantenbank | abgebaute Diamanterze (normal + Tiefenschiefer) | wie oben | misst Abbau, **nicht** Besitz |
| Schatzjäger, Weltenstufe | `lootr:looted_stat`, `apotheosis:world_tiers_activated` | wie oben | |
| Todesursachen, Killer | Mod: `LivingDeathEvent` | sofort | ohne Mod nur Anzahl und `killed_by`-Zähler |
| Boss- und Spieler-Kills mit Waffe | Mod: `LivingDeathEvent`, Boss = NeoForge-Tag `c:bosses` | sofort | normale Mob-Kills nur als Zähler (Last) |
| Advancements | Mod: `AdvancementEarnEvent`; SFTP: `world/advancements`; Log (nur für Spieler ohne Advancement-Datei) | sofort / ≤ 15 min | Rezept-Freischaltungen ausgenommen. Die Mod zählt nur Advancements mit Anzeige im Spiel; aus `world/advancements` kommen auch solche ohne Anzeige, weil die Datei keine Anzeige-Informationen enthält. Ohne Mod kann „Abenteurer“ daher etwas höher ausfallen |
| Besuchte Dimensionen | Mod: Dimensionswechsel | sofort | erst ab Installation der Mod |
| FTB-Quest-Fortschritt | `world/ftbquests/*.snbt` (Mod alle 2 min oder SFTP) | 2–15 min | pro Team; gezählt werden nur IDs, die in der Quest-Konfiguration als Quest vorkommen |
| Quest- und Kapiteltitel | `config/ftbquests/quests/chapters/*.snbt` + `lang/en_us/**/*.snbt_merged` (Mod oder SFTP; andere Sprachen und Belohnungstabellen werden nicht übertragen) | bei Änderung | Farbcodes entfernt |
| Party-Teams | `world/ftbteams/{player,party}/*.snbt` | bei Änderung | |
| TPS, Tickdauer (Ø, 95. Perzentil, Max), Spieler, Chunks, Speicher | eigene Messung der Mod um jeden Tick | jede Minute | unabhängig von Spark |
| Versionen, Modliste | Mod beim Serverstart (`ModList`) | bei jedem Start | |
| Verlaufsdiagramme, 7-/30-Tage-Ranglisten | stündliche und tägliche Snapshots durch den Worker | stündlich | Verläufe beginnen mit der Inbetriebnahme; Stunden 30 Tage, Tage unbegrenzt |
## Bewusst nicht erfasst
Ohne verlässliche serverseitige Quelle oder nur über ungeprüfte Interna einzelner Mods erreichbar:
- **Energie** (Erzeugung und Verbrauch von Mekanism-, Powah- und anderen Maschinen)
- **Inhalte von AE2- und Refined-Storage-Netzen**
- **Mana, Source, Zauber, Rituale** (Ars Nouveau, Iron's Spellbooks, Occultism); Magie-Aktivität stammt aus Items
- **MineColonies-Kolonien**
- **Spielerpositionen, Todesorte, Heatmaps der Welt**: Datenschutz (Basen) und Last
- **Inventare/Besitz** (daher misst die Diamantenbank den Abbau)
Erweiterungen sind über neue Ereignistypen im Protokoll möglich, jeweils erst nach Prüfung der Mod-API in
der installierten Version.
## Einschränkungen durch Hoster und Mods
| Einschränkung | Auswirkung | Status |
|---|---|---|
| Ob Mine-Hoster **ausgehendes HTTPS** erlaubt, ist nicht dokumentiert | ohne Freigabe nur SFTP-Modus (keine Echtzeit, keine Todesursachen, keine Leistungsdaten) | wird beim ersten Start geprüft: Log-Zeile `atmstats: backend reachable` |
| `world/stats` wird vom Server nur beim Speichern geschrieben | SFTP-Zähler hinken bis zum nächsten Autosave hinterher | Mod liefert Live-Werte alle 5 min |
| Der Log-Ausschnitt enthielt keinen Startabschnitt | Versionen nicht aus dem Log lesbar | Mod meldet sie beim Start |
| Log-Zeitstempel sind in der lokalen Zeitzone des Servers | Zeitzone muss stimmen (`TIMEZONE`, Standard Europe/Berlin) | für Mine-Hoster nicht bestätigt; betrifft nur log-basierte Daten |
| **FTB Teams**: Party-Dateien auf dem Server derzeit nicht vorhanden | Party-Mitgliedschaft wird aus `ranks` (member/officer/owner) gelesen; Format an den echten Spieler-Team-Dateien geprüft, Party-Dateien nutzen dieselben Felder | ohne Party zählt jeder Spieler als eigenes Team (wie im Spiel) |
| **Quest-Titel**: 1 789 von 4 790 Quests haben keinen eigenen Titel | sie zählen mit, erscheinen aber nicht namentlich (im Spiel heißen sie nach dem Aufgaben-Item, das serverseitig nicht übersetzbar ist) | geprüft an der echten Konfiguration (66 Kapitel, 3 029 Titel) |
| SFTP-Passwort = Gamepanel-Passwort | volle Schreibrechte im Worker-Container | Worker liest nur; `.env` schützen |
| Spieler-IPs stehen im Server-Log | Datenschutz | werden nie gespeichert; Test-Fixtures anonymisiert |
+173
View File
@@ -0,0 +1,173 @@
# Betrieb
Für den Betreiber des Portals: Installation auf dem DMZ-Server hinter OPNsense und Traefik, Einspielen der Mod
bei Mine-Hoster, laufender Betrieb und Fehlerbehebung.
## 1. Voraussetzungen
- Server in der DMZ mit Docker und Docker Compose v2
- Traefik **v3** mit Docker-Provider, einem HTTPS-Entrypoint und Cert-Resolver; Traefik hängt an einem externen
Docker-Netz (z. B. `traefik`)
- DNS-Eintrag für die Portal-Domain, der auf die öffentliche IP der OPNsense zeigt
- Zugang zum Mine-Hoster-Gamepanel (SFTP-Daten unter *Einstellungen → SFTP-Details*)
> Traefik **v2**: Die Middleware heißt dort `ipwhitelist` statt `ipallowlist`. In `deploy/docker-compose.yml`
> die Zeile `traefik.http.middlewares.atm-ingest-allow.ipallowlist.sourcerange` entsprechend umbenennen.
## 2. Portal installieren
```bash
git clone <repo> atm10-stats && cd atm10-stats
cp deploy/.env.example deploy/.env
openssl rand -hex 32 # Wert für INGEST_HMAC_SECRET
openssl rand -base64 24 # Wert für POSTGRES_PASSWORD
nano deploy/.env # DOMAIN, TRAEFIK_*, INGEST_ALLOWED_IPS, Secrets, optional SFTP_*
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build
docker compose -f deploy/docker-compose.yml --env-file deploy/.env ps # api, web, db: healthy
```
Admin-Konto anlegen (Passwort mindestens 12 Zeichen, wird interaktiv abgefragt) und Ingest-Token für die Mod erzeugen:
```bash
docker compose -f deploy/docker-compose.yml --env-file deploy/.env exec api node dist/cli.js admin:create lucas
docker compose -f deploy/docker-compose.yml --env-file deploy/.env exec api node dist/cli.js token:create
```
Das Token wird **nur einmal** angezeigt. Später lässt es sich unter `/admin` erneuern.
### Routing
| Pfad | Ziel | Hinweis |
|---|---|---|
| `https://DOMAIN/api/ingest*` | api:3001 | nur von `INGEST_ALLOWED_IPS` (Middleware `atm-ingest-allow`) |
| `https://DOMAIN/api/*` | api:3001 | öffentliche Lese-API, `/api/live` (SSE), `/api/admin/*` |
| `https://DOMAIN/*` | web:3000 | Dashboard |
`/api/live` ist ein Server-Sent-Events-Stream. Auf diese Route keine Kompressions- oder Buffering-Middleware legen.
Die API sendet dafür `X-Accel-Buffering: no` und alle 25 s einen Heartbeat.
### OPNsense
1. **Eingehend**: HTTPS (443) zur DMZ/Traefik wie für andere Dienste. Die Einschränkung von `/api/ingest` auf
den Gameserver übernimmt Traefik (`INGEST_ALLOWED_IPS`). Die IP des Mine-Hoster-Servers ergibt sich aus der
Serveradresse im Gamepanel (Hostname per `nslookup` auflösen).
Optional zusätzlich in OPNsense: Alias mit dieser IP und eine eigene Regel nur für den Ingest-Host.
2. **Ausgehend (nur für den SFTP-Abgleich)**: DMZ-Server → SFTP-Host/Port des Mine-Hoster-Servers (TCP).
Ohne diese Regel meldet `/sources` „Fehler beim letzten Abgleich“.
## 3. Mod bei Mine-Hoster einspielen
Die Mod ist **rein serverseitig**: Spieler installieren nichts, Clients ohne die Mod können normal verbinden.
1. Mod bauen (oder fertiges JAR aus einem Release verwenden):
```bash
./mod/gradlew -p mod build # → mod/build/libs/atmstats-1.0.0.jar
```
2. Im Gamepanel den Server **stoppen**.
3. Per SFTP (WinSCP/FileZilla) `atmstats-1.0.0.jar` nach `mods/` hochladen.
4. Server starten und nach dem Start wieder stoppen. Dabei entsteht `config/atmstats-server.toml`.
5. Konfiguration per SFTP bearbeiten:
```toml
[atmstats]
enabled = true
endpoint = "https://stats.example.de"
serverId = "atm10"
token = "<Token aus token:create>"
hmacSecret = "<INGEST_HMAC_SECRET aus deploy/.env>"
statsSnapshotMinutes = 5
questScanMinutes = 2
healthIntervalSeconds = 60
maxOutboxMb = 50
```
6. Server starten und in `logs/latest.log` nach `atmstats:` suchen:
- `atmstats: backend reachable`: fertig, Daten laufen.
- `atmstats: backend NOT reachable: …`: siehe Abschnitt 6. Die Ereignisse bleiben in `atmstats/outbox/`
auf dem Gameserver und werden gesendet, sobald die Verbindung klappt.
Ohne `endpoint`, `token` oder `hmacSecret` bleibt die Mod passiv und schreibt nur eine Hinweiszeile ins Log.
**ATM10-Updates**: Die Mod verwendet nur APIs, die in NeoForge 21.1.x stabil sind, und deklariert NeoForge ab
21.1.0. Bei Pack-Updates innerhalb von Minecraft 1.21.1 bleibt das JAR gültig. Der Mod-Ordner wird bei
Modpack-Updates manchmal ersetzt: danach prüfen, ob `mods/atmstats-*.jar` noch vorhanden ist.
## 4. SFTP-Abgleich (optional, empfohlen)
In `deploy/.env` `SFTP_HOST`, `SFTP_PORT`, `SFTP_USER`, `SFTP_PASSWORD` setzen (Gamepanel → Einstellungen →
SFTP-Details). `SFTP_ROOT` ist das Serververzeichnis, `SFTP_WORLD_DIR` der Weltordner (Standard `world`,
`level-name` aus `server.properties`). Danach `docker compose … up -d worker`.
Der Worker holt alle 15 Minuten nur geänderte Dateien. Unter `/admin` lässt sich ein Abgleich sofort anstoßen.
Ohne funktionierende Mod liefert SFTP trotzdem Gesamtzähler, Advancements, Quests und (aus dem Log) Beitritte;
Echtzeit-Daten, Todesursachen und Serverleistung fehlen dann.
Das SFTP-Passwort ist das des Gamepanels und gewährt Schreibzugriff. Der Worker liest ausschließlich.
## 5. Laufender Betrieb
### Updates
```bash
git pull
docker compose -f deploy/docker-compose.yml --env-file deploy/.env up -d --build
```
Datenbankmigrationen laufen beim Start von api und worker automatisch (serialisiert über einen Advisory-Lock).
### Token und Secret erneuern
- **Ingest-Token**: `/admin` → *Ingest-Token erneuern*. Das alte Token wird sofort ungültig. Das neue in
`config/atmstats-server.toml` eintragen und den Gameserver neu starten. Bis dahin puffert die Mod und
meldet `authentication failed` im Log; es geht nichts verloren, solange das Outbox-Limit nicht erreicht ist.
- **HMAC-Secret**: in `deploy/.env` und in der Mod-Konfiguration gleichzeitig ändern, dann api und Gameserver neu starten.
### Backups
Der Container `backup` erstellt täglich `pg_dump`-Dateien im Volume `backups` und löscht sie nach
`BACKUP_KEEP_DAYS` (Standard 14). Sofortiges Backup:
```bash
docker compose -f deploy/docker-compose.yml --env-file deploy/.env exec backup sh /backup.sh once
```
Wiederherstellung (ersetzt alle Daten):
```bash
C="docker compose -f deploy/docker-compose.yml --env-file deploy/.env"
$C stop api worker web
$C exec backup ls -t /backups # gewünschte Datei wählen
$C exec backup dropdb atm
$C exec backup createdb atm
$C exec backup pg_restore --dbname=atm --no-owner /backups/<datei>.dump
$C start api worker web
```
Das Volume `backups` liegt auf demselben Host. Für echte Ausfallsicherheit die Dateien zusätzlich extern sichern
(z. B. per Borg/Restic vom Docker-Volume-Pfad).
### Verhalten bei Neustarts
| Situation | Verhalten |
|---|---|
| Gameserver stoppt normal | Mod schreibt `player_leave` und `server_stop` in die Outbox und sendet sie beim nächsten Start; nach 10 min ohne Health-Meldung schließt der Worker offene Sitzungen |
| Gameserver stürzt ab | beim nächsten Start schließt `server_start` offene Sitzungen zum letzten Lebenszeichen (kein Aufblähen der Spielzeit) |
| Backend nicht erreichbar | Mod puffert auf der Festplatte (max. `maxOutboxMb`, 7 Tage), sendet mit Backoff 1 s … 5 min weiter; bei Platzmangel werden zuerst alte Statistik-Snapshots verworfen |
| Doppelte Lieferung | jede Meldung hat eine eindeutige ID, Duplikate werden ignoriert |
| API/Worker-Neustart | zustandslos; der Worker setzt den Log-Abgleich am gespeicherten Byte-Offset fort |
## 6. Fehlerbilder
| Meldung | Ursache | Lösung |
|---|---|---|
| Mod: `backend NOT reachable: …ConnectException` / `HttpTimeoutException` | Mine-Hoster blockiert ausgehende Verbindungen, DNS falsch oder OPNsense/Traefik lässt nicht durch | `curl -v https://DOMAIN/api/health` von außen testen; Traefik-Log prüfen (403 = IP nicht in `INGEST_ALLOWED_IPS`); falls der Hoster ausgehend blockiert: `enabled = false` und nur SFTP nutzen |
| Mod: `clock skew: server clock differs …` | Uhr des Gameservers oder des DMZ-Servers weicht > 5 min ab | NTP auf dem DMZ-Server prüfen; beim Hoster melden, falls der Gameserver falsch geht |
| Mod: `authentication failed (bad_token)` | Token falsch oder widerrufen | neues Token erzeugen und eintragen |
| Mod: `authentication failed (bad_signature)` | `hmacSecret` ≠ `INGEST_HMAC_SECRET` | Werte angleichen |
| Mod: `batch rejected by backend … moved to outbox/rejected` | Daten passten nicht zum Protokoll (z. B. veraltete Mod-Version) | Datei in `atmstats/outbox/rejected/` samt `.reason.txt` prüfen, Mod und Backend auf gleichen Stand bringen |
| Mod: `outbox limit reached, dropped …` | Backend länger nicht erreichbar | Verbindung reparieren; ggf. `maxOutboxMb` erhöhen |
| `/sources`: SFTP „Fehler beim letzten Abgleich“ | Zugangsdaten, Port oder OPNsense-Regel | Fehlertext unter `/admin` → *Letzte Fehler* |
| Live-Ticker bleibt leer, Seite sonst ok | Proxy puffert SSE | keine Kompressions-/Buffering-Middleware auf `/api/live` |
| Login unter `/admin` klappt nicht ohne HTTPS | Cookie ist `Secure` | Portal über HTTPS aufrufen (lokal: `NODE_ENV=development`) |
Spieler, die nicht im Portal erscheinen möchten, lassen sich unter `/admin` ausblenden: Sie verschwinden aus allen
öffentlichen Ansichten, Ranglisten und Live-Meldungen. Die Daten bleiben gespeichert, sodass sich das Ausblenden
rückgängig machen lässt.
@@ -365,4 +365,5 @@ Neustartverhalten, Fehlerbilder), `mod/README.md`.
3. Struktur von `config/ftbquests/quests/` und `world/ftbteams/` → Betreiber
stellt Kopie bereit, bevor Quest-Titel/Team-Namen-Parser finalisiert werden;
bis dahin werden Quest-IDs angezeigt.
→ Geklärt am 13.10.2026 anhand der echten Dateien, siehe `docs/DATA-SOURCES.md`.
4. SFTP-Host/Port/Benutzer → `.env` des Betreibers (nicht im Repo).