diff --git a/cli/selftune/dashboard-server.ts b/cli/selftune/dashboard-server.ts index bcbc97c..38c47c3 100644 --- a/cli/selftune/dashboard-server.ts +++ b/cli/selftune/dashboard-server.ts @@ -1,16 +1,22 @@ /** - * selftune dashboard server — Live Bun.serve HTTP server with SSE, data API, - * and action endpoints for the interactive dashboard. + * selftune dashboard server — Live Bun.serve HTTP server for the React SPA + * dashboard backed by SQLite materialized queries. * - * Endpoints: - * GET / — Serve dashboard HTML shell + live mode flag - * GET /api/data — JSON endpoint returning current telemetry data - * GET /api/events — SSE stream sending data updates every 5 seconds + * Primary endpoints (SPA + v2 API): + * GET / — Serve React SPA (default dashboard) + * GET /api/v2/overview — SQLite-backed overview payload + * GET /api/v2/skills/:name — SQLite-backed per-skill report * POST /api/actions/watch — Trigger `selftune watch` for a skill * POST /api/actions/evolve — Trigger `selftune evolve` for a skill * POST /api/actions/rollback — Trigger `selftune rollback` for a skill - * GET /api/v2/overview — SQLite-backed overview payload - * GET /api/v2/skills/:name — SQLite-backed per-skill report + * + * Legacy/compatibility endpoints: + * GET /legacy/ — Serve old HTML dashboard + * GET /api/data — Legacy JSON endpoint (JSONL-based) + * GET /api/events — Legacy SSE stream + * GET /api/evaluations/:name — Legacy per-skill evaluations + * GET /badge/:name — Skill health badge SVG + * GET /report/:name — Per-skill HTML report */ import type { Database } from "bun:sqlite"; @@ -590,11 +596,37 @@ export async function startDashboardServer( const executeAction = options?.actionRunner ?? runAction; // -- SPA serving ------------------------------------------------------------- - const spaDir = findSpaDir(); + let spaDir = findSpaDir(); + if (!spaDir) { + // Attempt auto-build if the SPA source exists + const repoRoot = resolve(dirname(import.meta.dir), ".."); + const spaSource = join(repoRoot, "apps", "local-dashboard", "package.json"); + if (existsSync(spaSource)) { + console.log("SPA build not found, attempting auto-build..."); + try { + const proc = Bun.spawnSync(["bun", "run", "build:dashboard"], { + cwd: repoRoot, + stdout: "ignore", + stderr: "pipe", + }); + if (proc.exitCode === 0) { + spaDir = findSpaDir(); + } else { + const stderr = new TextDecoder().decode(proc.stderr); + console.error(`SPA auto-build failed: ${stderr.slice(0, 200)}`); + } + } catch (err) { + console.error("SPA auto-build failed:", err); + } + } + } if (spaDir) { - console.log(`SPA found at ${spaDir}, serving as default dashboard`); + console.log(`SPA dashboard serving from ${spaDir}`); } else { - console.log("SPA build not found, serving legacy dashboard at /"); + console.log( + "SPA build not found — falling back to legacy dashboard at /\n" + + " To build the SPA: bun run build:dashboard", + ); } // -- SQLite v2 data layer --------------------------------------------------- diff --git a/cli/selftune/dashboard.ts b/cli/selftune/dashboard.ts index aedc5c1..1187b65 100644 --- a/cli/selftune/dashboard.ts +++ b/cli/selftune/dashboard.ts @@ -1,16 +1,14 @@ /** - * selftune dashboard — Exports JSONL data into a standalone HTML viewer. + * selftune dashboard — Live React SPA backed by SQLite materialized queries. * * Usage: - * selftune dashboard — Open dashboard in default browser - * selftune dashboard --export — Export data-embedded HTML to stdout - * selftune dashboard --out FILE — Write data-embedded HTML to FILE - * selftune dashboard --serve — Start live dashboard server (default port 3141) - * selftune dashboard --serve --port 8080 — Start on custom port + * selftune dashboard — Start live dashboard server (SPA, default) + * selftune dashboard --port 8080 — Start on custom port + * selftune dashboard --export — Export legacy data-embedded HTML to stdout + * selftune dashboard --out FILE — Write legacy data-embedded HTML to FILE */ -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { homedir } from "node:os"; +import { existsSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { EVOLUTION_AUDIT_LOG, QUERY_LOG, SKILL_LOG, TELEMETRY_LOG } from "./constants.js"; import { getLastDeployedProposal, readAuditTrail } from "./evolution/audit.js"; @@ -135,48 +133,21 @@ export async function cliMain(): Promise { const args = process.argv.slice(2); if (args.includes("--help") || args.includes("-h")) { - console.log(`selftune dashboard — Visual data dashboard + console.log(`selftune dashboard — Live React SPA dashboard Usage: - selftune dashboard Open dashboard in default browser - selftune dashboard --export Export data-embedded HTML to stdout - selftune dashboard --out FILE Write data-embedded HTML to FILE - selftune dashboard --serve Start live dashboard server (port 3141) - selftune dashboard --serve --port 8080 Start on custom port`); + selftune dashboard Start live dashboard server (default) + selftune dashboard --port 8080 Start on custom port + selftune dashboard --export Export legacy data-embedded HTML to stdout + selftune dashboard --out FILE Write legacy data-embedded HTML to FILE + +The default starts a live server with the React SPA at / backed by +SQLite materialized queries. The legacy HTML dashboard is available +at /legacy/ on the live server, or via --export / --out.`); process.exit(0); } - if (args.includes("--serve")) { - const portIdx = args.indexOf("--port"); - let port: number | undefined; - if (portIdx !== -1) { - const parsed = Number.parseInt(args[portIdx + 1], 10); - if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { - console.error( - `Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`, - ); - process.exit(1); - } - port = parsed; - } - const { startDashboardServer } = await import("./dashboard-server.js"); - const { stop } = await startDashboardServer({ port, openBrowser: true }); - await new Promise((resolve) => { - let closed = false; - const keepAlive = setInterval(() => {}, 1 << 30); - const shutdown = () => { - if (closed) return; - closed = true; - clearInterval(keepAlive); - stop(); - resolve(); - }; - process.on("SIGINT", shutdown); - process.on("SIGTERM", shutdown); - }); - return; - } - + // Legacy static export modes if (args.includes("--export")) { process.stdout.write(buildEmbeddedHTML()); return; @@ -195,27 +166,33 @@ Usage: return; } - // Default: write to temp file and open in browser - const tmpDir = join(homedir(), ".selftune"); - if (!existsSync(tmpDir)) { - mkdirSync(tmpDir, { recursive: true }); - } - const tmpPath = join(tmpDir, "dashboard.html"); - const html = buildEmbeddedHTML(); - writeFileSync(tmpPath, html, "utf-8"); - - console.log(`Dashboard saved to ${tmpPath}`); - console.log("Opening in browser..."); - - try { - const platform = process.platform; - const cmd = platform === "darwin" ? "open" : platform === "linux" ? "xdg-open" : null; - if (!cmd) throw new Error("Unsupported platform"); - const proc = Bun.spawn([cmd, tmpPath], { stdio: ["ignore", "ignore", "ignore"] }); - await proc.exited; - if (proc.exitCode !== 0) throw new Error(`Failed to launch ${cmd}`); - } catch { - console.log(`Open manually: file://${tmpPath}`); + // Default: start live dashboard server with SPA + // (--serve is accepted for backwards compatibility but is now the default) + const portIdx = args.indexOf("--port"); + let port: number | undefined; + if (portIdx !== -1) { + const parsed = Number.parseInt(args[portIdx + 1], 10); + if (!Number.isInteger(parsed) || parsed < 1 || parsed > 65535) { + console.error( + `Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`, + ); + process.exit(1); + } + port = parsed; } - process.exit(0); + const { startDashboardServer } = await import("./dashboard-server.js"); + const { stop } = await startDashboardServer({ port, openBrowser: true }); + await new Promise((resolve) => { + let closed = false; + const keepAlive = setInterval(() => {}, 1 << 30); + const shutdown = () => { + if (closed) return; + closed = true; + clearInterval(keepAlive); + stop(); + resolve(); + }; + process.on("SIGINT", shutdown); + process.on("SIGTERM", shutdown); + }); } diff --git a/cli/selftune/index.ts b/cli/selftune/index.ts index 4e8b479..02c6d54 100644 --- a/cli/selftune/index.ts +++ b/cli/selftune/index.ts @@ -21,7 +21,7 @@ * selftune doctor — Run health checks * selftune status — Show skill health summary * selftune last — Show last session details - * selftune dashboard [options] — Open visual data dashboard + * selftune dashboard [options] — Start live SPA dashboard server * selftune schedule [options] — Generate scheduling examples (cron, launchd, systemd) * selftune cron [options] — OpenClaw cron integration (setup, list, remove) * selftune baseline [options] — Measure skill value vs. no-skill baseline @@ -63,7 +63,7 @@ Commands: doctor Run health checks status Show skill health summary last Show last session details - dashboard Open visual data dashboard + dashboard Start live SPA dashboard (--export for legacy HTML) schedule Generate scheduling examples (cron, launchd, systemd) cron OpenClaw cron integration (setup, list, remove) badge Generate skill health badges for READMEs