Add operation flows and activity logs for betas and campaigns

This commit is contained in:
2026-09-17 19:34:26 -04:00
parent 9dcf4ac1d3
commit 5b5266da3a
8 changed files with 312 additions and 16 deletions
+27 -4
View File
@@ -8,6 +8,8 @@
import SectionHeader from '$ui/components/primitives/SectionHeader.svelte';
import Breadcrumbs from '$ui/components/primitives/Breadcrumbs.svelte';
import EmptyState from '$ui/components/primitives/EmptyState.svelte';
import OperationHistory from '$ui/components/OperationHistory.svelte';
import { campaignFlow, type ActivityEvent } from '$lib/operation-flow.js';
interface FileRow {
id: number; server: string; verb: string; rel_path: string;
@@ -27,20 +29,32 @@
};
servers: ServerRow[];
files: FileRow[];
events: ActivityEvent[];
}
let detail = $state<Detail | null>(null);
let loadError = $state('');
let request = 0;
let openFile = $state<{ id: number; before: string | null; after: string | null; verb: string; relPath: string } | null>(null);
async function loadDetail(): Promise<void> {
const id = $page.params.id;
const res = await fetch(`/api/campaigns/${id}`);
detail = (await res.json()) as Detail;
const current = ++request;
try {
const res = await fetch(`/api/campaigns/${encodeURIComponent(id ?? '')}`);
if (!res.ok) throw new Error(res.status === 404 ? 'Campaign not found.' : 'Could not refresh campaign.');
const body = await res.json() as Detail;
if (current !== request || id !== $page.params.id) return;
detail = body; loadError = '';
} catch (err) {
if (current === request && id === $page.params.id) loadError = (err as Error).message;
}
}
$effect(() => { $page.params.id; detail = null; openFile = null; loadError = ''; void loadDetail(); });
let poll: ReturnType<typeof setInterval> | null = null;
onMount(() => {
loadDetail();
poll = setInterval(loadDetail, 5000);
});
onDestroy(() => {
@@ -86,6 +100,9 @@
}
</script>
<svelte:head><title>Campaign | Electric Cherry</title></svelte:head>
{#if loadError}<p role="alert" class="err">{loadError} <button onclick={loadDetail}>Retry</button></p>{/if}
{#if detail}
{@const action = detail.deployment.action}
<div class="page">
@@ -107,6 +124,12 @@
</div>
</Card>
<OperationHistory
steps={campaignFlow(detail.servers, Boolean(detail.deployment.finished_at))}
events={detail.events}
active={detail.servers.some(s => s.restart_state === 'pending' || s.restart_state === 'failed')}
/>
<section class="section">
<div class="section-head">
<h2>Servers</h2>
@@ -189,7 +212,7 @@
</div>
{/if}
</div>
{:else}
{:else if !loadError}
<div class="loading">Loading…</div>
{/if}