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
This commit is contained in:
@@ -60,7 +60,7 @@
|
||||
const body = await res.json();
|
||||
if (!res.ok) throw new Error(body.error ?? 'Could not queue the collection.');
|
||||
open = false;
|
||||
if (body.collectionId) await goto(`/deployments/collections/${encodeURIComponent(body.collectionId)}`);
|
||||
if (body.collectionId) await goto(`/provisioning/collections/${encodeURIComponent(body.collectionId)}`);
|
||||
} catch (err) { error = (err as Error).message; }
|
||||
finally { busy = false; }
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
const previousName = preview?.name;
|
||||
busy = true; error = ''; savedName = ''; keyNotice = '';
|
||||
try {
|
||||
const res = await fetch('/api/deployments/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
const res = await fetch('/api/provisioning/preview', { method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(sourceDeploymentId ? { deploymentId: sourceDeploymentId } : blueprintId ? { blueprintId } : { game, mapSlug }) });
|
||||
const body = await res.json();
|
||||
if (!res.ok) throw new Error(body.error ?? 'Could not prepare launch preview.');
|
||||
@@ -97,7 +97,7 @@
|
||||
const body = await res.json();
|
||||
if (!res.ok || !body.ok) throw new Error(body.error ?? 'Could not queue this launch.');
|
||||
open = false;
|
||||
await goto(`/deployments/${encodeURIComponent(body.deploymentId)}`);
|
||||
await goto(`/provisioning/${encodeURIComponent(body.deploymentId)}`);
|
||||
} catch (err) { error = (err as Error).message; }
|
||||
finally { busy = false; }
|
||||
}
|
||||
@@ -119,7 +119,7 @@
|
||||
|
||||
<dialog bind:this={dialog} aria-label="Create server" onclick={event => { if (isDialogBackdrop(event, dialog) && !busy && !saving) open = false; }} oncancel={event => { if (busy || saving) event.preventDefault(); }} onclose={() => open = false}>
|
||||
<div class="composer">
|
||||
<header><div><h2 bind:this={heading} tabindex="-1">{preview ? 'Review server launch' : 'Create server'}</h2></div><button class="close" disabled={busy || saving} onclick={() => open = false} aria-label="Close deployment composer">×</button></header>
|
||||
<header><div><h2 bind:this={heading} tabindex="-1">{preview ? 'Review server launch' : 'Create server'}</h2></div><button class="close" disabled={busy || saving} onclick={() => open = false} aria-label="Close server creation dialog">×</button></header>
|
||||
<ol class="steps" aria-label="Launch steps"><li class:active={!preview} aria-current={!preview ? 'step' : undefined}>01 <span>Choose a starting point</span></li><li class:active={!!preview} aria-current={preview ? 'step' : undefined}>02 <span>Review & launch</span></li></ol>
|
||||
{#if error}<div class="error" role="alert">{error}</div>{/if}
|
||||
{#if !loaded}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
const res = await fetch(`/api/servers/${encodeURIComponent(name)}`, { method: 'DELETE' });
|
||||
const body = await res.json();
|
||||
if (!res.ok || !body.ok || !body.deploymentId) throw new Error(body.error ?? 'Could not queue destruction. Please retry.');
|
||||
await goto(`/deployments/${encodeURIComponent(body.deploymentId)}`);
|
||||
await goto(`/provisioning/${encodeURIComponent(body.deploymentId)}`);
|
||||
open = false;
|
||||
} catch (err) { error = (err as Error).message; }
|
||||
finally { submitting = false; }
|
||||
@@ -32,10 +32,10 @@
|
||||
<form onsubmit={(event) => { event.preventDefault(); void destroy(); }}>
|
||||
<h2 id="destroy-title">Queue server removal?</h2>
|
||||
<p class="name">{name}</p>
|
||||
<p>This stops the container, deletes its Komodo deployment, and removes its server files and campaign details. Deployment history and the saved server key are retained.</p>
|
||||
<p>This stops the container, deletes its Komodo deployment, and removes its server files and campaign details. Provisioning history and the saved server key are retained.</p>
|
||||
<div class="notice">
|
||||
{#if players !== null && players > 0}<strong>{players} player{players === 1 ? '' : 's'} currently online.</strong>{/if}
|
||||
Removal waits until a live check confirms the server is empty. Follow progress or cancel the waiting request on the deployment page.
|
||||
Removal waits until a live check confirms the server is empty. Follow progress or cancel the waiting request on the provisioning page.
|
||||
</div>
|
||||
<p class="irreversible">You can cancel while waiting. Once removal begins, it cannot be undone.</p>
|
||||
{#if error}<p role="alert" class="error">{error}</p>{/if}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
async function review() {
|
||||
busy = true; error = ''; servers = null;
|
||||
try {
|
||||
const res = await fetch('/api/deployments/house-party');
|
||||
const res = await fetch('/api/provisioning/house-party');
|
||||
if (!res.ok) throw new Error('Could not load the fleet.');
|
||||
servers = (await res.json()).servers;
|
||||
} catch (err) { error = (err as Error).message; }
|
||||
@@ -29,7 +29,7 @@
|
||||
if (busy || !servers?.length) return;
|
||||
busy = true; error = '';
|
||||
try {
|
||||
const res = await fetch('/api/deployments/house-party', {
|
||||
const res = await fetch('/api/provisioning/house-party', {
|
||||
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ servers })
|
||||
});
|
||||
@@ -50,7 +50,7 @@
|
||||
<header><h2>Queue fleet removal</h2></header>
|
||||
<p>Queue destruction of every server, including servers being created. Each waits until a fresh player count confirms it is empty. Unreachable servers stay queued.</p>
|
||||
<p>Destruction removes containers, server files, and fleet registrations. Saved keys and blueprints are kept.</p>
|
||||
<p>In Active deployments, cancel individual waiting requests or all waiting removals. Once a server begins removal, that removal cannot be cancelled or undone.</p>
|
||||
<p>In Provisioning’s Active tab, cancel individual waiting requests or all waiting removals. Once a server begins removal, that removal cannot be cancelled or undone.</p>
|
||||
{#if error}<p class="error" role="alert">{error}</p>{/if}
|
||||
{#if servers}
|
||||
<strong>{servers.length ? `${servers.length} server${servers.length === 1 ? '' : 's'} to destroy` : 'The house is already empty.'}</strong>
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user