feat(dashboard): rewrite dashboard in Svelte (#25)

Replace the hand-rolled dashboard with a Svelte component tree (App +
Sessions / StatsTable / LatestPng / RecentRequests / StatsHeader /
Cleanup / CompressionToggle / ToastTray), move the API layer into
lib/, and split state into stores/.

Add a dedicated dashboard build (`build:dashboard-ui` + a separate
tsconfig.dashboard.json) so dashboard sources typecheck independently
of the proxy core, which previously broke the root `tsc` build.
This commit is contained in:
teamchong
2026-05-21 21:43:28 -04:00
parent a19ce30a48
commit 45c83dedb5
22 changed files with 2517 additions and 821 deletions
+68
View File
@@ -0,0 +1,68 @@
// Build the Svelte dashboard UI into a single JS bundle string.
//
// Writes `src/dashboard-bundle.ts` with an exported `DASHBOARD_JS` constant
// containing the compiled IIFE bundle. `src/dashboard.ts` embeds this in the
// HTML template it serves; no runtime deps on the Node host.
//
// CSS is compiled inline (each component injects its own <style> on mount via
// the svelte compiler's `css: 'injected'` mode), so we ship ONE string.
import { build } from 'esbuild';
import sveltePlugin from 'esbuild-svelte';
import { sveltePreprocess } from 'svelte-preprocess';
import { writeFile, mkdir } from 'node:fs/promises';
import { dirname } from 'node:path';
const ENTRY = 'src/dashboard/main.ts';
const OUT_FILE = 'src/dashboard-bundle.ts';
const result = await build({
entryPoints: [ENTRY],
bundle: true,
format: 'iife',
platform: 'browser',
target: 'es2022',
// Svelte 5 ships ESM; we IIFE-wrap so the bundle is a single self-contained
// script tag in the dashboard HTML. No globals leak — the IIFE returns void.
minify: true,
write: false,
sourcemap: false,
// The dashboard runs in the operator's browser, not Workers — `console` is fine.
// We keep the bundle small by stripping debug logs but leaving warn/error.
drop: ['debugger'],
plugins: [
sveltePlugin({
preprocess: sveltePreprocess(),
compilerOptions: {
// Inject CSS via JS so we ship ONE artifact, not a separate stylesheet.
css: 'injected',
// Legacy reactivity (`$:`, `export let`) — Svelte 5 still supports it.
// Runes mode would require rewriting every component to `$state`/`$derived`/
// `$props`. Worth doing eventually; not blocking this rewrite.
runes: false,
},
}),
],
});
const js = result.outputFiles[0].text;
await mkdir(dirname(OUT_FILE), { recursive: true });
// Embed as a backtick string. JS bundle output is safe inside backticks EXCEPT
// for `${`, `` ` ``, and `\`. Escape all three. The compiled IIFE can contain
// any of these inside string literals (e.g. an HTML template).
const escaped = js
.replace(/\\/g, '\\\\')
.replace(/`/g, '\\`')
.replace(/\$\{/g, '\\${');
const header = `// AUTO-GENERATED by scripts/build-dashboard-ui.mjs — do not edit by hand.
// Source: src/dashboard/ (Svelte). Run \`pnpm run build:dashboard-ui\` to regenerate.
// Size: ${js.length} chars (${(js.length / 1024).toFixed(1)} KB).
export const DASHBOARD_JS = \`${escaped}\`;
`;
await writeFile(OUT_FILE, header);
console.log(`✓ built ${OUT_FILE} (${(js.length / 1024).toFixed(1)} KB)`);