+ |
+
+ {tmpl.name}
|
-
+ |
{#if tmpl.type === 'snapshot'}
-
+
+
snapshot
{:else}
-
+
+
base
{/if}
|
-
+ |
{#if tmpl.vcpus && tmpl.memory_mb}
-
+
{tmpl.vcpus} vCPU · {tmpl.memory_mb} MB
{:else}
—
{/if}
|
-
-
+ |
+
{tmpl.size_bytes ? formatBytes(tmpl.size_bytes) : '—'}
|
-
+ |
{timeAgo(tmpl.created_at)}
|
-
+ |
@@ -486,28 +509,30 @@
{/snippet}
{#snippet buildsTable()}
-
+
-
- | Build |
- Name |
- Base |
- Status |
- Progress |
- Started |
- Duration |
+
+
+
+
+
+
+
+
- {#each builds as build (build.id)}
+ {#each builds as build, i (build.id)}
toggleBuildExpand(build.id)}
>
-
-
+
+
|
-
- {build.name}
+ |
+ {build.name}
|
-
+ |
{build.base_template}
|
-
-
+ |
+
{#if build.status === 'running'}
-
-
-
+
+
+
{:else if build.status === 'success'}
-
+
{:else if build.status === 'failed'}
-
+
{:else}
-
+
{/if}
{build.status}
|
-
-
- {build.current_step} / {build.total_steps}
-
- {#if build.status === 'running' && build.total_steps > 0}
-
- {/if}
+ |
+
+
+ {build.current_step}/{build.total_steps}
+
+ {#if build.total_steps > 0}
+
+ {/if}
+
|
-
+ |
{build.started_at ? timeAgo(build.started_at) : '—'}
|
-
-
+ |
+
{formatDuration(build.started_at, build.completed_at)}
|
@@ -569,7 +596,7 @@
{#if expandedBuildId === build.id}
-
+
{#if build.status === 'pending' || build.status === 'running'}
-
+
+
+
+
+
Create Template
@@ -872,7 +903,7 @@
+
{/if}
@@ -897,10 +929,14 @@
onkeydown={(e) => { if (e.key === 'Escape' && !deleting) deleteTarget = null; }}
>
-
+
+
+
+
+
Delete Template
@@ -924,7 +960,7 @@
+
{/if}
@@ -959,4 +996,59 @@
background-size: 200% 100%;
animation: shimmer 1.4s ease infinite;
}
+
+ /* Stat pill — shared base */
+ .stat-pill {
+ display: flex;
+ align-items: baseline;
+ gap: 6px;
+ border-radius: var(--radius-button);
+ border-width: 1px;
+ padding: 6px 12px;
+ transition: transform 0.15s ease, box-shadow 0.15s ease;
+ }
+ .stat-pill:hover {
+ transform: translateY(-1px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
+ }
+
+ /* Table header */
+ .table-header {
+ padding: 10px 20px;
+ text-align: left;
+ font-size: var(--text-label);
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.06em;
+ color: var(--color-text-tertiary);
+ }
+
+ /* Staggered row entrance */
+ .table-row-animate {
+ animation: fadeUp 0.25s ease both;
+ }
+
+ /* Tab button */
+ .tab-button {
+ position: relative;
+ padding: 14px 20px 14px 0;
+ font-size: var(--text-ui);
+ transition: color 0.15s ease;
+ cursor: pointer;
+ }
+
+ /* Active build row — subtle left accent */
+ .build-row-active {
+ box-shadow: inset 3px 0 0 var(--color-blue);
+ }
+
+ /* Progress bar glow for running builds */
+ .progress-bar-glow {
+ box-shadow: 0 0 8px rgba(90, 159, 212, 0.4);
+ }
+
+ /* Empty state icon float */
+ .empty-icon-float {
+ animation: iconFloat 3s ease-in-out infinite;
+ }
diff --git a/frontend/src/routes/dashboard/snapshots/+page.svelte b/frontend/src/routes/dashboard/templates/+page.svelte
similarity index 99%
rename from frontend/src/routes/dashboard/snapshots/+page.svelte
rename to frontend/src/routes/dashboard/templates/+page.svelte
index 2fe2b37..b84a3d7 100644
--- a/frontend/src/routes/dashboard/snapshots/+page.svelte
+++ b/frontend/src/routes/dashboard/templates/+page.svelte
@@ -383,8 +383,8 @@
- {#if snapshot.type === 'snapshot' && snapshot.vcpus != null}
-
+ {#if snapshot.vcpus != null && snapshot.vcpus > 0}
+
@@ -397,8 +397,8 @@
- {#if snapshot.type === 'snapshot' && snapshot.memory_mb != null}
-
+ {#if snapshot.memory_mb != null && snapshot.memory_mb > 0}
+
From 000318f77ecc9afaa9c7e599f9d915453b2b9626 Mon Sep 17 00:00:00 2001
From: pptx704
Date: Sun, 12 Apr 2026 02:43:09 +0600
Subject: [PATCH 3/5] Fix runtime env leaking into templates, add hostname to
/etc/hosts
- Filter out user-specific env vars (HOME, USER, LOGNAME, SHELL, etc.)
from template default_env so they don't override envd's per-user
resolution. Fixes bash sourcing /root/.bashrc as wrenn-user.
- Keep WRENN_SANDBOX (legitimate runtime flag), only filter per-sandbox
IDs (WRENN_SANDBOX_ID, WRENN_TEMPLATE_ID).
- Add "127.0.0.1 sandbox" to /etc/hosts in wrenn-init.sh so sudo can
resolve the hostname. Fixes "unable to resolve host sandbox" error.
- Move capsule lifecycle buttons (Pause/Resume/Snapshot/Destroy) to the
same row as Stats/Files/Terminal tabs.
- Show vCPU/Memory for all template types with Required/Recommended
tooltips on the user templates page.
---
.../dashboard/capsules/[id]/+page.svelte | 108 +++++++-------
.../routes/dashboard/templates/+page.svelte | 133 +++++++++---------
images/wrenn-init.sh | 3 +-
internal/service/build.go | 28 +++-
4 files changed, 153 insertions(+), 119 deletions(-)
diff --git a/frontend/src/routes/dashboard/capsules/[id]/+page.svelte b/frontend/src/routes/dashboard/capsules/[id]/+page.svelte
index 8b1aa67..a90d334 100644
--- a/frontend/src/routes/dashboard/capsules/[id]/+page.svelte
+++ b/frontend/src/routes/dashboard/capsules/[id]/+page.svelte
@@ -478,60 +478,8 @@
{:else if capsule}
-
-
- {#if capsule.status === 'running'}
-
- {:else if capsule.status === 'paused'}
-
-
- {/if}
-
- {#if capsule.status === 'running' || capsule.status === 'paused'}
-
- {/if}
-
-
-
-
+
+
+
+
+
+ {#if capsule.status === 'running'}
+
+ {:else if capsule.status === 'paused'}
+
+
+ {/if}
+
+ {#if capsule.status === 'running' || capsule.status === 'paused'}
+
+ {/if}
+
diff --git a/frontend/src/routes/dashboard/templates/+page.svelte b/frontend/src/routes/dashboard/templates/+page.svelte
index b84a3d7..b852841 100644
--- a/frontend/src/routes/dashboard/templates/+page.svelte
+++ b/frontend/src/routes/dashboard/templates/+page.svelte
@@ -162,10 +162,10 @@
-
+
-
+
Templates
@@ -231,15 +231,15 @@
-
-
- Name
- Type
- vCPUs
- Memory
- Size
- Created
- Actions
+
+
+ Name
+ Type
+ vCPUs
+ Memory
+ Size
+ Created
+ Actions
{#each Array(4) as _, i}
-
-
-
-
+
+
-
+
{emptyHeading(typeFilter)}
-
+
{emptyDescription(typeFilter)}
{#if typeFilter === 'all' || typeFilter === 'snapshot'}
Go to Capsules
{:else}
-
+
-
- Name
- Type
- vCPUs
- Memory
- Size
- Created
- Actions
+
+ Name
+ Type
+ vCPUs
+ Memory
+ Size
+ Created
+ Actions
@@ -366,17 +364,14 @@
{#if isSnapshot}
-
-
- Snapshot
+
+
+ snapshot
{:else}
-
-
- Image
+
+
+ image
{/if}
@@ -475,7 +470,7 @@
-
{/if}
@@ -591,11 +591,15 @@
>
- Launch Capsule
-
+
+
+
+
+ Launch Capsule
+
Configure resources and launch a new capsule from this snapshot.
@@ -612,12 +616,9 @@
{#if launchTarget.type === 'snapshot'}
-
+
{:else}
-
+
{/if}
{launchTarget.name}
@@ -694,7 +695,7 @@
+
{/if}
@@ -715,17 +717,17 @@
.skeleton {
background: linear-gradient(
90deg,
- var(--color-bg-4) 0%,
- var(--color-bg-5) 50%,
- var(--color-bg-4) 100%
+ var(--color-bg-3) 25%,
+ var(--color-bg-4) 50%,
+ var(--color-bg-3) 75%
);
background-size: 200% 100%;
- animation: shimmer 1.6s ease-in-out infinite;
+ animation: shimmer 1.4s ease infinite;
}
@keyframes shimmer {
- 0% { background-position: 200% center; }
- 100% { background-position: -200% center; }
+ 0% { background-position: -200% 0; }
+ 100% { background-position: 200% 0; }
}
/* Left accent stripe — slides in on hover, color-keyed to snapshot type */
@@ -745,4 +747,9 @@
.snapshot-row.type-image:hover {
background: rgba(90, 159, 212, 0.04);
}
+
+ /* Empty state icon float — matches admin pattern */
+ .empty-icon-float {
+ animation: iconFloat 3s ease-in-out infinite;
+ }
diff --git a/images/wrenn-init.sh b/images/wrenn-init.sh
index d83be1c..8a9e22e 100644
--- a/images/wrenn-init.sh
+++ b/images/wrenn-init.sh
@@ -17,8 +17,9 @@ mkdir -p /sys/fs/cgroup
mount -t cgroup2 cgroup2 /sys/fs/cgroup 2>/dev/null || true
echo "+cpu +memory +io" > /sys/fs/cgroup/cgroup.subtree_control 2>/dev/null || true
-# Set hostname
+# Set hostname and make it resolvable (sudo requires this).
hostname sandbox
+echo "127.0.0.1 sandbox" >> /etc/hosts
# Configure networking if the kernel ip= boot arg did not already set it up.
if ! ip addr show eth0 2>/dev/null | grep -q "169.254.0.21"; then
diff --git a/internal/service/build.go b/internal/service/build.go
index 55defe6..c0aa8c6 100644
--- a/internal/service/build.go
+++ b/internal/service/build.go
@@ -420,8 +420,10 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
}
// Capture the final user and env vars as template defaults.
+ // Filter out user-specific and runtime vars that should be resolved at
+ // sandbox creation time, not baked in from the build environment.
templateDefaultUser := bctx.User
- templateDefaultEnv := bctx.EnvVars
+ templateDefaultEnv := filterBuildEnv(bctx.EnvVars)
// Phase 3: Post-build (as root) — cleanup.
bctx.User = "root"
@@ -739,3 +741,27 @@ func (s *BuildService) uploadAndExtractArchive(
return nil
}
+
+// runtimeEnvVars lists env vars that are user- or session-specific and should
+// not be persisted into template defaults. These are resolved at runtime by
+// envd based on the actual user and sandbox context.
+var runtimeEnvVars = map[string]bool{
+ "HOME": true, "USER": true, "LOGNAME": true, "SHELL": true,
+ "PWD": true, "OLDPWD": true, "HOSTNAME": true, "TERM": true,
+ "SHLVL": true, "_": true,
+ // Per-sandbox identifiers set by envd at boot via MMDS.
+ "WRENN_SANDBOX_ID": true, "WRENN_TEMPLATE_ID": true,
+}
+
+// filterBuildEnv returns a copy of envVars with runtime/user-specific
+// variables removed so they don't override envd's per-user resolution.
+func filterBuildEnv(envVars map[string]string) map[string]string {
+ filtered := make(map[string]string, len(envVars))
+ for k, v := range envVars {
+ if runtimeEnvVars[k] {
+ continue
+ }
+ filtered[k] = v
+ }
+ return filtered
+}
From 46c43b95c27895be6d2697330cef1c9ac0e75e4d Mon Sep 17 00:00:00 2001
From: pptx704
Date: Sun, 12 Apr 2026 02:44:40 +0600
Subject: [PATCH 4/5] Visual polish
---
frontend/src/routes/admin/hosts/+page.svelte | 255 ++++++++++++-------
1 file changed, 170 insertions(+), 85 deletions(-)
diff --git a/frontend/src/routes/admin/hosts/+page.svelte b/frontend/src/routes/admin/hosts/+page.svelte
index 16c7476..362b9ff 100644
--- a/frontend/src/routes/admin/hosts/+page.svelte
+++ b/frontend/src/routes/admin/hosts/+page.svelte
@@ -168,45 +168,48 @@
- | | |