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:
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* @file Express router for settings-related endpoints, providing system info, database statistics, hook status, and operations to clear data, re-import sessions, reinstall hooks, reset pricing, export data, and perform cleanup of stale sessions. This allows the frontend to manage and maintain the agent monitoring system effectively.
|
||||
* @file Express router for settings-related endpoints, providing system info, database statistics, hook status, and operations to clear data, re-import sessions, reinstall hooks, reset pricing, export data, and perform cleanup of stale sessions, plus the privacy controls (secret redaction, ignored folders) and the push-ingest token. This allows the frontend to manage and maintain the agent monitoring system effectively.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
@@ -24,7 +24,9 @@ const APP_VERSION = (() => {
|
||||
|
||||
const { getSettingsPath, getClaudeHome, setClaudeHome } = require("../lib/claude-home");
|
||||
const { getCodexHome, setCodexHome } = require("../lib/codex-home");
|
||||
const { envOwned } = require("../lib/settings-env");
|
||||
const { envOwned, applySetting } = require("../lib/settings-env");
|
||||
const privacy = require("../lib/privacy");
|
||||
const pushToken = require("../lib/push-token");
|
||||
const pricingSync = require("../lib/pricing-sync");
|
||||
const { EXPORT_FORMAT, EXPORT_VERSION, importExportBundle } = require("../lib/data-transfer");
|
||||
|
||||
@@ -171,10 +173,11 @@ function clearAll() {
|
||||
db.prepare("DELETE FROM events").run();
|
||||
db.prepare("DELETE FROM agents").run();
|
||||
db.prepare("DELETE FROM sessions").run();
|
||||
// What the Codex/Antigravity polls have read: kept, it would stop them ever
|
||||
// importing those sessions again (and bring one back half, on its next write).
|
||||
// What the Codex/Antigravity/Cursor polls have read: kept, it would stop them
|
||||
// ever importing those sessions again (and bring one back half, on its next write).
|
||||
db.prepare("DELETE FROM codex_rollouts").run();
|
||||
db.prepare("DELETE FROM antigravity_conversations").run();
|
||||
db.prepare("DELETE FROM cursor_transcripts").run();
|
||||
// Fired alerts reference the cleared sessions — wipe the feed too. Alert
|
||||
// *rules* survive: they're user configuration, like model_pricing.
|
||||
db.prepare("DELETE FROM alert_events").run();
|
||||
@@ -184,6 +187,11 @@ function clearAll() {
|
||||
// Workflow runs belong to the sessions; with foreign keys off they would
|
||||
// not cascade and stay as orphans.
|
||||
db.prepare("DELETE FROM workflows").run();
|
||||
// session_snapshots survive: a snapshot is a frozen copy the user chose to
|
||||
// keep, like alert rules — deleted only from Settings → Snapshots.
|
||||
// report_definitions and report_runs survive too: a definition is user
|
||||
// configuration and a run's files are reports already delivered — removed
|
||||
// only from Settings → Reports.
|
||||
}
|
||||
|
||||
// POST /api/settings/reimport — re-import legacy sessions from ~/.claude/
|
||||
@@ -438,6 +446,68 @@ homeRoutes("/claude-home", "CLAUDE_HOME", "claude_home", getClaudeHome, setClaud
|
||||
// into the current home at its spawn, so a move needs no repair here.
|
||||
homeRoutes("/codex-home", "CODEX_HOME", "codex_home", getCodexHome, setCodexHome);
|
||||
|
||||
// GET/PUT /api/settings/privacy — opt-in secret redaction of stored payloads
|
||||
// and the folders whose hooks are dropped (server/lib/privacy.js). Both persist
|
||||
// to the settings file; one the environment set is `env_owned` and refused.
|
||||
function privacyState() {
|
||||
return {
|
||||
redact_secrets: privacy.redactionEnabled(),
|
||||
ignore_cwd: privacy.ignoredCwds(),
|
||||
env_owned: {
|
||||
redact_secrets: envOwned.has(privacy.REDACT_KEY),
|
||||
ignore_cwd: envOwned.has(privacy.IGNORE_KEY),
|
||||
},
|
||||
};
|
||||
}
|
||||
router.get("/privacy", (_req, res) => res.json(privacyState()));
|
||||
router.put("/privacy", sameOriginGuard, (req, res) => {
|
||||
const body = req.body || {};
|
||||
const fail = (status, code, message) => res.status(status).json({ error: { code, message } });
|
||||
const writes = [];
|
||||
if (body.redact_secrets !== undefined) {
|
||||
if (typeof body.redact_secrets !== "boolean")
|
||||
return fail(400, "INVALID_INPUT", "redact_secrets must be a boolean");
|
||||
writes.push([privacy.REDACT_KEY, body.redact_secrets ? "1" : "0"]);
|
||||
}
|
||||
if (body.ignore_cwd !== undefined) {
|
||||
let list;
|
||||
try {
|
||||
list = privacy.validateIgnoreList(body.ignore_cwd);
|
||||
} catch (err) {
|
||||
return fail(400, err.code || "EBADPATH", err.message);
|
||||
}
|
||||
writes.push([privacy.IGNORE_KEY, list.join(path.delimiter)]);
|
||||
}
|
||||
const owned = writes.find(([k]) => envOwned.has(k));
|
||||
if (owned) return fail(409, "EENVOWNED", `${owned[0]} is set by the environment`);
|
||||
try {
|
||||
for (const [k, v] of writes) applySetting(k, v);
|
||||
} catch (err) {
|
||||
return fail(500, "ESAVE", err.message);
|
||||
}
|
||||
res.json({ ok: true, ...privacyState() });
|
||||
});
|
||||
|
||||
// GET/POST/DELETE /api/settings/push-token — the token for POST
|
||||
// /api/hooks/ingest-batch (server/lib/push-token.js). GET never returns it;
|
||||
// POST makes a new one (the old one stops working) and returns it this once.
|
||||
router.get("/push-token", (_req, res) => res.json(pushToken.pushTokenStatus()));
|
||||
function pushTokenWrite(fn) {
|
||||
return (_req, res) => {
|
||||
try {
|
||||
const token = fn();
|
||||
res.json({ ok: true, ...(token && { token }), ...pushToken.pushTokenStatus() });
|
||||
} catch (err) {
|
||||
const owned = err.code === "EENVOWNED";
|
||||
res
|
||||
.status(owned ? 409 : 500)
|
||||
.json({ error: { code: owned ? err.code : "ESAVE", message: err.message } });
|
||||
}
|
||||
};
|
||||
}
|
||||
router.post("/push-token", sameOriginGuard, pushTokenWrite(pushToken.rotatePushToken));
|
||||
router.delete("/push-token", sameOriginGuard, pushTokenWrite(pushToken.revokePushToken));
|
||||
|
||||
// POST /api/settings/cleanup — abandon stale sessions, purge old data.
|
||||
// `purge_days` is a whole number ≥ 1 (0.1 days would purge almost everything);
|
||||
// `dry_run: true` answers the same counts and changes nothing.
|
||||
|
||||
Reference in New Issue
Block a user