Add global match history and ending timestamps
This commit is contained in:
@@ -1,7 +1,9 @@
|
||||
export interface ServerMatch {
|
||||
id: number;
|
||||
server: string;
|
||||
map: string;
|
||||
startedAt: string;
|
||||
endedAt: string | null;
|
||||
status: 'active' | 'completed' | 'interrupted';
|
||||
startPlayers: number | null;
|
||||
uniquePlayers: number;
|
||||
|
||||
@@ -26,6 +26,7 @@ CREATE TABLE IF NOT EXISTS leaderboard_matches (
|
||||
last_fingerprint TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaderboard_server ON leaderboard_matches(server, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaderboard_started ON leaderboard_matches(started_at DESC, id DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_leaderboard_map ON leaderboard_matches(game, map_key);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS leaderboard_players (
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import { db } from '../db/index.js';
|
||||
import type { ServerMatch } from '../../match-history.js';
|
||||
|
||||
export function serverMatchHistory(server: string, limit = 25): { matches: ServerMatch[]; total: number } {
|
||||
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,
|
||||
const where = server === undefined ? '' : 'WHERE m.server = ?';
|
||||
const parameters = server === undefined ? [] : [server];
|
||||
const total = (handle.prepare(`SELECT COUNT(*) AS total FROM leaderboard_matches m ${where}`).get(...parameters) as { total: number }).total;
|
||||
const matches = handle.prepare(`SELECT m.id, m.server, m.map_name AS map, m.started_at AS startedAt, m.status,
|
||||
CASE WHEN m.status = 'completed' THEN m.ended_at END AS endedAt,
|
||||
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
|
||||
@@ -13,7 +16,7 @@ export function serverMatchHistory(server: string, limit = 25): { matches: Serve
|
||||
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[];
|
||||
FROM leaderboard_matches m ${where} ORDER BY m.started_at DESC, m.id DESC LIMIT ?`)
|
||||
.all(...parameters, limit) as ServerMatch[];
|
||||
return { matches, total };
|
||||
}
|
||||
|
||||
@@ -2,21 +2,23 @@
|
||||
import type { ServerMatch } from '$lib/match-history.js';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { formatDuration } from '$ui/duration.js';
|
||||
import { serverTitle } from '$ui/server-labels.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 { 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) {
|
||||
async function refresh(currentServer: string | undefined, currentLimit: number) {
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${encodeURIComponent(currentServer)}/matches?limit=${currentLimit}`);
|
||||
const endpoint = currentServer === undefined ? '/api/matches' : `/api/servers/${encodeURIComponent(currentServer)}/matches`;
|
||||
const response = await fetch(`${endpoint}?limit=${currentLimit}`);
|
||||
if (!response.ok) throw new Error('Could not load match history.');
|
||||
const body = await response.json();
|
||||
if (server !== currentServer || limit !== currentLimit) return;
|
||||
@@ -45,17 +47,19 @@
|
||||
{#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." />
|
||||
<EmptyState title="No matches recorded yet" description={server === undefined ? 'Matches appear here when servers report gameplay.' : '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>
|
||||
<thead><tr><th>Started</th><th>Ended</th>{#if server === undefined}<th>Server</th>{/if}<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>{#if match.endedAt}<time datetime={match.endedAt}>{new Date(match.endedAt).toLocaleString()}</time>{:else}—{/if}</td>
|
||||
{#if server === undefined}<td><a href={`/servers/${encodeURIComponent(match.server)}?tab=matches`}>{serverTitle(match.server)}</a></td>{/if}
|
||||
<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>
|
||||
@@ -79,4 +83,5 @@
|
||||
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; }
|
||||
a { color: var(--accent-primary-hover); }
|
||||
</style>
|
||||
|
||||
@@ -99,7 +99,8 @@
|
||||
{ href: '/deployments', label: 'Operations', match: (p: string) => /^\/(deployments|campaigns)(\/|$)/.test(p) },
|
||||
{ href: '/betas', label: 'Betas', match: (p: string) => p.startsWith('/betas') },
|
||||
{ href: '/settings', label: 'Settings', match: (p: string) => p.startsWith('/server-keys') || p.startsWith('/settings') },
|
||||
{ href: '/leaderboard', label: 'Leaderboard', match: (p: string) => p.startsWith('/leaderboard') }
|
||||
{ href: '/leaderboard', label: 'Leaderboard', match: (p: string) => p.startsWith('/leaderboard') },
|
||||
{ href: '/matches', label: 'Match History', match: (p: string) => p.startsWith('/matches') }
|
||||
];
|
||||
const secondary = $derived(tabs[1].match($page.url.pathname)
|
||||
? [{ href: '/scripts', label: 'Scripts' }, { href: '/configs', label: 'Configs' }, { href: '/plugins', label: 'Plugins' }]
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { serverMatchHistory } from '$lib/server/fleet/match-history.js';
|
||||
|
||||
export function GET({ url }: { 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(undefined, limit));
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<script lang="ts">
|
||||
import MatchHistory from '$ui/components/MatchHistory.svelte';
|
||||
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
|
||||
</script>
|
||||
|
||||
<svelte:head><title>Match History · Electric Cherry</title></svelte:head>
|
||||
|
||||
<div class="page">
|
||||
<SectionHeader title="Match History" subtitle="Recorded matches across all servers." />
|
||||
<MatchHistory />
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.page { display: grid; gap: var(--space-6); min-width: 0; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user