Files
electric-cherry/src/lib/ui/components/LifecycleDialog.svelte
T
josh cfed584614 feat: add provisioning lifecycle actions
Rename Deployments to Provisioning with compatible redirects and APIs. Add durable Start, Restart, Stop, and Rebuild operations alongside Create and Destroy; rebuild wipes the instance and recreates it in the same slot and port.

Add lifecycle, migration, recovery, and Komodo command tests. Type checks and production build pass; the full suite retains a telemetry assertion failure reproduced on the unchanged branch.

Refs #39
2026-09-27 14:20:04 -04:00

71 lines
4.1 KiB
Svelte

<script lang="ts">
import { goto } from '$app/navigation';
import { runtimeActions, actionLabels, runtimeDescriptions, type RuntimeAction } from '$lib/provisioning.js';
import { serverTitle } from '$ui/server-labels.js';
import Dialog from './primitives/Dialog.svelte';
import Button from './primitives/Button.svelte';
let { open = $bindable(false), initialServer = '', initialAction = 'start' }:
{ open?: boolean; initialServer?: string; initialAction?: RuntimeAction } = $props();
let servers = $state<{ name: string }[]>([]);
let server = $state('');
let action = $state<RuntimeAction | 'destroy'>('start');
let busy = $state(false);
let loading = $state(false);
let error = $state('');
let confirmed = $state(false);
const destructive = $derived(action === 'rebuild' || action === 'destroy');
$effect(() => {
if (open) { server = initialServer; action = initialAction; confirmed = false; error = ''; void load(); }
});
async function load() {
loading = true;
try {
const response = await fetch('/api/servers');
if (!response.ok) throw new Error('Could not load servers.');
servers = (await response.json()).servers;
} catch (err) { error = (err as Error).message; }
finally { loading = false; }
}
async function submit() {
if (busy || !server || (destructive && !confirmed)) return;
busy = true; error = '';
try {
const response = action === 'destroy'
? await fetch(`/api/servers/${encodeURIComponent(server)}`, { method: 'DELETE' })
: await fetch('/api/provisioning', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ server, action }) });
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? 'Could not queue this operation.');
await goto(`/provisioning/${encodeURIComponent(body.deploymentId)}`);
open = false;
} catch (err) { error = (err as Error).message; }
finally { busy = false; }
}
</script>
<Dialog bind:open title="Manage server" {busy}>
<div class="fields">
<label>Server <select bind:value={server} disabled={busy || loading} onchange={() => confirmed = false}>
<option value="">{loading ? 'Loading servers…' : 'Choose a server'}</option>
{#each servers as item}<option value={item.name}>{serverTitle(item.name)} · {item.name}</option>{/each}
</select></label>
<label>Action <select bind:value={action} disabled={busy} onchange={() => confirmed = false}>
{#each runtimeActions as value}<option value={value}>{actionLabels[value]}</option>{/each}
<option value="destroy">Destroy</option>
</select></label>
<p>{action === 'destroy' ? 'Destroy the container, delete its Komodo deployment, and remove all server files and registration after the server is empty. Provisioning history and the key saved separately in Keys are retained.' : runtimeDescriptions[action]}</p>
{#if destructive}<label class="confirmation"><input type="checkbox" bind:checked={confirmed} disabled={busy} /> I understand that all files and data in this instance will be permanently deleted.</label>{/if}
{#if error}<p class="ui-error" role="alert">{error}</p>{/if}
</div>
{#snippet actions()}
<Button disabled={busy} onclick={() => open = false}>Cancel</Button>
<Button variant={destructive ? 'danger' : 'primary'} disabled={busy || loading || !server || (destructive && !confirmed)} loading={busy} onclick={submit}>Queue {actionLabels[action].toLowerCase()}</Button>
{/snippet}
</Dialog>
<style>
.fields { display: grid; gap: 16px; } label { display: grid; gap: 8px; }
select { padding: 10px; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text-primary); }
p { margin: 0; line-height: 1.6; color: var(--text-secondary); } .confirmation { display: flex; align-items: flex-start; gap: 12px; line-height: 1.5; }
</style>