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:
2026-10-06 17:12:28 +07:00
parent 837da6e5de
commit 4aae61297f
146 changed files with 13565 additions and 537 deletions
+31 -11
View File
@@ -23,21 +23,40 @@ function durationSec(s) {
// ═══════════════════════════════════════════════════
/**
* Build a SQL WHERE clause for session status filtering.
* Returns { clause, params } where clause is either empty or " AND s.status = ?".
* Use `sessionAlias` to match the table alias used in your query (default "s").
* Build a SQL WHERE clause for the session filters `{status, tool, range}`
* (`range` = `parseRange`'s result: sessions started in it).
* Returns { clause, params } where clause is empty or " AND s.status = ? …".
* Use `alias` to match the sessions table alias used in your query (default "s").
*/
function statusClause(statusFilter, alias = "s") {
if (!statusFilter || statusFilter === "all") return { clause: "", params: [] };
return { clause: ` AND ${alias}.status = ?`, params: [statusFilter] };
function statusClause(filter, alias = "s") {
const parts = [];
const params = [];
if (filter?.status && filter.status !== "all") {
parts.push(`${alias}.status = ?`);
params.push(filter.status);
}
if (filter?.tool) {
parts.push(`${alias}.tool = ?`);
params.push(filter.tool);
}
if (filter?.range?.start) {
parts.push(`${alias}.started_at >= ?`);
params.push(filter.range.start);
}
if (filter?.range?.end) {
parts.push(`${alias}.started_at < ?`);
params.push(filter.range.end);
}
return { clause: parts.map((p) => ` AND ${p}`).join(""), params };
}
/** Same but for agents table joins where we need to filter via session_id */
function sessionIdFilter(statusFilter) {
if (!statusFilter || statusFilter === "all") return { clause: "", params: [] };
function sessionIdFilter(filter) {
const ss = statusClause(filter, "sessions");
if (!ss.clause) return { clause: "", params: [] };
return {
clause: " AND session_id IN (SELECT id FROM sessions WHERE status = ?)",
params: [statusFilter],
clause: ` AND session_id IN (SELECT id FROM sessions WHERE 1=1${ss.clause})`,
params: ss.params,
};
}
@@ -691,7 +710,8 @@ const WORKFLOW_SECTIONS = [
* The GET /api/workflows bundle, or only the named sections of it.
*
* @param {object} conn SQLite connection (compat-sqlite Database)
* @param {string|null} statusFilter "active"/"completed"/…, or null/"all"
* @param {{status?: string|null, tool?: string|null, range?: object|null}|null} statusFilter
* the session filters (see `statusClause`); null = every session
* @param {string[]|null} sections subset of WORKFLOW_SECTIONS; null = all
* @returns {object}
*/