feat: analytics parity — tool cost, date ranges, budgets, query explorer, replay, snapshots (v2.4.0)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files
Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
This commit is contained in:
+66
-14
@@ -141,21 +141,10 @@ function termWidth() {
|
||||
/**
|
||||
* Resolve the dashboard base URL. Env override first (matches the hook
|
||||
* handler's contract), then the live-server discovery file, then the default
|
||||
* port. The discovery module is best-effort and never throws.
|
||||
* port — shared with the MCP server and the statusline (`dashboard-url.js`).
|
||||
*/
|
||||
function baseUrl() {
|
||||
const envPort = process.env.CLAUDE_DASHBOARD_PORT || process.env.DASHBOARD_PORT;
|
||||
if (envPort) return `http://127.0.0.1:${envPort}`;
|
||||
try {
|
||||
const { resolveDashboardPort } = require(
|
||||
path.join(REPO_ROOT, "server", "lib", "server-info.js")
|
||||
);
|
||||
const port = resolveDashboardPort();
|
||||
if (port) return `http://127.0.0.1:${port}`;
|
||||
} catch {
|
||||
/* discovery unavailable — fall through to the default */
|
||||
}
|
||||
return "http://127.0.0.1:4820";
|
||||
return require(path.join(REPO_ROOT, "server", "lib", "dashboard-url.js")).dashboardUrl();
|
||||
}
|
||||
|
||||
/** Thrown when the dashboard server does not answer — the dispatcher decides
|
||||
@@ -1037,6 +1026,8 @@ async function cmdRemoteSources(flags, positional) {
|
||||
ssh_port: flags.port != null ? Number(flags.port) : null,
|
||||
identity_file: flags.identity != null ? String(flags.identity) : null,
|
||||
remote_home: flags["remote-home"] != null ? String(flags["remote-home"]) : null,
|
||||
sync_codex: !!flags.codex,
|
||||
remote_codex_home: flags["codex-home"] != null ? String(flags["codex-home"]) : null,
|
||||
enabled: flags.disabled ? false : true,
|
||||
};
|
||||
const r = await post("/api/remote-sources", body);
|
||||
@@ -2797,7 +2788,7 @@ const COMMAND_GROUPS = [
|
||||
[
|
||||
"remote-sources add",
|
||||
"",
|
||||
"--label X --host user@host [--port N --identity path --remote-home path]",
|
||||
"--label X --host user@host [--port N --identity path --remote-home path --codex --codex-home path]",
|
||||
],
|
||||
["remote-sources test <id>", "", "Probe SSH connectivity to a source"],
|
||||
["remote-sources sync [id]", "", "Pull history now (all sources if id omitted)"],
|
||||
@@ -2917,6 +2908,22 @@ const COMMAND_GROUPS = [
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
"Integrations",
|
||||
[
|
||||
[
|
||||
"mcp",
|
||||
"",
|
||||
"Run the read-only MCP server on stdio (claude mcp add agenthub -- agenthub mcp)",
|
||||
],
|
||||
["statusline", "", "Render the Claude Code statusline from the JSON on stdin"],
|
||||
[
|
||||
"statusline install|uninstall",
|
||||
"[--force]",
|
||||
"Add/remove AgentHub's statusLine in ~/.claude/settings.json (--force: replace or remove another one)",
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
"Administration",
|
||||
[
|
||||
@@ -3664,8 +3671,43 @@ const SUBCOMMANDS = {
|
||||
pricing: ["set", "delete", "reset"],
|
||||
webhooks: ["test"],
|
||||
import: ["rescan", "path"],
|
||||
statusline: ["install", "uninstall"],
|
||||
};
|
||||
|
||||
/** `agenthub statusline install|uninstall [--force]` — the user's settings.json. */
|
||||
function cmdStatusline(sub, flags) {
|
||||
const { install, uninstall } = require(path.join(REPO_ROOT, "statusline", "install.js"));
|
||||
const { getSettingsPath } = require(path.join(REPO_ROOT, "server", "lib", "claude-home.js"));
|
||||
const file = getSettingsPath();
|
||||
const force = Boolean(flags.force);
|
||||
const r = sub === "install" ? install(file, { force }) : uninstall(file, { force });
|
||||
const prev = r.previous ? JSON.stringify(r.previous) : "";
|
||||
switch (r.status) {
|
||||
case "installed":
|
||||
console.log(`${c.green("✔")} statusLine set in ${file}: ${r.command}`);
|
||||
if (prev) console.log(c.dim(` replaced ${prev} (backup beside the file)`));
|
||||
console.log(c.dim(" Claude Code picks it up on the next prompt."));
|
||||
return;
|
||||
case "unchanged":
|
||||
console.log(`statusLine already set in ${file}: ${r.command}`);
|
||||
return;
|
||||
case "removed":
|
||||
console.log(`${c.green("✔")} statusLine removed from ${file} (backup beside the file)`);
|
||||
return;
|
||||
case "absent":
|
||||
console.log(`No statusLine in ${file}.`);
|
||||
return;
|
||||
case "foreign":
|
||||
console.error(c.red(`✖ ${file} has a statusLine that is not AgentHub's: ${prev}`));
|
||||
console.error(
|
||||
c.dim(
|
||||
` Re-run with --force to ${sub === "install" ? "replace" : "remove"} it (a backup is kept).`
|
||||
)
|
||||
);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
/** Run one parsed command. Returns the handler's promise; may throw
|
||||
* ServerDownError (for the caller's offline routing) or call process.exit
|
||||
* for usage/validation failures. `cmd === "repl"` is handled by the entry
|
||||
@@ -3800,6 +3842,16 @@ async function runCommand(argv) {
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
case "mcp":
|
||||
return require(path.join(REPO_ROOT, "mcp", "server.js")).main();
|
||||
case "statusline":
|
||||
if (rest[0] === "install" || rest[0] === "uninstall") return cmdStatusline(rest[0], flags);
|
||||
if (rest[0]) {
|
||||
console.error("usage: agenthub statusline [install|uninstall [--force]]");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
return require(path.join(REPO_ROOT, "statusline", "statusline.js")).main();
|
||||
case "open":
|
||||
return cmdOpen();
|
||||
case "version":
|
||||
|
||||
Reference in New Issue
Block a user