1
0
forked from wrenn/wrenn

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.
This commit is contained in:
2026-04-12 02:43:09 +06:00
parent f5eeb0ffcc
commit 000318f77e
4 changed files with 153 additions and 119 deletions

View File

@ -478,60 +478,8 @@
{:else if capsule} {:else if capsule}
<div class="flex flex-1 flex-col min-h-0"> <div class="flex flex-1 flex-col min-h-0">
<!-- Action buttons --> <!-- Tabs + lifecycle actions -->
<div class="flex items-center justify-end gap-2 px-7 pt-5"> <div class="mt-5 flex items-center border-b border-[var(--color-border)] px-7">
{#if capsule.status === 'running'}
<button
onclick={handlePause}
disabled={actionLoading !== null}
class="flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-amber)]/30 bg-[var(--color-amber)]/8 px-3.5 py-2 text-ui font-medium text-[var(--color-amber)] transition-all duration-150 hover:bg-[var(--color-amber)]/15 hover:border-[var(--color-amber)]/50 disabled:opacity-50"
>
{#if actionLoading === 'pause'}
<svg class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Pausing...
{:else}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" /></svg>
Pause
{/if}
</button>
{:else if capsule.status === 'paused'}
<button
onclick={handleResume}
disabled={actionLoading !== null}
class="flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-accent)]/30 bg-[var(--color-accent)]/8 px-3.5 py-2 text-ui font-medium text-[var(--color-accent-bright)] transition-all duration-150 hover:bg-[var(--color-accent)]/15 hover:border-[var(--color-accent)]/50 disabled:opacity-50"
>
{#if actionLoading === 'resume'}
<svg class="animate-spin" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Resuming...
{:else}
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3" /></svg>
Resume
{/if}
</button>
<button
onclick={() => { showSnapshot = true; }}
disabled={actionLoading !== null}
class="flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-border)] bg-[var(--color-bg-3)] px-3.5 py-2 text-ui font-medium text-[var(--color-text-secondary)] transition-all duration-150 hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" /><circle cx="12" cy="15" r="3" /></svg>
Snapshot
</button>
{/if}
{#if capsule.status === 'running' || capsule.status === 'paused'}
<button
onclick={() => { showDestroy = true; }}
disabled={actionLoading !== null}
class="flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/8 px-3.5 py-2 text-ui font-medium text-[var(--color-red)] transition-all duration-150 hover:bg-[var(--color-red)]/15 hover:border-[var(--color-red)]/50 disabled:opacity-50"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" /></svg>
Destroy
</button>
{/if}
</div>
<!-- Tabs (matches Templates page pattern) -->
<div class="mt-5 flex gap-0 border-b border-[var(--color-border)] px-7">
<button <button
onclick={() => setTab('metrics')} onclick={() => setTab('metrics')}
class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-ui font-medium transition-colors duration-150 class="flex items-center gap-2 border-b-2 px-4 py-2.5 text-ui font-medium transition-colors duration-150
@ -570,6 +518,58 @@
</svg> </svg>
Terminal Terminal
</button> </button>
<!-- Lifecycle actions (right-aligned) -->
<div class="ml-auto flex items-center gap-2">
{#if capsule.status === 'running'}
<button
onclick={handlePause}
disabled={actionLoading !== null}
class="flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-amber)]/30 bg-[var(--color-amber)]/8 px-3 py-1.5 text-meta font-medium text-[var(--color-amber)] transition-all duration-150 hover:bg-[var(--color-amber)]/15 hover:border-[var(--color-amber)]/50 disabled:opacity-50"
>
{#if actionLoading === 'pause'}
<svg class="animate-spin" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Pausing...
{:else}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="6" y="4" width="4" height="16" /><rect x="14" y="4" width="4" height="16" /></svg>
Pause
{/if}
</button>
{:else if capsule.status === 'paused'}
<button
onclick={handleResume}
disabled={actionLoading !== null}
class="flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-accent)]/30 bg-[var(--color-accent)]/8 px-3 py-1.5 text-meta font-medium text-[var(--color-accent-bright)] transition-all duration-150 hover:bg-[var(--color-accent)]/15 hover:border-[var(--color-accent)]/50 disabled:opacity-50"
>
{#if actionLoading === 'resume'}
<svg class="animate-spin" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12a9 9 0 1 1-6.219-8.56" /></svg>
Resuming...
{:else}
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polygon points="5 3 19 12 5 21 5 3" /></svg>
Resume
{/if}
</button>
<button
onclick={() => { showSnapshot = true; }}
disabled={actionLoading !== null}
class="flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-border)] bg-[var(--color-bg-3)] px-3 py-1.5 text-meta font-medium text-[var(--color-text-secondary)] transition-all duration-150 hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)] disabled:opacity-50"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M14.5 4h-5L7 7H2v13a2 2 0 002 2h16a2 2 0 002-2V7h-5l-2.5-3z" /><circle cx="12" cy="15" r="3" /></svg>
Snapshot
</button>
{/if}
{#if capsule.status === 'running' || capsule.status === 'paused'}
<button
onclick={() => { showDestroy = true; }}
disabled={actionLoading !== null}
class="flex items-center gap-1.5 rounded-[var(--radius-button)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/8 px-3 py-1.5 text-meta font-medium text-[var(--color-red)] transition-all duration-150 hover:bg-[var(--color-red)]/15 hover:border-[var(--color-red)]/50 disabled:opacity-50"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6" /><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2" /></svg>
Destroy
</button>
{/if}
</div>
</div> </div>
<!-- Tab content --> <!-- Tab content -->

View File

@ -162,10 +162,10 @@
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
<main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]"> <main class="flex-1 overflow-y-auto bg-[var(--color-bg-0)]">
<!-- Header --> <!-- Header -->
<div class="px-7 pt-8"> <div class="px-8 pt-8">
<div class="flex items-start justify-between"> <div class="flex items-start justify-between">
<div> <div>
<h1 class="font-serif text-page tracking-[-0.02em] text-[var(--color-text-bright)]"> <h1 class="font-serif text-page tracking-[-0.03em] text-[var(--color-text-bright)]">
Templates Templates
</h1> </h1>
<p class="mt-2 text-ui text-[var(--color-text-secondary)]"> <p class="mt-2 text-ui text-[var(--color-text-secondary)]">
@ -231,15 +231,15 @@
</div> </div>
<div class="skeleton h-4 w-20 rounded-sm"></div> <div class="skeleton h-4 w-20 rounded-sm"></div>
</div> </div>
<div class="overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)]"> <div class="overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)] shadow-sm">
<div class="grid border-b border-[var(--color-border)] bg-[var(--color-bg-3)]" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px"> <div class="grid border-b border-[var(--color-border)] bg-[var(--color-bg-0)]/40" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px">
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Name</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Name</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Type</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Type</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">vCPUs</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">vCPUs</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Memory</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Memory</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Size</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Size</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Created</div>
<div class="px-5 py-3 text-right text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Actions</div> <div class="px-5 py-3 text-right text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Actions</div>
</div> </div>
{#each Array(4) as _, i} {#each Array(4) as _, i}
<div <div
@ -294,30 +294,28 @@
{#if filteredSnapshots.length === 0} {#if filteredSnapshots.length === 0}
<!-- Empty state --> <!-- Empty state -->
<div class="flex flex-col items-center justify-center py-[72px]"> <div class="flex flex-col items-center justify-center py-28 text-center">
<div class="relative mb-5"> <div class="relative mb-7">
<!-- Radial glow behind icon --> <div class="absolute -inset-3 rounded-2xl bg-[var(--color-accent-glow)] blur-xl"></div>
<div class="absolute inset-0 -m-4 rounded-full" style="background: radial-gradient(circle, rgba(94,140,88,0.08) 0%, transparent 70%)"></div>
<div <div
class="relative flex h-14 w-14 items-center justify-center rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-3)]" class="empty-icon-float relative flex h-18 w-18 items-center justify-center rounded-xl border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] shadow-card"
style="animation: iconFloat 4s ease-in-out infinite"
> >
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="var(--color-text-secondary)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" class="text-[var(--color-accent-mid)]">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" /> <path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z" />
<polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" /> <polyline points="3.27 6.96 12 12.01 20.73 6.96" /><line x1="12" y1="22.08" x2="12" y2="12" />
</svg> </svg>
</div> </div>
</div> </div>
<p class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]"> <p class="font-serif text-heading leading-snug text-[var(--color-text-secondary)]">
{emptyHeading(typeFilter)} {emptyHeading(typeFilter)}
</p> </p>
<p class="mt-1.5 max-w-[340px] text-center text-ui text-[var(--color-text-tertiary)]"> <p class="mt-2 max-w-[320px] text-ui text-[var(--color-text-muted)]">
{emptyDescription(typeFilter)} {emptyDescription(typeFilter)}
</p> </p>
{#if typeFilter === 'all' || typeFilter === 'snapshot'} {#if typeFilter === 'all' || typeFilter === 'snapshot'}
<a <a
href="/dashboard/capsules" href="/dashboard/capsules"
class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-border-mid)] bg-[var(--color-bg-3)] px-4 py-2 text-ui font-medium text-[var(--color-text-secondary)] transition-all duration-150 hover:border-[var(--color-border-mid)] hover:bg-[var(--color-bg-4)] hover:text-[var(--color-text-primary)] active:scale-95" class="mt-6 flex items-center gap-2 rounded-[var(--radius-button)] border border-[var(--color-accent)]/30 bg-[var(--color-accent)]/10 px-4 py-2 text-ui font-medium text-[var(--color-accent-bright)] transition-all duration-200 hover:bg-[var(--color-accent)]/20 hover:border-[var(--color-accent)]/50"
> >
Go to Capsules Go to Capsules
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
@ -329,16 +327,16 @@
</div> </div>
{:else} {:else}
<!-- Table --> <!-- Table -->
<div class="overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)]"> <div class="overflow-hidden rounded-[var(--radius-card)] border border-[var(--color-border)] shadow-sm">
<!-- Header --> <!-- Header -->
<div class="grid border-b border-[var(--color-border)] bg-[var(--color-bg-3)]" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px"> <div class="grid border-b border-[var(--color-border)] bg-[var(--color-bg-0)]/40" style="grid-template-columns: 2fr 1fr 0.7fr 0.9fr 0.8fr 1.3fr 140px">
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Name</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Name</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Type</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Type</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">vCPUs</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">vCPUs</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Memory</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Memory</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Size</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Size</div>
<div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Created</div> <div class="px-5 py-3 text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Created</div>
<div class="px-5 py-3 text-right text-label font-semibold uppercase tracking-[0.05em] text-[var(--color-text-muted)]">Actions</div> <div class="px-5 py-3 text-right text-label font-semibold uppercase tracking-[0.06em] text-[var(--color-text-tertiary)]">Actions</div>
</div> </div>
<!-- Rows --> <!-- Rows -->
@ -366,17 +364,14 @@
<!-- Type badge --> <!-- Type badge -->
<div class="px-5 py-4"> <div class="px-5 py-4">
{#if isSnapshot} {#if isSnapshot}
<span class="inline-flex items-center gap-1.5 rounded-[3px] border border-[var(--color-accent)]/25 bg-[var(--color-accent)]/10 px-2.5 py-1 text-badge font-semibold uppercase tracking-[0.04em] text-[var(--color-accent-mid)]"> <span class="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-accent)]/25 bg-[var(--color-accent)]/8 px-2.5 py-0.5 text-label font-medium text-[var(--color-accent-bright)]">
<span <span class="h-1.5 w-1.5 rounded-full bg-[var(--color-accent)]"></span>
class="inline-block h-[5px] w-[5px] shrink-0 rounded-full bg-[var(--color-accent)]" snapshot
style="box-shadow: 0 0 6px rgba(94,140,88,0.5); animation: wrenn-glow 1.8s ease-in-out infinite"
></span>
Snapshot
</span> </span>
{:else} {:else}
<span class="inline-flex items-center gap-1.5 rounded-[3px] border border-[var(--color-blue)]/25 bg-[var(--color-blue)]/10 px-2.5 py-1 text-badge font-semibold uppercase tracking-[0.04em] text-[var(--color-blue)]"> <span class="inline-flex items-center gap-1.5 rounded-full border border-[var(--color-blue)]/25 bg-[var(--color-blue)]/8 px-2.5 py-0.5 text-label font-medium text-[var(--color-blue)]">
<span class="inline-block h-[5px] w-[5px] shrink-0 rounded-full bg-[var(--color-blue)]"></span> <span class="h-1.5 w-1.5 rounded-full bg-[var(--color-blue)]"></span>
Image image
</span> </span>
{/if} {/if}
</div> </div>
@ -475,7 +470,7 @@
</main> </main>
<!-- Status bar --> <!-- Status bar -->
<footer class="flex h-7 shrink-0 items-center justify-end border-t border-[var(--color-border)] bg-[var(--color-bg-1)] px-7"> <footer class="flex h-7 shrink-0 items-center justify-end border-t border-[var(--color-border)] bg-[var(--color-bg-1)] px-8">
<div class="flex items-center gap-1.5"> <div class="flex items-center gap-1.5">
<span <span
class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]" class="inline-flex h-[5px] w-[5px] rounded-full bg-[var(--color-accent)]"
@ -526,12 +521,16 @@
></div> ></div>
<div <div
class="relative w-full max-w-[380px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] shadow-dialog"
style="animation: fadeUp 0.2s ease both" style="animation: fadeUp 0.18s cubic-bezier(0.25,1,0.5,1) both"
> >
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Delete snapshot</h2> <!-- Danger accent edge -->
<p class="mt-2 text-ui text-[var(--color-text-tertiary)]"> <div class="h-[2px] rounded-t-[var(--radius-card)] bg-gradient-to-r from-transparent via-[var(--color-red)] to-transparent"></div>
Permanently delete <span class="font-mono font-medium text-[var(--color-text-secondary)]">{deleteTarget.name}</span>.
<div class="p-6">
<h2 class="font-serif text-heading leading-tight tracking-[-0.02em] text-[var(--color-text-bright)]">Delete snapshot</h2>
<p class="mt-1.5 text-ui text-[var(--color-text-tertiary)]">
Permanently delete <code class="rounded bg-[var(--color-bg-4)] px-1.5 py-0.5 font-mono text-[0.8rem] text-[var(--color-text-primary)]">{deleteTarget.name}</code>.
Running capsules won't be affected, but you won't be able to launch new ones from it. Running capsules won't be affected, but you won't be able to launch new ones from it.
</p> </p>
@ -548,7 +547,7 @@
{/if} {/if}
{#if deleteError} {#if deleteError}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]"> <div class="mt-3 rounded-[var(--radius-input)] border border-[var(--color-red)]/30 bg-[var(--color-red)]/5 px-3 py-2 text-meta text-[var(--color-red)]">
{deleteError} {deleteError}
</div> </div>
{/if} {/if}
@ -564,7 +563,7 @@
<button <button
onclick={handleDelete} onclick={handleDelete}
disabled={deleting} disabled={deleting}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 active:scale-95 disabled:opacity-50 disabled:hover:translate-y-0" class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-red)] px-5 py-2.5 text-ui font-semibold text-white transition-all duration-200 hover:shadow-[0_0_16px_rgba(207,129,114,0.25)] hover:brightness-110 disabled:opacity-50"
> >
{#if deleting} {#if deleting}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -576,6 +575,7 @@
{/if} {/if}
</button> </button>
</div> </div>
</div>
</div> </div>
</div> </div>
{/if} {/if}
@ -591,11 +591,15 @@
></div> ></div>
<div <div
class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] p-6" class="relative w-full max-w-[420px] rounded-[var(--radius-card)] border border-[var(--color-border-mid)] bg-[var(--color-bg-2)] shadow-dialog"
style="animation: fadeUp 0.2s ease both" style="animation: fadeUp 0.18s cubic-bezier(0.25,1,0.5,1) both"
> >
<h2 class="font-serif text-heading tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2> <!-- Top accent edge -->
<p class="mt-1 text-ui text-[var(--color-text-tertiary)]"> <div class="h-[2px] rounded-t-[var(--radius-card)] bg-gradient-to-r from-transparent via-[var(--color-accent)] to-transparent"></div>
<div class="p-6">
<h2 class="font-serif text-heading leading-tight tracking-[-0.02em] text-[var(--color-text-bright)]">Launch Capsule</h2>
<p class="mt-1.5 text-ui text-[var(--color-text-tertiary)]">
Configure resources and launch a new capsule from this snapshot. Configure resources and launch a new capsule from this snapshot.
</p> </p>
@ -612,12 +616,9 @@
</label> </label>
<div class="flex items-center gap-2 rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2"> <div class="flex items-center gap-2 rounded-[var(--radius-input)] border border-[var(--color-border)] bg-[var(--color-bg-0)] px-3 py-2">
{#if launchTarget.type === 'snapshot'} {#if launchTarget.type === 'snapshot'}
<span <span class="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-accent)]"></span>
class="inline-block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-accent)]"
style="box-shadow: 0 0 6px rgba(94,140,88,0.5); animation: wrenn-glow 1.8s ease-in-out infinite"
></span>
{:else} {:else}
<span class="inline-block h-[6px] w-[6px] shrink-0 rounded-full bg-[var(--color-blue)]"></span> <span class="h-1.5 w-1.5 shrink-0 rounded-full bg-[var(--color-blue)]"></span>
{/if} {/if}
<span class="flex-1 font-mono text-ui text-[var(--color-text-bright)]">{launchTarget.name}</span> <span class="flex-1 font-mono text-ui text-[var(--color-text-bright)]">{launchTarget.name}</span>
<span class="text-label text-[var(--color-text-muted)]"> <span class="text-label text-[var(--color-text-muted)]">
@ -694,7 +695,7 @@
<button <button
onclick={handleLaunch} onclick={handleLaunch}
disabled={launching} disabled={launching}
class="flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2 text-ui font-semibold text-white transition-all duration-150 hover:brightness-115 hover:-translate-y-px active:translate-y-0 active:scale-95 disabled:opacity-50 disabled:hover:translate-y-0" class="group flex items-center gap-2 rounded-[var(--radius-button)] bg-[var(--color-accent)] px-5 py-2.5 text-ui font-semibold text-white transition-all duration-200 hover:shadow-[0_0_20px_var(--color-accent-glow-mid)] hover:brightness-115 hover:-translate-y-px active:translate-y-0 disabled:opacity-50 disabled:hover:translate-y-0 disabled:hover:shadow-none"
> >
{#if launching} {#if launching}
<svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> <svg class="animate-spin" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@ -706,6 +707,7 @@
{/if} {/if}
</button> </button>
</div> </div>
</div>
</div> </div>
</div> </div>
{/if} {/if}
@ -715,17 +717,17 @@
.skeleton { .skeleton {
background: linear-gradient( background: linear-gradient(
90deg, 90deg,
var(--color-bg-4) 0%, var(--color-bg-3) 25%,
var(--color-bg-5) 50%, var(--color-bg-4) 50%,
var(--color-bg-4) 100% var(--color-bg-3) 75%
); );
background-size: 200% 100%; background-size: 200% 100%;
animation: shimmer 1.6s ease-in-out infinite; animation: shimmer 1.4s ease infinite;
} }
@keyframes shimmer { @keyframes shimmer {
0% { background-position: 200% center; } 0% { background-position: -200% 0; }
100% { background-position: -200% center; } 100% { background-position: 200% 0; }
} }
/* Left accent stripe — slides in on hover, color-keyed to snapshot type */ /* Left accent stripe — slides in on hover, color-keyed to snapshot type */
@ -745,4 +747,9 @@
.snapshot-row.type-image:hover { .snapshot-row.type-image:hover {
background: rgba(90, 159, 212, 0.04); background: rgba(90, 159, 212, 0.04);
} }
/* Empty state icon float — matches admin pattern */
.empty-icon-float {
animation: iconFloat 3s ease-in-out infinite;
}
</style> </style>

View File

@ -17,8 +17,9 @@ mkdir -p /sys/fs/cgroup
mount -t cgroup2 cgroup2 /sys/fs/cgroup 2>/dev/null || true 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 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 hostname sandbox
echo "127.0.0.1 sandbox" >> /etc/hosts
# Configure networking if the kernel ip= boot arg did not already set it up. # 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 if ! ip addr show eth0 2>/dev/null | grep -q "169.254.0.21"; then

View File

@ -420,8 +420,10 @@ func (s *BuildService) executeBuild(ctx context.Context, buildIDStr string) {
} }
// Capture the final user and env vars as template defaults. // 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 templateDefaultUser := bctx.User
templateDefaultEnv := bctx.EnvVars templateDefaultEnv := filterBuildEnv(bctx.EnvVars)
// Phase 3: Post-build (as root) — cleanup. // Phase 3: Post-build (as root) — cleanup.
bctx.User = "root" bctx.User = "root"
@ -739,3 +741,27 @@ func (s *BuildService) uploadAndExtractArchive(
return nil 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
}