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
+41 -8
View File
@@ -5,7 +5,10 @@
* animation state (idle, run right, run left, waving, jumping, failed,
* waiting, running, review) — played one row at a time by Sagi's mood; an
* uploaded image is shown still and bobs with the same `data-mood` CSS as
* Sagi. Reduced motion holds the row's first frame. A sprite that fails to
* Sagi. An `action` (dragged left/right, hovered) plays the run-left/run-right
* or waving row over the mood's, like Codex's own pet. Each row steps at its
* own pace and holds its last frame longer, so a loop breathes instead of
* spinning. Reduced motion holds the row's first frame. A sprite that fails to
* load (deleted from another browser) calls `onBroken`, so the mascot falls
* back to Sagi instead of an empty box.
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
@@ -30,11 +33,28 @@ const ROW: Record<Mood, number> = {
worried: 5, // failed
disconnected: 5,
};
/** What the user is doing to the pet, which outranks its mood. */
export type PetAction = "run-left" | "run-right" | "wave";
const ACTION_ROW: Record<PetAction, number> = { "run-right": 1, "run-left": 2, wave: 3 };
/** Per row: ms per frame, and ms the last frame is held before the loop restarts. */
const PACE: [number, number][] = [
[220, 1400], // idle: a breath, then a pause
[120, 120], // run right — keeps up with the drag
[120, 120], // run left
[180, 400], // waving
[160, 700], // jumping
[200, 1200], // failed
[220, 900], // waiting
[170, 400], // running (working)
[220, 900], // review
];
const SLEEP_PACE: [number, number] = [450, 2000];
export function PetSprite({
id,
kind,
mood,
action,
size,
reducedMotion,
onBroken,
@@ -42,23 +62,36 @@ export function PetSprite({
id: string;
kind: "codex" | "image";
mood: Mood;
action?: PetAction | null;
size: number;
reducedMotion: boolean;
onBroken: () => void;
}) {
const url = api.pets.spriteUrl(id);
const row = ROW[mood] ?? 0;
const row = action ? ACTION_ROW[action] : (ROW[mood] ?? 0);
const sleeping = !action && mood === "sleeping";
const [frame, setFrame] = useState(0);
useEffect(() => {
setFrame(0);
if (kind !== "codex" || reducedMotion) return;
const timer = window.setInterval(
() => setFrame((f) => (f + 1) % (FRAMES[row] ?? 1)),
mood === "sleeping" ? 400 : 125
);
return () => window.clearInterval(timer);
}, [kind, row, mood, reducedMotion]);
const count = FRAMES[row] ?? 1;
const [step, hold] = sleeping ? SLEEP_PACE : (PACE[row] ?? [200, 200]);
let f = 0;
let timer = 0;
const tick = () => {
timer = window.setTimeout(
() => {
f = (f + 1) % count;
setFrame(f);
tick();
},
f === count - 1 ? hold : step
);
};
tick();
return () => window.clearTimeout(timer);
}, [kind, row, sleeping, reducedMotion]);
// A background image reports no error of its own: probe it once.
useEffect(() => {