Add timezone preference across time displays

This commit is contained in:
2026-09-20 22:12:27 -04:00
parent 1e6ff3abbf
commit 5c5e75526d
28 changed files with 297 additions and 52 deletions
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { timeFormat } from '$ui/stores/time-zone.js';
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import { pollVisible } from '$ui/poll.js';
@@ -74,7 +75,7 @@
{:else if !filtered.length && !error}<EmptyState title={search ? 'No matching betas' : tab === 'active' ? 'No active betas' : 'No beta history'} description={tab === 'active' && !search ? 'Start a beta when you have a version ready to test.' : undefined} />
{:else}<div class="rows">{#each filtered.slice(0,limit) as beta (beta.id)}
<a href={'/betas/' + beta.id} class="row">
<div class="identity"><strong>{beta.notes || `Beta #${beta.id}`}</strong>{#if beta.notes}<span class="beta-id"> · #{beta.id}</span>{/if}<span class="files">{beta.scripts.map(s => s.name + ' · ' + artifactVersionLabel(s.version)).join(', ')}</span><span class="blocker">{blocker(beta)}</span><span class="timing"><time datetime={beta.opened_at} title={new Date(beta.opened_at).toLocaleString()}>Opened {age(beta.opened_at)}</time>{#if beta.deadlineAt && active(beta)} · Ends {new Date(beta.deadlineAt).toLocaleString()}{:else if beta.closed_at} · Finished {new Date(beta.closed_at).toLocaleString()}{/if}</span></div>
<div class="identity"><strong>{beta.notes || `Beta #${beta.id}`}</strong>{#if beta.notes}<span class="beta-id"> · #{beta.id}</span>{/if}<span class="files">{beta.scripts.map(s => s.name + ' · ' + artifactVersionLabel(s.version)).join(', ')}</span><span class="blocker">{blocker(beta)}</span><span class="timing"><time datetime={beta.opened_at} title={$timeFormat.dateTime(beta.opened_at)}>Opened {age(beta.opened_at)}</time>{#if beta.deadlineAt && active(beta)} · Ends {$timeFormat.dateTime(beta.deadlineAt)}{:else if beta.closed_at} · Finished {$timeFormat.dateTime(beta.closed_at)}{/if}</span></div>
<div class="meta"><StatusBadge state={beta.state} />{#if beta.testOutcome && beta.testOutcome !== 'active'}<StatusBadge state={beta.testOutcome} />{/if}<span>{beta.servers.filter(server=>server.stage==='ready').length} / {beta.servers.length} servers passed</span></div>
</a>
{/each}</div><ListFooter total={filtered.length} bind:limit label="betas" />{/if}
+5 -4
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { timeFormat } from '$ui/stores/time-zone.js';
import { onMount } from 'svelte';
import { page } from '$app/stores';
import { artifactVersionLabel } from '$lib/artifact-version.js';
@@ -129,9 +130,9 @@
<div class="summary-heading"><h2>{closed ? beta.state === 'promoted' ? 'Version promoted' : 'Beta rolled back' : `${passed} / ${servers.length} servers passed`}</h2>{#if test}<span>{coverageSummary(test.plan)}</span>{/if}</div>
<p>{nextAction}</p>
{#if test?.reason}<p class="reason">{test.reason}</p>{/if}
<div class="timing"><span>Started <time datetime={beta.opened_at}>{new Date(beta.opened_at).toLocaleString()}</time></span>
{#if deadline && !closed}<strong>Ends <time datetime={deadline.toISOString()}>{deadline.toLocaleString()}</time> · {deadlineText}</strong>{/if}
{#if beta.closed_at}<span>Finished {new Date(beta.closed_at).toLocaleString()}</span>{/if}
<div class="timing"><span>Started <time datetime={beta.opened_at}>{$timeFormat.dateTime(beta.opened_at)}</time></span>
{#if deadline && !closed}<strong>Ends <time datetime={deadline.toISOString()}>{$timeFormat.dateTime(deadline)}</time> · {deadlineText}</strong>{/if}
{#if beta.closed_at}<span>Finished {$timeFormat.dateTime(beta.closed_at)}</span>{/if}
</div>
{#if !closed && test}<p class="muted">Waiting for empty servers uses this window. Pausing or retrying does not extend the deadline.</p>{/if}
<div class="actions">
@@ -163,7 +164,7 @@
{#if preview}
<p><strong>{preview.servers.filter(server => server.tested).length} tested servers</strong> and <strong>{preview.servers.filter(server => !server.tested).length} additional eligible servers</strong>.</p>
<p>Files are installed when you confirm. Tested servers have already restarted. Additional servers restart when empty, then their health is checked. Eligibility is determined by each file's current scope.</p>
<p class="muted">Reviewed {new Date(preview.createdAt).toLocaleString()}. If targets, versions, or composed settings change, you must review again.</p>
<p class="muted">Reviewed {$timeFormat.dateTime(preview.createdAt)}. If targets, versions, or composed settings change, you must review again.</p>
{#each preview.blockers as blocker}<p class="ui-error" role="alert">{blocker}</p>{/each}
<div class="file-list">{#each preview.files as file}<details open><summary>{file.name} · {artifactVersionLabel(file)} · {file.servers.length} servers</summary><p><code>{file.path}</code> · {file.scope} · SHA {file.sha256.slice(0,12)}</p><ul>{#each file.servers as name}<li><ServerIdentity {name}/><span>{preview.servers.find(server => server.name === name)?.tested ? 'Already tested' : 'Additional target · restart when empty'}</span></li>{/each}</ul></details>{/each}</div>
{#if preview.configWrites.length}<details><summary>Composed configuration files ({preview.configWrites.length})</summary>{#each preview.configWrites as write}<p>{write.server} · {write.path} · {write.sha256?.slice(0,12) ?? write.error}</p>{/each}</details>{/if}
+2 -1
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import { timeFormat } from '$ui/stores/time-zone.js';
import { onMount, tick } from 'svelte';
import { beforeNavigate, goto } from '$app/navigation';
import { defaultPlan, parseBetaPlan, betaPlanIssues, type BetaPlan } from '$lib/beta-plan.js';
@@ -178,7 +179,7 @@
{#if $fleetError}<div class="ui-error" role="alert">{$fleetError}<Button size="sm" onclick={refreshFleet}>Retry servers</Button></div>{/if}
{#if notice}<p role="status" class="notice">{notice}</p>{/if}
{#if loading}<EmptyState title="Loading beta setup…"/>
{:else if availableDraft}<section class="ui-panel draft"><h2>Resume unfinished beta?</h2><p>Saved {new Date(availableDraft.savedAt).toLocaleString()} · {availableDraft.picks.filter(p=>p.scriptId!==null).length} files · {availableDraft.servers.length} selected servers. Versions and eligibility will be rechecked.</p><div class="actions"><Button variant="primary" onclick={resumeDraft}>Resume draft</Button><Button onclick={discardDraft}>{hasLinkedSetup ? 'Discard draft and use linked files' : 'Discard draft'}</Button></div></section>
{:else if availableDraft}<section class="ui-panel draft"><h2>Resume unfinished beta?</h2><p>Saved {$timeFormat.dateTime(availableDraft.savedAt)} · {availableDraft.picks.filter(p=>p.scriptId!==null).length} files · {availableDraft.servers.length} selected servers. Versions and eligibility will be rechecked.</p><div class="actions"><Button variant="primary" onclick={resumeDraft}>Resume draft</Button><Button onclick={discardDraft}>{hasLinkedSetup ? 'Discard draft and use linked files' : 'Discard draft'}</Button></div></section>
{:else if !eligibleFiles.length && !flash}<EmptyState title="No files ready to test" description="Upload a file version or finish its active beta, then return here."/><Button href="/scripts">Open Library</Button>
{:else if scripts.length}
<ol class="steps" aria-label="Beta setup progress">{#each steps as label,index}<li aria-current={step===index?'step':undefined} class:current={step===index}><span>{index+1}</span>{label}</li>{/each}</ol>