From fc681c56c9589b5f59e85b9b15ae99bd260d2373 Mon Sep 17 00:00:00 2001 From: josh Date: Sun, 20 Sep 2026 21:35:35 -0400 Subject: [PATCH] Add persistent match history to server pages --- src/lib/match-history.ts | 11 +++ src/lib/server/db/index.ts | 1 + src/lib/server/db/schema.sql | 1 + src/lib/server/fleet/leaderboard-ingest.ts | 5 ++ src/lib/server/fleet/match-history.ts | 19 +++++ src/lib/ui/components/MatchHistory.svelte | 82 +++++++++++++++++++ .../api/servers/[name]/matches/+server.ts | 10 +++ src/routes/servers/[name]/+page.svelte | 6 +- tests/unit/leaderboard-ingest.test.ts | 36 ++++++++ 9 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/lib/match-history.ts create mode 100644 src/lib/server/fleet/match-history.ts create mode 100644 src/lib/ui/components/MatchHistory.svelte create mode 100644 src/routes/api/servers/[name]/matches/+server.ts diff --git a/src/lib/match-history.ts b/src/lib/match-history.ts new file mode 100644 index 0000000..258e80d --- /dev/null +++ b/src/lib/match-history.ts @@ -0,0 +1,11 @@ +export interface ServerMatch { + id: number; + map: string; + startedAt: string; + status: 'active' | 'completed' | 'interrupted'; + startPlayers: number | null; + uniquePlayers: number; + endPlayers: number | null; + durationMs: number | null; + finalRound: number | null; +} diff --git a/src/lib/server/db/index.ts b/src/lib/server/db/index.ts index 10123a8..bc600a0 100644 --- a/src/lib/server/db/index.ts +++ b/src/lib/server/db/index.ts @@ -260,6 +260,7 @@ export function db(): Database.Database { backfillMultiScriptJoins(handle); assertSnapshotSchemeSafe(handle); migrateGameTelemetry(handle); + migrateTable(handle, 'leaderboard_matches', [{ name: 'start_players', ddl: 'ALTER TABLE leaderboard_matches ADD COLUMN start_players INTEGER' }]); backfillLeaderboard(handle); } catch (err) { handle.close(); diff --git a/src/lib/server/db/schema.sql b/src/lib/server/db/schema.sql index 618d724..1cf360f 100644 --- a/src/lib/server/db/schema.sql +++ b/src/lib/server/db/schema.sql @@ -20,6 +20,7 @@ CREATE TABLE IF NOT EXISTS leaderboard_matches ( last_received_at TEXT NOT NULL, status TEXT NOT NULL DEFAULT 'active', -- active | completed | interrupted start_observed INTEGER NOT NULL DEFAULT 0, + start_players INTEGER, highest_round INTEGER NOT NULL DEFAULT 0, last_event TEXT NOT NULL, last_fingerprint TEXT NOT NULL diff --git a/src/lib/server/fleet/leaderboard-ingest.ts b/src/lib/server/fleet/leaderboard-ingest.ts index 05cd61f..f0593c6 100644 --- a/src/lib/server/fleet/leaderboard-ingest.ts +++ b/src/lib/server/fleet/leaderboard-ingest.ts @@ -77,6 +77,11 @@ export function recordLeaderboardTelemetry(handle: Database.Database, report: Ga } if (!match) return; + if (isStart) { + handle.prepare('UPDATE leaderboard_matches SET start_players = COALESCE(start_players, ?) WHERE id = ?') + .run(report.players.length, match.id); + } + const previous = new Map((handle.prepare('SELECT guid, last_round, present FROM leaderboard_match_players WHERE match_id = ?') .all(match.id) as Participation[]).map(player => [player.guid, player])); handle.prepare('UPDATE leaderboard_match_players SET present = 0 WHERE match_id = ?').run(match.id); diff --git a/src/lib/server/fleet/match-history.ts b/src/lib/server/fleet/match-history.ts new file mode 100644 index 0000000..37f2bc9 --- /dev/null +++ b/src/lib/server/fleet/match-history.ts @@ -0,0 +1,19 @@ +import { db } from '../db/index.js'; +import type { ServerMatch } from '../../match-history.js'; + +export function serverMatchHistory(server: string, limit = 25): { matches: ServerMatch[]; total: number } { + const handle = db(); + const total = (handle.prepare('SELECT COUNT(*) AS total FROM leaderboard_matches WHERE server = ?').get(server) as { total: number }).total; + const matches = handle.prepare(`SELECT m.id, m.map_name AS map, m.started_at AS startedAt, m.status, + m.start_players AS startPlayers, + (SELECT COUNT(*) FROM leaderboard_match_players p WHERE p.match_id = m.id) AS uniquePlayers, + CASE WHEN m.status = 'completed' THEN + (SELECT COUNT(*) FROM leaderboard_match_players p WHERE p.match_id = m.id AND p.present = 1) + END AS endPlayers, + CASE WHEN m.status = 'completed' AND m.start_observed = 1 THEN + MAX(0, ROUND((julianday(m.ended_at) - julianday(m.started_at)) * 86400000)) END AS durationMs, + CASE WHEN m.status = 'completed' THEN m.highest_round END AS finalRound + FROM leaderboard_matches m WHERE m.server = ? ORDER BY m.id DESC LIMIT ?`) + .all(server, limit) as ServerMatch[]; + return { matches, total }; +} diff --git a/src/lib/ui/components/MatchHistory.svelte b/src/lib/ui/components/MatchHistory.svelte new file mode 100644 index 0000000..5007d41 --- /dev/null +++ b/src/lib/ui/components/MatchHistory.svelte @@ -0,0 +1,82 @@ + + + +

Match history

+

Newest matches first. Unique players are counted across all match reports. — means the start or end wasn’t recorded, or the match is still in progress.

+ {#if error} +
{error}
+ {/if} + {#if loading} + + {:else if matches.length === 0 && !error} + + {:else if matches.length} + + +
+ + + + {#each matches as match (match.id)} + + + + + + + + {/each} + +
StartedMapStatusPlayers at startUnique playersPlayers at endGame timeFinal round
{match.map}{match.status === 'active' ? 'In progress' : match.status === 'completed' ? 'Completed' : 'Interrupted'}{match.startPlayers ?? '—'}{match.uniquePlayers}{match.endPlayers ?? '—'}{match.durationMs === null ? '—' : formatDuration(match.durationMs)}{match.finalRound ?? '—'}
+
+ + {#if total > 10000}

Showing the latest 10,000 matches.

{/if} + {/if} +
+ + diff --git a/src/routes/api/servers/[name]/matches/+server.ts b/src/routes/api/servers/[name]/matches/+server.ts new file mode 100644 index 0000000..a8829b7 --- /dev/null +++ b/src/routes/api/servers/[name]/matches/+server.ts @@ -0,0 +1,10 @@ +import { json } from '@sveltejs/kit'; +import { serverMatchHistory } from '$lib/server/fleet/match-history.js'; + +export function GET({ params, url }: { params: { name: string }; url: URL }) { + const limit = Number(url.searchParams.get('limit') ?? 25); + if (!Number.isSafeInteger(limit) || limit < 1 || limit > 10000) { + return json({ error: 'limit must be between 1 and 10000' }, { status: 400 }); + } + return json(serverMatchHistory(params.name, limit)); +} diff --git a/src/routes/servers/[name]/+page.svelte b/src/routes/servers/[name]/+page.svelte index 1967eab..00b8add 100644 --- a/src/routes/servers/[name]/+page.svelte +++ b/src/routes/servers/[name]/+page.svelte @@ -1,4 +1,5 @@