feat(dashboard): scaffold SvelteKit SPA for control plane

SvelteKit 2 + Svelte 5 (runes) + TS, built with `adapter-static`
into a single SPA bundle that Caddy serves verbatim in production.
The dashboard targets only `/api/admin/*` (manager); data-plane
invocations go through the orchestrator, not through here.

  * Vite dev server proxies `/api` and `/healthz` to PICLOUD_API
    (default `http://127.0.0.1:18080` to match the picloud bind
    override). Port configurable via PICLOUD_DASHBOARD_PORT.
  * `src/lib/api.ts` is a thin typed client over the control-plane
    paths; the scripts placeholder route shows the "load → error →
    list" shape so the missing-API state is informative, not blank.
  * SSR disabled at the layout level: the build is a pure SPA, no
    Node runtime is required at serve time.
  * `npm run check` and `npm run build` both green; `npm audit`
    clean (cookie override pins past the SvelteKit transitive
    advisory that doesn't apply in SPA mode).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-05-22 23:28:03 +02:00
parent b8b544816d
commit dca36a30d2
16 changed files with 3758 additions and 0 deletions

45
dashboard/src/lib/api.ts Normal file
View File

@@ -0,0 +1,45 @@
// Thin client for the PiCloud control-plane API.
//
// All admin/CRUD calls hit `/api/admin/*` (manager). Data-plane calls
// (script invocations) go to `/api/execute/*` (orchestrator). The
// dashboard only talks to the control plane — data-plane invocations
// from the dashboard go through the same path as any external caller.
export interface Script {
id: string;
name: string;
description: string | null;
version: number;
source: string;
timeout_seconds: number;
memory_limit_mb: number;
created_at: string;
updated_at: string;
}
async function request<T>(path: string, init?: RequestInit): Promise<T> {
const res = await fetch(path, {
...init,
headers: { 'content-type': 'application/json', ...(init?.headers ?? {}) }
});
if (!res.ok) {
const body = await res.text().catch(() => '');
throw new Error(`${res.status} ${res.statusText}: ${body}`);
}
return res.json() as Promise<T>;
}
export const api = {
health: () => fetch('/healthz').then((r) => r.text()),
scripts: {
list: () => request<Script[]>('/api/admin/scripts'),
get: (id: string) => request<Script>(`/api/admin/scripts/${id}`),
create: (input: Partial<Script>) =>
request<Script>('/api/admin/scripts', { method: 'POST', body: JSON.stringify(input) }),
update: (id: string, input: Partial<Script>) =>
request<Script>(`/api/admin/scripts/${id}`, { method: 'PUT', body: JSON.stringify(input) }),
remove: (id: string) =>
request<void>(`/api/admin/scripts/${id}`, { method: 'DELETE' })
}
};