Unify UI workflows and streamline fleet management

This commit is contained in:
2026-09-18 14:00:28 -04:00
parent 9cf0bd87f4
commit f54cac8c50
63 changed files with 1706 additions and 1286 deletions
+14 -28
View File
@@ -2,7 +2,11 @@
import { artifactVersionLabel } from '$lib/artifact-version.js';
import BetaTestProgress from "$ui/components/BetaTestProgress.svelte";
import type { BetaTestView } from "$lib/beta-plan.js";
import { onMount, onDestroy } from 'svelte';
import { onMount } from 'svelte';
import { pollVisible } from '$ui/poll.js';
import { confirmAction } from '$ui/confirm.js';
import ServerIdentity from '$ui/components/ServerIdentity.svelte';
import StatusBadge from '$ui/components/primitives/StatusBadge.svelte';
import { page } from '$app/stores';
import Card from '$ui/components/primitives/Card.svelte';
import Badge from '$ui/components/primitives/Badge.svelte';
@@ -63,11 +67,7 @@
$effect(() => { id; beta = null; loading = true; loadError = ''; void load(); });
let poll: ReturnType<typeof setInterval> | null = null;
onMount(() => {
poll = setInterval(load, 5000);
});
onDestroy(() => { if (poll) clearInterval(poll); });
onMount(() => pollVisible(load, 5000, false));
async function promote(): Promise<void> {
acting = 'promote'; flash = null;
@@ -76,7 +76,7 @@
const body = (await res.json()) as { ok: boolean; error?: string };
if (!body.ok) { flash = body.error ?? 'Promote failed'; return; }
await load();
} finally { acting = null; }
} catch (error) { flash = (error as Error).message; } finally { acting = null; }
}
async function rollback(): Promise<void> {
const isReject = beta?.state === 'ready-promote';
@@ -84,14 +84,14 @@
? `Reject beta #${id}? This restores original files on every beta server and marks the version as test-failed.`
: `Rollback beta #${id}? This restores original files on every beta server.`;
const restart = test ? test.plan.rollbackRestart === 'when-empty' ? ' Restarts will roll out when servers are empty.' : ' Restart servers manually to load the restored files.' : '';
if (!confirm(prompt + restart)) return;
if (!await confirmAction({title:isReject ? 'Reject beta?' : 'Roll back beta?',message:prompt + restart,confirmLabel:isReject ? 'Reject beta' : 'Roll back',danger:true})) return;
acting = 'rollback'; flash = null;
try {
const res = await fetch(`/api/betas/${id}/rollback`, { method: 'POST' });
const body = (await res.json()) as { ok: boolean; error?: string };
if (!body.ok) { flash = body.error ?? 'Rollback failed'; return; }
await load();
} finally { acting = null; }
} catch (error) { flash = (error as Error).message; } finally { acting = null; }
}
async function retry(server: string): Promise<void> {
acting = `retry:${server}`; flash = null;
@@ -104,21 +104,7 @@
const body = (await res.json()) as { ok: boolean; error?: string };
if (!body.ok) { flash = body.error ?? 'Retry failed'; return; }
await load();
} finally { acting = null; }
}
function betaStateVariant(s: string): 'online' | 'warning' | 'info' | 'neutral' {
return s === 'ready-promote' ? 'online' :
s === 'running' ? 'warning' :
s === 'promoted' ? 'info' :
'neutral';
}
function stageVariant(s: string): 'online' | 'warning' | 'danger' | 'neutral' {
return s === 'ready' ? 'online' :
s === 'failed' ? 'danger' :
s === 'awaiting-empty' || s === 'restarting' || s === 'awaiting-health' || s === 'awaiting-match' || s === 'deploying'
? 'warning' :
'neutral';
} catch (error) { flash = (error as Error).message; } finally { acting = null; }
}
</script>
@@ -137,7 +123,7 @@
<Breadcrumbs items={[{ label: 'Betas', href: '/betas' }, { label: `#${b.id}` }]} />
{/snippet}
{#snippet actions()}
<Badge variant={betaStateVariant(b.state)} size="md" uppercase>{b.state}</Badge>
<StatusBadge state={b.state} />
{/snippet}
</SectionHeader>
@@ -163,7 +149,7 @@
{#each scripts as s (s.id)}
<div class="script-row">
<Badge variant="neutral" size="sm" uppercase>{s.kind}</Badge>
<a class="s-name" href="/scripts/{s.id}">{s.name}</a>
<a class="s-name" href={"/" + (s.kind === "baseline" ? "configs" : s.kind + "s") + "/" + s.id}>{s.name}</a>
<span class="s-target">{s.target_rel_path}</span>
<span class="s-ver">{artifactVersionLabel(s.version)}</span>
<span class="s-sha">{s.version?.sha256.slice(0, 8) ?? ''}</span>
@@ -202,8 +188,8 @@
<div class="srv-list">
{#each servers as bs (bs.server)}
<div class="srv-row">
<a class="srv-name" href="/servers/{bs.server}">{bs.server}</a>
<Badge variant={stageVariant(bs.stage)} size="sm" uppercase>{bs.stage}</Badge>
<a class="srv-name" href="/servers/{bs.server}"><ServerIdentity name={bs.server} /></a>
<StatusBadge state={bs.stage} />
<div class="ticks" aria-label="pipeline progress">
<span class:on={bs.deployed_at}>Deploy</span>
<span class:on={bs.restarted_at || bs.stage === 'restarting'}>Empty</span>