Make UI language, timestamps and live feedback consistent

This commit is contained in:
2026-10-07 19:46:40 -04:00
parent 0ed3cd6b47
commit 70185eb98c
81 changed files with 656 additions and 332 deletions
+10 -8
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import RelativeTime from '$ui/components/RelativeTime.svelte';
import ActivityLog from '$ui/components/ActivityLog.svelte';
import BackLink from '$ui/components/BackLink.svelte';
import { contextHref } from '$lib/navigation-context.js';
@@ -23,6 +24,7 @@
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
import ListFooter from '$ui/components/primitives/ListFooter.svelte';
import { groupMaintenanceScanErrors, groupMaintenanceVersions } from '$ui/maintenance-presentation.js';
import { countLabel, plural } from '$ui/plural.js';
let data = $state<MaintenanceOverview | null>(null);
let error = $state('');
@@ -182,14 +184,14 @@
<p class="muted">Use <a href="/maintenance?view=overview#activity">active work and attention</a> to cancel waiting maintenance or retry a failed verification after inspection.</p>
{:else}
{#if view === 'overview'}
<div class="summary"><span><strong>{attention}</strong> {attention === 1 ? 'needs' : 'need'} attention</span><span><strong>{waiting}</strong> active</span><span><strong>{outdated.length}</strong> outdated</span><span><strong>{unknown.length}</strong> {unknown.length === 1 ? 'version' : 'versions'} unverified</span></div>
<div class="summary"><span><strong>{attention}</strong> {plural(attention, 'needs', 'need')} attention</span><span><strong>{waiting}</strong> active</span><span><strong>{outdated.length}</strong> outdated</span><span><strong>{unknown.length}</strong> {plural(unknown.length, 'version')} unverified</span></div>
{#if data.runtime.schedule.nextAt}<p class="muted">Next scheduled restart: {$timeFormat.dateTime(data.runtime.schedule.nextAt)} · <a href="/maintenance?view=policies#schedule">Manage schedule</a></p>{/if}
{#if data.recovery.lastScanAt}<p class="muted">Health and drift checked {$timeFormat.dateTime(data.recovery.lastScanAt)}</p>{/if}
{#if data.recovery.lastScanAt}<p class="muted">Health and drift checked <RelativeTime value={data.recovery.lastScanAt} /></p>{/if}
{#if scanErrors.length}
<section class="scan-errors" aria-labelledby="scan-errors-heading">
<div class="row-heading"><h2 id="scan-errors-heading">Checks need attention</h2><Button size="sm" disabled={busy} onclick={() => act({ action: 'scan' })}>Retry checks</Button></div>
<ul class="error-groups">{#each scanErrors as group (group.key)}
<li><h3><StatusPip tone="warning" />{group.title}{group.servers.length ? ` · ${group.servers.length} ${group.servers.length === 1 ? 'server' : 'servers'}` : ''}</h3>
<li><h3><StatusPip tone="warning" />{group.title}{group.servers.length ? ` · ${countLabel(group.servers.length, 'server')}` : ''}</h3>
<p>{group.description}</p>
{#if group.servers.length}<ul class="affected-servers">{#each group.servers as name}<li><a href={contextHref(`/servers/${encodeURIComponent(name)}?tab=diagnostics`, $page.url)}>{serverTitle(name)}</a></li>{/each}</ul>{/if}
<details><summary>Technical details</summary><ul class="technical-details">{#each group.details as detail}<li>{#if detail.server}<strong>{serverTitle(detail.server)}</strong>{/if}<pre>{detail.message}</pre></li>{/each}</ul></details>
@@ -207,15 +209,15 @@
<section class="card" id="updates">
<div class="row-heading"><h2>Plutonium updates</h2>{#if updatableServers.length}<Button disabled={busy || data.policies.paused} onclick={() => act({ action: 'update', servers: updatableServers })}>Update outdated servers on restart</Button>{/if}</div>
<p>Published revision: <strong>{data.runtime.release.revision ? `r${data.runtime.release.revision}` : 'Unknown'}</strong> · <a href={PLUTONIUM_MANIFEST_URL} target="_blank" rel="noreferrer">Production manifest</a> · <a href="https://plutonium.pw/docs/changelog/" target="_blank" rel="noreferrer">Changelog</a></p>
{#if data.runtime.release.checkedAt}<p class="muted">Checked {$timeFormat.dateTime(data.runtime.release.checkedAt)}</p>{/if}
{#if data.runtime.release.checkedAt}<p class="muted">Checked <RelativeTime value={data.runtime.release.checkedAt} /></p>{/if}
{#if data.runtime.release.error}<p class="ui-error" role="alert">{data.runtime.release.error} Displayed release information is cached.</p>{/if}
<p class="muted">Plutainer applies the available release at startup when auto-update is enabled. Maintenance verifies the running revision after recovery. Unknown versions require a successful check before automatic updates.</p>
<div class="version-groups">{#each versionGroups as group (group.key)}
<article class="version-group">
<div class="row-heading">
<div class="version-summary"><strong>{group.versions.length} {group.versions.length === 1 ? 'server' : 'servers'} · {group.revision ? `reported r${group.revision}` : 'revision unknown'}</strong><Badge variant={group.tone}><StatusPip tone={group.tone} size="sm" />{group.label}</Badge></div>
<div class="version-summary"><strong>{countLabel(group.versions.length, 'server')} · {group.revision ? `reported r${group.revision}` : 'revision unknown'}</strong><Badge variant={group.tone}><StatusPip tone={group.tone} size="sm" />{group.label}</Badge></div>
<div class="version-actions">
{#if group.pending.length}<span>{group.pending.length} {group.pending.length === 1 ? 'restart' : 'restarts'} pending</span>{/if}
{#if group.pending.length}<span>{countLabel(group.pending.length, 'restart')} pending</span>{/if}
{#if group.status === 'unknown'}<Button size="sm" disabled={busy} onclick={() => act({ action: 'scan' })}>Retry version check</Button>
{:else if group.updatable.length}<Button size="sm" disabled={busy || data.policies.paused} onclick={() => act({ action: 'update', servers: group.updatable })}>Update {group.updatable.length === 1 ? 'server' : `${group.updatable.length} servers`} on restart</Button>{/if}
</div>
@@ -226,7 +228,7 @@
<li><div class="row-heading"><a href={contextHref(`/servers/${encodeURIComponent(version.server)}`, $page.url)}><ServerIdentity name={version.server} /></a>
{#if pending}<span class="pending-job"><StatusBadge state={pending.state} />{#if pending.provisioning_id}<a href={contextHref(`/provisioning/${encodeURIComponent(pending.provisioning_id)}`, $page.url)}>View restart</a>{:else}<a href="#activity">View pending restart</a>{/if}</span>{/if}
</div>
{#if version.checked_at}<small>Last checked <time datetime={version.checked_at}>{$timeFormat.dateTime(version.checked_at)}</time></small>{/if}
{#if version.checked_at}<small>Last checked <RelativeTime value={version.checked_at} /></small>{/if}
{#if version.error}<pre class="version-error">{version.error}</pre>{/if}
</li>
{/each}</ul>
@@ -242,7 +244,7 @@
<article class="card" id={item.key}>
<div class="row-heading"><h3>{item.title} · <HistoricalServerIdentity name={item.server} deletedAt={item.server_deleted_at} /></h3><Badge variant={item.state === 'attention' ? 'danger' : item.state === 'active' ? 'info' : 'neutral'}>{item.label}</Badge></div>
{#if item.detail}<p>{item.detail}</p>{/if}
<p class="muted">{$timeFormat.dateTime(item.at)}{item.job?.target_revision ? ` · Expected r${item.job.target_revision} or newer` : ''}</p>
<p class="muted"><RelativeTime value={item.at} />{item.job?.target_revision ? ` · Expected r${item.job.target_revision} or newer` : ''}</p>
<div class="links">{#if item.href}<a href={contextHref(item.href, $page.url)}>{item.job ? 'View restart operation' : 'View evidence and recovery'}</a>{/if}
{#if item.job?.state === 'queued' && !item.server_deleted_at}<Button size="sm" disabled={busy} onclick={() => act({ action: 'cancel', id: item.job!.id })}>Cancel restart</Button>{/if}
{#if item.job?.state === 'failed' && !item.server_deleted_at && item.job.operation_state === 'done'}<Button size="sm" disabled={busy || data.policies.paused} onclick={() => act({ action: 'retry', id: item.job!.id })}>Retry after inspection</Button>{/if}
+4 -3
View File
@@ -1,4 +1,5 @@
<script lang="ts">
import RelativeTime from '$ui/components/RelativeTime.svelte';
import { contextHref } from '$lib/navigation-context.js';
import { onMount, untrack } from 'svelte';
import { page } from '$app/stores';
@@ -133,7 +134,7 @@
<div class="row-heading"><h3>{action.action === 'restart' ? 'Restart server' : 'Rebuild server'}</h3>{#if action.operation_state}<StatusBadge state={action.operation_state} />{/if}</div>
{#if action.operation_stage}<p>{action.operation_stage}</p>{/if}
{#if action.operation_error}<p class="error">{action.operation_error}</p>{/if}
<time datetime={action.created_at}>{$timeFormat.dateTime(action.created_at)}</time>
<span class="time"><RelativeTime value={action.created_at} /></span>
<div class="links"><a href={contextHref(`/provisioning/${encodeURIComponent(action.operation_id)}`, $page.url)}>View provisioning</a>{#if action.campaign_id}<a href={contextHref(`/campaigns/${encodeURIComponent(action.campaign_id)}`, $page.url)}>View repair campaign</a>{/if}</div>
</article>
{/each}
@@ -165,7 +166,7 @@
{#if !events.length}<EmptyState title="No activity recorded yet" />
{:else}<ol class="timeline">
{#each events.slice(0, eventLimit) as event (event.id)}
<li><time datetime={event.created_at}>{$timeFormat.dateTime(event.created_at)}</time><p>{event.message}</p></li>
<li><span class="time"><RelativeTime value={event.created_at} /></span><p>{event.message}</p></li>
{/each}
</ol><ListFooter total={events.length} bind:limit={eventLimit} label="events" />{/if}
</section>
@@ -190,7 +191,7 @@
.links, .row-heading a { font-size: var(--text-sm); }
a { color: var(--accent-primary-hover); }
dl { display: flex; gap: 16px 32px; flex-wrap: wrap; margin: 0; }
dt, .muted, time, summary, .notice { color: var(--text-secondary); font-size: var(--text-xs); }
dt, .muted, .time, summary, .notice { color: var(--text-secondary); font-size: var(--text-xs); }
dd { margin: 6px 0 0; font-size: var(--text-sm); overflow-wrap: anywhere; }
code { font-family: var(--font-mono); font-size: var(--text-xs); overflow-wrap: anywhere; }
dd code { display: block; margin-top: 8px; color: var(--text-secondary); }