Files
Claude-Code-Monitor/server/openapi-extra/override-ops.js
T
nntrivi2001 57dc91585d 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.
2026-07-30 14:39:03 +07:00

1655 lines
62 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @file ENRICHED OVERRIDE OpenAPI fragments for endpoints that are ALREADY
* documented in the base spec (server/openapi.js). This fragment does not add
* any new paths — instead, each entry here re-declares an already-existing path
* with the *identical* contract (same operationId, tags, parameters,
* requestBody schemas, and response `$ref`/media-type schemas) but layers on
* richer `description` text and realistic `example` values for parameters,
* request bodies, and responses. Because `server/openapi-extra.js` merges
* fragment `paths` with `Object.assign` (last-writer-wins) and the merge in
* `createOpenApiSpec()` spreads extras over the base, these override entries
* supersede the terser base definitions for the same path key.
*
* Covered paths (Webhooks / Settings / Import / Updates / Workflows):
* POST/GET /api/webhooks
* GET /api/webhooks/providers
* PATCH/DEL /api/webhooks/{id}
* POST /api/webhooks/{id}/test
* GET /api/webhooks/{id}/deliveries
* GET /api/settings/info
* POST /api/settings/clear-data ⚠ DESTRUCTIVE
* POST /api/settings/reimport
* POST /api/settings/reinstall-hooks
* POST /api/settings/reset-pricing ⚠ DESTRUCTIVE
* GET /api/settings/export
* POST /api/settings/import
* POST /api/settings/cleanup ⚠ DESTRUCTIVE (with purge_days)
* GET /api/import/guide
* POST /api/import/rescan
* POST /api/import/scan-path
* POST /api/import/upload (multipart/form-data)
* GET /api/updates/status
* POST /api/updates/check
* GET /api/workflows
* GET /api/workflows/session/{id}
*
* STRICTLY ADDITIVE: `tags` and `schemas` are intentionally empty — this module
* introduces no new tags and no new component schemas. Every schema reference
* reuses a base `$ref` (e.g. ErrorResponse, MessageErrorResponse, WorkflowAggregateResponse,
* SettingsInfoResponse, CleanupRequest/CleanupResponse, ImportGuideResponse,
* ImportResultResponse, ExportResponse, …) or an inline schema that matches the
* base byte-for-byte. The Webhooks/Settings/Import/Updates/Workflows tags are
* already declared in the base literal. Error bodies preserve the base's split:
* Webhooks / Settings / Import / Updates use `ErrorResponse` ({ error: { code,
* message } }) where the base had it (and keep the base's content-less 4xx
* descriptions where it had none); Workflows use the SHORT `MessageErrorResponse`
* ({ error: { message } }).
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
*/
const tags = [];
const schemas = {};
// ── Reusable realistic examples ──────────────────────────────────────────────
const WEBHOOK_TARGET_EXAMPLE = {
id: "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
name: "Eng on-call (Slack)",
type: "slack",
enabled: true,
url_preview: "https://hooks.slack.com/…BXqZ",
has_secret: false,
headers: null,
config: null,
rule_ids: ["rule_inactivity_30m"],
created_at: "2026-06-25T14:03:11.482Z",
updated_at: "2026-06-25T14:03:11.482Z",
last_delivery: {
status: "success",
status_code: 200,
attempts: 1,
error: null,
created_at: "2026-06-25T18:41:55.117Z",
},
};
const GENERIC_WEBHOOK_TARGET_EXAMPLE = {
id: "1a2b3c4d-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
name: "Internal alert sink",
type: "generic",
enabled: true,
url_preview: "https://alerts.internal.example.com/…hook",
has_secret: true,
headers: { "X-Api-Key": "••••", Authorization: "••••" },
config: null,
rule_ids: null,
created_at: "2026-06-20T09:15:00.000Z",
updated_at: "2026-06-24T22:48:30.901Z",
last_delivery: {
status: "failed",
status_code: 503,
attempts: 3,
error: "Service Unavailable",
created_at: "2026-06-24T22:48:30.901Z",
},
};
const SETTINGS_INFO_EXAMPLE = {
db: {
path: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor/server/data/agent-monitor.db",
size: 48922624,
counts: {
sessions: 412,
agents: 1893,
events: 58117,
model_pricing: 11,
token_usage: 401,
},
pragmas: {
journal_mode: "wal",
synchronous: 1,
auto_vacuum: 0,
encoding: "UTF-8",
foreign_keys: 1,
busy_timeout: 5000,
},
load_stats: { m5: 23, m15: 88, h1: 511 },
},
hooks: {
installed: true,
path: "/Users/son/.claude/settings.json",
hooks: {
PreToolUse: true,
PostToolUse: true,
Stop: true,
SubagentStop: true,
Notification: true,
SessionStart: true,
SessionEnd: true,
},
},
server: {
uptime: 14523.91,
node_version: "v22.14.0",
platform: "darwin",
ws_connections: 2,
memory: {
rss: 142802944,
heapTotal: 71303168,
heapUsed: 58392104,
external: 3211884,
arrayBuffers: 1048576,
},
cpu_load: [2.13, 2.45, 2.31],
arch: "arm64",
total_mem: 17179869184,
free_mem: 2147483648,
cpus: 10,
},
transcript_cache: {
entries: 37,
paths: [
"/Users/son/.claude/projects/-Users-son-code-foo/abc123.jsonl",
"/Users/son/.claude/projects/-Users-son-code-bar/def456.jsonl",
],
},
};
const CLEAR_DATA_EXAMPLE = {
ok: true,
cleared: { sessions: 412, agents: 1893, events: 58117, model_pricing: 11, token_usage: 401 },
};
const REIMPORT_EXAMPLE = { ok: true, imported: 38, skipped: 374, errors: 0 };
const REINSTALL_HOOKS_EXAMPLE = {
ok: true,
hooks: {
installed: true,
path: "/Users/son/.claude/settings.json",
hooks: {
PreToolUse: true,
PostToolUse: true,
Stop: true,
SubagentStop: true,
Notification: true,
SessionStart: true,
SessionEnd: true,
},
},
};
const RESET_PRICING_EXAMPLE = {
ok: true,
pricing: [
{
model_pattern: "claude-opus-4*",
display_name: "Claude Opus 4",
input_per_mtok: 15,
output_per_mtok: 75,
cache_read_per_mtok: 1.5,
cache_write_per_mtok: 18.75,
cache_write_1h_per_mtok: 30,
fast_input_per_mtok: 0,
fast_output_per_mtok: 0,
updated_at: "2026-06-26T00:00:00.000Z",
},
{
model_pattern: "claude-sonnet-4*",
display_name: "Claude Sonnet 4",
input_per_mtok: 3,
output_per_mtok: 15,
cache_read_per_mtok: 0.3,
cache_write_per_mtok: 3.75,
cache_write_1h_per_mtok: 6,
fast_input_per_mtok: 0,
fast_output_per_mtok: 0,
updated_at: "2026-06-26T00:00:00.000Z",
},
],
};
const EXPORT_EXAMPLE = {
format: "ccam-export",
version: 1,
exported_at: "2026-06-26T01:12:44.913Z",
sessions: [
{
id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
name: "Refactor auth flow",
status: "completed",
cwd: "/Users/son/code/foo",
model: "claude-opus-4-8",
started_at: "2026-06-25T13:00:00.000Z",
ended_at: "2026-06-25T13:42:18.220Z",
metadata: null,
updated_at: "2026-06-25T13:42:18.220Z",
},
],
agents: [
{
id: "agent_main_5f3c0e2a",
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
name: "Main agent",
type: "main",
subagent_type: null,
status: "completed",
task: null,
current_tool: null,
started_at: "2026-06-25T13:00:00.000Z",
ended_at: "2026-06-25T13:42:18.220Z",
parent_agent_id: null,
metadata: null,
updated_at: "2026-06-25T13:42:18.220Z",
},
],
events: [
{
id: 91021,
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
agent_id: "agent_main_5f3c0e2a",
event_type: "PreToolUse",
tool_name: "Edit",
summary: "Edit server/auth.js",
data: null,
created_at: "2026-06-25T13:05:42.001Z",
},
],
token_usage: [
{
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
model: "claude-opus-4-8",
input_tokens: 18422,
output_tokens: 9120,
cache_read_tokens: 220184,
cache_write_tokens: 41002,
},
],
model_pricing: RESET_PRICING_EXAMPLE.pricing,
};
const CLEANUP_EXAMPLE = {
ok: true,
abandoned: 3,
purged_sessions: 57,
purged_events: 14820,
purged_agents: 241,
};
const IMPORT_GUIDE_EXAMPLE = {
platform: "darwin",
default_projects_dir: "/Users/son/.claude/projects",
default_projects_dir_display: "~/.claude/projects",
default_projects_dir_exists: true,
default_projects_dir_stats: { projects: 24, jsonl_files: 312 },
archive_command: "tar -czf claude-history.tar.gz -C ~/.claude projects",
supported_extensions: [".jsonl", ".meta.json", ".zip", ".tar", ".tar.gz", ".tgz", ".gz"],
max_upload_bytes: 1073741824,
max_upload_files: 2000,
steps: [
{
id: "locate",
title: "Locate your Claude Code history",
body: "Claude Code stores every session as a JSONL transcript under ~/.claude/projects. Each subdirectory is named after the working directory where the session started (with slashes replaced by dashes).",
},
{
id: "archive",
title: "Bundle it for transfer (optional)",
body: "If you're importing from another machine, archive the whole projects folder first:\n\n tar -czf claude-history.tar.gz -C ~/.claude projects\n\nMove claude-history.tar.gz to this machine however you like (AirDrop, scp, USB, cloud storage).",
},
{
id: "choose",
title: "Pick an import mode",
body: "Rescan default: re-read ~/.claude/projects on this machine and import anything new. From folder: point the dashboard at any directory you've extracted history into. Upload: drag-drop JSONL files or an archive directly into the browser.",
},
{
id: "verify",
title: "Verify tokens and cost",
body: "Imports are idempotent: re-running is always safe. Token counts are deduplicated per session ID, with compaction baselines preserved so cost never double-counts. After import, open Analytics → Cost to confirm the breakdown.",
},
],
};
const IMPORT_RESCAN_EXAMPLE = {
ok: true,
source: "default",
imported: 14,
skipped: 298,
backfilled: 2,
errors: 0,
sessions_seen: 312,
files_scanned: 312,
};
const IMPORT_SCAN_PATH_EXAMPLE = {
ok: true,
source: "path",
path: "/Users/son/Downloads/claude-history/projects",
imported: 9,
skipped: 41,
backfilled: 0,
errors: 0,
sessions_seen: 50,
files_scanned: 50,
};
const IMPORT_UPLOAD_EXAMPLE = {
ok: true,
source: "upload",
files_received: 3,
rejected_files: ["notes.txt"],
entries_extracted: 128,
entries_skipped: 2,
imported: 12,
skipped: 116,
backfilled: 0,
errors: 0,
sessions_seen: 128,
files_scanned: 128,
};
const UPDATE_STATUS_AVAILABLE_EXAMPLE = {
git_repo: true,
repo_root: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
update_available: true,
commits_behind: 4,
remote_ref: "origin/master",
local_sha: "de5d0891a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7",
remote_sha: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
manual_command: "npm run self-update",
checked_at: "2026-06-26T01:20:09.553Z",
};
const UPDATE_STATUS_UP_TO_DATE_EXAMPLE = {
git_repo: true,
repo_root: "/Users/son/WebstormProjects/Claude-Code-Agent-Monitor",
update_available: false,
commits_behind: 0,
remote_ref: "origin/master",
local_sha: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
remote_sha: "a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0",
manual_command: "npm run self-update",
checked_at: "2026-06-26T01:25:41.002Z",
};
const UPDATE_STATUS_NON_REPO_EXAMPLE = {
git_repo: false,
update_available: false,
message: "Not a git checkout — update detection is unavailable for this install.",
};
const WORKFLOW_AGGREGATE_EXAMPLE = {
stats: {
totalSessions: 412,
totalAgents: 1893,
totalSubagents: 1481,
avgSubagents: 3.6,
successRate: 94.2,
avgDepth: 1.4,
avgDurationSec: 1187,
totalCompactions: 96,
avgCompactions: 0.2,
topFlow: { source: "Read", target: "Edit", count: 3120 },
},
orchestration: {
sessionCount: 412,
mainCount: 412,
subagentTypes: [
{ subagent_type: "general-purpose", count: 612, completed: 590, errors: 8 },
{ subagent_type: "Explore", count: 388, completed: 380, errors: 2 },
],
edges: [{ source: "main", target: "general-purpose", weight: 612 }],
outcomes: [
{ status: "completed", count: 1402 },
{ status: "error", count: 79 },
],
compactions: { total: 96, sessions: 71 },
},
toolFlow: {
transitions: [{ source: "Read", target: "Edit", value: 3120 }],
toolCounts: [
{ tool_name: "Read", count: 9821 },
{ tool_name: "Edit", count: 5402 },
{ tool_name: "Bash", count: 4810 },
],
},
effectiveness: [
{
subagent_type: "general-purpose",
total: 612,
completed: 590,
errors: 8,
sessions: 281,
successRate: 98.7,
avgDuration: 142,
trend: [88, 91, 102, 97, 110, 24, 12],
},
],
patterns: {
patterns: [{ steps: ["Explore", "general-purpose"], count: 142, percentage: 34.5 }],
soloSessionCount: 120,
soloPercentage: 29.1,
},
modelDelegation: {
mainModels: [{ model: "claude-opus-4-8", agent_count: 290, session_count: 290 }],
subagentModels: [{ model: "claude-opus-4-8", agent_count: 980 }],
tokensByModel: [
{
model: "claude-opus-4-8",
input_tokens: 4820112,
output_tokens: 1920334,
cache_read_tokens: 88201442,
cache_write_tokens: 12044210,
},
],
},
errorPropagation: {
byDepth: [
{ depth: 0, count: 41 },
{ depth: 1, count: 38 },
],
byType: [{ subagent_type: "general-purpose", count: 8 }],
eventErrors: [{ summary: "Error in Bash: command not found", count: 5 }],
sessionsWithErrors: 33,
totalSessions: 412,
errorRate: 8,
},
concurrency: {
aggregateLanes: [
{ name: "Main Agent", avgStart: 0, avgEnd: 1, count: 412 },
{ name: "Explore", avgStart: 0.12, avgEnd: 0.31, count: 388 },
],
},
complexity: [
{
id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
name: "Refactor auth flow",
status: "completed",
duration: 2538,
agentCount: 6,
subagentCount: 5,
totalTokens: 940212,
model: "claude-opus-4-8",
},
],
compaction: {
totalCompactions: 96,
tokensRecovered: 18402991,
perSession: [{ session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11", compactions: 3 }],
sessionsWithCompactions: 71,
totalSessions: 412,
},
cooccurrence: [{ source: "Explore", target: "general-purpose", weight: 142 }],
};
const WORKFLOW_SESSION_EXAMPLE = {
session: {
id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
name: "Refactor auth flow",
status: "completed",
cwd: "/Users/son/code/foo",
model: "claude-opus-4-8",
started_at: "2026-06-25T13:00:00.000Z",
ended_at: "2026-06-25T13:42:18.220Z",
metadata: null,
updated_at: "2026-06-25T13:42:18.220Z",
},
tree: [
{
id: "agent_main_5f3c0e2a",
name: "Main agent",
type: "main",
subagent_type: null,
status: "completed",
task: null,
started_at: "2026-06-25T13:00:00.000Z",
ended_at: "2026-06-25T13:42:18.220Z",
children: [
{
id: "agent_sub_a1",
name: "Explore the auth module",
type: "subagent",
subagent_type: "Explore",
status: "completed",
task: "Map auth call sites",
started_at: "2026-06-25T13:02:10.000Z",
ended_at: "2026-06-25T13:06:55.000Z",
children: [],
},
],
},
],
toolTimeline: [
{
id: 91021,
tool_name: "Read",
event_type: "PreToolUse",
agent_id: "agent_main_5f3c0e2a",
created_at: "2026-06-25T13:01:02.500Z",
summary: "Read server/auth.js",
},
],
swimLanes: [
{
id: "agent_main_5f3c0e2a",
name: "Main agent",
type: "main",
subagent_type: null,
status: "completed",
started_at: "2026-06-25T13:00:00.000Z",
ended_at: "2026-06-25T13:42:18.220Z",
parent_agent_id: null,
},
],
events: [
{
id: 91021,
session_id: "5f3c0e2a-1b9d-4c77-8a21-9e0f7b6d4c11",
agent_id: "agent_main_5f3c0e2a",
event_type: "PreToolUse",
tool_name: "Read",
summary: "Read server/auth.js",
data: null,
created_at: "2026-06-25T13:01:02.500Z",
},
],
};
const WEBHOOK_ID_PARAM = {
name: "id",
in: "path",
required: true,
schema: { type: "string" },
example: "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
};
// ── Paths (enriched overrides — contract preserved) ──────────────────────────
const paths = {
"/api/webhooks/providers": {
get: {
tags: ["Webhooks"],
summary: "List supported providers + their config fields (for the UI)",
description:
"Returns the redacted provider catalog the webhook-target editor renders. For each of the 14 supported providers (Slack, Discord, Teams, Google Chat, Mattermost, Rocket.Chat, Telegram, PagerDuty, Opsgenie, Splunk On-Call, Zapier, Make, n8n, Pipedream) plus the `generic` family, it lists: a human label, the provider family, whether the URL must be https / is user-supplied, and the per-provider config field definitions (key, label, type, required, options, and whether the field is secret). No secret values are ever included — this is purely the *shape* of the form, not stored credentials.",
operationId: "listWebhookProviders",
responses: {
200: {
description: "Provider catalog: label, family, url requirements, fields",
content: {
"application/json": {
schema: { type: "object", additionalProperties: true },
example: {
providers: {
slack: {
label: "Slack",
family: "chat",
https: true,
urlRequired: true,
fields: [],
},
telegram: {
label: "Telegram",
family: "chat",
https: true,
urlRequired: false,
fields: [
{
key: "bot_token",
label: "Bot token",
type: "string",
required: true,
secret: true,
},
{
key: "chat_id",
label: "Chat ID",
type: "string",
required: true,
secret: false,
},
],
},
pagerduty: {
label: "PagerDuty",
family: "incident",
https: true,
urlRequired: false,
fields: [
{
key: "routing_key",
label: "Routing key",
type: "string",
required: true,
secret: true,
},
{
key: "severity",
label: "Severity",
type: "enum",
required: false,
options: ["critical", "error", "warning", "info"],
secret: false,
},
],
},
generic: {
label: "Generic webhook",
family: "generic",
https: false,
urlRequired: true,
fields: [],
},
},
},
},
},
},
},
},
},
"/api/webhooks": {
get: {
tags: ["Webhooks"],
summary: "List webhook targets (URLs masked, secrets redacted)",
description:
"Lists every configured webhook target. Responses are always redacted: the full URL is never returned — only `url_preview` (protocol + host + a `…` + the last 4 chars), `has_secret` is a boolean (never the secret itself), and any secret-flagged provider config fields (routing keys, API keys, bot tokens) and custom header values are masked to `••••`. Each target also carries its most recent delivery outcome in `last_delivery` (or null if it has never fired).",
operationId: "listWebhooks",
responses: {
200: {
description: "All configured webhook targets",
content: {
"application/json": {
schema: { type: "object", additionalProperties: true },
example: { targets: [WEBHOOK_TARGET_EXAMPLE, GENERIC_WEBHOOK_TARGET_EXAMPLE] },
},
},
},
},
},
post: {
tags: ["Webhooks"],
summary: "Create a webhook target",
description:
"Creates a webhook target that fires when alerts match. `name` and `type` are required. `url` is required for most providers but is derived or defaulted for a few (Telegram and Opsgenie derive it from config; PagerDuty defaults it) — consult GET /api/webhooks/providers for which fields each provider needs. `config` carries provider-specific params (e.g. `{ chat_id }` for Telegram, `{ routing_key, severity }` for PagerDuty, `{ api_key, region }` for Opsgenie). `secret` (HMAC-SHA256 signing) and custom `headers` apply only to the generic family and are silently ignored for other providers. `rule_ids` optionally scopes the target to specific alert rules; omit it to fire for all rules. The response is the created target, REDACTED the same way as the list endpoint (URL masked, secrets shown only as `has_secret`/`••••`).",
operationId: "createWebhook",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["name", "type"],
properties: {
name: { type: "string" },
type: {
type: "string",
enum: [
"slack",
"discord",
"teams",
"google_chat",
"mattermost",
"rocketchat",
"telegram",
"pagerduty",
"opsgenie",
"splunk_oncall",
"zapier",
"make",
"n8n",
"pipedream",
"generic",
],
},
url: {
type: "string",
format: "uri",
description:
"Required for most providers; omit for those that derive their URL (Telegram, Opsgenie) or default it (PagerDuty). See GET /api/webhooks/providers.",
},
enabled: { type: "boolean", default: true },
config: {
type: "object",
additionalProperties: true,
description:
"Provider-specific params, e.g. { chat_id } (Telegram), { routing_key, severity } (PagerDuty), { api_key, region } (Opsgenie).",
},
secret: {
type: "string",
description: "Generic family only: HMAC-SHA256 signing secret",
},
headers: {
type: "object",
additionalProperties: { type: "string" },
description: "Generic family only: extra request headers",
},
rule_ids: {
type: "array",
items: { type: "string" },
description: "Optional: scope to specific alert rules (omit for all)",
},
},
},
examples: {
slack: {
summary: "Slack incoming webhook scoped to one rule",
value: {
name: "Eng on-call (Slack)",
type: "slack",
url: "https://hooks.slack.com/services/T0000/B0000/XXXXXXXXXXXXXXXXXXXXBXqZ",
enabled: true,
rule_ids: ["rule_inactivity_30m"],
},
},
pagerduty: {
summary: "PagerDuty (URL defaulted, routing key in config)",
value: {
name: "Sev1 pager",
type: "pagerduty",
config: { routing_key: "R0ABCDEF0123456789ABCDEF01", severity: "critical" },
},
},
generic: {
summary: "Generic webhook with HMAC secret and custom headers",
value: {
name: "Internal alert sink",
type: "generic",
url: "https://alerts.internal.example.com/cc-monitor/hook",
secret: "whsec_4f8c2a9e1b6d7f0a3c5e8b1d2f4a6c8e",
headers: { "X-Api-Key": "k_live_abc123", Authorization: "Bearer t0ken" },
},
},
},
},
},
},
responses: {
201: {
description: "Created target (redacted)",
content: {
"application/json": {
schema: { type: "object", additionalProperties: true },
example: { target: WEBHOOK_TARGET_EXAMPLE },
},
},
},
400: { description: "Validation error" },
},
},
},
"/api/webhooks/{id}": {
patch: {
tags: ["Webhooks"],
summary: "Update a webhook target (partial; type is immutable)",
description:
"Partially updates a webhook target. Only the keys present in the body are changed; omitted keys are left as-is. The provider `type` is immutable. `config` is MERGED over the existing config and re-validated, so a single field (e.g. `severity`, `region`) can change without re-sending the secret fields. For the generic family, `secret` may be omitted (keep current) or sent as `null` (clear it); `headers` and `rule_ids` replace their stored value when present. The response is the updated target, REDACTED (URL masked, secrets shown only as `has_secret`/`••••`).",
operationId: "updateWebhook",
parameters: [WEBHOOK_ID_PARAM],
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
properties: {
name: { type: "string" },
url: { type: "string", format: "uri", description: "Omit to keep current" },
enabled: { type: "boolean" },
config: {
type: "object",
additionalProperties: true,
description: "Provider params; merged over existing (secrets kept if omitted)",
},
secret: {
type: ["string", "null"],
description: "Generic family only: omit to keep, null to clear",
},
headers: { type: "object", additionalProperties: { type: "string" } },
rule_ids: { type: "array", items: { type: "string" } },
},
},
examples: {
disable: {
summary: "Disable a target without touching anything else",
value: { enabled: false },
},
renameAndRescope: {
summary: "Rename and re-scope to different rules",
value: {
name: "Eng on-call (Slack) — muted weekends",
rule_ids: ["rule_token_threshold_5m"],
},
},
mergeConfig: {
summary: "Change one PagerDuty config field (routing key kept)",
value: { config: { severity: "warning" } },
},
clearSecret: {
summary: "Generic family: clear the signing secret",
value: { secret: null },
},
},
},
},
},
responses: {
200: {
description: "Updated target (redacted)",
content: {
"application/json": {
schema: { type: "object", additionalProperties: true },
example: {
target: { ...WEBHOOK_TARGET_EXAMPLE, name: "Eng on-call (Slack) — muted weekends" },
},
},
},
},
400: { description: "Validation error" },
404: { description: "Target not found" },
},
},
delete: {
tags: ["Webhooks"],
summary: "Delete a webhook target and its delivery log",
description:
"Permanently deletes a webhook target. Its delivery-log history cascades away with it. Returns `{ ok: true }` on success. This only removes the delivery channel — alert *rules* and the fired-alert feed are untouched.",
operationId: "deleteWebhook",
parameters: [WEBHOOK_ID_PARAM],
responses: {
200: { description: "Deleted" },
404: { description: "Target not found" },
},
},
},
"/api/webhooks/{id}/test": {
post: {
tags: ["Webhooks"],
summary: "Send a synthetic test alert to a target",
description:
"Sends a synthetic test alert to the target and reports the delivery outcome synchronously. The HTTP status is always 200 when the target exists — the *request itself* succeeded — and the `ok` flag carries the downstream delivery result. `status` is the HTTP status code returned by the provider (or null if the request never completed), `attempts` is how many tries were made (the delivery layer retries transient failures), and `error` is a human-readable failure reason or null on success.",
operationId: "testWebhook",
parameters: [WEBHOOK_ID_PARAM],
responses: {
200: {
description: "Delivery result (ok flag carries the downstream outcome)",
content: {
"application/json": {
schema: { type: "object", additionalProperties: true },
examples: {
success: {
summary: "Provider accepted the test payload",
value: { ok: true, status: 200, attempts: 1, error: null },
},
failure: {
summary: "Provider rejected or was unreachable after retries",
value: { ok: false, status: 503, attempts: 3, error: "Service Unavailable" },
},
},
},
},
},
404: { description: "Target not found" },
},
},
},
"/api/webhooks/{id}/deliveries": {
get: {
tags: ["Webhooks"],
summary: "Recent delivery log for a target",
description:
"Returns the recent delivery log for a target, newest first. Each row records the alert that fired, the resulting HTTP status, the number of attempts, any error text, and a timestamp. `limit` is clamped to 1200 (default 20) and `offset` to ≥0 (default 0). This is an audit trail of past sends — it does not trigger a new delivery (use POST /{id}/test for that).",
operationId: "listWebhookDeliveries",
parameters: [
WEBHOOK_ID_PARAM,
{
name: "limit",
in: "query",
schema: { type: "integer", default: 20 },
description: "Max rows to return (clamped to 1200).",
example: 20,
},
{
name: "offset",
in: "query",
schema: { type: "integer", default: 0 },
description: "Pagination offset (clamped to ≥0).",
example: 0,
},
],
responses: {
200: {
description: "Delivery rows, newest first",
content: {
"application/json": {
schema: { type: "object", additionalProperties: true },
example: {
deliveries: [
{
id: 5012,
target_id: "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
alert_event_id: 8841,
status: "success",
status_code: 200,
attempts: 1,
error: null,
created_at: "2026-06-25T18:41:55.117Z",
},
{
id: 5008,
target_id: "9b1c2d3e-4f5a-6b7c-8d9e-0f1a2b3c4d5e",
alert_event_id: 8839,
status: "failed",
status_code: 429,
attempts: 3,
error: "Too Many Requests",
created_at: "2026-06-25T18:30:02.004Z",
},
],
limit: 20,
offset: 0,
},
},
},
},
404: { description: "Target not found" },
},
},
},
"/api/settings/info": {
get: {
tags: ["Settings"],
summary: "Get system/database/hook diagnostics",
description:
"Returns a diagnostics snapshot used by the Settings page: `db` (database file path, on-disk size in bytes, per-table row counts, SQLite pragmas, and recent event load over the last 5/15/60 minutes), `hooks` (whether the Claude Code hook-handler is installed in `settings.json`, the settings path, and a per-hook-type installed map), `server` (process uptime, Node version, platform, live WebSocket connection count, memory/CPU/host stats), and `transcript_cache` (number of cached transcript entries and the cached paths). Read-only and cheap to poll.",
operationId: "getSettingsInfo",
responses: {
200: {
description: "Settings and diagnostics",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/SettingsInfoResponse" },
example: SETTINGS_INFO_EXAMPLE,
},
},
},
},
},
},
"/api/settings/clear-data": {
post: {
tags: ["Settings"],
summary: "Delete all dashboard data",
description:
"⚠ DESTRUCTIVE — IRREVERSIBLE. Deletes ALL sessions, agents, events, token_usage rows, the fired-alert feed (alert_events), and the webhook delivery log. There is no confirmation step and no undo — export first via GET /api/settings/export if you need a backup. User CONFIGURATION survives: alert *rules*, webhook *targets*, and model_pricing are preserved (they're settings, not captured data). The response echoes the row counts that existed BEFORE the wipe so the UI can report what was removed.",
operationId: "clearData",
responses: {
200: {
description: "Data cleared",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ClearDataResponse" },
example: CLEAR_DATA_EXAMPLE,
},
},
},
},
},
},
"/api/settings/reimport": {
post: {
tags: ["Settings"],
summary: "Re-import legacy sessions from ~/.claude",
description:
"Re-runs the legacy history importer against the default `~/.claude` projects directory, funneling every transcript through the same parser + importSession pipeline the live server uses. This is IDEMPOTENT and ADDITIVE — already-imported sessions are deduplicated (counted under `skipped`), token counts and compaction baselines are preserved so cost never double-counts, and nothing existing is deleted. The response reports how many sessions were `imported` vs `skipped`, plus an `errors` count for transcripts that failed to parse.",
operationId: "reimportLegacySessions",
responses: {
200: {
description: "Import completed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ReimportResponse" },
example: REIMPORT_EXAMPLE,
},
},
},
500: {
description: "Import failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "IMPORT_FAILED",
message:
"ENOENT: no such file or directory, scandir '/Users/son/.claude/projects'",
},
},
},
},
},
},
},
},
"/api/settings/reinstall-hooks": {
post: {
tags: ["Settings"],
summary: "Reinstall Claude Code hooks",
description:
"Re-runs the hook installer to (re)wire the dashboard's hook-handler into Claude Code's `settings.json` for all seven hook types (PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd). Safe to re-run — it overwrites/repairs the dashboard's own hook entries without touching unrelated user hooks. The response returns the post-install hook status so the UI can confirm every hook type is now `installed: true`.",
operationId: "reinstallHooks",
responses: {
200: {
description: "Hooks reinstall result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ReinstallHooksResponse" },
example: REINSTALL_HOOKS_EXAMPLE,
},
},
},
500: {
description: "Hook installation failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "HOOK_INSTALL_FAILED",
message: "EACCES: permission denied, open '/Users/son/.claude/settings.json'",
},
},
},
},
},
},
},
},
"/api/settings/reset-pricing": {
post: {
tags: ["Settings"],
summary: "Reset pricing table to defaults",
description:
"⚠ DESTRUCTIVE to pricing customizations. Deletes EVERY row in the model_pricing table and re-seeds it from the dashboard's built-in DEFAULT_PRICING list. Any custom rates or custom model patterns you added are permanently lost — there is no undo. Captured session/token data is untouched (only the pricing rules used to *compute* cost change). The response returns the full freshly-seeded pricing table.",
operationId: "resetPricing",
responses: {
200: {
description: "Pricing defaults restored",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ResetPricingResponse" },
example: RESET_PRICING_EXAMPLE,
},
},
},
},
},
},
"/api/settings/export": {
get: {
tags: ["Settings"],
summary: "Export all dashboard data as JSON",
description:
'Exports the entire dataset as a single versioned JSON document — all sessions, agents, events, token_usage rows, workflows, dashboard_runs, alert_rules, and model_pricing — stamped with `format: "ccam-export"`, `version`, and `exported_at`. Served with a `Content-Disposition: attachment` header (filename `agent-monitor-export-YYYY-MM-DD.json`) so browsers download it. Use it to back up before a destructive operation (clear-data / cleanup with purge_days) or to migrate/consolidate data across machines — the bundle is re-importable via POST /api/settings/import. Read-only; nothing is modified.',
operationId: "exportData",
responses: {
200: {
description: "Export payload (served as attachment)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ExportResponse" },
example: EXPORT_EXAMPLE,
},
},
},
},
},
},
"/api/settings/import": {
post: {
tags: ["Settings"],
summary: "Restore (import) a previously exported data bundle",
description:
'Restores a bundle produced by GET /api/settings/export. Supply it either as `multipart/form-data` with a single `file` field (browser upload) or as a JSON body `{ "path": "<absolute path>" }` (the server reads the file from disk — used by the `ccam import-data` CLI, and it also sidesteps the global 1 MB JSON body cap for large bundles). The restore is idempotent and NON-DESTRUCTIVE: it is session-atomic, so a session already present (matched by its UUID) is skipped whole together with its agents/events/token_usage/workflows, and independent config rows (dashboard_runs, alert_rules, model_pricing) are inserted only when absent. Nothing existing is overwritten — ideal for consolidating several machines into one dashboard. The response reports per-table counts.',
operationId: "importData",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: {
file: { type: "string", format: "binary", description: "The export .json file." },
},
},
},
"application/json": {
schema: {
type: "object",
required: ["path"],
properties: {
path: {
type: "string",
description:
"Absolute path (server-side) to an export .json file. `~` is expanded.",
},
},
},
example: { path: "/Users/you/ccam-export-2026-06-26.json" },
},
},
},
responses: {
200: {
description: "Per-table restore counts",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ImportResponse" },
example: {
ok: true,
source: "agent-monitor-export-2026-06-26.json",
format: "ccam-export",
sessions_imported: 42,
sessions_skipped: 3,
agents: 96,
events: 5120,
token_usage: 42,
workflows: 4,
dashboard_runs: 11,
alert_rules: 2,
model_pricing: 0,
errors: 0,
},
},
},
},
400: {
description: "Missing file, invalid JSON, or unrecognized export format",
},
},
},
},
"/api/settings/cleanup": {
post: {
tags: ["Settings"],
summary: "Abandon stale sessions and optionally purge old history",
description:
"Two-phase maintenance. Phase 1 (`abandon_hours`, non-destructive): marks any still-`active` session with no events newer than that many hours as `abandoned`, and completes its lingering agents — a tidy-up of crashed/orphaned sessions. Phase 2 (`purge_days`) is ⚠ DESTRUCTIVE and IRREVERSIBLE: it permanently DELETES completed/error/abandoned sessions (and their events, agents, and token_usage) whose `started_at` is older than that many days. Active sessions are NEVER purged. Both fields are optional and independent — send only `abandon_hours` for a safe tidy-up, or include `purge_days` to also reclaim disk. Export first if the purged history matters. The response reports counts for each phase.",
operationId: "cleanupData",
requestBody: {
required: true,
content: {
"application/json": {
schema: { $ref: "#/components/schemas/CleanupRequest" },
examples: {
tidyOnly: {
summary: "Safe: abandon stale sessions only (no deletion)",
value: { abandon_hours: 12 },
},
abandonAndPurge: {
summary:
"Abandon stale sessions, then purge history older than 90 days (destructive)",
value: { abandon_hours: 12, purge_days: 90 },
},
},
},
},
},
responses: {
200: {
description: "Cleanup result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/CleanupResponse" },
example: CLEANUP_EXAMPLE,
},
},
},
},
},
},
"/api/import/guide": {
get: {
tags: ["Import"],
summary: "Import guide with OS-aware defaults and step-by-step instructions",
description:
"Returns the OS-aware import guide the Import page renders verbatim: the detected `platform`, the default `~/.claude/projects` directory (raw + display form), whether it exists and how many projects/JSONL files it holds, an OS-specific `archive_command` for bundling history off another machine, the supported file extensions, the upload size/count limits, and four ordered `steps` (locate → archive → choose mode → verify). Read-only; performs no import.",
operationId: "importGuide",
responses: {
200: {
description: "Guide payload",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ImportGuideResponse" },
example: IMPORT_GUIDE_EXAMPLE,
},
},
},
},
},
},
"/api/import/rescan": {
post: {
tags: ["Import"],
summary: "Rescan the default ~/.claude/projects directory",
description:
'Re-scans the default `~/.claude/projects` directory and imports anything new through the live ingestion pipeline. IDEMPOTENT and ADDITIVE — re-running is always safe, already-imported sessions are deduplicated (`skipped`), and token/compaction baselines are preserved so cost never double-counts. Progress is broadcast over the WebSocket as `import.progress` frames while it runs. The response reports `imported` / `skipped` / `backfilled` / `errors` plus `sessions_seen` and `files_scanned`, with `source: "default"`.',
operationId: "importRescan",
responses: {
200: {
description: "Import result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ImportResultResponse" },
example: IMPORT_RESCAN_EXAMPLE,
},
},
},
500: {
description: "Import failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: { code: "IMPORT_FAILED", message: "Failed to read projects directory" },
},
},
},
},
},
},
},
"/api/import/scan-path": {
post: {
tags: ["Import"],
summary: "Import transcripts from an arbitrary absolute directory",
description:
'Imports transcripts from an arbitrary directory you point the dashboard at (e.g. history extracted from another machine). The `path` must resolve to an existing directory: a leading `~` is expanded to the home directory, the result must be absolute, and subdirectories are walked recursively for `.jsonl` files. Same idempotent, baseline-preserving pipeline as the default rescan; progress is broadcast as `import.progress`. The response echoes the resolved `path` and the per-run counters with `source: "path"`.',
operationId: "importScanPath",
requestBody: {
required: true,
content: {
"application/json": {
schema: {
type: "object",
required: ["path"],
properties: {
path: {
type: "string",
description:
"Absolute directory path. Tilde (~) is expanded. Walks subdirectories recursively.",
},
},
},
examples: {
absolute: {
summary: "Absolute directory",
value: { path: "/Users/son/Downloads/claude-history/projects" },
},
tilde: {
summary: "Tilde expanded to home directory",
value: { path: "~/Downloads/claude-history/projects" },
},
},
},
},
},
responses: {
200: {
description: "Import result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ImportResultResponse" },
example: IMPORT_SCAN_PATH_EXAMPLE,
},
},
},
400: {
description: "Path validation failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
examples: {
missing: {
summary: "path omitted",
value: { error: { code: "INVALID_INPUT", message: "`path` is required" } },
},
notAbsolute: {
summary: "path is not absolute",
value: {
error: { code: "INVALID_INPUT", message: "`path` must be an absolute path" },
},
},
notFound: {
summary: "path does not exist",
value: {
error: { code: "PATH_NOT_FOUND", message: "Path does not exist: /tmp/nope" },
},
},
notDir: {
summary: "path is a file, not a directory",
value: {
error: {
code: "NOT_A_DIRECTORY",
message: "Path is not a directory: /tmp/foo.jsonl",
},
},
},
},
},
},
},
500: {
description: "Import failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: { code: "IMPORT_FAILED", message: "Unexpected end of JSON input" },
},
},
},
},
},
},
},
"/api/import/upload": {
post: {
tags: ["Import"],
summary: "Upload JSONL files or archives (.zip, .tar, .tar.gz, .tgz, .gz)",
description:
'Imports history uploaded directly from the browser as `multipart/form-data` under the `files` field. Accepts raw `.jsonl` / `.meta.json` transcripts and/or archives (`.zip`, `.tar`, `.tar.gz`, `.tgz`, `.gz`), which are extracted into a temp dir and walked for JSONL content. Unsupported extensions are silently rejected and reported in `rejected_files`. Extraction is bounded to defend against zip bombs — exceeding the limit returns 413. Same idempotent import pipeline; progress is broadcast as `import.progress`. On success the response carries `source: "upload"` plus `files_received`, `rejected_files`, `entries_extracted`, `entries_skipped`, and the standard import counters. (Requires the optional `multer` dependency; a missing install yields a 500.)',
operationId: "importUpload",
requestBody: {
required: true,
content: {
"multipart/form-data": {
schema: {
type: "object",
properties: {
files: {
type: "array",
items: { type: "string", format: "binary" },
description:
"Files to import. Supports .jsonl, .meta.json, .zip, .tar, .tar.gz, .tgz, .gz.",
},
},
},
example: { files: ["claude-history.tar.gz", "session-extra.jsonl"] },
},
},
},
responses: {
200: {
description: "Import result",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ImportResultResponse" },
example: IMPORT_UPLOAD_EXAMPLE,
},
},
},
400: {
description: "No files or no JSONL content",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
examples: {
noFiles: {
summary: "No supported files in the upload",
value: {
error: {
code: "NO_FILES",
message:
"No supported files in upload. 1 file(s) rejected (unsupported extension).",
},
},
},
noJsonl: {
summary: "Archive extracted but contained no .jsonl",
value: {
error: {
code: "NO_JSONL",
message:
"No .jsonl files were found in the uploaded content. Supported inputs: .jsonl, .meta.json, .zip, .tar, .tar.gz, .tgz, .gz.",
},
},
},
},
},
},
},
413: {
description: "Extraction limit exceeded (possible zip bomb)",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "EXTRACTION_LIMIT_EXCEEDED",
message: "Archive exceeded the extraction size limit (possible zip bomb).",
},
},
},
},
},
500: {
description: "Upload or import failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
examples: {
noMulter: {
summary: "Optional multer dependency not installed",
value: {
error: {
code: "UPLOADER_UNAVAILABLE",
message:
"File upload requires `multer`. Run `npm install` to pick up new deps.",
},
},
},
importFailed: {
summary: "Import pipeline error",
value: {
error: { code: "IMPORT_FAILED", message: "Unexpected end of JSON input" },
},
},
},
},
},
},
},
},
},
"/api/updates/status": {
get: {
tags: ["Updates"],
summary: "Check whether the dashboard git checkout is behind origin",
description:
"Reports whether the dashboard's own git checkout is behind its upstream remote, so a user can pull and restart manually (the dashboard never self-restarts). The response is a variant object: when it IS a git checkout it includes `git_repo: true`, `repo_root`, `update_available`, `commits_behind`, `remote_ref`, `local_sha`, `remote_sha`, and a copy-pasteable `manual_command`; when it is NOT a git checkout (e.g. an npm/tarball install) it returns `git_repo: false` with a `message` and no diff fields. Read-only — this only inspects git, it does not fetch destructively or modify the working tree.",
operationId: "getUpdatesStatus",
responses: {
200: {
description: "Update check result",
content: {
"application/json": {
schema: {
type: "object",
additionalProperties: true,
description:
"Includes git_repo, update_available, commits_behind, remote_ref, local_sha, remote_sha, manual_command, and optional error/message fields.",
},
examples: {
updateAvailable: {
summary: "Behind origin — update available",
value: UPDATE_STATUS_AVAILABLE_EXAMPLE,
},
upToDate: {
summary: "Up to date with origin",
value: UPDATE_STATUS_UP_TO_DATE_EXAMPLE,
},
notARepo: {
summary: "Not a git checkout — detection unavailable",
value: UPDATE_STATUS_NON_REPO_EXAMPLE,
},
},
},
},
},
500: {
description: "Update status query failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: { code: "UPDATE_STATUS_FAILED", message: "git: command not found" },
},
},
},
},
},
},
},
"/api/updates/check": {
post: {
tags: ["Updates"],
summary: "Run an update check immediately and broadcast the result",
description:
"Runs the same upstream check as GET /api/updates/status immediately and, in addition to returning the result, broadcasts it to every connected client as an `update_status` WebSocket message — so all open dashboard tabs refresh their update banner at once. The response shape is identical to GET /api/updates/status (the same git_repo / update_available / commits_behind / manual_command variant object). Read-only with respect to the working tree.",
operationId: "triggerUpdatesCheck",
responses: {
200: {
description: "Fresh update status payload (also broadcast over WebSocket)",
content: {
"application/json": {
schema: {
type: "object",
additionalProperties: true,
description:
"Same shape as GET /api/updates/status. Also sent as an update_status WebSocket message to all connected clients.",
},
examples: {
updateAvailable: {
summary: "Behind origin — update available",
value: UPDATE_STATUS_AVAILABLE_EXAMPLE,
},
upToDate: {
summary: "Up to date with origin",
value: UPDATE_STATUS_UP_TO_DATE_EXAMPLE,
},
},
},
},
},
500: {
description: "Update check failed",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/ErrorResponse" },
example: {
error: {
code: "UPDATE_CHECK_FAILED",
message: "git fetch failed: network unreachable",
},
},
},
},
},
},
},
},
"/api/workflows": {
get: {
tags: ["Workflows"],
summary: "Get workflow intelligence aggregates",
description:
"Returns the full workflow-intelligence aggregate powering the Workflows analytics page — 11 sections in one payload: `stats` (headline counters: sessions, agents, subagents, success rate, avg depth/duration, compactions, top tool flow), `orchestration` (subagent-type breakdown + delegation edges + outcomes), `toolFlow` (tool-to-tool transitions + tool counts), `effectiveness` (per-subagent-type success rate, avg duration, weekly trend), `patterns` (frequent subagent sequences + solo-session share), `modelDelegation` (model usage for main/sub agents + tokens by model), `errorPropagation` (errors by depth/type + error rate), `concurrency` (averaged agent swim-lane start/end), `complexity` (per-session agent/token/duration rows), `compaction` (compaction counts + tokens recovered), and `cooccurrence` (directed subagent-after-subagent pairs). The optional `status` query filter scopes every section to sessions of one status. Errors use the SHORT `{ error: { message } }` shape.",
operationId: "getWorkflowIntelligence",
parameters: [{ $ref: "#/components/parameters/WorkflowStatusQuery" }],
responses: {
200: {
description: "Workflow aggregate data",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/WorkflowAggregateResponse" },
example: WORKFLOW_AGGREGATE_EXAMPLE,
},
},
},
500: {
description: "Internal server error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/MessageErrorResponse" },
example: { error: { message: "no such table: agents" } },
},
},
},
},
},
},
"/api/workflows/session/{id}": {
get: {
tags: ["Workflows"],
summary: "Get workflow drill-in for one session",
description:
"Returns the workflow drill-in for a single session, used by the session-level Workflow view: `session` (the session row), `tree` (the recursive parent→child agent tree rooted at the main agent), `toolTimeline` (chronological tool events with tool_name/event_type/agent_id/summary), `swimLanes` (a flat per-agent start/end lane list for the Gantt-style view), and `events` (the chronological event stream, capped at the first 500 rows). Returns 404 with the SHORT `{ error: { message } }` shape when the session id is unknown.",
operationId: "getWorkflowSession",
parameters: [{ $ref: "#/components/parameters/SessionIdPath" }],
responses: {
200: {
description: "Workflow session detail",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/WorkflowSessionResponse" },
example: WORKFLOW_SESSION_EXAMPLE,
},
},
},
404: {
description: "Session not found",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/MessageErrorResponse" },
example: { error: { message: "Session not found" } },
},
},
},
500: {
description: "Internal server error",
content: {
"application/json": {
schema: { $ref: "#/components/schemas/MessageErrorResponse" },
example: { error: { message: "no such table: agents" } },
},
},
},
},
},
},
};
module.exports = { tags, schemas, paths };