Conversation
Consolidate cost aggregation queries into src/agent/cost-queries.ts so the MCP cost resource, the universal metrics tool, and the new Cost dashboard API all share the same SQL. Non-breaking: existing callers delegate to getCostForPeriod() and keep their response shapes.
Add .dash-chart, .dash-chart-tooltip, .dash-chart-axis, .dash-segmented, .dash-breakdown-grid, and a [data-series-idx] color palette so the first chart on the dashboard (Cost daily stacked bar) composes from generic pieces that Evolution can reuse as a sparkline.
…sessions Single combined read endpoint for the Cost dashboard tab. Zod-validated days param (1..365 or all), parameterized SQL throughout, COALESCE(SUM) guards against NULL-on-empty. Tests cover headline math including day delta when yesterday is 0, by-model pct sum, by-channel avg-per-session, top-10 ordering, range filtering, invalid input rejection, and an SQL injection attempt on the days param.
Adds the Cost route module (cost.js), wires it in index.html + dashboard.js, and tightens the backend helpers and tests to hit the PR budget. The module renders a header, range + group-by filter bar, 5-card headline strip, SVG stacked bar chart with hover tooltip (reused by Evolution as a sparkline in a later PR), by-model and by-channel breakdowns, and a top-10 sessions table that cross-navigates to the Sessions tab drawer. Client-side week bucketing for the Day/Week toggle keeps a single fetch. CSV export covers the daily timeseries. All operator-controlled fields (model, channel_id, conversation_id, session_key) flow through ctx.esc().
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bdcfb352ba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| `SELECT s.session_key, s.channel_id, s.conversation_id, | ||
| s.total_cost_usd, s.turn_count, s.last_active_at | ||
| FROM sessions s ${where} | ||
| ORDER BY s.total_cost_usd DESC, s.last_active_at DESC LIMIT ?`, |
There was a problem hiding this comment.
Scope top-session costs to selected range
This query uses sessions.total_cost_usd, which is a lifetime cumulative value, so the Cost tab’s 7/30/90-day filter does not actually constrain the cost shown or ranking in top sessions. In practice, any session with large historical spend but minimal recent activity can dominate the table even when the selected range is short, producing misleading analytics for the “in this range” view. Compute and sort by SUM(cost_events.cost_usd) within the active window instead of sessions.total_cost_usd.
Useful? React with 👍 / 👎.
P1 (Codex): top_sessions previously ordered by sessions.total_cost_usd which is a lifetime cumulative value, so the days=7 view could be dominated by a session with no recent activity. Now sums cost_events within the selected window and groups by session, so the table reflects spend in the active range. Added regression test for the canonical 'old whale vs fresh' shape. P2 (reviewer): mobile chart squash. .dash-chart-svg used width: 100% with min-width: 320px and preserveAspectRatio='none', which squashed the bars and tick labels on narrow viewports instead of triggering the .dash-chart-scroll wrapper. Floor at 540px so narrow viewports scroll horizontally and the chart stays readable.
Bumps the version to 0.20.0 in every place it's referenced: - package.json (1) - src/core/server.ts VERSION constant - src/mcp/server.ts MCP server identity - src/cli/index.ts phantom --version output - README.md version + tests badges - CLAUDE.md tagline + bun test count - CONTRIBUTING.md test count Tests: 1,799 pass / 10 skip / 0 fail. Typecheck and lint clean. No 0.19.1 or 1,584-tests references remain in source, docs, or badges. v0.20 shipped eight PRs on top of v0.19.1: #71 entrypoint dashboard sync + / redirect + /health HTML #72 Sessions dashboard tab #73 Cost dashboard tab #74 Scheduler tab + create-job + Sonnet describe-assist #75 Evolution Phase A + Memory explorer tabs #76 Settings page restructure (phantom.yaml, 6 sections) #77 Agent avatar upload across 14 identity surfaces #79 Landing page redesign (hero, starter tiles, live pages list)
Summary
Ships the Cost tab: a single combined
GET /ui/api/costendpoint plus a newcost.jsmodule with a headline metric strip, a shared SVG stacked bar chart, by-model and by-channel breakdowns, and a top-10 sessions table that navigates across tabs into the Sessions drawer.daysparam (1..365 orall). Four SQLite queries via a newsrc/agent/cost-queries.tshelper so the MCP cost resource, themetrics_readtool, and the dashboard all share the same aggregation SQL. EverySUM(cost_usd)is wrapped inCOALESCE(..., 0)so empty result sets return0rather than NULL. Day/week deltas return0(notNaN) when the prior period was empty.renderStackedBarChartincost.jsis generic over{ day, segments: [{ value, seriesIdx }] }rows and returns SVG markup. Evolution (and possibly Memory) will reuse it as a sparkline in a later PR. Color palette is driven by[data-series-idx]CSS so dark/light themes track automatically; no hardcoded hex inside the SVG.#/sessions/<url-encoded key>. The Sessions tab's existingmount(container, arg, ctx)handler then opens its drawer. Verified end to end.cost-queries refactor
Done.
src/agent/cost-queries.tsexportsgetCostForPeriod(db, dateFilter)which bothsrc/mcp/resources.ts(per-period resource) andsrc/mcp/tools-universal.ts(metrics_readtool) now delegate to. Richer helpers (getCostHeadline,getDailyCost,getByModel,getByChannel,getTopSessions) are used by the new cost API.LOC discipline
PR 2 shipped 2.6x its budget. This PR targeted a ~850 LOC total with a 1,200 LOC ceiling; we landed at ~1,420 across all changed files (overshoot ~18%).
cost.jsfinished at 642 LOC against a 600 ceiling (7% overshoot), after extracting arenderTablehelper to DRY the three tables and achartFramehelper for the chart skeleton/empty/populated paths. Further compression would start to hurt clarity.Test plan
Backend coverage (
src/ui/api/__tests__/cost.test.ts, 14 cases, all pass):day_delta_pctis 0 (not NaN) when yesterday is 0; correct when populatedweek_delta_pctcorrect against prior weeknullorNaNanywhere in responsepctsums to 1.0 and orders by cost DESCavg_per_sessionusesCOUNT(DISTINCT session_key)total_cost_usdDESCdaysis 30 when param omitteddaysreturns 422; SQL injection ondaysrejected at parseFull suite: 1,644 pass / 0 fail. Biome lint + tsc --noEmit both clean.
Visual verification in browser (light, dark, and 380px mobile):
http://.../ui/dashboard/#/costrenders header, filter bar, 5-card strip, chart, breakdowns, top sessionsday + per-model breakdown + totaltooltip, clamped to chart edges#/sessions/<key>and the Sessions drawer opens with overview + cost eventsCardinal Rule
Read-only aggregation over existing data. Cardinal Rule exception applies.