CREATE TABLE servers ( server_id TEXT PRIMARY KEY, name TEXT NOT NULL, token_hash TEXT NOT NULL, active INTEGER NOT NULL DEFAULT 1 CHECK (active IN (0, 1)), created_at INTEGER NOT NULL ); CREATE TABLE events ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id TEXT NOT NULL REFERENCES servers(server_id), event_id TEXT NOT NULL, mission_id TEXT NOT NULL, received_at INTEGER NOT NULL, payload_hash TEXT NOT NULL, payload TEXT NOT NULL CHECK (json_valid(payload)), UNIQUE (server_id, event_id) ); CREATE INDEX events_mission ON events(server_id, mission_id, id); CREATE TABLE missions ( id INTEGER PRIMARY KEY AUTOINCREMENT, server_id TEXT NOT NULL REFERENCES servers(server_id), mission_id TEXT NOT NULL, event_id TEXT NOT NULL, terminal INTEGER NOT NULL, outcome TEXT NOT NULL, elapsed_seconds INTEGER NOT NULL, occurred_at INTEGER NOT NULL, received_at INTEGER NOT NULL, partial INTEGER NOT NULL, peak_players INTEGER NOT NULL, dropped_events INTEGER NOT NULL, payload TEXT NOT NULL, UNIQUE (server_id, mission_id) ); CREATE INDEX missions_server_page ON missions(server_id, id DESC); CREATE INDEX missions_outcome ON missions(outcome, server_id); -- An event and its mission projection commit together. Duplicate inserts never -- fire this trigger. First terminal snapshot wins; late starts/heartbeats cannot -- reopen a mission. Elapsed mission time takes precedence over wall-clock skew. CREATE TRIGGER project_mission AFTER INSERT ON events BEGIN INSERT INTO missions ( server_id, mission_id, event_id, terminal, outcome, elapsed_seconds, occurred_at, received_at, partial, peak_players, dropped_events, payload ) VALUES ( NEW.server_id, NEW.mission_id, NEW.event_id, json_extract(NEW.payload, '$.type') = 'mission_finished', json_extract(NEW.payload, '$.outcome'), json_extract(NEW.payload, '$.elapsedSeconds'), json_extract(NEW.payload, '$.occurredAt'), NEW.received_at, json_extract(NEW.payload, '$.partial'), json_extract(NEW.payload, '$.peakPlayers'), json_extract(NEW.payload, '$.droppedEvents'), NEW.payload ) ON CONFLICT(server_id, mission_id) DO UPDATE SET event_id = excluded.event_id, terminal = excluded.terminal, outcome = excluded.outcome, elapsed_seconds = excluded.elapsed_seconds, occurred_at = excluded.occurred_at, received_at = excluded.received_at, partial = MAX(missions.partial, excluded.partial), peak_players = MAX(missions.peak_players, excluded.peak_players), dropped_events = MAX(missions.dropped_events, excluded.dropped_events), payload = excluded.payload WHERE missions.terminal = 0 AND ( excluded.terminal = 1 OR excluded.elapsed_seconds > missions.elapsed_seconds OR (excluded.elapsed_seconds = missions.elapsed_seconds AND excluded.occurred_at >= missions.occurred_at) ); END; -- Final snapshots alone feed rankings: spectator and temporary connection IDs -- never become global player records. Missing legacy counters remain NULL. CREATE VIEW final_players AS SELECT m.server_id, m.mission_id, m.outcome, m.elapsed_seconds, m.partial, json_extract(m.payload, '$.playersTruncated') AS players_truncated, json_extract(p.value, '$.id') AS player_id, json_extract(p.value, '$.name') AS name, json_extract(p.value, '$.escaped') AS escaped, json_extract(p.value, '$.connected') AS connected, json_extract(p.value, '$.connectedSeconds') AS connected_seconds, json_extract(p.value, '$.kills') AS kills, json_extract(p.value, '$.friendlyKills') AS friendly_kills, json_extract(p.value, '$.deaths') AS deaths, json_extract(p.value, '$.downed') AS downed, json_extract(p.value, '$.recoveries') AS recoveries FROM missions m, json_each(m.payload, '$.players') p WHERE m.terminal = 1 AND m.outcome IN ('victory', 'defeat') AND json_extract(p.value, '$.authenticated') = 1 AND json_extract(p.value, '$.participated') = 1;