/**
 * @file Central OpenAPI 3.0 specification for the dashboard HTTP API.
 * @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
 */

const pkg = require("../package.json");
const extraSpec = require("./openapi-extra");

function normalizeRepositoryUrl(url) {
  if (!url || typeof url !== "string") return null;
  return url.replace(/^git\+/, "").replace(/\.git$/, "");
}

/**
 * Merge supplementary tags/schemas/paths (from server/openapi-extra) into the
 * base spec. Extra `paths` and `schemas` override base entries with the same
 * key; `tags` are appended only when a tag with that `name` isn't already
 * present. Mutates and returns `spec`.
 */
function mergeExtraSpec(spec, extra) {
  if (!extra) return spec;
  if (Array.isArray(extra.tags) && extra.tags.length > 0) {
    const have = new Set((spec.tags || []).map((t) => t && t.name));
    for (const tag of extra.tags) {
      if (tag && !have.has(tag.name)) spec.tags.push(tag);
    }
  }
  if (extra.schemas) {
    spec.components.schemas = { ...spec.components.schemas, ...extra.schemas };
  }
  if (extra.paths) {
    spec.paths = { ...spec.paths, ...extra.paths };
  }
  return spec;
}

function createOpenApiSpec() {
  const repositoryUrl = normalizeRepositoryUrl(pkg.repository?.url);
  const issuesUrl =
    typeof pkg.bugs?.url === "string" && pkg.bugs.url.length > 0
      ? pkg.bugs.url
      : repositoryUrl
        ? `${repositoryUrl}/issues`
        : null;
  const defaultPort = Number.parseInt(process.env.DASHBOARD_PORT || "4820", 10) || 4820;

  const spec = {
    openapi: "3.0.3",
    info: {
      title: "Agent Dashboard for Claude Code API",
      version: pkg.version || "1.0.0",
      description:
        "HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.",
      contact: {
        name: "Nguyễn Ngọc Trí Vĩ",
        email: "vinnt@smartgift.vn",
        ...(repositoryUrl ? { url: repositoryUrl } : {}),
      },
      // No license block: this is an internal build with no public licence
      // file, and pointing at a LICENSE that does not exist would 404.
    },
    externalDocs: repositoryUrl
      ? {
          description: "Project documentation",
          url: `${repositoryUrl}#readme`,
        }
      : undefined,
    servers: [
      {
        url: `http://localhost:${defaultPort}`,
        description: "Local dashboard server (default)",
      },
      {
        url: "http://127.0.0.1:4820",
        description: "Local loopback endpoint used by hook-handler",
      },
    ],
    tags: [
      { name: "Health", description: "Service liveness checks" },
      { name: "Metrics", description: "Prometheus / OpenMetrics scrape endpoint" },
      { name: "Sessions", description: "Claude Code session lifecycle" },
      { name: "Agents", description: "Main/subagent records and status" },
      { name: "Events", description: "Event stream persistence" },
      { name: "Stats", description: "High-level dashboard counters" },
      { name: "Analytics", description: "Aggregated analytics views" },
      { name: "Hooks", description: "Claude hook ingestion endpoint" },
      { name: "Pricing", description: "Model pricing and token cost calculations" },
      { name: "Workflows", description: "Workflow intelligence and session drill-in" },
      { name: "Settings", description: "Operational maintenance endpoints" },
      {
        name: "Updates",
        description:
          "Detect upstream git changes so users can pull and restart manually (local dashboard installs)",
      },
      {
        name: "Alerts",
        description: "Rules-based alerting: rule CRUD, fired-alert feed, acknowledgement",
      },
      {
        name: "Webhooks",
        description:
          "Universal webhook delivery for fired alerts: target CRUD (Slack/Discord/Teams/generic), test probe, and delivery log. Secrets are never returned.",
      },
      {
        name: "Remote Sources",
        description:
          "Config for remote SSH machines the dashboard pulls Claude Code history from. No secrets are stored — SSH auth defers to the host's SSH stack (agent, ~/.ssh/config, keys).",
      },
      { name: "Documentation", description: "OpenAPI/Swagger endpoints" },
    ],
    components: {
      parameters: {
        SessionIdPath: {
          name: "id",
          in: "path",
          required: true,
          schema: { type: "string" },
          description: "Session ID",
        },
        AgentIdPath: {
          name: "id",
          in: "path",
          required: true,
          schema: { type: "string" },
          description: "Agent ID",
        },
        PatternPath: {
          name: "pattern",
          in: "path",
          required: true,
          schema: { type: "string" },
          description: "Model pattern (URL-encoded)",
        },
        LimitQuery: {
          name: "limit",
          in: "query",
          required: false,
          schema: { type: "integer", minimum: 0 },
          description: "Page size",
        },
        OffsetQuery: {
          name: "offset",
          in: "query",
          required: false,
          schema: { type: "integer", minimum: 0 },
          description: "Pagination offset",
        },
        SessionStatusQuery: {
          name: "status",
          in: "query",
          required: false,
          schema: {
            type: "string",
            enum: ["active", "completed", "error", "abandoned"],
          },
          description: "Filter by session status",
        },
        AgentStatusQuery: {
          name: "status",
          in: "query",
          required: false,
          schema: {
            type: "string",
            enum: ["working", "waiting", "completed", "error"],
          },
          description: "Filter by agent status",
        },
        SessionFilterQuery: {
          name: "session_id",
          in: "query",
          required: false,
          schema: { type: "string" },
          description: "Filter by session ID",
        },
        WorkflowStatusQuery: {
          name: "status",
          in: "query",
          required: false,
          schema: {
            type: "string",
            enum: ["all", "active", "completed", "error", "abandoned"],
          },
          description: "Filter workflow aggregates by session status",
        },
        SourcesQuery: {
          name: "sources",
          in: "query",
          required: false,
          schema: { type: "string" },
          description:
            "Comma-separated list of data-source ids to include. The built-in local history is the id `local`; remote SSH machines use their `remote_sources.id`. Omit to include every source. Filters on the `sessions.source` column.",
        },
      },
      schemas: {
        ErrorObject: {
          type: "object",
          required: ["code", "message"],
          properties: {
            code: { type: "string" },
            message: { type: "string" },
          },
        },
        ErrorResponse: {
          type: "object",
          required: ["error"],
          properties: {
            error: { $ref: "#/components/schemas/ErrorObject" },
          },
        },
        MessageErrorObject: {
          type: "object",
          required: ["message"],
          properties: { message: { type: "string" } },
        },
        MessageErrorResponse: {
          type: "object",
          required: ["error"],
          properties: { error: { $ref: "#/components/schemas/MessageErrorObject" } },
        },
        CountMap: {
          type: "object",
          additionalProperties: { type: "integer" },
        },
        Session: {
          type: "object",
          required: ["id", "status", "started_at", "updated_at"],
          properties: {
            id: { type: "string" },
            name: { type: "string", nullable: true },
            status: {
              type: "string",
              enum: ["active", "completed", "error", "abandoned"],
            },
            cwd: { type: "string", nullable: true },
            model: { type: "string", nullable: true },
            started_at: { type: "string", format: "date-time" },
            ended_at: { type: "string", format: "date-time", nullable: true },
            metadata: {
              type: "string",
              nullable: true,
              description: "JSON-encoded session metadata",
            },
            updated_at: { type: "string", format: "date-time" },
            agent_count: { type: "integer", nullable: true },
            last_activity: { type: "string", format: "date-time", nullable: true },
            cost: { type: "number", nullable: true },
            awaiting_input_since: {
              type: "string",
              format: "date-time",
              nullable: true,
              description:
                "ISO timestamp set when Claude Code is blocked waiting for the user (permission prompt or input request). Null when not waiting; cleared on the next non-Notification hook event.",
            },
            awaiting_reason: {
              type: "string",
              enum: ["notification", "stop", "session_start", "interrupted"],
              nullable: true,
              description:
                "Why the session is awaiting input: 'notification' (Claude asked the user something), 'stop' (turn completed, waiting for the next prompt), 'session_start' (new/resumed session waiting for the first prompt), or 'interrupted' (watchdog/Esc recovery). Null when not waiting; cleared alongside awaiting_input_since.",
            },
          },
        },
        Agent: {
          type: "object",
          required: ["id", "session_id", "name", "type", "status", "started_at", "updated_at"],
          properties: {
            id: { type: "string" },
            session_id: { type: "string" },
            name: { type: "string" },
            type: { type: "string", enum: ["main", "subagent"] },
            subagent_type: { type: "string", nullable: true },
            status: {
              type: "string",
              enum: ["working", "waiting", "completed", "error"],
            },
            task: { type: "string", nullable: true },
            current_tool: { type: "string", nullable: true },
            started_at: { type: "string", format: "date-time" },
            ended_at: { type: "string", format: "date-time", nullable: true },
            parent_agent_id: { type: "string", nullable: true },
            metadata: {
              type: "string",
              nullable: true,
              description: "JSON-encoded agent metadata",
            },
            updated_at: { type: "string", format: "date-time" },
            awaiting_input_since: {
              type: "string",
              format: "date-time",
              nullable: true,
              description:
                "ISO timestamp set when this agent is blocked waiting for user input. Cleared on the next non-Notification hook event for the session.",
            },
            awaiting_reason: {
              type: "string",
              enum: ["notification", "stop", "session_start", "interrupted"],
              nullable: true,
              description:
                "Why this agent is awaiting input: 'notification' (Claude asked the user something), 'stop' (turn completed, waiting for the next prompt), 'session_start' (new/resumed session waiting for the first prompt), or 'interrupted' (watchdog/Esc recovery). Null when not waiting; cleared alongside awaiting_input_since.",
            },
          },
        },
        DashboardEvent: {
          type: "object",
          required: ["session_id", "event_type", "created_at"],
          properties: {
            id: { type: "integer", nullable: true },
            session_id: { type: "string" },
            agent_id: { type: "string", nullable: true },
            event_type: { type: "string" },
            tool_name: { type: "string", nullable: true },
            summary: { type: "string", nullable: true },
            data: {
              type: "string",
              nullable: true,
              description: "JSON-encoded event payload",
            },
            created_at: { type: "string", format: "date-time" },
          },
        },
        HealthResponse: {
          type: "object",
          required: ["status", "timestamp", "version"],
          properties: {
            status: { type: "string", enum: ["ok"] },
            version: {
              type: "string",
              description: "Dashboard release version from package.json",
              example: "1.4.6",
            },
            timestamp: { type: "string", format: "date-time" },
          },
        },
        SessionsListResponse: {
          type: "object",
          required: ["sessions", "limit", "offset", "total"],
          properties: {
            sessions: { type: "array", items: { $ref: "#/components/schemas/Session" } },
            limit: { type: "integer" },
            offset: { type: "integer" },
            total: {
              type: "integer",
              description:
                "Total sessions matching the filters (independent of limit/offset). Used by paginators.",
            },
          },
        },
        SessionCreateRequest: {
          type: "object",
          required: ["id"],
          properties: {
            id: { type: "string" },
            name: { type: "string" },
            cwd: { type: "string" },
            model: { type: "string" },
            metadata: {
              type: "object",
              additionalProperties: true,
            },
          },
        },
        SessionCreateResponse: {
          type: "object",
          required: ["session", "created"],
          properties: {
            session: { $ref: "#/components/schemas/Session" },
            created: { type: "boolean" },
          },
        },
        SessionDetailResponse: {
          type: "object",
          required: ["session", "agents", "events"],
          properties: {
            session: { $ref: "#/components/schemas/Session" },
            agents: { type: "array", items: { $ref: "#/components/schemas/Agent" } },
            events: { type: "array", items: { $ref: "#/components/schemas/DashboardEvent" } },
          },
        },
        TranscriptInfo: {
          type: "object",
          required: ["id", "name", "type", "has_transcript"],
          properties: {
            id: {
              type: "string",
              description:
                "'main' for the session's main transcript, otherwise a subagent or compaction id.",
            },
            name: { type: "string" },
            type: { type: "string", enum: ["main", "subagent", "compaction"] },
            subagent_type: { type: "string", nullable: true },
            has_transcript: { type: "boolean" },
            db_agent_id: {
              type: "string",
              nullable: true,
              description:
                "Foreign key into agents.id when this transcript belongs to a tracked subagent.",
            },
          },
        },
        TranscriptListResponse: {
          type: "object",
          required: ["transcripts"],
          properties: {
            transcripts: {
              type: "array",
              items: { $ref: "#/components/schemas/TranscriptInfo" },
            },
          },
        },
        TranscriptContent: {
          type: "object",
          required: ["type"],
          properties: {
            type: {
              type: "string",
              enum: ["text", "tool_use", "tool_result", "thinking"],
            },
            text: { type: "string" },
            name: { type: "string", description: "Tool name when type === tool_use." },
            id: {
              type: "string",
              description: "Tool-use id used to pair tool_use with tool_result.",
            },
            input: {
              description: "Tool input payload (object) or { _truncated: string } when oversized.",
              oneOf: [{ type: "object", additionalProperties: true }, { type: "string" }],
            },
            output: { type: "string", description: "Tool output text when type === tool_result." },
            is_error: { type: "boolean" },
          },
        },
        TranscriptMessage: {
          type: "object",
          required: ["type", "content"],
          properties: {
            type: {
              type: "string",
              enum: ["user", "assistant", "session_event"],
              description:
                "Raw line type. session_event is a synthetic marker (e.g. a /rename) injected by the server, not a real transcript line. Mid-turn queued messages (queued_command attachments) surface as type user.",
            },
            sender: {
              type: "string",
              enum: ["user", "assistant", "orchestrator", "system", "tool"],
              description:
                "TRUE sender of the line: the human (user), the agent (assistant), a subagent's task assigned by its parent (orchestrator), a harness injection such as a task-notification or [SYSTEM NOTIFICATION …] banner (system), or a tool_result echoed on a user line (tool).",
            },
            timestamp: { type: "string", format: "date-time", nullable: true },
            content: {
              type: "array",
              items: { $ref: "#/components/schemas/TranscriptContent" },
            },
            line: {
              type: "integer",
              minimum: 1,
              description: "JSONL line number the message was parsed from (pagination cursor).",
            },
            model: { type: "string" },
            usage: {
              type: "object",
              properties: {
                input_tokens: { type: "integer", minimum: 0 },
                output_tokens: { type: "integer", minimum: 0 },
                cache_read_input_tokens: { type: "integer", minimum: 0 },
                cache_creation_input_tokens: { type: "integer", minimum: 0 },
              },
            },
            event_kind: {
              type: "string",
              enum: ["rename"],
              description: "For type === session_event: the TUI action this marker represents.",
            },
            title: {
              type: "string",
              description: "For type === session_event: the new session title.",
            },
          },
        },
        TranscriptResponse: {
          type: "object",
          required: ["messages", "total", "has_more", "last_line", "first_line"],
          properties: {
            messages: {
              type: "array",
              items: { $ref: "#/components/schemas/TranscriptMessage" },
            },
            total: {
              type: "integer",
              minimum: 0,
              description: "Total messages available in the transcript.",
            },
            has_more: {
              type: "boolean",
              description: "True when older messages remain (use `before` to load them).",
            },
            last_line: {
              type: "integer",
              minimum: 0,
              description:
                "JSONL line number of the newest returned message — pass back as `after` for incremental fetches.",
            },
            first_line: {
              type: "integer",
              minimum: 0,
              description:
                "JSONL line number of the oldest returned message — pass back as `before` to page backwards.",
            },
          },
        },
        SessionStatsResponse: {
          type: "object",
          description:
            "Aggregated counts powering the SessionOverview panel on the Session Detail page. All aggregation runs in SQL.",
          required: [
            "session_id",
            "total_events",
            "events_by_type",
            "tools_used",
            "error_count",
            "first_event_at",
            "last_event_at",
            "agents",
            "subagent_types",
            "tokens",
          ],
          properties: {
            session_id: { type: "string" },
            total_events: { type: "integer", minimum: 0 },
            events_by_type: {
              type: "array",
              items: {
                type: "object",
                required: ["event_type", "count"],
                properties: {
                  event_type: { type: "string" },
                  count: { type: "integer", minimum: 0 },
                },
              },
            },
            tools_used: {
              type: "array",
              description: "Top 15 tools used in this session, sorted by count descending.",
              items: {
                type: "object",
                required: ["tool_name", "count"],
                properties: {
                  tool_name: { type: "string" },
                  count: { type: "integer", minimum: 0 },
                },
              },
            },
            error_count: {
              type: "integer",
              minimum: 0,
              description:
                "Events whose event_type or summary matches /error/i or /failed/i (case-insensitive).",
            },
            first_event_at: { type: "string", format: "date-time", nullable: true },
            last_event_at: { type: "string", format: "date-time", nullable: true },
            agents: {
              type: "object",
              required: ["total", "main", "subagent", "compaction", "by_status"],
              properties: {
                total: { type: "integer", minimum: 0 },
                main: { type: "integer", minimum: 0 },
                subagent: { type: "integer", minimum: 0 },
                compaction: { type: "integer", minimum: 0 },
                by_status: {
                  type: "object",
                  additionalProperties: { type: "integer", minimum: 0 },
                },
              },
            },
            subagent_types: {
              type: "array",
              description:
                "Subagent types in this session with counts. Excludes the special 'compaction' type which is surfaced via agents.compaction.",
              items: {
                type: "object",
                required: ["subagent_type", "count"],
                properties: {
                  subagent_type: { type: "string" },
                  count: { type: "integer", minimum: 0 },
                },
              },
            },
            tokens: {
              type: "object",
              required: [
                "input_tokens",
                "output_tokens",
                "cache_read_tokens",
                "cache_write_tokens",
              ],
              properties: {
                input_tokens: { type: "integer", minimum: 0 },
                output_tokens: { type: "integer", minimum: 0 },
                cache_read_tokens: { type: "integer", minimum: 0 },
                cache_write_tokens: { type: "integer", minimum: 0 },
              },
            },
          },
        },
        SessionUpdateRequest: {
          type: "object",
          properties: {
            name: { type: "string" },
            status: { type: "string", enum: ["active", "completed", "error", "abandoned"] },
            ended_at: { type: "string", format: "date-time" },
            metadata: { type: "object", additionalProperties: true },
          },
        },
        SessionUpdateResponse: {
          type: "object",
          required: ["session"],
          properties: { session: { $ref: "#/components/schemas/Session" } },
        },
        AgentsListResponse: {
          type: "object",
          required: ["agents", "limit", "offset"],
          properties: {
            agents: { type: "array", items: { $ref: "#/components/schemas/Agent" } },
            limit: { type: "integer" },
            offset: { type: "integer" },
          },
        },
        AgentCreateRequest: {
          type: "object",
          required: ["id", "session_id", "name"],
          properties: {
            id: { type: "string" },
            session_id: { type: "string" },
            name: { type: "string" },
            type: { type: "string", enum: ["main", "subagent"] },
            subagent_type: { type: "string" },
            status: {
              type: "string",
              enum: ["working", "waiting", "completed", "error"],
            },
            task: { type: "string" },
            parent_agent_id: { type: "string" },
            metadata: { type: "object", additionalProperties: true },
          },
        },
        AgentCreateResponse: {
          type: "object",
          required: ["agent", "created"],
          properties: {
            agent: { $ref: "#/components/schemas/Agent" },
            created: { type: "boolean" },
          },
        },
        AgentDetailResponse: {
          type: "object",
          required: ["agent"],
          properties: { agent: { $ref: "#/components/schemas/Agent" } },
        },
        AgentUpdateRequest: {
          type: "object",
          properties: {
            name: { type: "string" },
            status: {
              type: "string",
              enum: ["working", "waiting", "completed", "error"],
            },
            task: { type: "string" },
            current_tool: { type: "string", nullable: true },
            ended_at: { type: "string", format: "date-time" },
            metadata: { type: "object", additionalProperties: true },
          },
        },
        AgentUpdateResponse: {
          type: "object",
          required: ["agent"],
          properties: { agent: { $ref: "#/components/schemas/Agent" } },
        },
        EventsListResponse: {
          type: "object",
          required: ["events", "limit", "offset", "total"],
          properties: {
            events: {
              type: "array",
              items: { $ref: "#/components/schemas/DashboardEvent" },
            },
            limit: { type: "integer" },
            offset: { type: "integer" },
            total: {
              type: "integer",
              description: "Total rows matching the current filter (for UI pagination)",
            },
          },
        },
        EventsFacetsResponse: {
          type: "object",
          required: ["event_types", "tool_names"],
          properties: {
            event_types: { type: "array", items: { type: "string" } },
            tool_names: { type: "array", items: { type: "string" } },
          },
        },
        StatsResponse: {
          type: "object",
          required: [
            "total_sessions",
            "active_sessions",
            "active_agents",
            "total_agents",
            "total_events",
            "events_today",
            "ws_connections",
            "agents_by_status",
            "sessions_by_status",
          ],
          properties: {
            total_sessions: { type: "integer" },
            active_sessions: { type: "integer" },
            active_agents: { type: "integer" },
            total_agents: { type: "integer" },
            total_events: { type: "integer" },
            events_today: { type: "integer" },
            ws_connections: { type: "integer" },
            agents_by_status: { $ref: "#/components/schemas/CountMap" },
            sessions_by_status: { $ref: "#/components/schemas/CountMap" },
          },
        },
        AnalyticsResponse: {
          type: "object",
          required: [
            "tokens",
            "tool_usage",
            "daily_events",
            "daily_sessions",
            "agent_types",
            "event_types",
            "avg_events_per_session",
            "total_subagents",
            "overview",
            "agents_by_status",
            "sessions_by_status",
          ],
          properties: {
            tokens: {
              type: "object",
              required: ["total_input", "total_output", "total_cache_read", "total_cache_write"],
              properties: {
                total_input: { type: "integer" },
                total_output: { type: "integer" },
                total_cache_read: { type: "integer" },
                total_cache_write: { type: "integer" },
              },
            },
            tool_usage: {
              type: "array",
              items: {
                type: "object",
                required: ["tool_name", "count"],
                properties: { tool_name: { type: "string" }, count: { type: "integer" } },
              },
            },
            daily_events: {
              type: "array",
              items: {
                type: "object",
                required: ["date", "count"],
                properties: { date: { type: "string" }, count: { type: "integer" } },
              },
            },
            daily_sessions: {
              type: "array",
              items: {
                type: "object",
                required: ["date", "count"],
                properties: { date: { type: "string" }, count: { type: "integer" } },
              },
            },
            agent_types: {
              type: "array",
              items: {
                type: "object",
                required: ["subagent_type", "count"],
                properties: {
                  subagent_type: { type: "string", nullable: true },
                  count: { type: "integer" },
                },
              },
            },
            event_types: {
              type: "array",
              items: {
                type: "object",
                required: ["event_type", "count"],
                properties: { event_type: { type: "string" }, count: { type: "integer" } },
              },
            },
            avg_events_per_session: { type: "number" },
            total_subagents: { type: "integer" },
            overview: {
              type: "object",
              required: [
                "total_sessions",
                "active_sessions",
                "active_agents",
                "total_agents",
                "total_events",
              ],
              properties: {
                total_sessions: { type: "integer" },
                active_sessions: { type: "integer" },
                active_agents: { type: "integer" },
                total_agents: { type: "integer" },
                total_events: { type: "integer" },
              },
            },
            agents_by_status: { $ref: "#/components/schemas/CountMap" },
            sessions_by_status: { $ref: "#/components/schemas/CountMap" },
          },
        },
        HookEventRequest: {
          type: "object",
          required: ["hook_type", "data"],
          properties: {
            hook_type: {
              type: "string",
              description:
                "Hook type from Claude Code (common values: PreToolUse, PostToolUse, Stop, SubagentStop, Notification, SessionStart, SessionEnd)",
            },
            data: {
              type: "object",
              required: ["session_id"],
              properties: {
                session_id: { type: "string" },
                tool_name: { type: "string" },
                transcript_path: { type: "string" },
              },
              additionalProperties: true,
            },
          },
        },
        HookEventResponse: {
          type: "object",
          required: ["ok", "event"],
          properties: {
            ok: { type: "boolean", enum: [true] },
            event: { $ref: "#/components/schemas/DashboardEvent" },
          },
        },
        PricingRule: {
          type: "object",
          required: [
            "model_pattern",
            "display_name",
            "input_per_mtok",
            "output_per_mtok",
            "cache_read_per_mtok",
            "cache_write_per_mtok",
            "cache_write_1h_per_mtok",
            "updated_at",
          ],
          properties: {
            model_pattern: { type: "string" },
            display_name: { type: "string" },
            input_per_mtok: { type: "number" },
            output_per_mtok: { type: "number" },
            cache_read_per_mtok: { type: "number" },
            cache_write_per_mtok: { type: "number", description: "5m ephemeral cache-write rate" },
            cache_write_1h_per_mtok: {
              type: "number",
              description: "1h ephemeral cache-write rate",
            },
            fast_input_per_mtok: { type: "number", description: "Fast-mode input rate (0 = none)" },
            fast_output_per_mtok: {
              type: "number",
              description: "Fast-mode output rate (0 = none)",
            },
            updated_at: { type: "string", format: "date-time" },
          },
        },
        PricingUpsertRequest: {
          type: "object",
          required: ["model_pattern", "display_name"],
          properties: {
            model_pattern: { type: "string" },
            display_name: { type: "string" },
            input_per_mtok: { type: "number" },
            output_per_mtok: { type: "number" },
            cache_read_per_mtok: { type: "number" },
            cache_write_per_mtok: { type: "number", description: "5m ephemeral cache-write rate" },
            cache_write_1h_per_mtok: {
              type: "number",
              description: "1h ephemeral cache-write rate",
            },
            fast_input_per_mtok: { type: "number", description: "Fast-mode input rate (0 = none)" },
            fast_output_per_mtok: {
              type: "number",
              description: "Fast-mode output rate (0 = none)",
            },
          },
        },
        PricingListResponse: {
          type: "object",
          required: ["pricing"],
          properties: {
            pricing: { type: "array", items: { $ref: "#/components/schemas/PricingRule" } },
          },
        },
        PricingUpsertResponse: {
          type: "object",
          required: ["pricing"],
          properties: { pricing: { $ref: "#/components/schemas/PricingRule" } },
        },
        CostBreakdownItem: {
          type: "object",
          required: [
            "model",
            "input_tokens",
            "output_tokens",
            "cache_read_tokens",
            "cache_write_tokens",
            "cost",
            "matched_rule",
          ],
          properties: {
            model: { type: "string" },
            speed: { type: "string" },
            inference_geo: { type: "string" },
            service_tier: { type: "string" },
            input_tokens: { type: "integer" },
            output_tokens: { type: "integer" },
            cache_read_tokens: { type: "integer" },
            cache_write_tokens: { type: "integer" },
            cache_write_1h_tokens: { type: "integer" },
            web_search_requests: { type: "integer" },
            web_fetch_requests: { type: "integer" },
            code_execution_requests: { type: "integer" },
            cost: { type: "number" },
            matched_rule: { type: "string", nullable: true },
          },
        },
        DailyCostItem: {
          type: "object",
          required: ["date", "cost"],
          properties: {
            date: { type: "string", format: "date" },
            cost: { type: "number" },
          },
        },
        CostResult: {
          type: "object",
          required: ["total_cost", "breakdown", "daily_costs"],
          properties: {
            total_cost: { type: "number" },
            breakdown: { type: "array", items: { $ref: "#/components/schemas/CostBreakdownItem" } },
            daily_costs: { type: "array", items: { $ref: "#/components/schemas/DailyCostItem" } },
            feature_costs: {
              type: "object",
              description: "Server-tool surcharges separate from token cost",
              properties: {
                web_search_cost: { type: "number" },
                web_fetch_cost: { type: "number" },
                code_execution_cost: { type: "number" },
                code_execution_hours_estimated: { type: "number" },
                code_execution_free_hours: { type: "number" },
              },
            },
            unpriced_models: {
              type: "array",
              description: "Models with usage but no matching pricing rule (cost not counted)",
              items: {
                type: "object",
                properties: {
                  model: { type: "string" },
                  input_tokens: { type: "integer" },
                  output_tokens: { type: "integer" },
                  cache_read_tokens: { type: "integer" },
                  cache_write_tokens: { type: "integer" },
                },
              },
            },
          },
        },
        DeleteOkResponse: {
          type: "object",
          required: ["ok"],
          properties: { ok: { type: "boolean", enum: [true] } },
        },
        WorkflowAggregateResponse: {
          type: "object",
          required: [
            "stats",
            "orchestration",
            "toolFlow",
            "effectiveness",
            "patterns",
            "modelDelegation",
            "errorPropagation",
            "concurrency",
            "complexity",
            "compaction",
            "cooccurrence",
          ],
          properties: {
            stats: {
              type: "object",
              required: [
                "totalSessions",
                "totalAgents",
                "totalSubagents",
                "avgSubagents",
                "successRate",
                "avgDepth",
                "avgDurationSec",
                "totalCompactions",
                "avgCompactions",
              ],
              properties: {
                totalSessions: { type: "integer" },
                totalAgents: { type: "integer" },
                totalSubagents: { type: "integer" },
                avgSubagents: { type: "number" },
                successRate: { type: "number" },
                avgDepth: { type: "number" },
                avgDurationSec: { type: "integer" },
                totalCompactions: { type: "integer" },
                avgCompactions: { type: "number" },
                topFlow: {
                  type: "object",
                  nullable: true,
                  properties: {
                    source: { type: "string" },
                    target: { type: "string" },
                    count: { type: "integer" },
                  },
                },
              },
            },
            orchestration: { type: "object", additionalProperties: true },
            toolFlow: { type: "object", additionalProperties: true },
            effectiveness: { type: "array", items: { type: "object", additionalProperties: true } },
            patterns: { type: "object", additionalProperties: true },
            modelDelegation: { type: "object", additionalProperties: true },
            errorPropagation: { type: "object", additionalProperties: true },
            concurrency: { type: "object", additionalProperties: true },
            complexity: { type: "array", items: { type: "object", additionalProperties: true } },
            compaction: { type: "object", additionalProperties: true },
            cooccurrence: { type: "array", items: { type: "object", additionalProperties: true } },
          },
        },
        AgentTreeNode: {
          type: "object",
          required: ["id", "name", "type", "status", "children"],
          properties: {
            id: { type: "string" },
            name: { type: "string" },
            type: { type: "string", enum: ["main", "subagent"] },
            subagent_type: { type: "string", nullable: true },
            status: { type: "string" },
            task: { type: "string", nullable: true },
            started_at: { type: "string", format: "date-time" },
            ended_at: { type: "string", format: "date-time", nullable: true },
            children: {
              type: "array",
              items: { $ref: "#/components/schemas/AgentTreeNode" },
            },
          },
        },
        WorkflowSessionResponse: {
          type: "object",
          required: ["session", "tree", "toolTimeline", "swimLanes", "events"],
          properties: {
            session: { $ref: "#/components/schemas/Session" },
            tree: {
              type: "array",
              items: { $ref: "#/components/schemas/AgentTreeNode" },
            },
            toolTimeline: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  id: { type: "integer" },
                  tool_name: { type: "string" },
                  event_type: { type: "string" },
                  agent_id: { type: "string", nullable: true },
                  created_at: { type: "string", format: "date-time" },
                  summary: { type: "string", nullable: true },
                },
              },
            },
            swimLanes: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  id: { type: "string" },
                  name: { type: "string" },
                  type: { type: "string" },
                  subagent_type: { type: "string", nullable: true },
                  status: { type: "string" },
                  started_at: { type: "string", format: "date-time" },
                  ended_at: { type: "string", format: "date-time", nullable: true },
                  parent_agent_id: { type: "string", nullable: true },
                },
              },
            },
            events: {
              type: "array",
              items: { $ref: "#/components/schemas/DashboardEvent" },
            },
          },
        },
        SettingsInfoResponse: {
          type: "object",
          required: ["db", "hooks", "server", "transcript_cache"],
          properties: {
            db: {
              type: "object",
              required: ["path", "size", "counts"],
              properties: {
                path: { type: "string" },
                size: { type: "integer" },
                counts: {
                  type: "object",
                  additionalProperties: { type: "integer" },
                },
              },
            },
            hooks: {
              type: "object",
              required: ["installed", "path", "hooks"],
              properties: {
                installed: { type: "boolean" },
                path: { type: "string" },
                hooks: {
                  type: "object",
                  additionalProperties: { type: "boolean" },
                },
              },
            },
            server: {
              type: "object",
              required: ["version", "uptime", "node_version", "platform", "ws_connections"],
              properties: {
                version: {
                  type: "string",
                  description: "Dashboard release version from package.json",
                },
                uptime: { type: "number" },
                node_version: { type: "string" },
                platform: { type: "string" },
                ws_connections: { type: "integer" },
              },
            },
            transcript_cache: {
              type: "object",
              required: ["entries", "paths"],
              properties: {
                entries: { type: "integer" },
                paths: { type: "array", items: { type: "string" } },
              },
            },
          },
        },
        ClearDataResponse: {
          type: "object",
          required: ["ok", "cleared"],
          properties: {
            ok: { type: "boolean", enum: [true] },
            cleared: {
              type: "object",
              additionalProperties: { type: "integer" },
            },
          },
        },
        ReimportResponse: {
          type: "object",
          required: ["ok", "imported", "skipped", "errors"],
          properties: {
            ok: { type: "boolean", enum: [true] },
            imported: { type: "integer" },
            skipped: { type: "integer" },
            errors: { type: "integer" },
          },
        },
        ImportGuideResponse: {
          type: "object",
          properties: {
            platform: { type: "string" },
            default_projects_dir: { type: "string" },
            default_projects_dir_display: { type: "string" },
            default_projects_dir_exists: { type: "boolean" },
            default_projects_dir_stats: {
              type: "object",
              properties: {
                projects: { type: "integer" },
                jsonl_files: { type: "integer" },
              },
            },
            archive_command: { type: "string" },
            supported_extensions: { type: "array", items: { type: "string" } },
            max_upload_bytes: { type: "integer" },
            max_upload_files: { type: "integer" },
            steps: {
              type: "array",
              items: {
                type: "object",
                properties: {
                  id: { type: "string" },
                  title: { type: "string" },
                  body: { type: "string" },
                },
              },
            },
          },
        },
        ImportResultResponse: {
          type: "object",
          required: ["ok", "source", "imported", "skipped", "errors"],
          properties: {
            ok: { type: "boolean", enum: [true] },
            source: { type: "string", enum: ["default", "path", "upload"] },
            path: { type: "string", nullable: true },
            imported: { type: "integer" },
            backfilled: { type: "integer" },
            skipped: { type: "integer" },
            errors: { type: "integer" },
            sessions_seen: { type: "integer" },
            files_scanned: { type: "integer" },
            files_received: { type: "integer" },
            rejected_files: { type: "array", items: { type: "string" } },
            entries_extracted: { type: "integer" },
            entries_skipped: { type: "integer" },
          },
        },
        ReinstallHooksResponse: {
          type: "object",
          required: ["ok", "hooks"],
          properties: {
            ok: { type: "boolean" },
            hooks: {
              type: "object",
              required: ["installed", "path", "hooks"],
              properties: {
                installed: { type: "boolean" },
                path: { type: "string" },
                hooks: {
                  type: "object",
                  additionalProperties: { type: "boolean" },
                },
              },
            },
          },
        },
        ResetPricingResponse: {
          type: "object",
          required: ["ok", "pricing"],
          properties: {
            ok: { type: "boolean", enum: [true] },
            pricing: { type: "array", items: { $ref: "#/components/schemas/PricingRule" } },
          },
        },
        ExportResponse: {
          type: "object",
          required: ["exported_at", "sessions", "agents", "events", "token_usage", "model_pricing"],
          properties: {
            format: {
              type: "string",
              description:
                'Bundle format marker (always "ccam-export" for exports from this version).',
              example: "ccam-export",
            },
            version: { type: "integer", description: "Bundle schema version.", example: 1 },
            exported_at: { type: "string", format: "date-time" },
            sessions: { type: "array", items: { $ref: "#/components/schemas/Session" } },
            agents: { type: "array", items: { $ref: "#/components/schemas/Agent" } },
            events: { type: "array", items: { $ref: "#/components/schemas/DashboardEvent" } },
            token_usage: {
              type: "array",
              items: {
                type: "object",
                additionalProperties: true,
              },
            },
            workflows: { type: "array", items: { type: "object", additionalProperties: true } },
            dashboard_runs: {
              type: "array",
              items: { type: "object", additionalProperties: true },
            },
            alert_rules: { type: "array", items: { type: "object", additionalProperties: true } },
            model_pricing: { type: "array", items: { $ref: "#/components/schemas/PricingRule" } },
          },
        },
        ImportResponse: {
          type: "object",
          required: [
            "ok",
            "sessions_imported",
            "sessions_skipped",
            "agents",
            "events",
            "token_usage",
            "workflows",
            "dashboard_runs",
            "alert_rules",
            "model_pricing",
            "errors",
          ],
          description:
            "Per-table counts from restoring an export bundle. Session-scoped counts are rows newly inserted; sessions_skipped counts sessions already present (skipped whole to stay idempotent).",
          properties: {
            ok: { type: "boolean" },
            source: { type: "string", description: "Uploaded filename or server-side path read." },
            format: {
              type: "string",
              nullable: true,
              description: "Bundle format marker, or null for a legacy export.",
            },
            sessions_imported: { type: "integer" },
            sessions_skipped: { type: "integer" },
            agents: { type: "integer" },
            events: { type: "integer" },
            token_usage: { type: "integer" },
            workflows: { type: "integer" },
            dashboard_runs: { type: "integer" },
            alert_rules: { type: "integer" },
            model_pricing: { type: "integer" },
            errors: { type: "integer" },
          },
        },
        CleanupRequest: {
          type: "object",
          properties: {
            abandon_hours: {
              type: "number",
              minimum: 0,
              description: "Mark active sessions abandoned if stale for this many hours",
            },
            purge_days: {
              type: "number",
              minimum: 0,
              description:
                "Delete old completed/error/abandoned sessions older than this many days",
            },
          },
        },
        CleanupResponse: {
          type: "object",
          required: ["ok", "abandoned", "purged_sessions", "purged_events", "purged_agents"],
          properties: {
            ok: { type: "boolean", enum: [true] },
            abandoned: { type: "integer" },
            purged_sessions: { type: "integer" },
            purged_events: { type: "integer" },
            purged_agents: { type: "integer" },
          },
        },
      },
    },
    paths: {
      "/api/health": {
        get: {
          tags: ["Health"],
          summary: "Health check",
          operationId: "getHealth",
          responses: {
            200: {
              description: "Service is healthy",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/HealthResponse" },
                },
              },
            },
          },
        },
      },
      "/api/sessions": {
        get: {
          tags: ["Sessions"],
          summary: "List sessions",
          description:
            "Returns a paginated list of sessions with agent counts and per-session cost. Status filter, search, and pagination compose. Cost computation runs over the returned page only — independent of total session count.",
          operationId: "listSessions",
          parameters: [
            { $ref: "#/components/parameters/SessionStatusQuery" },
            {
              name: "q",
              in: "query",
              schema: { type: "string" },
              description:
                "Case-insensitive search across `id` / `name` / `cwd`. Composes with the status filter when both are present.",
            },
            { $ref: "#/components/parameters/LimitQuery" },
            { $ref: "#/components/parameters/OffsetQuery" },
          ],
          responses: {
            200: {
              description: "Session list",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SessionsListResponse" },
                },
              },
            },
          },
        },
        post: {
          tags: ["Sessions"],
          summary: "Create session (idempotent)",
          operationId: "createSession",
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/SessionCreateRequest" },
              },
            },
          },
          responses: {
            201: {
              description: "Session created",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SessionCreateResponse" },
                },
              },
            },
            200: {
              description: "Session already exists",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SessionCreateResponse" },
                },
              },
            },
            400: {
              description: "Invalid request body",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/sessions/{id}": {
        get: {
          tags: ["Sessions"],
          summary: "Get session details",
          operationId: "getSession",
          parameters: [{ $ref: "#/components/parameters/SessionIdPath" }],
          responses: {
            200: {
              description: "Session with associated agents/events",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SessionDetailResponse" },
                },
              },
            },
            404: {
              description: "Session not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
        patch: {
          tags: ["Sessions"],
          summary: "Update session",
          operationId: "updateSession",
          parameters: [{ $ref: "#/components/parameters/SessionIdPath" }],
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/SessionUpdateRequest" },
              },
            },
          },
          responses: {
            200: {
              description: "Session updated",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SessionUpdateResponse" },
                },
              },
            },
            404: {
              description: "Session not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/sessions/{id}/stats": {
        get: {
          tags: ["Sessions"],
          summary: "Get aggregated session stats",
          description:
            "Returns aggregated counts for the SessionOverview panel: events, events-by-type, top tool usage, error count, agent type/status counts, subagent type breakdown, and token totals. All aggregation runs in SQL — cheap to call even for sessions with tens of thousands of events. Frontend debounces calls to this endpoint on `new_event` / `agent_*` / `session_updated` websocket frames so counters track the running session.",
          operationId: "getSessionStats",
          parameters: [{ $ref: "#/components/parameters/SessionIdPath" }],
          responses: {
            200: {
              description: "Aggregated session stats",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SessionStatsResponse" },
                },
              },
            },
            404: {
              description: "Session not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/sessions/{id}/transcripts": {
        get: {
          tags: ["Sessions"],
          summary: "List available transcripts for a session",
          description:
            "Lists every JSONL transcript file associated with a session — the main agent's transcript plus any subagent and compaction transcripts. Used by the Conversation tab on the Session Detail page to populate the transcript switcher.",
          operationId: "listSessionTranscripts",
          parameters: [{ $ref: "#/components/parameters/SessionIdPath" }],
          responses: {
            200: {
              description: "List of transcripts available for the session",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/TranscriptListResponse" },
                },
              },
            },
            404: {
              description: "Session not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/sessions/{id}/transcript": {
        get: {
          tags: ["Sessions"],
          summary: "Stream messages from a specific transcript",
          description:
            "Returns parsed messages from a JSONL transcript with cursor-based pagination. Pass `agent_id` to select a specific subagent or compaction transcript. The frontend uses `after` for incremental live updates on `new_event` and `before` to load older messages on scroll-up.",
          operationId: "getSessionTranscript",
          parameters: [
            { $ref: "#/components/parameters/SessionIdPath" },
            {
              name: "agent_id",
              in: "query",
              schema: { type: "string" },
              description:
                "Transcript identifier — 'main' for the session's main transcript, or a subagent / compaction id from /transcripts.",
            },
            {
              name: "limit",
              in: "query",
              schema: { type: "integer", default: 50, minimum: 1, maximum: 500 },
              description: "Maximum number of messages to return.",
            },
            {
              name: "offset",
              in: "query",
              schema: { type: "integer", minimum: 0 },
              description:
                "Offset from the start of the transcript (mutually exclusive with after/before).",
            },
            {
              name: "after",
              in: "query",
              schema: { type: "integer", minimum: 0 },
              description:
                "Only return messages whose JSONL line number is strictly greater than this value. Used for incremental live updates.",
            },
            {
              name: "before",
              in: "query",
              schema: { type: "integer", minimum: 0 },
              description:
                "Only return messages whose JSONL line number is strictly less than this value. Used to load older messages on scroll-up.",
            },
          ],
          responses: {
            200: {
              description: "Parsed messages with cursor metadata",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/TranscriptResponse" },
                },
              },
            },
            404: {
              description: "Session or transcript not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/agents": {
        get: {
          tags: ["Agents"],
          summary: "List agents",
          operationId: "listAgents",
          parameters: [
            { $ref: "#/components/parameters/AgentStatusQuery" },
            { $ref: "#/components/parameters/SessionFilterQuery" },
            { $ref: "#/components/parameters/LimitQuery" },
            { $ref: "#/components/parameters/OffsetQuery" },
          ],
          responses: {
            200: {
              description: "Agent list",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/AgentsListResponse" },
                },
              },
            },
          },
        },
        post: {
          tags: ["Agents"],
          summary: "Create agent (idempotent)",
          operationId: "createAgent",
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/AgentCreateRequest" },
              },
            },
          },
          responses: {
            201: {
              description: "Agent created",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/AgentCreateResponse" },
                },
              },
            },
            200: {
              description: "Agent already exists",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/AgentCreateResponse" },
                },
              },
            },
            400: {
              description: "Invalid request body",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/agents/{id}": {
        get: {
          tags: ["Agents"],
          summary: "Get agent",
          operationId: "getAgent",
          parameters: [{ $ref: "#/components/parameters/AgentIdPath" }],
          responses: {
            200: {
              description: "Agent details",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/AgentDetailResponse" },
                },
              },
            },
            404: {
              description: "Agent not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
        patch: {
          tags: ["Agents"],
          summary: "Update agent",
          operationId: "updateAgent",
          parameters: [{ $ref: "#/components/parameters/AgentIdPath" }],
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/AgentUpdateRequest" },
              },
            },
          },
          responses: {
            200: {
              description: "Agent updated",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/AgentUpdateResponse" },
                },
              },
            },
            404: {
              description: "Agent not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/events": {
        get: {
          tags: ["Events"],
          summary: "List events with multi-dimensional filtering",
          operationId: "listEvents",
          parameters: [
            {
              in: "query",
              name: "event_type",
              description: "Comma-separated event_type values (e.g. Stop,PreToolUse)",
              schema: { type: "string" },
            },
            {
              in: "query",
              name: "tool_name",
              description: "Comma-separated tool_name values (e.g. Bash,Edit)",
              schema: { type: "string" },
            },
            {
              in: "query",
              name: "agent_id",
              description: "Comma-separated agent_id values",
              schema: { type: "string" },
            },
            {
              in: "query",
              name: "session_id",
              description: "Comma-separated session_id values",
              schema: { type: "string" },
            },
            {
              in: "query",
              name: "q",
              description: "Text search across summary, tool_name, and data",
              schema: { type: "string" },
            },
            {
              in: "query",
              name: "from",
              description: "ISO datetime lower bound (inclusive) on created_at",
              schema: { type: "string", format: "date-time" },
            },
            {
              in: "query",
              name: "to",
              description: "ISO datetime upper bound (inclusive) on created_at",
              schema: { type: "string", format: "date-time" },
            },
            {
              in: "query",
              name: "limit",
              description: "Max rows to return (1-500, default 50)",
              schema: { type: "integer", minimum: 1, maximum: 500, default: 50 },
            },
            { $ref: "#/components/parameters/OffsetQuery" },
          ],
          responses: {
            200: {
              description: "Event list with total count for pagination",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/EventsListResponse" },
                },
              },
            },
          },
        },
      },
      "/api/events/facets": {
        get: {
          tags: ["Events"],
          summary: "Distinct event_type and tool_name values available in the DB",
          operationId: "listEventFacets",
          responses: {
            200: {
              description: "Facet values for populating filter dropdowns",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/EventsFacetsResponse" },
                },
              },
            },
          },
        },
      },
      "/api/stats": {
        get: {
          tags: ["Stats"],
          summary: "Get aggregate dashboard stats",
          operationId: "getStats",
          responses: {
            200: {
              description: "Statistics overview",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/StatsResponse" },
                },
              },
            },
          },
        },
      },
      "/api/metrics": {
        get: {
          tags: ["Metrics"],
          summary: "Prometheus metrics exposition",
          operationId: "getMetrics",
          description:
            "Exposes the dashboard's live counters (sessions/agents by status, " +
            "event and token totals, connected realtime clients, configured " +
            "remote sources, process uptime/memory, build version) in the " +
            "Prometheus text-exposition format (v0.0.4) for scraping into " +
            "Prometheus / Grafana. Read-only. Mounted under `/api`, so it honors " +
            "the optional `DASHBOARD_TOKEN` guard — point the scrape job's bearer " +
            "token at it when the server is bound to a LAN.",
          responses: {
            200: {
              description: "Prometheus text exposition (v0.0.4).",
              content: {
                "text/plain": {
                  schema: { type: "string" },
                  example:
                    "# HELP ccam_up 1 when the dashboard API is serving this scrape.\n" +
                    "# TYPE ccam_up gauge\n" +
                    "ccam_up 1\n" +
                    "# HELP ccam_sessions Number of sessions by lifecycle status.\n" +
                    "# TYPE ccam_sessions gauge\n" +
                    'ccam_sessions{status="active"} 3\n',
                },
              },
            },
          },
        },
      },
      "/api/analytics": {
        get: {
          tags: ["Analytics"],
          summary: "Get analytics aggregates",
          operationId: "getAnalytics",
          responses: {
            200: {
              description: "Analytics response",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/AnalyticsResponse" },
                },
              },
            },
          },
        },
      },
      "/api/hooks/event": {
        post: {
          tags: ["Hooks"],
          summary: "Ingest Claude Code hook event",
          operationId: "ingestHookEvent",
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/HookEventRequest" },
              },
            },
          },
          responses: {
            200: {
              description: "Event processed",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/HookEventResponse" },
                },
              },
            },
            400: {
              description: "Invalid hook payload",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/pricing": {
        get: {
          tags: ["Pricing"],
          summary: "List pricing rules",
          operationId: "listPricingRules",
          responses: {
            200: {
              description: "Pricing rules",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/PricingListResponse" },
                },
              },
            },
          },
        },
        put: {
          tags: ["Pricing"],
          summary: "Create/update pricing rule",
          operationId: "upsertPricingRule",
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/PricingUpsertRequest" },
              },
            },
          },
          responses: {
            200: {
              description: "Pricing rule stored",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/PricingUpsertResponse" },
                },
              },
            },
            400: {
              description: "Invalid request body",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/pricing/{pattern}": {
        delete: {
          tags: ["Pricing"],
          summary: "Delete pricing rule",
          operationId: "deletePricingRule",
          parameters: [{ $ref: "#/components/parameters/PatternPath" }],
          responses: {
            200: {
              description: "Rule deleted",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/DeleteOkResponse" },
                },
              },
            },
            404: {
              description: "Pricing rule not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/pricing/cost": {
        get: {
          tags: ["Pricing"],
          summary: "Get total token cost across all sessions",
          operationId: "getTotalCost",
          responses: {
            200: {
              description: "Cost result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/CostResult" },
                },
              },
            },
          },
        },
      },
      "/api/pricing/cost/{sessionId}": {
        get: {
          tags: ["Pricing"],
          summary: "Get token cost for one session",
          operationId: "getSessionCost",
          parameters: [
            {
              name: "sessionId",
              in: "path",
              required: true,
              schema: { type: "string" },
            },
          ],
          responses: {
            200: {
              description: "Session cost result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/CostResult" },
                },
              },
            },
          },
        },
      },
      "/api/workflows": {
        get: {
          tags: ["Workflows"],
          summary: "Get workflow intelligence aggregates",
          operationId: "getWorkflowIntelligence",
          parameters: [{ $ref: "#/components/parameters/WorkflowStatusQuery" }],
          responses: {
            200: {
              description: "Workflow aggregate data",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/WorkflowAggregateResponse" },
                },
              },
            },
            500: {
              description: "Internal server error",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/MessageErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/workflows/session/{id}": {
        get: {
          tags: ["Workflows"],
          summary: "Get workflow drill-in for one session",
          operationId: "getWorkflowSession",
          parameters: [{ $ref: "#/components/parameters/SessionIdPath" }],
          responses: {
            200: {
              description: "Workflow session detail",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/WorkflowSessionResponse" },
                },
              },
            },
            404: {
              description: "Session not found",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/MessageErrorResponse" },
                },
              },
            },
            500: {
              description: "Internal server error",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/MessageErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/info": {
        get: {
          tags: ["Settings"],
          summary: "Get system/database/hook diagnostics",
          operationId: "getSettingsInfo",
          responses: {
            200: {
              description: "Settings and diagnostics",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/SettingsInfoResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/clear-data": {
        post: {
          tags: ["Settings"],
          summary: "Delete all dashboard data",
          operationId: "clearData",
          responses: {
            200: {
              description: "Data cleared",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ClearDataResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/reimport": {
        post: {
          tags: ["Settings"],
          summary: "Re-import legacy sessions from ~/.claude",
          operationId: "reimportLegacySessions",
          responses: {
            200: {
              description: "Import completed",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ReimportResponse" },
                },
              },
            },
            500: {
              description: "Import failed",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/reinstall-hooks": {
        post: {
          tags: ["Settings"],
          summary: "Reinstall Claude Code hooks",
          operationId: "reinstallHooks",
          responses: {
            200: {
              description: "Hooks reinstall result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ReinstallHooksResponse" },
                },
              },
            },
            500: {
              description: "Hook installation failed",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ErrorResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/reset-pricing": {
        post: {
          tags: ["Settings"],
          summary: "Reset pricing table to defaults",
          operationId: "resetPricing",
          responses: {
            200: {
              description: "Pricing defaults restored",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ResetPricingResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/export": {
        get: {
          tags: ["Settings"],
          summary: "Export all dashboard data as JSON",
          operationId: "exportData",
          responses: {
            200: {
              description: "Export payload (served as attachment)",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ExportResponse" },
                },
              },
            },
          },
        },
      },
      "/api/settings/cleanup": {
        post: {
          tags: ["Settings"],
          summary: "Abandon stale sessions and optionally purge old history",
          operationId: "cleanupData",
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: { $ref: "#/components/schemas/CleanupRequest" },
              },
            },
          },
          responses: {
            200: {
              description: "Cleanup result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/CleanupResponse" },
                },
              },
            },
          },
        },
      },
      "/api/import/guide": {
        get: {
          tags: ["Import"],
          summary: "Import guide with OS-aware defaults and step-by-step instructions",
          operationId: "importGuide",
          responses: {
            200: {
              description: "Guide payload",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ImportGuideResponse" },
                },
              },
            },
          },
        },
      },
      "/api/import/rescan": {
        post: {
          tags: ["Import"],
          summary: "Rescan the default ~/.claude/projects directory",
          operationId: "importRescan",
          responses: {
            200: {
              description: "Import result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ImportResultResponse" },
                },
              },
            },
            500: {
              description: "Import failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/import/scan-path": {
        post: {
          tags: ["Import"],
          summary: "Import transcripts from an arbitrary absolute directory",
          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.",
                    },
                  },
                },
              },
            },
          },
          responses: {
            200: {
              description: "Import result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ImportResultResponse" },
                },
              },
            },
            400: {
              description: "Path validation failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
            500: {
              description: "Import failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/import/upload": {
        post: {
          tags: ["Import"],
          summary: "Upload JSONL files or archives (.zip, .tar, .tar.gz, .tgz, .gz)",
          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.",
                    },
                  },
                },
              },
            },
          },
          responses: {
            200: {
              description: "Import result",
              content: {
                "application/json": {
                  schema: { $ref: "#/components/schemas/ImportResultResponse" },
                },
              },
            },
            400: {
              description: "No files or no JSONL content",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
            413: {
              description: "Extraction limit exceeded (possible zip bomb)",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
            500: {
              description: "Upload or import failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/updates/status": {
        get: {
          tags: ["Updates"],
          summary: "Check whether the dashboard git checkout is behind origin",
          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.",
                  },
                },
              },
            },
            500: {
              description: "Update status query failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/updates/check": {
        post: {
          tags: ["Updates"],
          summary: "Run an update check immediately and broadcast the result",
          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.",
                  },
                },
              },
            },
            500: {
              description: "Update check failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/updates/apply": {
        post: {
          tags: ["Updates"],
          summary: "Pull, rebuild, and restart the dashboard when fast-forwardable",
          operationId: "applyUpdate",
          responses: {
            200: {
              description:
                "Checkout fast-forwarded and rebuilt; the process is about to self-restart",
              content: {
                "application/json": {
                  schema: {
                    type: "object",
                    additionalProperties: true,
                    description: "Same shape as GET /api/updates/status, plus applied: true.",
                  },
                },
              },
            },
            409: {
              description:
                "Declined - nothing to apply, or the checkout isn't safely fast-forwardable",
              content: {
                "application/json": {
                  schema: {
                    type: "object",
                    additionalProperties: true,
                    description:
                      'applied: false plus reason: "up_to_date" | "not_fast_forwardable".',
                  },
                },
              },
            },
            500: {
              description: "Update apply failed",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/alerts": {
        get: {
          tags: ["Alerts"],
          summary: "List fired alerts, newest first",
          operationId: "listAlerts",
          parameters: [
            { $ref: "#/components/parameters/LimitQuery" },
            { $ref: "#/components/parameters/OffsetQuery" },
            {
              name: "unacked",
              in: "query",
              required: false,
              schema: { type: "boolean" },
              description: "When true, return only unacknowledged alerts",
            },
          ],
          responses: {
            200: {
              description: "Paginated alert feed with total and unacked counts",
              content: {
                "application/json": {
                  schema: {
                    type: "object",
                    additionalProperties: true,
                    description: "Includes alerts[], total, unacked, limit, offset.",
                  },
                },
              },
            },
          },
        },
      },
      "/api/alerts/rules": {
        get: {
          tags: ["Alerts"],
          summary: "List alert rules",
          operationId: "listAlertRules",
          responses: {
            200: {
              description: "All alert rules with parsed config objects",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
          },
        },
        post: {
          tags: ["Alerts"],
          summary: "Create an alert rule",
          operationId: "createAlertRule",
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: {
                  type: "object",
                  required: ["name", "rule_type", "config"],
                  properties: {
                    name: { type: "string" },
                    rule_type: {
                      type: "string",
                      enum: ["event_pattern", "inactivity", "status_duration", "token_threshold"],
                    },
                    config: {
                      type: "object",
                      additionalProperties: true,
                      description:
                        "Type-specific config. event_pattern: event_type/tool_name/summary_contains + optional count/window_minutes. inactivity: minutes. status_duration: status + minutes. token_threshold: total_tokens.",
                    },
                    enabled: { type: "boolean", default: true },
                    cooldown_seconds: { type: "integer", default: 300 },
                  },
                },
              },
            },
          },
          responses: {
            201: {
              description: "Created rule",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
            400: {
              description: "Validation error",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/alerts/rules/{id}": {
        patch: {
          tags: ["Alerts"],
          summary: "Update an alert rule (partial; rule_type is immutable)",
          operationId: "updateAlertRule",
          parameters: [
            {
              name: "id",
              in: "path",
              required: true,
              schema: { type: "string" },
              description: "Alert rule ID",
            },
          ],
          requestBody: {
            required: true,
            content: {
              "application/json": {
                schema: {
                  type: "object",
                  properties: {
                    name: { type: "string" },
                    config: { type: "object", additionalProperties: true },
                    enabled: { type: "boolean" },
                    cooldown_seconds: { type: "integer" },
                  },
                },
              },
            },
          },
          responses: {
            200: {
              description: "Updated rule",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
            400: {
              description: "Validation error",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
            404: {
              description: "Rule not found",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
        delete: {
          tags: ["Alerts"],
          summary: "Delete an alert rule and its fired-alert history",
          operationId: "deleteAlertRule",
          parameters: [
            {
              name: "id",
              in: "path",
              required: true,
              schema: { type: "string" },
              description: "Alert rule ID",
            },
          ],
          responses: {
            200: {
              description: "Deletion confirmation",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
            404: {
              description: "Rule not found",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/alerts/{id}/ack": {
        post: {
          tags: ["Alerts"],
          summary: "Acknowledge one fired alert",
          operationId: "ackAlert",
          parameters: [
            {
              name: "id",
              in: "path",
              required: true,
              schema: { type: "integer" },
              description: "Alert event ID",
            },
          ],
          responses: {
            200: {
              description: "Acknowledged alert row",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
            404: {
              description: "Alert not found",
              content: {
                "application/json": { schema: { $ref: "#/components/schemas/ErrorResponse" } },
              },
            },
          },
        },
      },
      "/api/alerts/ack-all": {
        post: {
          tags: ["Alerts"],
          summary: "Acknowledge all unacked alerts",
          operationId: "ackAllAlerts",
          responses: {
            200: {
              description: "Count of acknowledged alerts",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
          },
        },
      },
      "/api/webhooks/providers": {
        get: {
          tags: ["Webhooks"],
          summary: "List supported providers + their config fields (for the UI)",
          operationId: "listWebhookProviders",
          responses: {
            200: {
              description: "Provider catalog: label, family, url requirements, fields",
              content: {
                "application/json": { schema: { type: "object", additionalProperties: true } },
              },
            },
          },
        },
      },
      "/api/webhooks": {
        get: {
          tags: ["Webhooks"],
          summary: "List webhook targets (URLs masked, secrets redacted)",
          operationId: "listWebhooks",
          responses: {
            200: {
              description: "All configured webhook targets",
              content: {
                "application/json": { schema: { type: "object", additionalProperties: true } },
              },
            },
          },
        },
        post: {
          tags: ["Webhooks"],
          summary: "Create a webhook target",
          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)",
                    },
                  },
                },
              },
            },
          },
          responses: {
            201: {
              description: "Created target (redacted)",
              content: {
                "application/json": { schema: { type: "object", additionalProperties: true } },
              },
            },
            400: { description: "Validation error" },
          },
        },
      },
      "/api/webhooks/{id}": {
        patch: {
          tags: ["Webhooks"],
          summary: "Update a webhook target (partial; type is immutable)",
          operationId: "updateWebhook",
          parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
          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" } },
                  },
                },
              },
            },
          },
          responses: {
            200: {
              description: "Updated target (redacted)",
              content: {
                "application/json": { schema: { type: "object", additionalProperties: true } },
              },
            },
            400: { description: "Validation error" },
            404: { description: "Target not found" },
          },
        },
        delete: {
          tags: ["Webhooks"],
          summary: "Delete a webhook target and its delivery log",
          operationId: "deleteWebhook",
          parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
          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",
          operationId: "testWebhook",
          parameters: [{ name: "id", in: "path", required: true, schema: { type: "string" } }],
          responses: {
            200: {
              description: "Delivery result (ok flag carries the downstream outcome)",
              content: {
                "application/json": { schema: { type: "object", additionalProperties: true } },
              },
            },
            404: { description: "Target not found" },
          },
        },
      },
      "/api/webhooks/{id}/deliveries": {
        get: {
          tags: ["Webhooks"],
          summary: "Recent delivery log for a target",
          operationId: "listWebhookDeliveries",
          parameters: [
            { name: "id", in: "path", required: true, schema: { type: "string" } },
            { name: "limit", in: "query", schema: { type: "integer", default: 20 } },
            { name: "offset", in: "query", schema: { type: "integer", default: 0 } },
          ],
          responses: {
            200: {
              description: "Delivery rows, newest first",
              content: {
                "application/json": { schema: { type: "object", additionalProperties: true } },
              },
            },
            404: { description: "Target not found" },
          },
        },
      },
      "/api/openapi.json": {
        get: {
          tags: ["Documentation"],
          summary: "Get OpenAPI specification JSON",
          description:
            "Returns this OpenAPI 3.0 document as JSON. Both API explorers consume it: Swagger UI (`/api/docs`) for interactive try-it-out requests, and ReDoc (`/api/redoc`) for a read-optimized reference.",
          operationId: "getOpenApiJson",
          responses: {
            200: {
              description: "OpenAPI document",
              content: {
                "application/json": {
                  schema: { type: "object", additionalProperties: true },
                },
              },
            },
          },
        },
      },
      "/api/docs": {
        get: {
          tags: ["Documentation"],
          summary: "Swagger UI explorer",
          description:
            "Interactive Swagger UI rendering of this specification, with try-it-out request execution against the live local server.",
          operationId: "getSwaggerUi",
          responses: {
            200: {
              description: "Swagger UI HTML page",
              content: { "text/html": { schema: { type: "string" } } },
            },
          },
        },
      },
      "/api/redoc": {
        get: {
          tags: ["Documentation"],
          summary: "ReDoc API reference",
          description:
            "Read-optimized, three-panel ReDoc rendering of this specification. The ReDoc bundle is served locally from `/api/redoc/redoc.standalone.js` (bundled with the server, never fetched from a CDN), so the reference works fully offline.",
          operationId: "getRedoc",
          responses: {
            200: {
              description: "ReDoc HTML page",
              content: { "text/html": { schema: { type: "string" } } },
            },
          },
        },
      },
    },
    ...(issuesUrl
      ? {
          "x-issues-url": issuesUrl,
        }
      : {}),
  };

  return mergeExtraSpec(spec, extraSpec);
}

module.exports = { createOpenApiSpec };
