Skip to content

v0.20 PR 3: Cost dashboard tab#73

Merged
mcheemaa merged 5 commits intomainfrom
v0.20-pr-03-cost-tab
Apr 16, 2026
Merged

v0.20 PR 3: Cost dashboard tab#73
mcheemaa merged 5 commits intomainfrom
v0.20-pr-03-cost-tab

Conversation

@mcheemaa
Copy link
Copy Markdown
Member

Summary

Ships the Cost tab: a single combined GET /ui/api/cost endpoint plus a new cost.js module 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.

  • Backend: one read-only endpoint with Zod-validated days param (1..365 or all). Four SQLite queries via a new src/agent/cost-queries.ts helper so the MCP cost resource, the metrics_read tool, and the dashboard all share the same aggregation SQL. Every SUM(cost_usd) is wrapped in COALESCE(..., 0) so empty result sets return 0 rather than NULL. Day/week deltas return 0 (not NaN) when the prior period was empty.
  • Chart primitive: renderStackedBarChart in cost.js is 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.
  • Cross-tab nav: clicking a top-session row hashes to #/sessions/<url-encoded key>. The Sessions tab's existing mount(container, arg, ctx) handler then opens its drawer. Verified end to end.
  • Interactions: Range picker (7/30/90/All) refetches; Day | Week toggle buckets client-side (single fetch); hover on a chart bar shows a tooltip with day + per-model breakdown + total; CSV export covers the daily timeseries.
  • States: skeleton metric strip + skeleton chart on first load, empty state messaging per section if no rows, error state with retry.

cost-queries refactor

Done. src/agent/cost-queries.ts exports getCostForPeriod(db, dateFilter) which both src/mcp/resources.ts (per-period resource) and src/mcp/tools-universal.ts (metrics_read tool) 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.js finished at 642 LOC against a 600 ceiling (7% overshoot), after extracting a renderTable helper to DRY the three tables and a chartFrame helper 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):

  • Headline math: today / yesterday / this_week / this_month / all_time match hand-calculated totals
  • day_delta_pct is 0 (not NaN) when yesterday is 0; correct when populated
  • week_delta_pct correct against prior week
  • Empty DB: every numeric is 0, no null or NaN anywhere in response
  • Daily timeseries groups by date with per-model breakdown
  • By-model pct sums to 1.0 and orders by cost DESC
  • By-channel avg_per_session uses COUNT(DISTINCT session_key)
  • Top sessions limits to 10, ordered by total_cost_usd DESC
  • Range param filters daily/by_model/by_channel
  • Default days is 30 when param omitted
  • Invalid days returns 422; SQL injection on days rejected at parse
  • POST returns 405; 401 without cookie

Full suite: 1,644 pass / 0 fail. Biome lint + tsc --noEmit both clean.

Visual verification in browser (light, dark, and 380px mobile):

  • http://.../ui/dashboard/#/cost renders header, filter bar, 5-card strip, chart, breakdowns, top sessions
  • Range picker re-fetches
  • Day/Week toggle re-buckets client-side (chart title becomes "Weekly spend")
  • Hover over a bar shows day + per-model breakdown + total tooltip, clamped to chart edges
  • Click a top-session row navigates to #/sessions/<key> and the Sessions drawer opens with overview + cost events
  • Dark theme picks up palette via CSS vars
  • Mobile: breakdown grid collapses to one column, metric cards wrap, top-sessions table stays legible
  • No cost.js-originated console errors

Cardinal Rule

Read-only aggregation over existing data. Cardinal Rule exception applies.

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().
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/agent/cost-queries.ts Outdated
Comment on lines +178 to +181
`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 ?`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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.
@mcheemaa mcheemaa merged commit f8a19c6 into main Apr 16, 2026
1 check passed
mcheemaa added a commit that referenced this pull request Apr 17, 2026
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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant