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:
40
frontend/src/lib/api/admin-capsules.ts
Normal file
40
frontend/src/lib/api/admin-capsules.ts
Normal 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 };
|
||||
}
|
||||
@ -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 }),
|
||||
|
||||
@ -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'];
|
||||
|
||||
Reference in New Issue
Block a user