Add clear host deletion controls with workload safeguards

This commit is contained in:
2026-10-04 09:59:27 -04:00
parent 840f02b53e
commit 3d62dfff27
11 changed files with 334 additions and 13 deletions
@@ -0,0 +1,65 @@
<script lang="ts">
import type { Host, HostDeletion } from '$lib/hosts.js';
import Button from './primitives/Button.svelte';
import Dialog from './primitives/Dialog.svelte';
let { open = $bindable(false), host, ondeleted }:
{ open?: boolean; host: Pick<Host, 'id' | 'name'> | null; ondeleted: () => void | Promise<void> } = $props();
let deletion = $state<HostDeletion | null>(null);
let checking = $state(false); let deleting = $state(false); let error = $state('');
async function inspect(id: string, signal?: AbortSignal) {
checking = true; deletion = null; error = '';
try {
const response = await fetch(`/api/hosts/${encodeURIComponent(id)}`, { signal });
const body = await response.json();
if (!response.ok) throw new Error(body.error ?? body.message ?? 'Could not check this host.');
if (!signal?.aborted) deletion = body.deletion;
} catch (cause) { if (!signal?.aborted) error = (cause as Error).message; }
finally { if (!signal?.aborted) checking = false; }
}
$effect(() => {
if (!open || !host) return;
const controller = new AbortController();
void inspect(host.id, controller.signal);
return () => controller.abort();
});
async function remove() {
if (!host || deleting || checking || !deletion?.allowed) return;
const id = host.id;
deleting = true; error = '';
try {
const response = await fetch(`/api/hosts/${encodeURIComponent(id)}`, { method: 'DELETE' });
const body = await response.json();
if (!response.ok) {
await inspect(id);
throw new Error(body.error ?? body.message ?? 'Could not delete this host.');
}
open = false;
await ondeleted();
} catch (cause) { error = (cause as Error).message; }
finally { deleting = false; }
}
</script>
<Dialog bind:open title={`Delete ${host?.name ?? 'host'}?`} busy={deleting}
description="Remove this host from Electric Cherry and revoke its agent access and unused installer links.">
<div class="content">
<p>The agent and game files stay on the machine. Published game libraries and operation history are kept. Commands that have never been sent to the host will be cancelled.</p>
{#if checking}<p role="status">Checking servers and unfinished operations…</p>
{:else if deletion && !deletion.allowed}<div role="status" class="blocked"><strong>This host still has work to finish:</strong><ul>{#each deletion.blockers as blocker}<li>{blocker}</li>{/each}</ul><p>Open <a href={`/hosts/${host?.id}`} onclick={() => open = false}>host details</a> to review its operations or drain its servers.</p></div>
{:else if deletion?.allowed}<p>This host is ready to delete. Adding it again requires a new enrollment.</p>{/if}
{#if error}<p role="alert" class="ui-error">{error}</p>{/if}
</div>
{#snippet actions()}
<Button variant="ghost" disabled={deleting} onclick={() => open = false}>Cancel</Button>
{#if !deletion?.allowed}<Button disabled={checking || deleting || !host} onclick={() => { if (host) void inspect(host.id); }}>Check again</Button>{/if}
<Button variant="danger" loading={deleting} disabled={checking || !deletion?.allowed} onclick={remove}>Delete host</Button>
{/snippet}
</Dialog>
<style>
.content { display:grid; gap:16px; } p,li { margin:0; color:var(--text-secondary); font-size:var(--text-sm); line-height:1.65; }
.blocked { display:grid; gap:12px; } strong { font-size:var(--text-sm); } ul { margin:0; padding-left:20px; }
a { color:var(--accent-primary-hover); } .ui-error { color:var(--state-danger); }
</style>