feat: Claude Code Monitor — lanes, pipelines and a merged workspace

Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
This commit is contained in:
2026-07-29 17:07:45 +07:00
commit 57dc91585d
783 changed files with 221743 additions and 0 deletions
+469
View File
@@ -0,0 +1,469 @@
/**
* @file Enriched OpenAPI OVERRIDE operations for the core read/ingest endpoints
* that already exist in `server/openapi.js`:
*
* GET /api/events
* GET /api/events/facets
* GET /api/stats
* GET /api/analytics
* POST /api/hooks/event
*
* These operations are intentionally CONTRACT-IDENTICAL to the base spec. Every
* `operationId`, `tags` value, parameter name/`in`/schema, request-body `$ref`,
* and response `$ref` is copied verbatim from `server/openapi.js`. The ONLY
* additions here are richer prose `description`s, realistic per-parameter
* `example`s, and realistic media-type `example`s on request/response bodies —
* none of which change the wire contract.
*
* This module exports the override-merge surface expected by the spec builder:
* - `tags`: [] (no new tags — reuse the base Events/Stats/Analytics/Hooks tags)
* - `schemas`: {} (no new schemas — reuse base `$ref`s only)
* - `paths`: the enriched override operations keyed by path
*
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
"use strict";
// No new tags. The base operations already belong to the Events / Stats /
// Analytics / Hooks tags; overriding those tag arrays here would risk drift.
const tags = [];
// No new schemas. Every response and request body below reuses an existing
// `#/components/schemas/...` `$ref` defined in `server/openapi.js`.
const schemas = {};
const paths = {
"/api/events": {
get: {
tags: ["Events"],
summary: "List events with multi-dimensional filtering",
operationId: "listEvents",
description:
"Returns a paginated, reverse-chronological slice of the `events` table " +
"(ordered by `created_at DESC, id DESC`) together with the total row count " +
"matching the active filters, so the UI can drive a paginator without a " +
"second request.\n\n" +
"All four entity filters — `event_type`, `tool_name`, `agent_id`, and " +
"`session_id` — accept a **comma-separated list (CSV)** of values and match " +
"with `IN (...)` semantics: passing `event_type=Stop,PreToolUse` returns rows " +
"whose `event_type` is either `Stop` OR `PreToolUse`. Values are trimmed and " +
"blank entries are dropped. Filters are combined with one another using AND.\n\n" +
"`q` performs a case-insensitive substring (`LIKE %q%`) search across the " +
"`summary`, `tool_name`, and the JSON-encoded `data` columns. `from`/`to` are " +
"inclusive ISO-8601 datetime bounds on `created_at`; unparseable values are " +
"ignored rather than rejected. `limit` is clamped to 1500 (default 50) and " +
"`offset` is clamped to >= 0 (default 0).\n\n" +
"Note: each returned event's `data` field is a **JSON-encoded string**, not a " +
"nested object — callers must `JSON.parse` it to inspect the payload.",
parameters: [
{
in: "query",
name: "event_type",
description:
"Comma-separated (CSV) list of `event_type` values; matched with IN semantics " +
"(OR within the list). Common values: PreToolUse, PostToolUse, Stop, " +
"SubagentStop, Notification, SessionStart, SessionEnd.",
schema: { type: "string" },
example: "Stop,PreToolUse",
},
{
in: "query",
name: "tool_name",
description:
"Comma-separated (CSV) list of `tool_name` values; matched with IN semantics " +
"(OR within the list). Common values: Bash, Edit, Read, Write, Grep, Glob, Task.",
schema: { type: "string" },
example: "Bash,Edit",
},
{
in: "query",
name: "agent_id",
description:
"Comma-separated (CSV) list of `agent_id` values; matched with IN semantics. " +
"The main agent of a session uses the id `<session_id>-main`.",
schema: { type: "string" },
example: "8f3c2a10-1b2c-4d5e-9f80-112233445566-main",
},
{
in: "query",
name: "session_id",
description:
"Comma-separated (CSV) list of `session_id` values; matched with IN semantics " +
"(OR within the list).",
schema: { type: "string" },
example: "8f3c2a10-1b2c-4d5e-9f80-112233445566,2a7d9e44-3c1f-4a6b-bc20-aabbccddeeff",
},
{
in: "query",
name: "q",
description:
"Case-insensitive substring search (`LIKE %q%`) applied across the `summary`, " +
"`tool_name`, and JSON-encoded `data` columns.",
schema: { type: "string" },
example: "curl",
},
{
in: "query",
name: "from",
description:
"ISO-8601 datetime lower bound (inclusive) on `created_at`. Unparseable values " +
"are ignored.",
schema: { type: "string", format: "date-time" },
example: "2026-06-25T00:00:00.000Z",
},
{
in: "query",
name: "to",
description:
"ISO-8601 datetime upper bound (inclusive) on `created_at`. Unparseable values " +
"are ignored.",
schema: { type: "string", format: "date-time" },
example: "2026-06-26T00:00:00.000Z",
},
{
$ref: "#/components/parameters/SourcesQuery",
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
},
{
in: "query",
name: "limit",
description: "Max rows to return; clamped to 1500 (default 50).",
schema: { type: "integer", minimum: 1, maximum: 500, default: 50 },
example: 50,
},
{ $ref: "#/components/parameters/OffsetQuery" },
],
responses: {
200: {
description: "Event list with total count for pagination",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/EventsListResponse" },
example: {
events: [
{
id: 48213,
session_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566",
agent_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566-main",
event_type: "PreToolUse",
tool_name: "Bash",
summary: "Bash: curl -s https://api.example.com/health",
data: '{"session_id":"8f3c2a10-1b2c-4d5e-9f80-112233445566","hook_event_name":"PreToolUse","tool_name":"Bash","tool_input":{"command":"curl -s https://api.example.com/health","description":"Check upstream health"},"cwd":"/Users/dev/project"}',
created_at: "2026-06-25T18:42:07.512Z",
},
{
id: 48212,
session_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566",
agent_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566-main",
event_type: "Stop",
tool_name: null,
summary: "Session finished responding",
data: '{"session_id":"8f3c2a10-1b2c-4d5e-9f80-112233445566","hook_event_name":"Stop"}',
created_at: "2026-06-25T18:41:55.004Z",
},
],
limit: 50,
offset: 0,
total: 1342,
},
},
},
},
},
},
},
"/api/events/facets": {
get: {
tags: ["Events"],
summary: "Distinct event_type and tool_name values available in the DB",
operationId: "listEventFacets",
description:
"Returns the distinct, non-null `event_type` and `tool_name` values currently " +
"present in the `events` table, each sorted alphabetically. The UI uses this to " +
"populate the filter dropdowns on the Events screen without hardcoding the set of " +
"tools or hook types — so the lists automatically reflect whatever has actually " +
"been ingested. Both arrays are independent and may be empty when the table holds " +
"no matching rows.",
responses: {
200: {
description: "Facet values for populating filter dropdowns",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/EventsFacetsResponse" },
example: {
event_types: [
"Notification",
"PostToolUse",
"PreToolUse",
"SessionEnd",
"SessionStart",
"Stop",
"SubagentStop",
],
tool_names: ["Bash", "Edit", "Glob", "Grep", "Read", "Task", "Write"],
},
},
},
},
},
},
},
"/api/stats": {
get: {
tags: ["Stats"],
summary: "Get aggregate dashboard stats",
operationId: "getStats",
description:
"Returns the headline counters shown across the top of the dashboard: total and " +
"active session/agent counts, total event count, today's event count, and the " +
"current number of live WebSocket connections.\n\n" +
"The overview counters are spread at the top level of the response object. Two " +
"additional maps, `agents_by_status` and `sessions_by_status`, break the counts " +
"down by lifecycle status (e.g. agents: working/waiting/completed/error; sessions: " +
"active/completed/error/abandoned). **Statuses with a zero count are omitted from " +
"these maps**, so callers must not assume every status key is present.\n\n" +
"`events_today` is computed in the caller's local day. Pass `tz_offset` as the " +
"minutes value from JavaScript's `Date.prototype.getTimezoneOffset()` (for example " +
"`420` for US Pacific Daylight Time, `300` for US Eastern Daylight Time, `0` for " +
"UTC). When omitted or non-numeric, the server falls back to UTC (offset 0).",
parameters: [
{
in: "query",
name: "tz_offset",
description:
"Caller timezone offset in MINUTES, as returned by JS " +
"`Date.prototype.getTimezoneOffset()` (e.g. 420 for PDT, 300 for EDT, 0 for " +
"UTC). Used to bucket `events_today` into the caller's local day. Defaults to " +
"0 (UTC) when omitted or non-numeric.",
schema: { type: "integer" },
example: 420,
},
{
$ref: "#/components/parameters/SourcesQuery",
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
},
],
responses: {
200: {
description: "Statistics overview",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/StatsResponse" },
example: {
total_sessions: 184,
active_sessions: 3,
active_agents: 5,
total_agents: 372,
total_events: 28451,
events_today: 612,
ws_connections: 2,
agents_by_status: {
working: 4,
waiting: 1,
completed: 360,
error: 7,
},
sessions_by_status: {
active: 3,
completed: 175,
error: 6,
},
},
},
},
},
},
},
},
"/api/analytics": {
get: {
tags: ["Analytics"],
summary: "Get analytics aggregates",
operationId: "getAnalytics",
description:
"Returns the full analytics rollup powering the Analytics screen: aggregate token " +
"usage (`tokens`), total estimated spend across all sessions (`total_cost`, in USD, " +
"computed from the configured pricing rules), per-tool invocation counts " +
"(`tool_usage`), per-day event and session time series (`daily_events`, " +
"`daily_sessions`), the distribution of subagent types (`agent_types`), per-type " +
"event counts (`event_types`), the mean number of events per session " +
"(`avg_events_per_session`), the total subagent count (`total_subagents`), and a " +
"nested `overview` object mirroring the headline session/agent/event counters.\n\n" +
"As with `/api/stats`, the top-level `agents_by_status` and `sessions_by_status` " +
"maps **omit statuses whose count is zero**. The `agent_types[].subagent_type` field " +
"may be `null` for the main agent / untyped subagents.\n\n" +
"The daily time series are bucketed by the caller's local day. Pass `tz_offset` as " +
"the minutes value from JS `Date.prototype.getTimezoneOffset()` (e.g. `420` for " +
"PDT). When omitted or non-numeric, the server buckets in UTC.",
parameters: [
{
in: "query",
name: "tz_offset",
description:
"Caller timezone offset in MINUTES, as returned by JS " +
"`Date.prototype.getTimezoneOffset()` (e.g. 420 for PDT, 300 for EDT, 0 for " +
"UTC). Used to bucket the `daily_events` / `daily_sessions` time series into " +
"the caller's local day. Defaults to UTC when omitted or non-numeric.",
schema: { type: "integer" },
example: 420,
},
{
$ref: "#/components/parameters/SourcesQuery",
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
},
],
responses: {
200: {
description: "Analytics response",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/AnalyticsResponse" },
example: {
tokens: {
total_input: 4821002,
total_output: 1933517,
total_cache_read: 19288440,
total_cache_write: 2044120,
},
total_cost: 42.7183,
tool_usage: [
{ tool_name: "Bash", count: 5821 },
{ tool_name: "Read", count: 4310 },
{ tool_name: "Edit", count: 2980 },
{ tool_name: "Grep", count: 1744 },
],
daily_events: [
{ date: "2026-06-23", count: 488 },
{ date: "2026-06-24", count: 921 },
{ date: "2026-06-25", count: 612 },
],
daily_sessions: [
{ date: "2026-06-23", count: 4 },
{ date: "2026-06-24", count: 9 },
{ date: "2026-06-25", count: 6 },
],
agent_types: [
{ subagent_type: null, count: 184 },
{ subagent_type: "general-purpose", count: 96 },
{ subagent_type: "Explore", count: 71 },
{ subagent_type: "code-reviewer", count: 21 },
],
event_types: [
{ event_type: "PreToolUse", count: 14210 },
{ event_type: "PostToolUse", count: 13988 },
{ event_type: "Stop", count: 168 },
{ event_type: "SubagentStop", count: 85 },
],
avg_events_per_session: 154.6,
total_subagents: 188,
overview: {
total_sessions: 184,
active_sessions: 3,
active_agents: 5,
total_agents: 372,
total_events: 28451,
},
agents_by_status: {
working: 4,
waiting: 1,
completed: 360,
error: 7,
},
sessions_by_status: {
active: 3,
completed: 175,
error: 6,
},
},
},
},
},
},
},
},
"/api/hooks/event": {
post: {
tags: ["Hooks"],
summary: "Ingest Claude Code hook event",
operationId: "ingestHookEvent",
description:
"Primary ingestion endpoint for Claude Code lifecycle hooks. The hook handler posts " +
"an envelope of the form `{ hook_type, data }`, where `hook_type` is the Claude " +
"Code hook name (PreToolUse, PostToolUse, Stop, SubagentStop, Notification, " +
"SessionStart, SessionEnd) and `data` carries the raw hook payload — at minimum a " +
"`session_id`. The server upserts the session and its main agent on first sight, " +
"applies the appropriate lifecycle state transition, extracts token usage and " +
"compaction signals from the transcript when present, persists an `events` row " +
"(storing `data` as a JSON-encoded string), and broadcasts a `new_event` message " +
"over the WebSocket.\n\n" +
"On success the response is `{ ok: true, event: { ... } }`, where `event` echoes " +
"the normalized row that was just inserted (`session_id`, `agent_id`, `event_type`, " +
"`tool_name`, `summary`, `created_at`). Ingestion is designed to be fail-safe and " +
"non-blocking for the hook caller.\n\n" +
"Validation failures return HTTP 400 with an `ErrorResponse` body " +
"(`{ error: { code, message } }`): `INVALID_INPUT` when `hook_type` or `data` is " +
"missing, and `MISSING_SESSION` when `data.session_id` is absent.",
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/HookEventRequest" },
example: {
hook_type: "PreToolUse",
data: {
session_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566",
hook_event_name: "PreToolUse",
tool_name: "Bash",
tool_input: {
command: "curl -s https://api.example.com/health",
description: "Check upstream health",
},
cwd: "/Users/dev/project",
transcript_path:
"/Users/dev/.claude/projects/-Users-dev-project/8f3c2a10-1b2c-4d5e-9f80-112233445566.jsonl",
},
},
},
},
},
responses: {
200: {
description: "Event processed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/HookEventResponse" },
example: {
ok: true,
event: {
session_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566",
agent_id: "8f3c2a10-1b2c-4d5e-9f80-112233445566-main",
event_type: "PreToolUse",
tool_name: "Bash",
summary: "Bash: curl -s https://api.example.com/health",
created_at: "2026-06-25T18:42:07.512Z",
},
},
},
},
},
400: {
description: "Invalid hook payload",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "MISSING_SESSION",
message: "session_id is required in data",
},
},
},
},
},
},
},
},
};
module.exports = { tags, schemas, paths };