diff --git a/src/lib/ui/components/MatchHistory.svelte b/src/lib/ui/components/MatchHistory.svelte index bf75bf0..0e79fb1 100644 --- a/src/lib/ui/components/MatchHistory.svelte +++ b/src/lib/ui/components/MatchHistory.svelte @@ -2,9 +2,12 @@ import RelativeTime from '$ui/components/RelativeTime.svelte'; import { untrack } from 'svelte'; import { page } from '$app/stores'; + import { browser } from '$app/environment'; import { contextHref } from '$lib/navigation-context.js'; + import { timestampISO } from '$lib/time-zone.js'; import { goto } from '$app/navigation'; import { timeFormat, timeZone } from '$ui/stores/time-zone.js'; + import { uiNow } from '$ui/stores/servers.js'; import type { MatchHistoryResponse } from '$lib/match-history.js'; import { matchApiParams, @@ -16,7 +19,7 @@ } from '$lib/match-history-query.js'; import { matchInstanceLabel } from '$ui/match-history.js'; import { pollVisible } from '$ui/poll.js'; - import { formatDuration } from '$ui/duration.js'; + import { elapsedSince, formatDuration } from '$ui/duration.js'; import Button from './primitives/Button.svelte'; import EmptyState from './primitives/EmptyState.svelte'; import Dialog from './primitives/Dialog.svelte'; @@ -33,6 +36,7 @@ let tableRegion = $state(); let pendingUrl: URL | undefined; let navigationId = 0; + const now = $derived(browser ? $uiNow : Date.now()); const filters = $derived(readMatchQuery(matchApiParams($page.url))); const apiUrl = $derived.by(() => { const params = matchApiParams($page.url); @@ -308,7 +312,7 @@ ? 'Unique players observed throughout the match' : item.key === 'round' ? 'Final round, or highest observed round for unfinished matches' - : 'Recorded duration; unknown durations sort last'} + : 'Recorded duration; in-progress and unknown durations sort last'} aria-describedby={item.key === 'players' ? 'match-player-help' : undefined} @@ -324,6 +328,12 @@ > {#each data.matches as match (match.id)} + {@const duration = + match.status === 'active' + ? elapsedSince(timestampISO(match.startedAt) ?? null, now) + : match.durationMs === null + ? null + : formatDuration(match.durationMs)} @@ -333,6 +343,15 @@ 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}{match.map} {:else}{match.map}{/if} · {match.status === @@ -340,8 +359,8 @@ ? 'In progress' : match.status === 'completed' ? 'Completed' - : 'Interrupted'}{match.durationMs !== null - ? ` · ${formatDuration(match.durationMs)}` + : 'Interrupted'}{duration !== null + ? ` · ${duration}` : ''} {#if server === undefined}
@@ -366,12 +385,9 @@ {match.uniquePlayers} {match.durationMs === null - ? '—' - : formatDuration(match.durationMs)}{duration ?? '—'} About match history

Players counts unique players across all reports. A dash means the value is unavailable, - not zero. Duration requires a recorded start and completed ending. Round shows the final - round for completed matches and the highest observed round for unfinished matches. Open - a completed match for exact timestamps and starting and ending player counts. + not zero. In-progress matches show elapsed time since their recorded start and link to + the live server. Completed durations require a recorded start and completed ending. + Round shows the final round for completed matches and the highest observed round for + unfinished matches. Open a completed match for exact timestamps and starting and ending + player counts.

diff --git a/tests/unit/ui-match-history.test.ts b/tests/unit/ui-match-history.test.ts index b263a75..6f7aa3b 100644 --- a/tests/unit/ui-match-history.test.ts +++ b/tests/unit/ui-match-history.test.ts @@ -103,6 +103,43 @@ it('keeps mobile filters and sorting in one labelled sheet with distinct control expect(new Set(ids).size).toBe(ids.length); expect(body).toContain('Updates every 10 seconds'); }); +it('links in-progress matches to their live server with the current filters and page', () => { + scope('?matchGame=t6&matchPage=2&matchStatus=active'); + const data = report(); + data.matches = [ + { ...match, server: 'Weekend lobby #1', status: 'active', endedAt: null, durationMs: null } + ]; + const body = render(MatchHistory, { props: { initial: data } }).body; + const rows = body.slice(body.indexOf('')); + expect(rows.match(/