Capture team chat and expose moderation match archives

This commit is contained in:
2026-10-17 20:11:49 -04:00
parent b678de2f6f
commit 870f13b150
28 changed files with 943 additions and 81 deletions
+5 -1
View File
@@ -9,6 +9,7 @@
import { pollVisible } from '$ui/poll.js';
import { timeFormat } from '$ui/stores/time-zone.js';
import Button from './primitives/Button.svelte';
import Badge from './primitives/Badge.svelte';
import Freshness from './Freshness.svelte';
import MatchEvidenceDialog from './moderation/MatchEvidenceDialog.svelte';
@@ -337,7 +338,10 @@
<span class="sr-only">Select evidence</span>
</label>
{/if}
<strong>{item.entry.message.player}</strong
<strong
>{item.entry.message
.player}{#if item.entry.message.channel === 'team'}
<Badge>Team chat</Badge>{/if}</strong
>{@render chatContext(item.entry.context)}
{#if canReadModeration($page.data?.user?.role) && (moderationGame === 't5' || moderationGame === 't6')}<a
class="moderation-link"
+53 -12
View File
@@ -5,6 +5,7 @@
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { untrack } from 'svelte';
import { page } from '$app/stores';
import { canReadModeration } from '$lib/auth.js';
import { browser } from '$app/environment';
import { contextHref } from '$lib/navigation-context.js';
import { timestampISO } from '$lib/time-zone.js';
@@ -42,6 +43,7 @@
let navigationId = 0;
const now = $derived(browser ? $uiNow : Date.now());
const filters = $derived(readMatchQuery(matchApiParams($page.url)));
const canViewArchive = $derived(canReadModeration($page.data?.user?.role));
const apiUrl = $derived.by(() => {
const params = matchApiParams($page.url);
params.set('limit', String(matchPageSize));
@@ -131,6 +133,19 @@
</script>
<section class="history" aria-label="Match history">
{#if canViewArchive}<div class="archive-control">
<label
><input
type="checkbox"
checked={filters.archive}
onchange={(event) =>
update({ archive: event.currentTarget.checked ? '1' : '' })}
/> Moderation archive</label
>
{#if filters.archive}<p>
Includes round-one, interrupted, and unfinished matches for evidence review.
</p>{/if}
</div>{/if}
{#snippet filterFields(suffix: string)}
<div class="filters">
{#if server === undefined}<Field forId={`match-game${suffix}`} label="Game">
@@ -357,9 +372,12 @@
<tr>
<td><RelativeTime value={match.startedAt} /></td>
<td class="match">
{#if match.status === 'completed'}<a
{#if match.status === 'completed' || (canViewArchive && filters.archive)}<a
class="match-link"
href={contextHref(`/matches/${match.id}`, $page.url)}
href={contextHref(
`/matches/${match.id}${canViewArchive && filters.archive ? '?matchArchive=1' : ''}`,
$page.url
)}
aria-label={`View ${match.map} match ${match.id} from ${$timeFormat.dateTime(match.startedAt)}`}
>{match.map} · Match #{match.id}</a
>
@@ -379,7 +397,8 @@
? ` · ${duration}`
: ''}</small
>
{#if notReporting}<small class="mobile-match-context"
{#if notReporting || (filters.archive && match.status !== 'completed')}<small
class="mobile-match-context"
>Last report <RelativeTime
value={match.lastReportedAt}
missing="Not recorded"
@@ -403,7 +422,7 @@
class:interrupted={match.status === 'interrupted' ||
match.status === 'end-report-missing' ||
notReporting}>{status}</span
>{#if notReporting}<small
>{#if notReporting || (filters.archive && match.status !== 'completed')}<small
>Last report <RelativeTime
value={match.lastReportedAt}
missing="Not recorded"
@@ -469,14 +488,16 @@
<summary>About match history</summary>
<p>
Players counts unique players across all reports. A dash means the value is unavailable,
not zero. In-progress matches show elapsed time since their recorded start and link to
the live server while reports are current. Otherwise, they show Not reporting with the
last report time. Completed durations require a recorded start and completed ending. End
report missing means a newer game was reported before this game's end report arrived.
Missing report counts show gaps in the reporter's sequence and can decrease as delayed
reports arrive. Only completed games contribute to the leaderboard. 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. {#if filters.archive}Archive
links open retained match reports and chat.{:else}They link to the live server while
reports are current.{/if} Otherwise, they show Not reporting with the last report time.
Completed durations require a recorded start and completed ending. End report missing means
a newer game was reported before this game's end report arrived. Missing report counts show
gaps in the reporter's sequence and can decrease as delayed reports arrive. Only completed
games contribute to the leaderboard. Round shows the final round for completed matches and
the highest observed round for unfinished matches. Open a {filters.archive
? 'recorded'
: 'completed'} match for timestamps and participant details.
</p>
</details>
</section>
@@ -487,6 +508,26 @@
gap: var(--space-4);
min-width: 0;
}
.archive-control {
display: grid;
gap: var(--space-2);
font-size: var(--text-sm);
}
.archive-control label {
display: flex;
align-items: center;
gap: var(--space-2);
width: fit-content;
cursor: pointer;
}
.archive-control input {
accent-color: var(--accent-primary);
}
.archive-control p {
margin: 0;
color: var(--text-secondary);
line-height: var(--leading-body);
}
.filters {
display: flex;
align-items: flex-start;
@@ -27,6 +27,7 @@
<li class:highlighted>
<div class="author">
<strong>{entry.message.player}</strong>
{#if entry.message.channel === 'team'}<Badge>Team chat</Badge>{/if}
{#if highlighted}<Badge variant="warning">Subject message</Badge>
{:else if selected}<Badge>Selected context</Badge>
{:else}<span>Surrounding context</span>{/if}