diff --git a/src/lib/match-history.ts b/src/lib/match-history.ts index 74c7240..584bb2e 100644 --- a/src/lib/match-history.ts +++ b/src/lib/match-history.ts @@ -11,6 +11,7 @@ export interface ServerMatch { server_deleted_at?: string | null; map: string; startedAt: string; + lastReportedAt: string | null; endedAt: string | null; status: 'active' | 'completed' | 'interrupted'; startPlayers: number | null; diff --git a/src/lib/server/fleet/leaderboard-ingest.ts b/src/lib/server/fleet/leaderboard-ingest.ts index f8f7967..8362207 100644 --- a/src/lib/server/fleet/leaderboard-ingest.ts +++ b/src/lib/server/fleet/leaderboard-ingest.ts @@ -67,7 +67,15 @@ export function recordLeaderboardTelemetry(handle: Database.Database, report: Ga 'SELECT * FROM leaderboard_matches WHERE server = ? AND server_deleted_at IS NULL ORDER BY id DESC LIMIT 1' ) .get(report.server) as Match | undefined; - if (match?.last_fingerprint === signature && match.map_key === identity.key) return; + if (match?.last_fingerprint === signature && match.map_key === identity.key) { + // Unchanged gameplay still proves an open match is reporting. Keep the + // duplicate from changing participation, counters, or completed results. + if (match.status === 'active') + handle + .prepare('UPDATE leaderboard_matches SET last_received_at = ? WHERE id = ?') + .run(report.receivedAt, match.id); + if (match.status !== 'interrupted') return; + } const isStart = report.event === 'game_start'; const round = isStart ? 0 : report.round; diff --git a/src/lib/server/fleet/match-history.ts b/src/lib/server/fleet/match-history.ts index 14f983c..f7a541b 100644 --- a/src/lib/server/fleet/match-history.ts +++ b/src/lib/server/fleet/match-history.ts @@ -11,6 +11,7 @@ import { import { readMatchQuery, type MatchHistoryQuery } from '../../match-history-query.js'; const matchSummary = `m.id, m.game, m.highest_round AS observedRound, m.server, m.server_deleted_at, m.map_name AS map, m.started_at AS startedAt, m.status, + m.last_received_at AS lastReportedAt, 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, diff --git a/src/lib/server/provisioning/native.ts b/src/lib/server/provisioning/native.ts index 82b5779..968cadf 100644 --- a/src/lib/server/provisioning/native.ts +++ b/src/lib/server/provisioning/native.ts @@ -1551,12 +1551,20 @@ async function processJob(job: Job, eligibility: (job: Job) => string | null) { hold(job, 'Waiting for the agent to verify a stopped container.'); return; } - db() - .prepare( - 'UPDATE servers SET container_state=?,last_players=NULL,last_roster=NULL WHERE name=?' - ) - .run(health.state, job.server); - complete(job); + db().transaction(() => { + db() + .prepare( + 'UPDATE servers SET container_state=?,last_players=NULL,last_roster=NULL WHERE name=?' + ) + .run(health.state, job.server); + db() + .prepare( + `UPDATE leaderboard_matches SET status='interrupted', ended_at=? + WHERE server=? AND status='active' AND server_deleted_at IS NULL` + ) + .run(containerObservedAt, job.server); + complete(job); + })(); return; } check( diff --git a/src/lib/ui/components/MatchHistory.svelte b/src/lib/ui/components/MatchHistory.svelte index 9574b6b..e6d315d 100644 --- a/src/lib/ui/components/MatchHistory.svelte +++ b/src/lib/ui/components/MatchHistory.svelte @@ -8,6 +8,7 @@ import { browser } from '$app/environment'; import { contextHref } from '$lib/navigation-context.js'; import { timestampISO } from '$lib/time-zone.js'; + import { freshStatusTimestamp } from '$lib/server-freshness.js'; import { goto } from '$app/navigation'; import { timeFormat, timeZone } from '$ui/stores/time-zone.js'; import { uiNow } from '$ui/stores/servers.js'; @@ -331,9 +332,23 @@ >
{#each data.matches as match (match.id)} + {@const reporting = + match.status === 'active' && + !match.server_deleted_at && + freshStatusTimestamp(timestampISO(match.lastReportedAt) ?? null, now)} + {@const notReporting = match.status === 'active' && !reporting} + {@const status = notReporting + ? 'Not reporting' + : match.status === 'active' + ? 'In progress' + : match.status === 'completed' + ? 'Completed' + : 'Interrupted'} {@const duration = match.status === 'active' - ? elapsedSince(timestampISO(match.startedAt) ?? null, now) + ? reporting + ? elapsedSince(timestampISO(match.startedAt) ?? null, now) + : null : match.durationMs === null ? null : formatDuration(match.durationMs)} @@ -346,7 +361,7 @@ aria-label={`View ${match.map} match ${match.id} from ${$timeFormat.dateTime(match.startedAt)}`} >{match.map} · Match #{match.id} - {:else if match.status === 'active' && !match.server_deleted_at} {:else}{match.map}{/if}