Add timezone preference across time displays
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { confirmAction } from '$ui/confirm.js';
|
||||
@@ -350,7 +351,7 @@
|
||||
</div></details>
|
||||
</Card>
|
||||
|
||||
{#if installedVersions.length}<details class="installed"><summary>Recorded installed files · {installedVersions.length} servers</summary><p>Last recorded file writes, not a verification of the currently running process.</p><ul>{#each installedVersions as installed}<li><a href={'/servers/' + encodeURIComponent(installed.server)}>{installed.server}</a> · v{installed.version} · {new Date(installed.recordedAt).toLocaleString()}</li>{/each}</ul></details>{:else if family !== 'config'}<p class="dim">No installed file versions recorded. Eligibility does not mean a file is installed.</p>{/if}
|
||||
{#if installedVersions.length}<details class="installed"><summary>Recorded installed files · {installedVersions.length} servers</summary><p>Last recorded file writes, not a verification of the currently running process.</p><ul>{#each installedVersions as installed}<li><a href={'/servers/' + encodeURIComponent(installed.server)}>{installed.server}</a> · v{installed.version} · {$timeFormat.dateTime(installed.recordedAt)}</li>{/each}</ul></details>{:else if family !== 'config'}<p class="dim">No installed file versions recorded. Eligibility does not mean a file is installed.</p>{/if}
|
||||
|
||||
{#if a.kind === 'config' || a.kind === 'baseline'}
|
||||
<ConfigEditor {id} kind={a.kind} {versions} currentVersionId={a.current_version_id}
|
||||
@@ -372,7 +373,7 @@
|
||||
<span class="v-notes">{v.notes ?? ''}
|
||||
{#if v.source}<a href={v.source.release_url} target="_blank" rel="noreferrer">GitHub · {v.source.asset_name} ↗</a>{/if}
|
||||
</span>
|
||||
<time class="v-when" datetime={v.uploaded_at}>{new Date(v.uploaded_at).toLocaleString()}</time>
|
||||
<time class="v-when" datetime={v.uploaded_at}>{$timeFormat.dateTime(v.uploaded_at)}</time>
|
||||
{#if a.current_version_id === v.id}
|
||||
<Badge variant="online" size="sm">Promoted</Badge>
|
||||
{:else}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { tick } from 'svelte';
|
||||
import ServerIdentity from './ServerIdentity.svelte';
|
||||
import StatusBadge from './primitives/StatusBadge.svelte';
|
||||
@@ -22,7 +23,7 @@
|
||||
$effect(() => { evidenceServer; evidenceKind; evidenceQuery; evidenceLimit = 25; });
|
||||
function reportAge(at:string) { const minutes=Math.max(0,Math.floor((Date.now()-Date.parse(at))/60000));return minutes<1?'less than a minute ago':minutes<60?`${minutes}m ago`:`${Math.floor(minutes/60)}h ${minutes%60}m ago`; }
|
||||
async function action(action:string, extra:Record<string,string>={}) {
|
||||
if (action === 'retry' && !await confirmAction({title:'Start a fresh test attempt?',message:`Reset current progress and checklist sign-offs on ${test.servers.length} deployed servers. They will restart when empty and must collect fresh evidence. Previous evidence remains in history. The deadline stays ${deadline ? new Date(deadline).toLocaleString() : 'unchanged'}.`,confirmLabel:'Reset and restart testing'})) return;
|
||||
if (action === 'retry' && !await confirmAction({title:'Start a fresh test attempt?',message:`Reset current progress and checklist sign-offs on ${test.servers.length} deployed servers. They will restart when empty and must collect fresh evidence. Previous evidence remains in history. The deadline stays ${deadline ? $timeFormat.dateTime(deadline) : 'unchanged'}.`,confirmLabel:'Reset and restart testing'})) return;
|
||||
busy = true; error = ''; message = '';
|
||||
try {
|
||||
const response = await fetch(`/api/betas/${betaId}/test`,{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({action,...extra})}), body = await response.json();
|
||||
@@ -57,7 +58,7 @@
|
||||
{#each ranked as server (server.server)}
|
||||
<details class="server" open={!server.passed}>
|
||||
<summary><ServerIdentity name={server.server}/><span>{server.progress.filter(p=>p.passed).length} / {server.progress.length} checks passed</span>{#if test.plan.requirements.some(r=>r.kind==='recovery')}<span>Recovery cycles: {server.phase}</span>{/if}</summary>
|
||||
{#if test.plan.requirements.some(needsGameTelemetry)}<p class="observation">{#if server.observation?.lastGameplayAt}Latest gameplay: {server.observation.lastEvent?.replaceAll('_',' ')} · <time datetime={server.observation.lastGameplayAt} title={new Date(server.observation.lastGameplayAt).toLocaleString()}>{reportAge(server.observation.lastGameplayAt)}</time> · {server.observation.players ?? 'Unknown'} players{#if server.observation.round !== null} · Round {server.observation.round}{/if}{:else}No gameplay reports in this attempt yet.{/if} <a href={`/servers/${encodeURIComponent(server.server)}?tab=diagnostics#telemetry`}>Telemetry diagnostics</a></p>{/if}
|
||||
{#if test.plan.requirements.some(needsGameTelemetry)}<p class="observation">{#if server.observation?.lastGameplayAt}Latest gameplay: {server.observation.lastEvent?.replaceAll('_',' ')} · <time datetime={server.observation.lastGameplayAt} title={$timeFormat.dateTime(server.observation.lastGameplayAt)}>{reportAge(server.observation.lastGameplayAt)}</time> · {server.observation.players ?? 'Unknown'} players{#if server.observation.round !== null} · Round {server.observation.round}{/if}{:else}No gameplay reports in this attempt yet.{/if} <a href={`/servers/${encodeURIComponent(server.server)}?tab=diagnostics#telemetry`}>Telemetry diagnostics</a></p>{/if}
|
||||
{#each server.progress as progress (progress.id)}
|
||||
{@const requirement = test.plan.requirements.find(r=>r.id===progress.id)}
|
||||
{#if requirement}<div class="requirement" class:failed={progress.failed}>
|
||||
@@ -80,7 +81,7 @@
|
||||
{#if evidenceOpen}
|
||||
<p>Newest first. Includes retained evidence from earlier attempts and recovery cycles; current progress uses only the current attempt.</p>
|
||||
<div class="filters"><label>Server<select bind:value={evidenceServer}><option value="">All servers</option>{#each [...new Set(test.evidence.map(event=>event.server))] as server}<option value={server}>{server}</option>{/each}</select></label><label>Event type<select bind:value={evidenceKind}><option value="">All events</option><option value="gameplay">Gameplay reports</option><option value="health">Health observations</option><option value="manual-sign-off">Tester sign-offs</option></select></label><label>Search evidence<input type="search" bind:value={evidenceQuery} placeholder="Round, map, tester, observation…"/></label></div>
|
||||
{#each filteredEvidence.slice(0,evidenceLimit) as event(event.id)}<article class="evidence"><div class="heading"><ServerIdentity name={event.server}/><time datetime={event.received_at}>{new Date(event.received_at).toLocaleString()}</time></div><p>{betaEvidenceSummary(event)}</p><details><summary>Raw payload · {event.kind}</summary><pre>{event.payload}</pre></details></article>{/each}
|
||||
{#each filteredEvidence.slice(0,evidenceLimit) as event(event.id)}<article class="evidence"><div class="heading"><ServerIdentity name={event.server}/><time datetime={event.received_at}>{$timeFormat.dateTime(event.received_at)}</time></div><p>{betaEvidenceSummary(event)}</p><details><summary>Raw payload · {event.kind}</summary><pre>{event.payload}</pre></details></article>{/each}
|
||||
<ListFooter total={filteredEvidence.length} bind:limit={evidenceLimit} label="events"/>
|
||||
{#if !filteredEvidence.length}<p>{test.evidence.length ? 'No evidence matches these filters.' : 'No evidence recorded yet. Check deployment progress and telemetry diagnostics.'}</p>{/if}
|
||||
{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { isDialogBackdrop } from '$ui/dialog.js';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -100,7 +101,7 @@
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
<div class="launch-footer"><div><p role="status"><strong>{keysReady} / {preview.servers.length} keys available</strong></p><p class="caption">Validity is checked at startup. Preview expires at {new Date(preview.expiresAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}.</p>{#if keysReady < preview.servers.length}<button class="text-button" type="button" onclick={nextMissing}>Jump to next missing key</button>{/if}</div><div class="actions"><Button variant="ghost" disabled={busy} onclick={() => open = false}>Cancel</Button><Button type="submit" variant="primary" loading={busy} disabled={busy || expired || !canLaunch}>Queue {preview.servers.length} server{preview.servers.length === 1 ? '' : 's'} →</Button></div></div>
|
||||
<div class="launch-footer"><div><p role="status"><strong>{keysReady} / {preview.servers.length} keys available</strong></p><p class="caption">Validity is checked at startup. Preview expires at {$timeFormat.minuteTime(preview.expiresAt)}.</p>{#if keysReady < preview.servers.length}<button class="text-button" type="button" onclick={nextMissing}>Jump to next missing key</button>{/if}</div><div class="actions"><Button variant="ghost" disabled={busy} onclick={() => open = false}>Cancel</Button><Button type="submit" variant="primary" loading={busy} disabled={busy || expired || !canLaunch}>Queue {preview.servers.length} server{preview.servers.length === 1 ? '' : 's'} →</Button></div></div>
|
||||
</form>
|
||||
{:else}<p class="muted">Your fleet already meets or exceeds every target in this blueprint. No servers will be created or removed.</p><div class="actions"><Button onclick={() => open = false}>Done</Button></div>{/if}
|
||||
<details class="recipes"><summary>Setup and launch requirements</summary><p class="muted requirements">Each server uses the latest promoted setup when its turn begins. The host must have the selected games and DLC. This preview checks settings and capacity; it does not probe game files, images, credentials, or network access.</p>{#each preview.entries as entry}<div class="recipe"><LaunchRecipe recipe={entry.recipe} /></div>{/each}</details>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount, tick } from 'svelte';
|
||||
import { isDialogBackdrop } from '$ui/dialog.js';
|
||||
import { goto } from '$app/navigation';
|
||||
@@ -136,7 +137,7 @@
|
||||
<p class="muted">{preview.hasSavedKey ? 'A key is stored for this exact server ID; its validity is checked when the game starts. Enter a replacement here to update it at launch.' : 'Add the key here. It is saved for this exact server ID when you launch, including if provisioning fails.'} <a href={`/server-keys?server=${encodeURIComponent(preview.name ?? '')}`} target="_blank" rel="noopener">Manage saved keys in a new tab</a>; your launch stays open here.</p>
|
||||
<div class="launch-actions">{#if !sourceDeploymentId}<Button variant="ghost" disabled={busy || saving} onclick={() => { preview = null; plutoKey = ''; error = ''; }}>Back</Button>{/if}<Button type="submit" variant="primary" loading={busy} disabled={busy || saving || expired || !preview.ready || (!preview.hasSavedKey && !plutoKey.trim())}>Launch server</Button></div>
|
||||
{#if !preview.hasSavedKey && !plutoKey.trim()}<p class="muted">Enter this server’s key to enable launch.</p>{/if}
|
||||
<p class="caption">Preview valid until {new Date(preview.expiresAt).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' })}. Lobby and port availability are checked again at launch.</p>
|
||||
<p class="caption">Preview valid until {$timeFormat.minuteTime(preview.expiresAt)}. Lobby and port availability are checked again at launch.</p>
|
||||
</form>
|
||||
<details class="save"><summary>Make this a reusable blueprint</summary><p class="muted">Save this game and map with a target of one lobby. Future launches use the latest promoted setup and create only missing lobbies.</p>
|
||||
{#if savedName}<p class="success" role="status">“{savedName}” is ready in your blueprint library.</p>{:else}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import type { LaunchRecipeSummary } from '$lib/deployment-plan.js';
|
||||
let { recipe, historical = false }: { recipe: LaunchRecipeSummary; historical?: boolean } = $props();
|
||||
</script>
|
||||
@@ -13,7 +14,7 @@
|
||||
<details>
|
||||
<summary>{recipe.files.length} managed file{recipe.files.length === 1 ? '' : 's'} {historical ? 'recorded for this deployment' : 'in the current setup'}</summary>
|
||||
<ul>{#each recipe.files as file}<li><div><strong>{file.name}</strong><span>{file.version !== null ? `v${file.version}` : 'Generated'} · {file.kind} · {file.size.toLocaleString()} bytes</span></div><code>{file.path}</code><small title={file.sha256}>SHA-256 {file.sha256.slice(0, 12)}</small></li>{/each}</ul>
|
||||
<p>{historical ? 'Recorded' : 'Previewed'} {new Date(recipe.capturedAt).toLocaleString()}. {historical ? 'This records the files selected for this deployment. Launch again uses the latest promoted setup for this map.' : 'This preview shows the promoted setup at the time above. The deployment history records the files selected when provisioning begins.'}</p>
|
||||
<p>{historical ? 'Recorded' : 'Previewed'} {$timeFormat.dateTime(recipe.capturedAt)}. {historical ? 'This records the files selected for this deployment. Launch again uses the latest promoted setup for this map.' : 'This preview shows the promoted setup at the time above. The deployment history records the files selected when provisioning begins.'}</p>
|
||||
<small class="fingerprint" title={recipe.fingerprint}>Recipe {recipe.fingerprint.slice(0, 16)}</small>
|
||||
</details>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import type { ServerMatch } from '$lib/match-history.js';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
import { formatDuration } from '$ui/duration.js';
|
||||
@@ -57,8 +58,8 @@
|
||||
<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>
|
||||
<td><time datetime={match.startedAt}>{$timeFormat.dateTime(match.startedAt)}</time></td>
|
||||
<td>{#if match.endedAt}<time datetime={match.endedAt}>{$timeFormat.dateTime(match.endedAt)}</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>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import Dialog from './primitives/Dialog.svelte';
|
||||
import Button from './primitives/Button.svelte';
|
||||
import { notificationHistory, markNotificationsRead, clearNotifications } from '$ui/toast.js';
|
||||
@@ -8,7 +9,7 @@
|
||||
|
||||
<Dialog bind:open title="Recent notifications" description="Recent action results and completed lobby watches in this browser tab. Watches run while the app is open.">
|
||||
{#if $notificationHistory.length}
|
||||
<ol>{#each $notificationHistory as item (item.id)}<li class:attention={item.variant === 'danger' || item.variant === 'warning'}><p>{item.message}</p><time datetime={item.createdAt}>{new Date(item.createdAt).toLocaleString()}</time>{#if item.href}<a href={item.href} onclick={() => open = false}>View details →</a>{/if}</li>{/each}</ol>
|
||||
<ol>{#each $notificationHistory as item (item.id)}<li class:attention={item.variant === 'danger' || item.variant === 'warning'}><p>{item.message}</p><time datetime={item.createdAt}>{$timeFormat.dateTime(item.createdAt)}</time>{#if item.href}<a href={item.href} onclick={() => open = false}>View details →</a>{/if}</li>{/each}</ol>
|
||||
{:else}<p class="empty">No recent notifications. Completed lobby watches and action results will appear here.</p>{/if}
|
||||
{#snippet actions()}<Button disabled={!$notificationHistory.length} onclick={clearNotifications}>Clear history</Button><Button onclick={() => open = false}>Close</Button>{/snippet}
|
||||
</Dialog>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import type { ActivityEvent, OperationStep } from '$lib/operation-flow.js';
|
||||
import Button from './primitives/Button.svelte';
|
||||
let { steps, events, active = false }: { steps: OperationStep[]; events: ActivityEvent[]; active?: boolean } = $props();
|
||||
@@ -22,14 +23,14 @@
|
||||
</ol>
|
||||
</section>
|
||||
<section>
|
||||
{#if latest}<p class="latest"><strong>Latest update</strong> {latest.message}<time datetime={latest.created_at}>{new Date(latest.created_at).toLocaleString()}</time></p>{/if}
|
||||
{#if latest}<p class="latest"><strong>Latest update</strong> {latest.message}<time datetime={latest.created_at}>{$timeFormat.dateTime(latest.created_at)}</time></p>{/if}
|
||||
<details>
|
||||
<summary>Activity history · {events.length} events{active ? ' · updates automatically' : ''}</summary>
|
||||
{#if events.length}
|
||||
<div class="tools"><label>Search activity<input type="search" bind:value={query} oninput={() => limit = 25} placeholder="Server, stage, or message" /></label><label>Order<select bind:value={newestFirst}><option value={true}>Newest first</option><option value={false}>Oldest first</option></select></label></div>
|
||||
<ol class="timeline">
|
||||
{#each visible as event (event.id)}
|
||||
<li><time datetime={event.created_at}>{new Date(event.created_at).toLocaleString()}</time><p>{event.message}</p></li>
|
||||
<li><time datetime={event.created_at}>{$timeFormat.dateTime(event.created_at)}</time><p>{event.message}</p></li>
|
||||
{/each}
|
||||
</ol>
|
||||
{#if !matching.length}<p class="muted">No matching activity.</p>{/if}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import type { GameTelemetry } from '$lib/telemetry.js';
|
||||
import { uiNow } from '$ui/stores/servers.js';
|
||||
import { elapsedSince } from '$ui/duration.js';
|
||||
@@ -51,7 +52,7 @@
|
||||
</div>
|
||||
<div class="report-meta">
|
||||
<span>{players.length} {players.length === 1 ? 'player' : 'players'}</span>
|
||||
<time datetime={report.receivedAt} title={new Date(report.receivedAt).toLocaleString()}>
|
||||
<time datetime={report.receivedAt} title={$timeFormat.dateTime(report.receivedAt)}>
|
||||
Updated {elapsedSince(report.receivedAt, $uiNow)} ago
|
||||
</time>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import type { GithubPluginRelease, PluginUpdate } from '$lib/plugin-github.js';
|
||||
@@ -118,7 +119,7 @@
|
||||
{#if release}
|
||||
<div class="preview">
|
||||
<div class="heading"><h3>{release.name || release.tag}</h3><Badge variant={release.prerelease ? 'warning' : 'neutral'}>{release.prerelease ? 'Prerelease' : 'Release'} · {release.tag}</Badge></div>
|
||||
<p><a href={release.url} target="_blank" rel="noreferrer">View on GitHub ↗</a> · Published {new Date(release.publishedAt).toLocaleDateString()}</p>
|
||||
<p><a href={release.url} target="_blank" rel="noreferrer">View on GitHub ↗</a> · Published {$timeFormat.date(release.publishedAt)}</p>
|
||||
<details open><summary>Release notes</summary><pre>{release.notes || 'No release notes provided.'}</pre></details>
|
||||
{#if release.assets.length === 0}
|
||||
<p>No importable DLL assets found. Choose another release or upload a DLL manually. Direct DLL assets up to 64 MB are supported.</p>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { telemetryByServer, uiNow } from '$ui/stores/servers.js';
|
||||
import { elapsedSince } from '$ui/duration.js';
|
||||
let { name, since = null }: { name: string; since?: string | null } = $props();
|
||||
@@ -11,7 +12,7 @@
|
||||
{#if report && (report.event === 'round_change' || report.event === 'heartbeat') && fresh}
|
||||
<div class="round" aria-live="polite">
|
||||
<strong>Round {report.round}</strong>
|
||||
<span title={report.receivedAt}>Last reported {elapsedSince(report.receivedAt, $uiNow)} ago · {report.players.length} players</span>
|
||||
<span title={$timeFormat.dateTime(report.receivedAt)}>Last reported {elapsedSince(report.receivedAt, $uiNow)} ago · {report.players.length} players</span>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { timeFormat } from '$ui/stores/time-zone.js';
|
||||
import { onMount } from 'svelte';
|
||||
import { TELEMETRY_REQUEST_RETENTION, type TelemetryRequestLogEntry } from '$lib/telemetry-diagnostics.js';
|
||||
import { pollVisible } from '$ui/poll.js';
|
||||
@@ -61,10 +62,7 @@
|
||||
});
|
||||
|
||||
function timestamp(value: string): string {
|
||||
return new Date(value).toLocaleString(undefined, {
|
||||
year: 'numeric', month: 'short', day: 'numeric',
|
||||
hour: '2-digit', minute: '2-digit', second: '2-digit', timeZoneName: 'short'
|
||||
});
|
||||
return $timeFormat.dateTime(value);
|
||||
}
|
||||
|
||||
function outcome(status: number): string {
|
||||
|
||||
Reference in New Issue
Block a user