3b2a16b151
Introduce a minimal operations console reachable on a dedicated UI port (default 8081), kept separate from the OpenAI-compatible API port. - ts_web/ holds the SPA shell (index.html, app.css, vanilla app.js) with sidebar navigation for all views from readme.ui.md and a working Overview page backed by a new /_ui/overview JSON endpoint. - scripts/bundle-ui.ts walks ts_web/ and emits ts_bundled/bundle.ts, a single generated module exporting every asset as base64. Mirrors the @stack.gallery/registry pattern so deno compile binaries embed the entire UI with no external filesystem dependency at runtime. - ts/ui/server.ts (UiServer) serves assets from either the bundled map (default, prod) or directly from ts_web/ on disk (dev). The source is chosen per-config and can be overridden by UI_ASSET_SOURCE=disk|bundle. SPA fallback routes unknown extensionless paths to index.html. - IModelGridConfig.ui block with enabled/port/host/assetSource defaults; config init writes the block, the normalizer fills in defaults on load, and the daemon starts/stops the UI server alongside the API. - deno.json gains a bundle:ui task; compile:all now depends on it so released binaries always contain an up-to-date bundle. dev task sets UI_ASSET_SOURCE=disk for hot edits. - ts_bundled/ is gitignored (generated on build). - test/ui-server.smoke.ts exercises bundle and disk modes end to end (index, app.js, SPA fallback, /_ui/overview, 404).
68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
// Smoke test for the UI server: bundle mode serves /index.html,
|
|
// disk mode serves /app.js, /_ui/overview returns structured JSON.
|
|
// Run with: deno run --allow-all test/ui-server.smoke.ts
|
|
|
|
import { UiServer } from '../ts/ui/server.ts';
|
|
import { ContainerManager } from '../ts/containers/container-manager.ts';
|
|
import { ClusterManager } from '../ts/cluster/cluster-manager.ts';
|
|
|
|
async function probe(source: 'bundle' | 'disk', port: number): Promise<void> {
|
|
const cm = new ContainerManager();
|
|
const cluster = new ClusterManager();
|
|
cluster.configure({
|
|
enabled: false,
|
|
nodeName: 'test-node',
|
|
role: 'standalone',
|
|
bindHost: '127.0.0.1',
|
|
gossipPort: 7946,
|
|
heartbeatIntervalMs: 5000,
|
|
seedNodes: [],
|
|
});
|
|
|
|
const server = new UiServer(
|
|
{ enabled: true, port, host: '127.0.0.1', assetSource: source },
|
|
cm,
|
|
cluster,
|
|
);
|
|
await server.start();
|
|
|
|
try {
|
|
const index = await fetch(`http://127.0.0.1:${port}/`);
|
|
const indexBody = await index.text();
|
|
if (!index.ok || !indexBody.includes('ModelGrid')) {
|
|
throw new Error(`[${source}] index.html missing expected content (status=${index.status})`);
|
|
}
|
|
|
|
const app = await fetch(`http://127.0.0.1:${port}/app.js`);
|
|
const appBody = await app.text();
|
|
if (!app.ok || !appBody.includes('ModelGrid UI')) {
|
|
throw new Error(`[${source}] app.js missing expected content (status=${app.status})`);
|
|
}
|
|
|
|
const spa = await fetch(`http://127.0.0.1:${port}/cluster/nodes`);
|
|
const spaBody = await spa.text();
|
|
if (!spa.ok || !spaBody.includes('ModelGrid')) {
|
|
throw new Error(`[${source}] SPA fallback did not return index.html (status=${spa.status})`);
|
|
}
|
|
|
|
const overview = await fetch(`http://127.0.0.1:${port}/_ui/overview`);
|
|
const data = await overview.json();
|
|
if (!overview.ok || data.node?.name !== 'test-node' || !data.health?.status) {
|
|
throw new Error(`[${source}] /_ui/overview unexpected: ${JSON.stringify(data)}`);
|
|
}
|
|
|
|
const missing = await fetch(`http://127.0.0.1:${port}/nope.png`);
|
|
if (missing.status !== 404) {
|
|
throw new Error(`[${source}] expected 404 for missing asset, got ${missing.status}`);
|
|
}
|
|
|
|
console.log(`ok: ${source} mode — index, app.js, SPA fallback, /_ui/overview, 404`);
|
|
} finally {
|
|
await server.stop();
|
|
}
|
|
}
|
|
|
|
await probe('bundle', 18081);
|
|
await probe('disk', 18082);
|
|
console.log('UI server smoke test passed');
|