forked from wrenn/wrenn
Rename API routes /v1/sandboxes → /v1/capsules
This commit is contained in:
@ -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[]>> {
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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'];
|
||||
|
||||
@ -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> = {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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'); }}
|
||||
/>
|
||||
|
||||
@ -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>
|
||||
|
||||
Reference in New Issue
Block a user