1
0
forked from wrenn/wrenn

Rename API routes /v1/sandboxes → /v1/capsules

This commit is contained in:
2026-04-12 21:51:04 +06:00
parent ea65fb584c
commit 565817273d
22 changed files with 208 additions and 208 deletions

View File

@ -17,11 +17,11 @@ export type Capsule = {
export async function listCapsules(): Promise<ApiResult<Capsule[]>> {
return apiFetch('GET', '/api/v1/sandboxes');
return apiFetch('GET', '/api/v1/capsules');
}
export async function getCapsule(id: string): Promise<ApiResult<Capsule>> {
return apiFetch('GET', `/api/v1/sandboxes/${id}`);
return apiFetch('GET', `/api/v1/capsules/${id}`);
}
export type CreateCapsuleParams = {
@ -32,19 +32,19 @@ export type CreateCapsuleParams = {
};
export async function createCapsule(params: CreateCapsuleParams): Promise<ApiResult<Capsule>> {
return apiFetch('POST', '/api/v1/sandboxes', params);
return apiFetch('POST', '/api/v1/capsules', params);
}
export async function pauseCapsule(id: string): Promise<ApiResult<Capsule>> {
return apiFetch('POST', `/api/v1/sandboxes/${id}/pause`);
return apiFetch('POST', `/api/v1/capsules/${id}/pause`);
}
export async function resumeCapsule(id: string): Promise<ApiResult<Capsule>> {
return apiFetch('POST', `/api/v1/sandboxes/${id}/resume`);
return apiFetch('POST', `/api/v1/capsules/${id}/resume`);
}
export async function destroyCapsule(id: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/sandboxes/${id}`);
return apiFetch('DELETE', `/api/v1/capsules/${id}`);
}
export type Snapshot = {
@ -57,8 +57,8 @@ export type Snapshot = {
platform: boolean;
};
export async function createSnapshot(sandboxId: string, name?: string): Promise<ApiResult<Snapshot>> {
return apiFetch('POST', '/api/v1/snapshots', { sandbox_id: sandboxId, name });
export async function createSnapshot(capsuleId: string, name?: string): Promise<ApiResult<Snapshot>> {
return apiFetch('POST', '/api/v1/snapshots', { sandbox_id: capsuleId, name });
}
export async function listSnapshots(typeFilter?: string): Promise<ApiResult<Snapshot[]>> {

View File

@ -53,12 +53,12 @@ export function formatFileSize(bytes: number): string {
return `${val < 10 ? val.toFixed(1) : Math.round(val)} ${units[i]}`;
}
export async function listDir(sandboxId: string, path: string, depth = 1): Promise<ApiResult<ListDirResponse>> {
export async function listDir(capsuleId: string, path: string, depth = 1): Promise<ApiResult<ListDirResponse>> {
try {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/list`, {
const res = await fetch(`/api/v1/capsules/${capsuleId}/files/list`, {
method: 'POST',
headers,
body: JSON.stringify({ path, depth }),
@ -72,12 +72,12 @@ export async function listDir(sandboxId: string, path: string, depth = 1): Promi
}
}
export async function readFile(sandboxId: string, path: string): Promise<ApiResult<string>> {
export async function readFile(capsuleId: string, path: string): Promise<ApiResult<string>> {
try {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/read`, {
const res = await fetch(`/api/v1/capsules/${capsuleId}/files/read`, {
method: 'POST',
headers,
body: JSON.stringify({ path }),
@ -100,11 +100,11 @@ export async function readFile(sandboxId: string, path: string): Promise<ApiResu
}
}
export async function downloadFile(sandboxId: string, path: string, filename: string): Promise<void> {
export async function downloadFile(capsuleId: string, path: string, filename: string): Promise<void> {
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
if (auth.token) headers['Authorization'] = `Bearer ${auth.token}`;
const res = await fetch(`/api/v1/sandboxes/${sandboxId}/files/read`, {
const res = await fetch(`/api/v1/capsules/${capsuleId}/files/read`, {
method: 'POST',
headers,
body: JSON.stringify({ path }),

View File

@ -15,8 +15,8 @@ export type MetricsResponse = {
points: MetricPoint[];
};
export async function fetchSandboxMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
return apiFetch('GET', `/api/v1/sandboxes/${id}/metrics?range=${range}`);
export async function fetchCapsuleMetrics(id: string, range: MetricRange): Promise<ApiResult<MetricsResponse>> {
return apiFetch('GET', `/api/v1/capsules/${id}/metrics?range=${range}`);
}
export const METRIC_RANGES: MetricRange[] = ['5m', '10m', '1h', '6h', '24h'];

View File

@ -24,7 +24,7 @@ export type StatsResponse = {
};
export async function fetchStats(range: TimeRange): Promise<ApiResult<StatsResponse>> {
return apiFetch('GET', `/api/v1/sandboxes/stats?range=${range}`);
return apiFetch('GET', `/api/v1/capsules/stats?range=${range}`);
}
export const POLL_INTERVALS: Record<TimeRange, number> = {

View File

@ -11,11 +11,11 @@
import { tokenize, type ThemedToken } from '$lib/highlight';
type Props = {
sandboxId: string;
capsuleId: string;
isRunning: boolean;
};
let { sandboxId, isRunning }: Props = $props();
let { capsuleId, isRunning }: Props = $props();
// Directory navigation state
let currentPath = $state('~');
@ -124,7 +124,7 @@
dirLoading = true;
dirError = null;
const gen = ++dirGeneration;
const result = await listDir(sandboxId, currentPath);
const result = await listDir(capsuleId, currentPath);
if (gen !== dirGeneration) return; // stale response
if (result.ok) {
entries = result.data.entries ?? [];
@ -159,7 +159,7 @@
fileLoading = true;
const gen = ++fileGeneration;
const result = await readFile(sandboxId, entry.path);
const result = await readFile(capsuleId, entry.path);
if (gen !== fileGeneration) return; // stale response — user clicked another file
if (result.ok) {
if (looksLikeBinary(result.data)) {
@ -197,7 +197,7 @@
if (!selectedFile || downloading) return;
downloading = true;
try {
await downloadFile(sandboxId, selectedFile.path, selectedFile.name);
await downloadFile(capsuleId, selectedFile.path, selectedFile.name);
} catch {
fileError = 'Download failed';
}
@ -214,7 +214,7 @@
async function navigateOrOpenFile(path: string) {
// First try as directory
const dirResult = await listDir(sandboxId, path);
const dirResult = await listDir(capsuleId, path);
if (dirResult.ok) {
// Resolve actual path from entries (handles ~ expansion by envd)
const resolvedEntries = dirResult.data.entries ?? [];
@ -245,7 +245,7 @@
// Navigate to parent directory
currentPath = parentPath;
pathInput = parentPath;
const parentResult = await listDir(sandboxId, parentPath);
const parentResult = await listDir(capsuleId, parentPath);
if (parentResult.ok) {
entries = parentResult.data.entries ?? [];
// Find the file in parent listing

View File

@ -3,12 +3,12 @@
import { auth } from '$lib/auth.svelte';
type Props = {
sandboxId: string;
capsuleId: string;
isRunning: boolean;
visible?: boolean;
};
let { sandboxId, isRunning, visible = true }: Props = $props();
let { capsuleId, isRunning, visible = true }: Props = $props();
type ConnectionState = 'idle' | 'connecting' | 'connected' | 'disconnected' | 'error';
@ -93,7 +93,7 @@
function getWsUrl(): string {
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const token = auth.token ? `?token=${encodeURIComponent(auth.token)}` : '';
return `${proto}//${window.location.host}/api/v1/sandboxes/${sandboxId}/pty${token}`;
return `${proto}//${window.location.host}/api/v1/capsules/${capsuleId}/pty${token}`;
}
function wsSend(ws: WebSocket | null, data: string) {

View File

@ -17,7 +17,7 @@ let loadingPromise: Promise<HighlighterGeneric<any, any>> | null = null;
const THEME = 'vesper';
// Extensions → shiki language IDs.
// Only map what we expect users to encounter in sandboxes.
// Only map what we expect users to encounter in capsules.
const EXT_TO_LANG: Record<string, string> = {
// Go
go: 'go', mod: 'go', sum: 'go',

View File

@ -77,7 +77,7 @@
// Delete confirmation
let deleteTarget = $state<Host | null>(null);
let deletePreviewSandboxes = $state<string[]>([]);
let deletePreviewCapsules = $state<string[]>([]);
let deletePreviewLoading = $state(false);
let deleting = $state(false);
let deleteError = $state<string | null>(null);
@ -124,12 +124,12 @@
async function openDeleteConfirm(host: Host) {
deleteTarget = host;
deleteError = null;
deletePreviewSandboxes = [];
deletePreviewCapsules = [];
deletePreviewLoading = true;
const preview = await getDeletePreview(host.id);
deletePreviewLoading = false;
if (preview.ok) {
deletePreviewSandboxes = preview.data.sandbox_ids;
deletePreviewCapsules = preview.data.sandbox_ids;
}
}
@ -137,7 +137,7 @@
if (!deleteTarget) return;
deleting = true;
deleteError = null;
const result = await deleteHost(deleteTarget.id, deletePreviewSandboxes.length > 0);
const result = await deleteHost(deleteTarget.id, deletePreviewCapsules.length > 0);
if (result.ok) {
allHosts = allHosts.filter((h) => h.id !== deleteTarget!.id);
deleteTarget = null;
@ -627,10 +627,10 @@
<svg class="animate-spin" width="13" height="13" 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>
Checking active capsules…
</div>
{:else if deletePreviewSandboxes.length > 0}
{:else if deletePreviewCapsules.length > 0}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-amber)]/30 bg-[var(--color-amber)]/6 px-3 py-2.5">
<p class="text-meta font-semibold text-[var(--color-amber)]">
{deletePreviewSandboxes.length} active capsule{deletePreviewSandboxes.length === 1 ? '' : 's'} will be destroyed.
{deletePreviewCapsules.length} active capsule{deletePreviewCapsules.length === 1 ? '' : 's'} will be destroyed.
</p>
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
All running workloads on this host will be terminated immediately.
@ -661,7 +661,7 @@
<svg class="animate-spin" width="13" height="13" 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>
Deleting…
{:else}
{deletePreviewSandboxes.length > 0 ? 'Force Delete' : 'Delete'}
{deletePreviewCapsules.length > 0 ? 'Force Delete' : 'Delete'}
{/if}
</button>
</div>

View File

@ -9,14 +9,14 @@
import FilesTab from '$lib/components/FilesTab.svelte';
import TerminalTab from '$lib/components/TerminalTab.svelte';
import {
fetchSandboxMetrics,
fetchCapsuleMetrics,
METRIC_RANGES,
METRIC_POLL_INTERVALS,
type MetricRange,
type MetricPoint
} from '$lib/api/metrics';
const sandboxId: string = $page.params.id ?? '';
const capsuleId: string = $page.params.id ?? '';
let capsule = $state<Capsule | null>(null);
let capsuleLoading = $state(true);
@ -96,7 +96,7 @@
);
async function loadCapsule() {
const result = await getCapsule(sandboxId);
const result = await getCapsule(capsuleId);
if (result.ok) {
capsule = result.data;
capsuleError = null;
@ -108,7 +108,7 @@
async function loadMetrics() {
if (!metricsAvailable) return;
const result = await fetchSandboxMetrics(sandboxId, range);
const result = await fetchCapsuleMetrics(capsuleId, range);
if (result.ok) {
points = result.data.points;
metricsError = null;
@ -441,7 +441,7 @@
</script>
<svelte:head>
<title>Wrenn — {sandboxId}</title>
<title>Wrenn — {capsuleId}</title>
</svelte:head>
<style>
@ -575,11 +575,11 @@
<!-- Tab content -->
<!-- Terminal stays mounted so sessions survive tab switches -->
<div class="flex flex-1 min-h-0" style:display={activeTab === 'terminal' ? 'flex' : 'none'}>
<TerminalTab sandboxId={sandboxId} isRunning={capsule.status === 'running'} visible={activeTab === 'terminal'} />
<TerminalTab capsuleId={capsuleId} isRunning={capsule.status === 'running'} visible={activeTab === 'terminal'} />
</div>
{#if activeTab === 'files'}
<div class="anim-in flex flex-1 min-h-0" style="animation-delay: 0.05s">
<FilesTab sandboxId={sandboxId} isRunning={capsule.status === 'running'} />
<FilesTab capsuleId={capsuleId} isRunning={capsule.status === 'running'} />
</div>
{:else if activeTab === 'metrics'}
<div
@ -757,14 +757,14 @@
<SnapshotDialog
open={showSnapshot}
capsuleId={sandboxId}
capsuleId={capsuleId}
onclose={() => { showSnapshot = false; }}
onsnapshot={() => { goto('/dashboard/capsules'); }}
/>
<DestroyDialog
open={showDestroy}
capsuleId={sandboxId}
capsuleId={capsuleId}
onclose={() => { showDestroy = false; }}
ondestroyed={() => { goto('/dashboard/capsules'); }}
/>

View File

@ -43,7 +43,7 @@
// Delete confirmation
let deleteTarget = $state<Host | null>(null);
let deletePreviewSandboxes = $state<string[]>([]);
let deletePreviewCapsules = $state<string[]>([]);
let deletePreviewLoading = $state(false);
let deleting = $state(false);
let deleteError = $state<string | null>(null);
@ -97,12 +97,12 @@
async function openDeleteConfirm(host: Host) {
deleteTarget = host;
deleteError = null;
deletePreviewSandboxes = [];
deletePreviewCapsules = [];
deletePreviewLoading = true;
const preview = await getDeletePreview(host.id);
deletePreviewLoading = false;
if (preview.ok) {
deletePreviewSandboxes = preview.data.sandbox_ids;
deletePreviewCapsules = preview.data.sandbox_ids;
}
}
@ -110,7 +110,7 @@
if (!deleteTarget) return;
deleting = true;
deleteError = null;
const result = await deleteHost(deleteTarget.id, deletePreviewSandboxes.length > 0);
const result = await deleteHost(deleteTarget.id, deletePreviewCapsules.length > 0);
if (result.ok) {
hosts = hosts.filter((h) => h.id !== deleteTarget!.id);
deleteTarget = null;
@ -583,10 +583,10 @@
<svg class="animate-spin" width="13" height="13" 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>
Checking active capsules…
</div>
{:else if deletePreviewSandboxes.length > 0}
{:else if deletePreviewCapsules.length > 0}
<div class="mt-4 rounded-[var(--radius-input)] border border-[var(--color-amber)]/20 bg-[var(--color-amber)]/5 px-3 py-2.5">
<p class="text-meta font-semibold text-[var(--color-amber)]">
{deletePreviewSandboxes.length} active capsule{deletePreviewSandboxes.length === 1 ? '' : 's'} will be destroyed.
{deletePreviewCapsules.length} active capsule{deletePreviewCapsules.length === 1 ? '' : 's'} will be destroyed.
</p>
<p class="mt-0.5 text-meta text-[var(--color-amber)]/70">
All running workloads on this host will be terminated immediately.
@ -617,7 +617,7 @@
<svg class="animate-spin" width="13" height="13" 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>
Deleting…
{:else}
{deletePreviewSandboxes.length > 0 ? 'Force Delete' : 'Delete Host'}
{deletePreviewCapsules.length > 0 ? 'Force Delete' : 'Delete Host'}
{/if}
</button>
</div>