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

+179
View File
@@ -0,0 +1,179 @@
-- Kernschema (Spec §6)
CREATE TABLE players (
uuid uuid PRIMARY KEY,
name text,
first_seen timestamptz NOT NULL DEFAULT now(),
last_seen timestamptz,
hidden boolean NOT NULL DEFAULT false
);
CREATE INDEX players_name_lower ON players (lower(name));
CREATE TABLE name_history (
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
name text NOT NULL,
seen_at timestamptz NOT NULL,
PRIMARY KEY (uuid, name)
);
CREATE INDEX name_history_name_lower ON name_history (lower(name));
CREATE TABLE teams (
team_id uuid PRIMARY KEY,
name text,
type text
);
CREATE TABLE team_members (
team_id uuid NOT NULL REFERENCES teams(team_id) ON DELETE CASCADE,
uuid uuid NOT NULL,
PRIMARY KEY (team_id, uuid)
);
CREATE INDEX team_members_uuid ON team_members (uuid);
CREATE TABLE events (
event_id uuid PRIMARY KEY,
server_id text NOT NULL,
type text NOT NULL,
ts timestamptz NOT NULL,
received_at timestamptz NOT NULL DEFAULT now(),
source text NOT NULL CHECK (source IN ('push', 'sftp')),
payload jsonb NOT NULL
);
CREATE INDEX events_ts ON events (ts);
CREATE INDEX events_source_received ON events (source, received_at DESC);
CREATE INDEX events_player ON events ((payload->>'uuid'), ts);
CREATE INDEX events_type_ts ON events (type, ts);
CREATE TABLE sessions (
id bigserial PRIMARY KEY,
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
started_at timestamptz NOT NULL,
ended_at timestamptz,
end_reason text
);
CREATE INDEX sessions_open ON sessions (uuid) WHERE ended_at IS NULL;
CREATE INDEX sessions_uuid_started ON sessions (uuid, started_at DESC);
CREATE TABLE stat_current (
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
category text NOT NULL,
key text NOT NULL,
value bigint NOT NULL,
observed_at timestamptz NOT NULL,
source text NOT NULL,
PRIMARY KEY (uuid, category, key)
);
CREATE INDEX stat_current_board ON stat_current (category, key, value DESC);
CREATE TABLE stat_snapshots (
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
metric text NOT NULL,
bucket timestamptz NOT NULL,
granularity text NOT NULL CHECK (granularity IN ('hour', 'day')),
value bigint NOT NULL,
PRIMARY KEY (uuid, metric, granularity, bucket)
);
CREATE INDEX stat_snapshots_bucket ON stat_snapshots (granularity, bucket);
CREATE TABLE deaths (
event_id uuid PRIMARY KEY,
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
ts timestamptz NOT NULL,
dimension text NOT NULL,
cause text NOT NULL,
killer_type text,
killer_name text
);
CREATE INDEX deaths_uuid ON deaths (uuid, ts DESC);
CREATE TABLE kills (
event_id uuid PRIMARY KEY,
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
ts timestamptz NOT NULL,
dimension text NOT NULL,
victim_type text NOT NULL,
is_boss boolean NOT NULL,
weapon text
);
CREATE INDEX kills_uuid ON kills (uuid, ts DESC);
CREATE TABLE advancements (
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
advancement_id text NOT NULL,
achieved_at timestamptz NOT NULL,
title text,
PRIMARY KEY (uuid, advancement_id)
);
CREATE TABLE dimension_visits (
uuid uuid NOT NULL REFERENCES players(uuid) ON DELETE CASCADE,
dimension text NOT NULL,
first_at timestamptz NOT NULL,
PRIMARY KEY (uuid, dimension)
);
CREATE TABLE quest_defs (
quest_id text PRIMARY KEY,
chapter_id text,
chapter_title text,
title text,
def_hash text
);
CREATE TABLE quest_progress (
team_id uuid NOT NULL,
quest_id text NOT NULL,
completed_at timestamptz NOT NULL,
PRIMARY KEY (team_id, quest_id)
);
CREATE TABLE server_info (
id bigserial PRIMARY KEY,
started_at timestamptz NOT NULL,
mc_version text NOT NULL,
neoforge_version text NOT NULL,
java_version text NOT NULL,
mods jsonb NOT NULL
);
CREATE TABLE server_health (
bucket timestamptz PRIMARY KEY,
mspt_avg real NOT NULL,
mspt_p95 real NOT NULL,
mspt_max real NOT NULL,
tps real NOT NULL,
players integer NOT NULL,
chunks integer NOT NULL,
heap_used_mb real NOT NULL,
heap_max_mb real NOT NULL
);
CREATE TABLE sync_state (
path text PRIMARY KEY,
size bigint NOT NULL,
mtime bigint NOT NULL,
"offset" bigint NOT NULL DEFAULT 0,
hash text,
synced_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE admin_users (
id bigserial PRIMARY KEY,
username text NOT NULL UNIQUE,
password_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE admin_sessions (
id text PRIMARY KEY,
user_id bigint NOT NULL REFERENCES admin_users(id) ON DELETE CASCADE,
csrf text NOT NULL,
expires_at timestamptz NOT NULL
);
CREATE TABLE ingest_tokens (
id bigserial PRIMARY KEY,
hash text NOT NULL UNIQUE,
created_at timestamptz NOT NULL DEFAULT now(),
revoked_at timestamptz
);
+16
View File
@@ -0,0 +1,16 @@
-- Zuletzt bekannte Inhalte synchronisierter Dateien (Quest-Konfiguration, Teams) und Fehlerprotokoll
CREATE TABLE file_cache (
path text PRIMARY KEY,
sha256 text NOT NULL,
content text NOT NULL,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE app_errors (
id bigserial PRIMARY KEY,
at timestamptz NOT NULL DEFAULT now(),
source text NOT NULL,
message text NOT NULL
);
CREATE INDEX app_errors_at ON app_errors (at DESC);
+15
View File
@@ -0,0 +1,15 @@
-- Stundenaggregate der Servergesundheit (Minutenwerte werden nach 14 Tagen verdichtet)
CREATE TABLE server_health_hourly (
bucket timestamptz PRIMARY KEY,
mspt_avg real NOT NULL,
mspt_p95 real NOT NULL,
mspt_max real NOT NULL,
tps real NOT NULL,
players integer NOT NULL,
chunks integer NOT NULL,
heap_used_mb real NOT NULL,
heap_max_mb real NOT NULL,
samples integer NOT NULL
);
CREATE INDEX stat_snapshots_metric ON stat_snapshots (metric, granularity, uuid, bucket DESC);
+6
View File
@@ -0,0 +1,6 @@
-- Manuell angestoßene SFTP-Synchronisationen (vom Worker abgearbeitet)
CREATE TABLE sync_requests (
id bigserial PRIMARY KEY,
requested_at timestamptz NOT NULL DEFAULT now(),
done_at timestamptz
);
+9
View File
@@ -0,0 +1,9 @@
-- Herkunft jeder Sitzung (Mod oder Log-Fallback), damit sich beide Quellen nicht doppeln
ALTER TABLE sessions ADD COLUMN source text NOT NULL DEFAULT 'push';
-- Zeitpunkt der Beobachtung des aktuellen Namens: verspätete ältere Ereignisse ändern ihn nicht mehr
ALTER TABLE players ADD COLUMN name_seen_at timestamptz;
UPDATE players p SET name_seen_at = (SELECT max(seen_at) FROM name_history h WHERE h.uuid = p.uuid AND h.name = p.name);
-- "letzte Aktualisierung" ohne Tabellenscan
CREATE INDEX events_received ON events (received_at DESC);