diff --git a/frontend/src/lib/components/DestroyDialog.svelte b/frontend/src/lib/components/DestroyDialog.svelte index 6f9ec92..03b6e94 100644 --- a/frontend/src/lib/components/DestroyDialog.svelte +++ b/frontend/src/lib/components/DestroyDialog.svelte @@ -1,13 +1,15 @@ @@ -112,169 +220,307 @@ Wrenn Admin — Capsules + +
-
-
- -
+
+

Capsules

-

+

Launch temporary capsules to build and snapshot platform templates.

- -
- - {#if !loading && !error} -
- - {capsules.length} - total - - {#if runningCount > 0} - - - {runningCount} - running - +
+ {#if !loading && runningCount > 0} +
+ + + + + {runningCount} + running +
{/if} - {#if pausedCount > 0} - - {pausedCount} - paused - - {/if} -
- {/if} -
- -
- {#if loading} -
-
- - Loading capsules... -
-
- {:else if error} -
- - - - {error} -
- {:else if capsules.length === 0} -
-
- - - -
-

No capsules

-

- Launch a capsule, configure it interactively, then snapshot it as a platform template. -

- {:else} - -
- - - - - - - - - - - - - {#each capsules as capsule (capsule.id)} - - - - - - - - - {/each} - -
IDStatusTemplateSpecsStartedActions
- - - - {#if capsule.status === 'running'} - - - - - {/if} - {capsule.status} - - - {capsule.template} - - - {capsule.vcpus}v · {capsule.memory_mb}MB - - - {fmtDate(capsule.started_at)} - -
- - Open - - {#if capsule.status === 'running' || capsule.status === 'paused'} - - {/if} -
-
+
+
+ + +
+ +
+
+ + + + +
+ {filteredCapsules.length} capsule{filteredCapsules.length !== 1 ? 's' : ''} + +
+ + + + + + +
+ + {#if error} +
+ + + + {error}. Try refreshing the page.
{/if} + + +
+ +
+
ID
+ {@render sortableHeader('Template', 'template')} + {@render sortableHeader('CPU', 'vcpus')} + {@render sortableHeader('Memory', 'memory_mb')} + {@render sortableHeader('Started', 'started_at')} + {@render sortableHeader('Status', 'status')} +
Actions
+
+ + {#if loading && capsules.length === 0} +
+
+ + + + Loading capsules... +
+
+ {:else if filteredCapsules.length === 0 && searchQuery} +
+
+
+ + + +
+
+

+ No matching capsules +

+

+ No capsules match "{searchQuery}". +

+ +
+ {:else if filteredCapsules.length === 0} +
+
+
+
+ + + +
+
+

+ No capsules +

+

+ Launch a capsule, configure it interactively, then snapshot it as a platform template. +

+ +
+ {:else} + {#each filteredCapsules as capsule, i (capsule.id)} + {@const stripeColor = capsule.status === 'running' ? 'bg-[var(--color-accent)]' : capsule.status === 'paused' ? 'bg-[var(--color-amber)]' : capsule.status === 'error' ? 'bg-[var(--color-red)]' : 'bg-[var(--color-text-muted)]'} +
+ +
+ + +
+ {#if capsule.status === 'running'} + + + + + {:else if capsule.status === 'paused'} + + {:else if capsule.status === 'error'} + + {:else} + + {/if} + {#if searchQuery && capsule.id.toLowerCase().includes(searchQuery.toLowerCase())} + {@const matchIdx = capsule.id.toLowerCase().indexOf(searchQuery.toLowerCase())} + {capsule.id.slice(0, matchIdx)}{capsule.id.slice(matchIdx, matchIdx + searchQuery.length)}{capsule.id.slice(matchIdx + searchQuery.length)} + {:else} + {capsule.id} + {/if} + +
+ + +
+ {capsule.template} +
+ + +
+ {capsule.vcpus} +
+ + +
+ {capsule.memory_mb}MB +
+ + +
+ {formatTime(capsule.started_at)} + {#if capsule.last_active_at} + active {timeAgo(capsule.last_active_at)} + {/if} +
+ + +
+ + {capsule.status} + +
+ + +
+ {#if capsule.status === 'running' || capsule.status === 'paused'} + + {/if} +
+
+ {/each} + {/if} +
+ + +
@@ -285,50 +531,30 @@ templateSource="platform" /> - {#if destroyTarget} -
- -
{ if (!destroying) { destroyTarget = null; } }} - onkeydown={(e) => { if (e.key === 'Escape' && !destroying) { destroyTarget = null; } }} - >
-
-

Destroy Capsule

-

- Terminate {destroyTarget.id} and destroy all data inside it. This cannot be undone. -

- - {#if destroyError} -
- {destroyError} -
- {/if} - -
- - -
-
-
+ { destroyTarget = null; }} + ondestroyed={handleDestroyed} + destroyFn={destroyAdminCapsule} + /> {/if} + +{#snippet sortableHeader(label: string, key: SortKey)} + +{/snippet} diff --git a/frontend/src/routes/admin/capsules/[id]/+page.svelte b/frontend/src/routes/admin/capsules/[id]/+page.svelte index 11dfc46..2712421 100644 --- a/frontend/src/routes/admin/capsules/[id]/+page.svelte +++ b/frontend/src/routes/admin/capsules/[id]/+page.svelte @@ -6,6 +6,8 @@ import TerminalTab from '$lib/components/TerminalTab.svelte'; import FilesTab from '$lib/components/FilesTab.svelte'; import MetricsPanel from '$lib/components/MetricsPanel.svelte'; + import DestroyDialog from '$lib/components/DestroyDialog.svelte'; + import CopyButton from '$lib/components/CopyButton.svelte'; import { toast } from '$lib/toast.svelte'; import { getAdminCapsule, @@ -29,8 +31,6 @@ // Destroy dialog let showDestroy = $state(false); - let destroying = $state(false); - let destroyError = $state(null); // Snapshot dialog let showSnapshot = $state(false); @@ -53,19 +53,6 @@ capsuleLoading = false; } - async function handleDestroy() { - destroying = true; - destroyError = null; - const result = await destroyAdminCapsule(capsuleId); - if (result.ok) { - toast.success('Capsule destroyed'); - goto('/admin/capsules'); - } else { - destroyError = result.error; - } - destroying = false; - } - async function handleSnapshot() { snapshotting = true; snapshotError = null; @@ -108,13 +95,33 @@ let pollTimer: ReturnType | null = null; + function startPolling() { + stopPolling(); + pollTimer = setInterval(loadCapsule, 10_000); + } + + function stopPolling() { + if (pollTimer) { clearInterval(pollTimer); pollTimer = null; } + } + + function handleVisibility() { + if (document.hidden) { + stopPolling(); + } else { + loadCapsule(); + startPolling(); + } + } + onMount(() => { loadCapsule(); - pollTimer = setInterval(loadCapsule, 10_000); + startPolling(); + document.addEventListener('visibilitychange', handleVisibility); }); onDestroy(() => { - if (pollTimer) clearInterval(pollTimer); + stopPolling(); + document.removeEventListener('visibilitychange', handleVisibility); }); @@ -143,56 +150,59 @@ {:else if capsule} - -
- - - Capsules - -
- {capsuleId} - - {#if capsule.status === 'running'} - - - - - {/if} - {capsule.status} - -
- {capsule.template} - / - {capsule.vcpus}v · {capsule.memory_mb}MB -
-
+ +
+
+ + Capsules + + + + {capsuleId} + + - {#if capsule.status === 'running' || capsule.status === 'paused'} - - - {/if} + {#if capsule.status === 'running'} + + + + + {/if} + {capsule.status} + + {capsule.template} · {capsule.vcpus}v · {capsule.memory_mb}MB + +
+ {#if capsule.status === 'running' || capsule.status === 'paused'} + + + {/if} +
+
+
+
@@ -214,6 +224,19 @@
{/if} + + +
+
+ + + + + All systems operational +
+
@@ -302,50 +325,10 @@ {/if} - -{#if showDestroy} -
- -
{ if (!destroying) showDestroy = false; }} - onkeydown={(e) => { if (e.key === 'Escape' && !destroying) showDestroy = false; }} - >
-
-

Destroy Capsule

-

- Terminate {capsuleId} and destroy all data inside it. This cannot be undone. -

- - {#if destroyError} -
- {destroyError} -
- {/if} - -
- - -
-
-
-{/if} + { showDestroy = false; }} + ondestroyed={() => { toast.success('Capsule destroyed'); goto('/admin/capsules'); }} + destroyFn={destroyAdminCapsule} +/>