/** * `dashboard` — serve the fleet dashboard in a browser (epic #292, slice 1/5): * the UI shell plus one bearer-authenticated read endpoint, `/api/projects`, * from a dedicated `Bun.serve` process — never a route on the daemon port, * whose unauthenticated loopback surface stays exactly as * `verbs/server.test.ts` pins it. * * Argument parsing follows `stats.ts`'s template: every token after the verb * is accounted for, and an unexpected one exits 2 rather than being silently * ignored. */ import type { CommandContext } from "./context.ts"; import { DASHBOARD_HOST, startDashboard } from "../dashboard/server.ts"; const DASHBOARD_USAGE = `omp-conductor dashboard — serve the fleet dashboard in your browser. usage: omp-conductor dashboard [--port N] [--host ADDR] Serves the dashboard UI and GET /api/projects from a dedicated process — separate from the daemon, which it only ever reads. /api/projects answers with every configured project annotated with its daemon state (the same livingDaemon + /healthz classification status and board use), the daemon's port, and the raw /healthz body when it answers. Every /api request must carry Authorization: Bearer ; anything else is a bare 401. The token is minted mode 0600 at /dashboard-token on first start (the $OMP_CONDUCTOR_HOME state directory, next to config.json) and reused afterwards; the page asks for it once and keeps it in localStorage. Static assets are served without auth. Binds 127.0.0.1:8788 by default. A non-loopback --host (say, a tailnet IP) still starts, but prints a one-line warning naming the token file: the token is then the only thing between anyone who can reach that address and the project list. Ctrl-C stops the server. flags: --port PORT listen port (default 8788) --host ADDR bind address (default 127.0.0.1)`; export async function dashboardCommand(ctx: CommandContext): Promise { // Help first: parsing stops before any config read, token mint or socket. if (ctx.argv[1] === "--help" || ctx.argv[1] === "-h") { process.stdout.write(`${DASHBOARD_USAGE}\n`); return; } // Every remaining token after the verb is accounted for: the two value // flags cli.ts already parsed (--port via portFlag, --host via flag). for (let i = 1; i < ctx.argv.length; i++) { const token = ctx.argv[i]; if (token === undefined) continue; if ( token !== "--port" && token !== "--host" && !token.startsWith("--port=") && !token.startsWith("--host=") ) { process.stderr.write(`omp-conductor: dashboard: unexpected argument "${token}"\n`); process.exit(2); } if (token === "--port" || token === "--host") i += 1; } const host = ctx.flag("host"); if (host === undefined && ctx.argv.includes("--host")) { process.stderr.write(`omp-conductor: dashboard --host needs an address\n`); process.exit(2); } if (host !== undefined && host.length === 0) { process.stderr.write(`omp-conductor: dashboard --host needs an address\n`); process.exit(2); } await startDashboard({ port: ctx.portFlag(), host: host ?? DASHBOARD_HOST }); }