diff --git a/frontend/src/lib/components/Sidebar.svelte b/frontend/src/lib/components/Sidebar.svelte index b7e65f5..1b11726 100644 --- a/frontend/src/lib/components/Sidebar.svelte +++ b/frontend/src/lib/components/Sidebar.svelte @@ -21,7 +21,8 @@ IconDocs, IconAudit, IconServer, - IconShield + IconShield, + IconMetrics } from './icons'; let { collapsed = $bindable(false) }: { collapsed: boolean } = $props(); @@ -47,6 +48,7 @@ const platformItems: NavItem[] = [ { label: 'Capsules', icon: IconMonitor, href: '/dashboard/capsules' }, + { label: 'Metrics', icon: IconMetrics, href: '/dashboard/metrics' }, { label: 'Templates', icon: IconBox, href: '/dashboard/snapshots' } ]; diff --git a/frontend/src/lib/components/StatsPanel.svelte b/frontend/src/lib/components/StatsPanel.svelte index f067447..e5380c3 100644 --- a/frontend/src/lib/components/StatsPanel.svelte +++ b/frontend/src/lib/components/StatsPanel.svelte @@ -250,22 +250,18 @@ } -
+
- -
-
-
-

Usage Statistics

- {#if !loading} - - - Live - - {/if} -
-

Resource consumption across all capsules.

-
+ +
+ {#if !loading} + + + Live + + {:else} +
+ {/if}
diff --git a/frontend/src/lib/components/icons/IconMetrics.svelte b/frontend/src/lib/components/icons/IconMetrics.svelte new file mode 100644 index 0000000..3110642 --- /dev/null +++ b/frontend/src/lib/components/icons/IconMetrics.svelte @@ -0,0 +1,20 @@ + + + diff --git a/frontend/src/lib/components/icons/index.ts b/frontend/src/lib/components/icons/index.ts index fa90069..babf0a5 100644 --- a/frontend/src/lib/components/icons/index.ts +++ b/frontend/src/lib/components/icons/index.ts @@ -26,3 +26,4 @@ export { default as IconBox } from './IconBox.svelte'; export { default as IconServer } from './IconServer.svelte'; export { default as IconGear } from './IconGear.svelte'; export { default as IconShield } from './IconShield.svelte'; +export { default as IconMetrics } from './IconMetrics.svelte'; diff --git a/frontend/src/routes/dashboard/capsules/+layout.svelte b/frontend/src/routes/dashboard/capsules/+layout.svelte index 1f85886..8551513 100644 --- a/frontend/src/routes/dashboard/capsules/+layout.svelte +++ b/frontend/src/routes/dashboard/capsules/+layout.svelte @@ -1,7 +1,6 @@ @@ -26,8 +21,7 @@
-
- +

@@ -39,7 +33,6 @@

-
@@ -54,33 +47,6 @@
- - -
{@render children()} diff --git a/frontend/src/routes/dashboard/capsules/+page.svelte b/frontend/src/routes/dashboard/capsules/+page.svelte index 9c19d67..a04a9e7 100644 --- a/frontend/src/routes/dashboard/capsules/+page.svelte +++ b/frontend/src/routes/dashboard/capsules/+page.svelte @@ -1,3 +1,734 @@ + + + + + { if (e.key === 'Escape') openMenuId = null; }} /> + +
+ +
+
+ + + + +
+ {filteredCapsules.length} total + +
+ + + + + + + + +
+ + {#if error} +
+ {error} +
+ {/if} + + +
+ +
+
ID
+
Template
+ {@render sortableHeader('CPU', 'vcpus')} + {@render sortableHeader('Memory', 'memory_mb')} + {@render sortableHeader('Idle Timeout', 'timeout_sec')} + {@render sortableHeader('Started', 'started_at')} + {@render sortableHeader('Status', 'status')} +
+ + {#if loading && capsules.length === 0} +
+
+ + + + Loading capsules... +
+
+ {:else if filteredCapsules.length === 0} +
+
+
+
+ + + + + +
+
+

+ No capsules yet +

+

+ Each capsule is an isolated VM. Launch one to get started. +

+ +
+ {:else} + {#each filteredCapsules as capsule, i (capsule.id)} + {@const stripeColor = capsule.status === 'running' ? 'bg-[var(--color-accent)]' : capsule.status === 'paused' ? 'bg-[var(--color-amber)]' : 'bg-[var(--color-text-muted)]'} +
+ +
+ + +
+ {#if capsule.status === 'running'} + + + + + {:else if capsule.status === 'paused'} + + {: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 +
+ + +
+ {capsule.timeout_sec ? `${capsule.timeout_sec}s` : '—'} +
+ + +
+ {formatTime(capsule.started_at)} + {#if capsule.last_active_at} + {timeAgo(capsule.last_active_at)} + {/if} +
+ + +
+ {#if actionLoading === capsule.id} + + + + + + {:else} + + {/if} +
+
+ {/each} + {/if} +
+
+ + +{#if openMenuId} + {@const openCapsule = capsules.find((c) => c.id === openMenuId)} + {#if openCapsule} +
+ {#if openCapsule.status === 'running'} + + + {:else if openCapsule.status === 'paused'} + + + {/if} +
+ +
+ {/if} +{/if} + + +{#if snapshotTarget} +
+ +
{ if (!snapshotting) snapshotTarget = null; }} + onkeydown={(e) => { if (e.key === 'Escape' && !snapshotting) snapshotTarget = null; }} + >
+ +
+
+
+ + + + +
+
+

Capture snapshot

+

{snapshotTarget.capsule.id}

+
+
+ +
+ {#if snapshotTarget.pauseFirst} +
+ + + + + +

This capsule will be paused first — memory state is captured at rest.

+
+ {:else} +

The capsule's current memory state will be captured and stored as a reusable snapshot.

+ {/if} + + {#if snapshotError} +
+ {snapshotError} +
+ {/if} + +
+
+ + optional +
+ { if (e.key === 'Enter' && !snapshotting) handleSnapshotConfirm(); }} + /> +

Leave blank to use an auto-generated name.

+
+ +
+ + +
+
+
+
+{/if} + + +{#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} + +
+ + +
+
+
+{/if} + + + { showCreateDialog = false; }} + oncreated={handleCapsuleCreated} +/> + +{#snippet sortableHeader(label: string, key: SortKey)} + +{/snippet} diff --git a/frontend/src/routes/dashboard/capsules/+page.ts b/frontend/src/routes/dashboard/capsules/+page.ts deleted file mode 100644 index 029fe7c..0000000 --- a/frontend/src/routes/dashboard/capsules/+page.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { redirect } from '@sveltejs/kit'; - -export function load() { - throw redirect(307, '/dashboard/capsules/list'); -} diff --git a/frontend/src/routes/dashboard/capsules/list/+page.svelte b/frontend/src/routes/dashboard/capsules/list/+page.svelte deleted file mode 100644 index a04a9e7..0000000 --- a/frontend/src/routes/dashboard/capsules/list/+page.svelte +++ /dev/null @@ -1,734 +0,0 @@ - - - - - - { if (e.key === 'Escape') openMenuId = null; }} /> - -
- -
-
- - - - -
- {filteredCapsules.length} total - -
- - - - - - - - -
- - {#if error} -
- {error} -
- {/if} - - -
- -
-
ID
-
Template
- {@render sortableHeader('CPU', 'vcpus')} - {@render sortableHeader('Memory', 'memory_mb')} - {@render sortableHeader('Idle Timeout', 'timeout_sec')} - {@render sortableHeader('Started', 'started_at')} - {@render sortableHeader('Status', 'status')} -
- - {#if loading && capsules.length === 0} -
-
- - - - Loading capsules... -
-
- {:else if filteredCapsules.length === 0} -
-
-
-
- - - - - -
-
-

- No capsules yet -

-

- Each capsule is an isolated VM. Launch one to get started. -

- -
- {:else} - {#each filteredCapsules as capsule, i (capsule.id)} - {@const stripeColor = capsule.status === 'running' ? 'bg-[var(--color-accent)]' : capsule.status === 'paused' ? 'bg-[var(--color-amber)]' : 'bg-[var(--color-text-muted)]'} -
- -
- - -
- {#if capsule.status === 'running'} - - - - - {:else if capsule.status === 'paused'} - - {: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 -
- - -
- {capsule.timeout_sec ? `${capsule.timeout_sec}s` : '—'} -
- - -
- {formatTime(capsule.started_at)} - {#if capsule.last_active_at} - {timeAgo(capsule.last_active_at)} - {/if} -
- - -
- {#if actionLoading === capsule.id} - - - - - - {:else} - - {/if} -
-
- {/each} - {/if} -
-
- - -{#if openMenuId} - {@const openCapsule = capsules.find((c) => c.id === openMenuId)} - {#if openCapsule} -
- {#if openCapsule.status === 'running'} - - - {:else if openCapsule.status === 'paused'} - - - {/if} -
- -
- {/if} -{/if} - - -{#if snapshotTarget} -
- -
{ if (!snapshotting) snapshotTarget = null; }} - onkeydown={(e) => { if (e.key === 'Escape' && !snapshotting) snapshotTarget = null; }} - >
- -
-
-
- - - - -
-
-

Capture snapshot

-

{snapshotTarget.capsule.id}

-
-
- -
- {#if snapshotTarget.pauseFirst} -
- - - - - -

This capsule will be paused first — memory state is captured at rest.

-
- {:else} -

The capsule's current memory state will be captured and stored as a reusable snapshot.

- {/if} - - {#if snapshotError} -
- {snapshotError} -
- {/if} - -
-
- - optional -
- { if (e.key === 'Enter' && !snapshotting) handleSnapshotConfirm(); }} - /> -

Leave blank to use an auto-generated name.

-
- -
- - -
-
-
-
-{/if} - - -{#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} - -
- - -
-
-
-{/if} - - - { showCreateDialog = false; }} - oncreated={handleCapsuleCreated} -/> - -{#snippet sortableHeader(label: string, key: SortKey)} - -{/snippet} diff --git a/frontend/src/routes/dashboard/capsules/stats/+page.svelte b/frontend/src/routes/dashboard/capsules/stats/+page.svelte deleted file mode 100644 index 4b2e637..0000000 --- a/frontend/src/routes/dashboard/capsules/stats/+page.svelte +++ /dev/null @@ -1,17 +0,0 @@ - - - { showCreateDialog = true; }} - launchDisabled={!auth.teamId} -/> - - { showCreateDialog = false; }} -/> diff --git a/frontend/src/routes/dashboard/metrics/+page.svelte b/frontend/src/routes/dashboard/metrics/+page.svelte new file mode 100644 index 0000000..271c757 --- /dev/null +++ b/frontend/src/routes/dashboard/metrics/+page.svelte @@ -0,0 +1,57 @@ + + + + Wrenn — Metrics + + +
+ + +
+
+
+

+ Metrics +

+

+ Resource usage and performance across all capsules. +

+
+ + { showCreateDialog = true; }} + launchDisabled={!auth.teamId} + /> +
+ +
+
+ + + + + All systems operational +
+
+
+
+ + { showCreateDialog = false; }} +/>