Add persistent match history to server pages

This commit is contained in:
2026-09-20 21:35:35 -04:00
parent 070e7fe9f7
commit fc681c56c9
9 changed files with 170 additions and 1 deletions
+82
View File
@@ -0,0 +1,82 @@
<script lang="ts">
import type { ServerMatch } from '$lib/match-history.js';
import { pollVisible } from '$ui/poll.js';
import { formatDuration } from '$ui/duration.js';
import Card from './primitives/Card.svelte';
import Button from './primitives/Button.svelte';
import EmptyState from './primitives/EmptyState.svelte';
import ListFooter from './primitives/ListFooter.svelte';
let { server }: { server: string } = $props();
let matches = $state<ServerMatch[]>([]);
let total = $state(0);
let limit = $state(25);
let loading = $state(true);
let error = $state('');
async function refresh(currentServer: string, currentLimit: number) {
try {
const response = await fetch(`/api/servers/${encodeURIComponent(currentServer)}/matches?limit=${currentLimit}`);
if (!response.ok) throw new Error('Could not load match history.');
const body = await response.json();
if (server !== currentServer || limit !== currentLimit) return;
matches = body.matches;
total = body.total;
error = '';
} catch (cause) {
if (server === currentServer && limit === currentLimit) error = (cause as Error).message;
} finally {
if (server === currentServer && limit === currentLimit) loading = false;
}
}
$effect(() => {
const currentServer = server;
const currentLimit = limit;
return pollVisible(() => refresh(currentServer, currentLimit), 10000);
});
</script>
<Card padding="lg">
<h2>Match history</h2>
<p>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.</p>
{#if error}
<div role="alert">{error} <Button onclick={() => refresh(server, limit)}>Retry</Button></div>
{/if}
{#if loading}
<EmptyState title="Loading matches…" />
{:else if matches.length === 0 && !error}
<EmptyState title="No matches recorded yet" description="Matches appear here when this server reports gameplay." />
{:else if matches.length}
<!-- Keyboard focus lets users scroll the wide table horizontally. -->
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
<div class="table-scroll" tabindex="0" role="region" aria-label="Match history">
<table>
<thead><tr><th>Started</th><th>Map</th><th>Status</th><th>Players at start</th><th>Unique players</th><th>Players at end</th><th>Game time</th><th>Final round</th></tr></thead>
<tbody>
{#each matches as match (match.id)}
<tr>
<td><time datetime={match.startedAt}>{new Date(match.startedAt).toLocaleString()}</time></td>
<td>{match.map}</td>
<td>{match.status === 'active' ? 'In progress' : match.status === 'completed' ? 'Completed' : 'Interrupted'}</td>
<td>{match.startPlayers ?? '—'}</td><td>{match.uniquePlayers}</td><td>{match.endPlayers ?? '—'}</td>
<td>{match.durationMs === null ? '—' : formatDuration(match.durationMs)}</td><td>{match.finalRound ?? '—'}</td>
</tr>
{/each}
</tbody>
</table>
</div>
<ListFooter total={Math.min(total, 10000)} bind:limit label="matches" />
{#if total > 10000}<p>Showing the latest 10,000 matches.</p>{/if}
{/if}
</Card>
<style>
h2 { margin: 0; font-size: var(--text-md); }
p { color: var(--text-secondary); font-size: var(--text-sm); }
.table-scroll { overflow-x: auto; }
.table-scroll:focus-visible { outline: none; box-shadow: var(--focus-ring); }
table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
th, td { padding: var(--space-3); text-align: left; border-bottom: 1px solid var(--border-subtle); white-space: nowrap; }
th { color: var(--text-secondary); font-size: var(--text-xs); }
td { font-variant-numeric: tabular-nums; }
</style>