Skip to content

v0.20 PR 2: Sessions dashboard tab#72

Merged
mcheemaa merged 4 commits intomainfrom
v0.20-pr-02-sessions-tab
Apr 16, 2026
Merged

v0.20 PR 2: Sessions dashboard tab#72
mcheemaa merged 4 commits intomainfrom
v0.20-pr-02-sessions-tab

Conversation

@mcheemaa
Copy link
Copy Markdown
Member

Summary

Read-only Sessions dashboard tab: lists every conversation across every channel (Slack, chat, scheduler, CLI, MCP, webhook, trigger, etc.) with filters, summary strip, channel breakdown, and a detail drawer with cost events.

Also establishes the shared dashboard primitives the next four Wave 2 tabs (Cost, Scheduler, Memory, Evolution) reuse: .dash-table, .dash-drawer, .dash-metric-strip, .dash-metric-card, .dash-channel-bar, .dash-status-chip, .dash-filter-bar. All generic, no Sessions-specific coupling at the primitive layer.

Spec: local/2026-04-16-v0.20-next-level/research/02-wave-2-dashboard-tabs-extended.md

What ships

  • GET /ui/api/sessions?channel=&days=&status=&q= - list + summary, chat-enriched rows when channel_id = 'chat'
  • GET /ui/api/sessions/:session_key - session + cost_events (404 with { error } for missing keys)
  • Zod-validated filters; all SQL parameterized; DROP TABLE payload test proves q is literal
  • 19 backend tests, all existing 1,610 tests still green
  • public/dashboard/sessions.js (self-registering module) with skeleton-first render, debounced search, sortable table, deep-link drawer, CSV export
  • ARIA-correct drawer: role="dialog", aria-modal="true", aria-labelledby, focus trap, focus restore on close, Esc and backdrop close
  • Keyboard: / focuses search when not typing elsewhere, Esc closes the drawer
  • XSS-safe: every operator-controlled field (conversation_id, session_key, chat title, sdk_session_id) flows through ctx.esc() or textContent; no innerHTML concatenation with raw data
  • Dark theme verified, mobile responsive (drawer full-width below 720px, table sheds the Status column below 540px)

Test plan

  • bun run lint clean
  • bun run typecheck clean
  • bun test green (1,629 pass, 0 fail)
  • New sessions.test.ts (19 tests) covers all filter combinations, summary math, chat enrichment, SQL injection parameterization, 404, 405, 422
  • Visual verification against a seeded SQLite DB in a local Bun server: list, filters, sort, drawer open/close, deep-link, Esc, backdrop click, search debounce, CSV export, dark theme, mobile 380px, ARIA attributes, XSS payload rendered as literal text
  • Reviewer: spot-check the drawer animation on a real agent instance with live sessions data
  • Reviewer: confirm the shared primitives feel right for Cost to reuse (same metric strip, same channel bar as model bar)

Out of scope

  • No writes anywhere on this tab (read-only over existing data)
  • No cross-tab link from Cost top-sessions yet (lands with the Cost PR)
  • Pagination (the 500-row cap plus filters bounds the working set for v1)

…r, status chip)

Generic building blocks for the Wave 2 dashboard tabs. Sessions uses
them first; Cost, Scheduler, Memory, and Evolution reuse the same
selectors.

- .dash-table with sortable headers, hover rows, sheds cells below 540px
- .dash-drawer right-slide dialog, full-width below 720px, reduced-motion
  aware, ARIA-ready structure (role/aria-modal set at the consumer site)
- .dash-metric-strip + .dash-metric-card with serif values and skeleton
- .dash-channel-bar stacked segment bar with 8-color palette
- .dash-status-chip active/expired/error/paused/info variants
- .dash-filter-bar + .dash-filter-select + .dash-filter-search

All tokens from the existing vocabulary; no new colors or fonts.
Read-only endpoints backing the Sessions dashboard tab.

