Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 43 additions & 11 deletions cli/selftune/dashboard-server.ts
Original file line number Diff line number Diff line change
@@ -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/dataJSON 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/overviewSQLite-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";
Expand Down Expand Up @@ -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 ---------------------------------------------------
Expand Down
111 changes: 44 additions & 67 deletions cli/selftune/dashboard.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -135,48 +133,21 @@ export async function cliMain(): Promise<void> {
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<void>((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;
Expand All @@ -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);
Comment on lines +176 to +179
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix formatting to pass CI.

Pipeline failure indicates Biome expects this console.error call on a single line.

🔧 Proposed fix
-      console.error(
-        `Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`,
-      );
+      console.error(`Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
console.error(
`Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`,
);
process.exit(1);
console.error(`Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.`);
process.exit(1);
🧰 Tools
🪛 GitHub Actions: CI

[error] 176-178: Biome formatter failed: expected formatting of console.error call. The formatter shows the preferred single-line template: console.error(Invalid port "${args[portIdx + 1]}": must be an integer between 1 and 65535.);

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cli/selftune/dashboard.ts` around lines 176 - 179, The console.error call
that logs the invalid port (using args and portIdx) must be formatted as a
single-line statement to satisfy Biome; update the multi-line template string
invocation of console.error to a single-line call (keep the same message content
referencing args[portIdx + 1]) and ensure the subsequent process.exit(1) remains
unchanged.

}
port = parsed;
}
process.exit(0);
const { startDashboardServer } = await import("./dashboard-server.js");
const { stop } = await startDashboardServer({ port, openBrowser: true });
await new Promise<void>((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);
});
}
4 changes: 2 additions & 2 deletions cli/selftune/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading