57dc91585d
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.
470 lines
19 KiB
JavaScript
470 lines
19 KiB
JavaScript
/**
|
||
* @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 1–500 (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 1–500 (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 };
|