List (GET /ui/api/sessions?channel=&days=&status=&q=) returns every
session with chat enrichment (title, message_count, pinned,
deleted_at, forked_from) when channel_id = 'chat', plus a summary
block: total_sessions, total_cost_usd, avg_turns, active_count, and
by_channel counts with cost.

Detail (GET /ui/api/sessions/:session_key) returns the session plus
its cost_events ordered ASC by created_at, 404 with a stable
{ error } shape for missing keys.

Zod-validated query params: channel string (default 'all'),
days 1..365 or 'all' (default 7), status active/expired/all (default
all), q up to 100 chars. All SQL uses parameterized queries; a test
proves a DROP TABLE payload on q is treated as a literal.

19 tests cover filters, summary math, chat enrichment, detail 404,
SQL injection parameterization, 405 on POST, and 422 on invalid
enums.
Read-only view of every conversation across every channel, wired to
the new /ui/api/sessions endpoints.

UI:
- Filter bar: channel, window (1/7/30/90/all), status, debounced search
- Metric strip (total sessions, total cost, avg turns, active count)
- Stacked channel breakdown bar with legend
- Sortable table with channel glyph, conversation, turns, cost,
  relative last-active, status chip
- Row click opens an ARIA-correct detail drawer with session overview,
  chat enrichment, cost events table, and an Open in Slack / Open chat
  session footer
- Skeleton-first render so deep links do not flash an empty list
- Empty, error (with retry), and loading states
- Client-side CSV export
- Keyboard: / focuses search, Esc closes drawer
- Deep-link #/sessions/<urlencoded session_key> opens the drawer
  directly; back-nav collapses it

All operator-controlled data (conversation_id, session_key, chat
title, sdk_session_id) flows through ctx.esc() or textContent.
Verified visually that a session with <script>alert('x')</script> in
its conversation_id renders as text with zero script elements.

Wiring:
- dashboard.js: sessions moves from comingSoon to liveRoutes
- index.html: sidebar link promoted into the Workspace nav, route
  div added, sessions.js script registered
- CSS palette selector extended so .dash-channel-glyph-dot picks up
  the per-channel color alongside bar segments and legend swatches
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: a6ca174f52

ℹ️ 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 on lines +795 to +797
};
document.addEventListener("keydown", drawerKeyHandler, true);
}
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 Remove stale Escape handlers when drawer re-renders

Each renderDrawer() call rewires interactions and adds a new capturing keydown listener on document, but previous listeners are never removed first. Because the drawer re-renders at least once per open (skeleton → loaded/error), older handlers survive and removeDrawerDom() only removes the most recent one, leaving leaked listeners that keep intercepting Escape globally after the drawer closes.

Useful? React with 👍 / 👎.

Comment thread src/ui/api/sessions.ts Outdated
COUNT(*) AS total,
COALESCE(SUM(s.total_cost_usd), 0) AS cost,
COALESCE(AVG(s.turn_count), 0) AS avg_turns,
SUM(CASE WHEN s.status = 'active' THEN 1 ELSE 0 END) AS active
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Coalesce active_count in empty-result summaries

The totals query coalesces SUM(total_cost_usd) and AVG(turn_count) but not the active-session sum. In SQLite, SUM(...) over zero matching rows returns NULL, so summary.active_count is null whenever filters produce no rows, violating the numeric API contract and forcing every consumer to defensively patch this case.

Useful? React with 👍 / 👎.

Independent reviewer + Codex flagged:

P1: drawer keydown handler accumulated on document because
renderDrawer() fires twice per open (skeleton then content) and
re-installed the listener without removing the previous one. Now
detach the prior handler before installing a new one.

P1: opening the drawer did not lock body scroll, so the wheel
still moved the page behind the backdrop. Save the previous
overflow on first open and restore on close.

P2: SUM(CASE WHEN status = 'active' ...) returns NULL in SQLite
when no rows match the filter, breaking the numeric API contract.
Wrap in COALESCE(..., 0).

Cleanup: remove dead drawerTrapHandler and state.initialized refs.
@mcheemaa mcheemaa merged commit 478b98a 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