1
0
forked from wrenn/wrenn

Add admin capsule management, fix file browser for special files, normalize dialog styles

- Admin capsule CRUD: list, create (platform templates), get detail with
  terminal/files/metrics, snapshot, destroy
- First signup auto-promotes to platform admin
- JWT auth via query param for WebSocket connections
- File browser: handle non-regular files (devices, pipes, sockets) gracefully
  instead of showing raw backend errors
- Normalize admin template dialogs to match established dialog patterns:
  remove accent bars, unify animation/shadow/button styles
This commit is contained in:
2026-04-13 04:12:36 +06:00
parent f920023ecf
commit 90bea52ccd
19 changed files with 1417 additions and 50 deletions

View File

@ -0,0 +1,40 @@
import { apiFetch, type ApiResult } from '$lib/api/client';
import type { Capsule, CreateCapsuleParams, Snapshot } from '$lib/api/capsules';
import type { AdminTemplate } from '$lib/api/builds';
export async function listAdminCapsules(): Promise<ApiResult<Capsule[]>> {
return apiFetch('GET', '/api/v1/admin/capsules');
}
export async function getAdminCapsule(id: string): Promise<ApiResult<Capsule>> {
return apiFetch('GET', `/api/v1/admin/capsules/${id}`);
}
export async function createAdminCapsule(params: CreateCapsuleParams): Promise<ApiResult<Capsule>> {
return apiFetch('POST', '/api/v1/admin/capsules', params);
}
export async function destroyAdminCapsule(id: string): Promise<ApiResult<void>> {
return apiFetch('DELETE', `/api/v1/admin/capsules/${id}`);
}
export async function snapshotAdminCapsule(id: string, name?: string): Promise<ApiResult<Snapshot>> {
return apiFetch('POST', `/api/v1/admin/capsules/${id}/snapshot`, { name });
}
/** Fetch platform templates for the admin create dialog. */
export async function listPlatformTemplates(): Promise<ApiResult<Snapshot[]>> {
const result = await apiFetch<AdminTemplate[]>('GET', '/api/v1/admin/templates');
if (!result.ok) return result;
// Map AdminTemplate → Snapshot shape.
const snapshots: Snapshot[] = result.data.map((t) => ({
name: t.name,
type: t.type,
vcpus: t.vcpus || undefined,
memory_mb: t.memory_mb || undefined,
size_bytes: t.size_bytes,
created_at: t.created_at,
platform: true,
}));
return { ok: true, data: snapshots };
}

View File

@ -4,7 +4,7 @@ import { type ApiResult } from '$lib/api/client';
export type FileEntry = {
name: string;
path: string;
type: 'file' | 'directory' | 'symlink';
type: 'file' | 'directory' | 'symlink' | 'unknown';
size: number;
mode: number;
permissions: string;
@ -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(capsuleId: string, path: string, depth = 1): Promise<ApiResult<ListDirResponse>> {
export async function listDir(capsuleId: string, path: string, depth = 1, basePath = '/api/v1/capsules'): 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/capsules/${capsuleId}/files/list`, {
const res = await fetch(`${basePath}/${capsuleId}/files/list`, {
method: 'POST',
headers,
body: JSON.stringify({ path, depth }),
@ -76,12 +76,13 @@ export async function readFile(
capsuleId: string,
path: string,
signal?: AbortSignal,
basePath = '/api/v1/capsules',
): 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/capsules/${capsuleId}/files/read`, {
const res = await fetch(`${basePath}/${capsuleId}/files/read`, {
method: 'POST',
headers,
body: JSON.stringify({ path }),
@ -113,11 +114,12 @@ export async function downloadFile(
path: string,
filename: string,
signal?: AbortSignal,
basePath = '/api/v1/capsules',
): 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/capsules/${capsuleId}/files/read`, {
const res = await fetch(`${basePath}/${capsuleId}/files/read`, {
method: 'POST',
headers,
body: JSON.stringify({ path }),

View File

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