Ctrl K
ComunidadGuías…

server/docs/openapi.yaml

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
openapi: "3.1.0"
info:
  title: Blackbox API
  version: "0.1.0"
  description: |
    Blackbox is a self-hosted forensic timeline server. This API is used by the
    web UI, agents, and external webhook sources.

    ## Authentication

    Three separate mechanisms are used depending on the caller:

    | Mechanism | Header / Cookie | Used by |
    |-----------|-----------------|---------|
    | Session cookie | `Cookie: sessionid=<jwt>` | Browser / web UI |
    | Agent key | `X-Blackbox-Agent-Key: <token>` + `X-Blackbox-Node-Name: <node>` | Blackbox agents |
    | Webhook secret | `X-Webhook-Secret: <secret>` | External webhook sources |

    Session cookies are set automatically on login and cleared on logout.

servers:
  - url: /
    description: Current server

tags:
  - name: Setup
    description: Server status and bootstrap
  - name: Auth
    description: Login, logout, registration, and session management
  - name: OIDC
    description: OpenID Connect login flow (public endpoints)
  - name: Account
    description: Authenticated user account management
  - name: Invites
    description: Invite code management (admin only)
  - name: Entries
    description: Timeline event entries
  - name: Notes
    description: Manual notes attached to entries
  - name: Incidents
    description: Correlated incident groups
  - name: Nodes
    description: Connected agent nodes
  - name: Agent
    description: Agent push endpoints (agent token required)
  - name: Webhooks
    description: External webhook ingestion (webhook secret required)
  - name: Admin — Users
    description: User management (admin only)
  - name: Admin — Config
    description: Server configuration (admin only)
  - name: Admin — OIDC
    description: OIDC provider management (admin only)
  - name: Admin — Notifications
    description: Notification destination management (admin only)
  - name: WebSocket
    description: Real-time event stream

components:
  securitySchemes:
    sessionCookie:
      type: apiKey
      in: cookie
      name: sessionid
      description: JWT session cookie set on login
    agentKey:
      type: apiKey
      in: header
      name: X-Blackbox-Agent-Key
      description: "Agent authentication token (paired with X-Blackbox-Node-Name)"
    agentNode:
      type: apiKey
      in: header
      name: X-Blackbox-Node-Name
      description: Agent node name (paired with X-Blackbox-Agent-Key)
    webhookSecret:
      type: apiKey
      in: header
      name: X-Webhook-Secret
      description: Shared webhook secret

  schemas:
    Error:
      type: object
      required: [error]
      properties:
        error:
          type: string
          example: "entry not found"

    SessionUser:
      type: object
      required: [user_id, username, email, oidc_linked, is_admin]
      properties:
        user_id:
          type: string
          example: "01HXYZ..."
        username:
          type: string
          example: "alice"
        email:
          type: string
          format: email
          example: "alice@example.com"
        oidc_linked:
          type: boolean
        is_admin:
          type: boolean

    SessionResponse:
      type: object
      required: [user]
      properties:
        user:
          $ref: "#/components/schemas/SessionUser"

    Entry:
      type: object
      required: [id, timestamp, node_name, source, service, event, content, metadata]
      properties:
        id:
          type: string
          description: ULID
          example: "01HXYZ..."
        timestamp:
          type: string
          format: date-time
        node_name:
          type: string
          description: Originating agent node name, or "api" for manual entries
          example: "homelab-01"
        source:
          type: string
          description: Event source
          enum: [docker, file, systemd, agent, webhook, api]
          example: "docker"
        service:
          type: string
          example: "nginx"
        compose_service:
          type: string
          description: Docker Compose service name, if applicable
          example: "proxy"
        event:
          type: string
          description: Event type within the source
          example: "die"
        content:
          type: string
          description: Human-readable summary of the event
        metadata:
          type: object
          description: Source-specific structured data
          additionalProperties: true
        correlated_id:
          type: string
          readOnly: true
          description: ID of a related entry this was correlated to
        replace_id:
          type: string
          writeOnly: true
          description: Entry ID replaced by a Docker restart (write-only on push)

    EntryNote:
      type: object
      required: [id, entry_id, user_id, username, content, created_at]
      properties:
        id:
          type: string
        entry_id:
          type: string
        user_id:
          type: string
        username:
          type: string
        content:
          type: string
          maxLength: 2000
        created_at:
          type: string
          format: date-time

    Incident:
      type: object
      required: [id, opened_at, status, confidence, title, services, node_names, metadata]
      properties:
        id:
          type: string
        opened_at:
          type: string
          format: date-time
        resolved_at:
          type: string
          format: date-time
          nullable: true
        status:
          type: string
          enum: [open, resolved]
        confidence:
          type: string
          enum: [confirmed, suspected]
        title:
          type: string
        services:
          type: array
          items:
            type: string
        root_cause_id:
          type: string
          description: Entry ID identified as root cause
        trigger_id:
          type: string
          description: Entry ID that triggered incident creation
        node_names:
          type: array
          items:
            type: string
        metadata:
          type: object
          description: AI analysis and other enrichment data
          additionalProperties: true

    IncidentEntry:
      type: object
      required: [incident_id, entry_id, role, score, reason]
      properties:
        incident_id:
          type: string
        entry_id:
          type: string
        role:
          type: string
          enum: [trigger, cause, evidence, recovery, ai_cause]
        score:
          type: integer
          description: Correlation score; 0 for non-cause roles
        reason:
          type: string
          description: AI explanation for ai_cause role; empty for deterministic links

    IncidentMembership:
      type: object
      required: [id, confidence]
      properties:
        id:
          type: string
          description: Incident ID
        confidence:
          type: string
          enum: [confirmed, suspected]

    Node:
      type: object
      required: [id, name, last_seen, status, agent_version, ip_address, os_info]
      properties:
        id:
          type: string
        name:
          type: string
        last_seen:
          type: string
          format: date-time
        status:
          type: string
          enum: [online, offline]
        agent_version:
          type: string
        ip_address:
          type: string
        os_info:
          type: string

    InviteCode:
      type: object
      required: [id, code, created_by, used_by, expires_at, created_at]
      properties:
        id:
          type: string
        code:
          type: string
        created_by:
          type: string
        used_by:
          type: string
        expires_at:
          type: string
          format: date-time
        created_at:
          type: string
          format: date-time

    OIDCProvider:
      type: object
      required: [id, name, issuer, client_id, client_secret, redirect_url, require_verified_email, enabled, created_at, updated_at]
      properties:
        id:
          type: string
        name:
          type: string
        issuer:
          type: string
          format: uri
        client_id:
          type: string
        client_secret:
          type: string
          description: Always returned as "***"
          example: "***"
        redirect_url:
          type: string
          format: uri
        require_verified_email:
          type: boolean
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    OIDCProviderPublic:
      type: object
      required: [id, name]
      properties:
        id:
          type: string
        name:
          type: string

    NotificationDest:
      type: object
      required: [id, name, type, url, events, enabled, created_at, updated_at]
      properties:
        id:
          type: string
        name:
          type: string
        type:
          type: string
          enum: [discord, slack, ntfy]
        url:
          type: string
          format: uri
        events:
          type: array
          items:
            type: string
            enum: [incident_opened_confirmed, incident_opened_suspected, incident_confirmed, incident_resolved]
        enabled:
          type: boolean
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    AdminUser:
      type: object
      required: [id, username, is_admin, token_version, created_at]
      properties:
        id:
          type: string
        username:
          type: string
        is_admin:
          type: boolean
        token_version:
          type: integer
        created_at:
          type: string
          format: date-time

    AdminConfig:
      type: object
      required: [webhook_secret, file_watcher_redact_secrets, ai_provider, ai_url, ai_model, ai_api_key_set, ai_mode]
      properties:
        webhook_secret:
          type: string
        file_watcher_redact_secrets:
          type: boolean
        ai_provider:
          type: string
          enum: [ollama, openai_compat]
        ai_url:
          type: string
        ai_model:
          type: string
        ai_api_key_set:
          type: boolean
          description: True if an API key is stored; the key itself is never returned
        ai_mode:
          type: string
          enum: [analysis, enhanced]

    AgentPushBatchResult:
      type: object
      required: [accepted, failed]
      properties:
        accepted:
          type: array
          items:
            type: string
          description: IDs of successfully accepted entries
        failed:
          type: array
          items:
            type: object
            required: [id, reason, permanent]
            properties:
              id:
                type: string
              reason:
                type: string
              permanent:
                type: boolean

paths:
  # ── Setup ─────────────────────────────────────────────────────────────────

  /api/setup/status:
    get:
      tags: [Setup]
      summary: Bootstrap status
      description: Returns whether an admin account has been created. Used by the UI to redirect to the bootstrap wizard on first run.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [bootstrapped]
                properties:
                  bootstrapped:
                    type: boolean

  /api/setup/health:
    get:
      tags: [Setup]
      summary: Health check
      description: Returns the health status of the database and OIDC subsystem. Used by the container health check.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [database, oidc, oidc_enabled]
                properties:
                  database:
                    type: string
                    enum: [ok, error]
                  oidc:
                    type: string
                    enum: [ok, disabled, unavailable, error]
                  oidc_enabled:
                    type: boolean

  # ── Auth ──────────────────────────────────────────────────────────────────

  /api/auth/bootstrap:
    post:
      tags: [Auth]
      summary: Create the first admin account
      description: |
        Creates the initial admin user. Only succeeds if no users exist yet.
        Rate limited to 3 requests per 10 minutes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password
                email:
                  type: string
                  format: email
      responses:
        "201":
          description: Admin account created
          headers:
            Set-Cookie:
              description: Session cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        "409":
          description: Already bootstrapped
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/login:
    post:
      tags: [Auth]
      summary: Log in with username and password
      description: Rate limited to 10 requests per minute.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password
      responses:
        "200":
          description: Logged in
          headers:
            Set-Cookie:
              description: Session cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        "401":
          description: Invalid credentials
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/register:
    post:
      tags: [Auth]
      summary: Register a new account using an invite code
      description: Rate limited to 10 requests per minute.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [username, password, email, invite_code]
              properties:
                username:
                  type: string
                password:
                  type: string
                  format: password
                email:
                  type: string
                  format: email
                invite_code:
                  type: string
      responses:
        "201":
          description: Registered and logged in
          headers:
            Set-Cookie:
              description: Session cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        "400":
          description: Invalid email or missing fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "401":
          description: Invalid or expired invite code
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Username or email already taken
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/logout:
    post:
      tags: [Auth]
      summary: Log out
      description: Clears the session cookie. No authentication required.
      responses:
        "200":
          description: Logged out
          headers:
            Set-Cookie:
              description: Expired session cookie
              schema:
                type: string
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
                    example: true

  /api/auth/me:
    get:
      tags: [Account]
      summary: Get current session user
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Account]
      summary: Update account email
      description: Not available for OIDC-linked accounts.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [email]
              properties:
                email:
                  type: string
                  format: email
      responses:
        "200":
          description: Updated
          headers:
            Set-Cookie:
              description: New session cookie reflecting updated email
              schema:
                type: string
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/SessionResponse"
        "403":
          description: OIDC-linked accounts cannot change email here
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: Email already in use
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── OIDC (public) ─────────────────────────────────────────────────────────

  /api/auth/oidc/providers:
    get:
      tags: [OIDC]
      summary: List enabled OIDC providers
      description: Returns the name and ID of each enabled provider for the login screen.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [providers]
                properties:
                  providers:
                    type: array
                    items:
                      $ref: "#/components/schemas/OIDCProviderPublic"

  /api/auth/oidc/{provider_id}/login:
    get:
      tags: [OIDC]
      summary: Start OIDC login flow
      description: |
        Redirects the browser to the OIDC provider's authorization endpoint.
        Rate limited to 10 requests per minute. An optional `invite_code` can be
        supplied to allow a new user to register via OIDC.
      parameters:
        - in: path
          name: provider_id
          required: true
          schema:
            type: string
        - in: query
          name: invite_code
          schema:
            type: string
          description: Invite code for new-user registration
      responses:
        "302":
          description: Redirect to OIDC provider

  /api/auth/oidc/{provider_id}/callback:
    get:
      tags: [OIDC]
      summary: OIDC callback
      description: Handles the authorization code returned by the OIDC provider. On success, redirects to `/timeline` and sets a session cookie.
      parameters:
        - in: path
          name: provider_id
          required: true
          schema:
            type: string
        - in: query
          name: code
          required: true
          schema:
            type: string
        - in: query
          name: state
          required: true
          schema:
            type: string
      responses:
        "302":
          description: Redirect to /timeline on success or /login on failure

  # ── Invites ───────────────────────────────────────────────────────────────

  /api/auth/invite:
    post:
      tags: [Invites]
      summary: Create an invite code
      security:
        - sessionCookie: []
      responses:
        "201":
          description: Invite created
          content:
            application/json:
              schema:
                type: object
                required: [code, expires_at]
                properties:
                  code:
                    type: string
                  expires_at:
                    type: string
                    format: date-time
        "403":
          description: Admin role required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    get:
      tags: [Invites]
      summary: List all invite codes
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/InviteCode"
        "403":
          description: Admin role required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/auth/invite/{id}:
    delete:
      tags: [Invites]
      summary: Revoke an invite code
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Revoked
        "403":
          description: Admin role required
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Invite not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Entries ───────────────────────────────────────────────────────────────

  /api/entries:
    get:
      tags: [Entries]
      summary: List timeline entries
      description: Returns entries in reverse-chronological order with cursor-based pagination.
      security:
        - sessionCookie: []
      parameters:
        - in: query
          name: cursor
          schema:
            type: string
          description: Opaque pagination cursor returned as `next_cursor` from a previous response
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
        - in: query
          name: node
          schema:
            type: string
          description: Filter by exact node name
        - in: query
          name: source
          schema:
            type: string
          description: Filter by exact source
        - in: query
          name: service
          schema:
            type: string
          description: Filter by exact service name
        - in: query
          name: q
          schema:
            type: string
          description: Full-text search against content and service name
        - in: query
          name: time_start
          schema:
            type: string
            format: date-time
          description: Include only entries at or after this time (RFC3339)
        - in: query
          name: time_end
          schema:
            type: string
            format: date-time
          description: Include only entries at or before this time (RFC3339)
        - in: query
          name: hide_heartbeat
          schema:
            type: boolean
          description: Exclude agent heartbeat entries
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [entries]
                properties:
                  entries:
                    type: array
                    items:
                      $ref: "#/components/schemas/Entry"
                  next_cursor:
                    type: string
                    description: Present when more entries exist; pass as `cursor` on the next request
        "401":
          description: Not authenticated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [Entries]
      summary: Create a manual entry
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [title]
              properties:
                title:
                  type: string
                  description: Event summary (stored as `content`)
                note:
                  type: string
                  description: Optional longer note stored in metadata
                services:
                  type: array
                  items:
                    type: string
                timestamp:
                  type: string
                  format: date-time
                  description: Defaults to current server time if omitted
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Entry"
        "400":
          description: Missing title
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/entries/services:
    get:
      tags: [Entries]
      summary: List distinct service names
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [services]
                properties:
                  services:
                    type: array
                    items:
                      type: string

  /api/entries/{id}:
    get:
      tags: [Entries]
      summary: Get a single entry
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Entry"
        "404":
          description: Entry not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/entries/{id}/notes:
    post:
      tags: [Notes]
      summary: Add a note to an entry
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Entry ID
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [content]
              properties:
                content:
                  type: string
                  maxLength: 2000
      responses:
        "201":
          description: Note created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/EntryNote"
        "404":
          description: Entry not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    get:
      tags: [Notes]
      summary: List notes for an entry
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Entry ID
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 50
        - in: query
          name: offset
          schema:
            type: integer
            minimum: 0
            default: 0
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [notes, has_more]
                properties:
                  notes:
                    type: array
                    items:
                      $ref: "#/components/schemas/EntryNote"
                  has_more:
                    type: boolean
                  next_offset:
                    type: integer

  /api/notes/{id}:
    delete:
      tags: [Notes]
      summary: Delete a note
      description: Only the note's author may delete it.
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
          description: Note ID
      responses:
        "204":
          description: Deleted
        "403":
          description: Not the note author
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: Note not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Incidents ─────────────────────────────────────────────────────────────

  /api/incidents:
    get:
      tags: [Incidents]
      summary: List incidents
      security:
        - sessionCookie: []
      parameters:
        - in: query
          name: status
          schema:
            type: string
            enum: [open, resolved]
        - in: query
          name: confidence
          schema:
            type: string
            enum: [confirmed, suspected]
        - in: query
          name: service
          schema:
            type: string
          description: Filter to incidents involving this service
        - in: query
          name: limit
          schema:
            type: integer
            minimum: 1
            maximum: 200
            default: 50
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [incidents, has_more]
                properties:
                  incidents:
                    type: array
                    items:
                      $ref: "#/components/schemas/Incident"
                  has_more:
                    type: boolean

  /api/incidents/summary:
    get:
      tags: [Incidents]
      summary: Get open incident counts
      description: Lightweight summary used by the navigation badge.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [open_count, has_confirmed_open]
                properties:
                  open_count:
                    type: integer
                  has_confirmed_open:
                    type: boolean

  /api/incidents/membership:
    post:
      tags: [Incidents]
      summary: Check which entries belong to open incidents
      description: |
        Accepts a list of entry IDs and returns a map of entry ID → incident membership
        for any entry that is part of a currently open incident. Entries not in any open
        incident are omitted from the response.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [entry_ids]
              properties:
                entry_ids:
                  type: array
                  items:
                    type: string
                  maxItems: 200
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [memberships]
                properties:
                  memberships:
                    type: object
                    description: Keys are entry IDs; values are incident membership details
                    additionalProperties:
                      $ref: "#/components/schemas/IncidentMembership"

  /api/incidents/{id}:
    get:
      tags: [Incidents]
      summary: Get a single incident with all linked entries
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [incident, entries]
                properties:
                  incident:
                    $ref: "#/components/schemas/Incident"
                  entries:
                    type: array
                    items:
                      type: object
                      required: [link, entry]
                      properties:
                        link:
                          $ref: "#/components/schemas/IncidentEntry"
                        entry:
                          $ref: "#/components/schemas/Entry"
        "404":
          description: Incident not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Nodes ─────────────────────────────────────────────────────────────────

  /api/nodes:
    get:
      tags: [Nodes]
      summary: List agent nodes
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/Node"

  # ── Agent ─────────────────────────────────────────────────────────────────

  /api/agent/config:
    get:
      tags: [Agent]
      summary: Fetch agent configuration from server
      security:
        - agentKey: []
          agentNode: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [file_watcher_redact_secrets, systemd_units]
                properties:
                  file_watcher_redact_secrets:
                    type: boolean
                  systemd_units:
                    type: array
                    items:
                      type: string

  /api/agent/push:
    post:
      tags: [Agent]
      summary: Push a single event entry
      description: |
        Idempotent: if an entry with the same ID already exists and the content matches,
        returns 200. A conflicting duplicate (same ID, different content) returns 409.
      security:
        - agentKey: []
          agentNode: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/Entry"
      responses:
        "201":
          description: Entry created
        "200":
          description: Duplicate — entry already exists with identical content
        "400":
          description: Missing required fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "409":
          description: ID conflict with different content
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/agent/push/batch:
    post:
      tags: [Agent]
      summary: Push multiple event entries in one request
      description: |
        Accepts up to 200 entries in a single request (10 MB body limit).
        Each entry is processed independently; partial success is possible.
      security:
        - agentKey: []
          agentNode: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: array
              items:
                $ref: "#/components/schemas/Entry"
              maxItems: 200
      responses:
        "200":
          description: Batch processed (check `failed` for per-entry errors)
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AgentPushBatchResult"

  # ── Webhooks ──────────────────────────────────────────────────────────────

  /api/webhooks/uptime:
    post:
      tags: [Webhooks]
      summary: Uptime Kuma webhook
      description: Accepts the JSON payload sent by Uptime Kuma's webhook notification type.
      security:
        - webhookSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [monitor]
              properties:
                heartbeat:
                  type: object
                  properties:
                    status:
                      type: integer
                      description: "0 = down, >0 = up"
                    time:
                      type: string
                      format: date-time
                    msg:
                      type: string
                monitor:
                  type: object
                  required: [name]
                  properties:
                    name:
                      type: string
      responses:
        "201":
          description: Ingested
        "400":
          description: Missing required fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/webhooks/watchtower:
    post:
      tags: [Webhooks]
      summary: Watchtower webhook
      description: Accepts the JSON payload sent by Watchtower's HTTP API notifications.
      security:
        - webhookSecret: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [Message]
              properties:
                Title:
                  type: string
                Message:
                  type: string
                Level:
                  type: string
      responses:
        "201":
          description: Ingested
        "400":
          description: Missing Message field
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Admin — Users ─────────────────────────────────────────────────────────

  /api/admin/users:
    get:
      tags: [Admin — Users]
      summary: List all users
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/AdminUser"

  /api/admin/users/{id}:
    patch:
      tags: [Admin — Users]
      summary: Update a user's admin status
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [is_admin]
              properties:
                is_admin:
                  type: boolean
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminUser"
        "403":
          description: Cannot demote your own account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "404":
          description: User not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags: [Admin — Users]
      summary: Delete a user
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean
        "403":
          description: Cannot delete your own account
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/users/{id}/force-logout:
    post:
      tags: [Admin — Users]
      summary: Force-logout a user
      description: Invalidates all existing sessions for the user by bumping their token version.
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Done
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  # ── Admin — Config ────────────────────────────────────────────────────────

  /api/admin/config:
    get:
      tags: [Admin — Config]
      summary: Get server configuration
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/AdminConfig"

  /api/admin/settings/file-watcher:
    put:
      tags: [Admin — Config]
      summary: Update file watcher settings
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [redact_secrets]
              properties:
                redact_secrets:
                  type: boolean
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                required: [redact_secrets]
                properties:
                  redact_secrets:
                    type: boolean

  /api/admin/settings/systemd:
    get:
      tags: [Admin — Config]
      summary: Get systemd unit watch configuration
      description: Returns a map of node name → list of monitored unit names.
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                additionalProperties:
                  type: array
                  items:
                    type: string

  /api/admin/settings/systemd/{node_name}:
    put:
      tags: [Admin — Config]
      summary: Set systemd units to monitor for a node
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: node_name
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [units]
              properties:
                units:
                  type: array
                  items:
                    type: string
      responses:
        "204":
          description: Updated

  /api/admin/settings/ai:
    put:
      tags: [Admin — Config]
      summary: Update AI settings
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ai_provider:
                  type: string
                  enum: [ollama, openai_compat]
                  default: ollama
                ai_url:
                  type: string
                  format: uri
                ai_model:
                  type: string
                ai_api_key:
                  type: string
                  description: Omit to keep the existing stored key
                ai_clear_api_key:
                  type: boolean
                  description: Set to true to remove the stored API key
                ai_mode:
                  type: string
                  enum: [analysis, enhanced]
                  default: analysis
      responses:
        "204":
          description: Updated
        "400":
          description: Invalid settings
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/admin/settings/ai/test:
    post:
      tags: [Admin — Config]
      summary: Test AI connectivity
      description: Sends a trivial prompt to the configured AI endpoint and returns the response.
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                ai_provider:
                  type: string
                  enum: [ollama, openai_compat]
                ai_url:
                  type: string
                  format: uri
                ai_model:
                  type: string
                ai_api_key:
                  type: string
                ai_mode:
                  type: string
                  enum: [analysis, enhanced]
      responses:
        "200":
          description: Test result
          content:
            application/json:
              schema:
                type: object
                required: [ok, response]
                properties:
                  ok:
                    type: boolean
                  response:
                    type: string
        "400":
          description: Missing required fields
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: AI endpoint unreachable or returned an error
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── Admin — OIDC ──────────────────────────────────────────────────────────

  /api/admin/oidc/providers:
    get:
      tags: [Admin — OIDC]
      summary: List OIDC providers
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/OIDCProvider"
    post:
      tags: [Admin — OIDC]
      summary: Create an OIDC provider
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, issuer, client_id, client_secret, redirect_url]
              properties:
                id:
                  type: string
                  description: Auto-generated ULID if omitted
                name:
                  type: string
                issuer:
                  type: string
                  format: uri
                client_id:
                  type: string
                client_secret:
                  type: string
                redirect_url:
                  type: string
                  format: uri
                require_verified_email:
                  type: boolean
                  default: false
                enabled:
                  type: boolean
                  default: true
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OIDCProvider"

  /api/admin/oidc/providers/{id}:
    patch:
      tags: [Admin — OIDC]
      summary: Update an OIDC provider
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                issuer:
                  type: string
                  format: uri
                client_id:
                  type: string
                client_secret:
                  type: string
                redirect_url:
                  type: string
                  format: uri
                require_verified_email:
                  type: boolean
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/OIDCProvider"
        "404":
          description: Provider not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    delete:
      tags: [Admin — OIDC]
      summary: Delete an OIDC provider
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Deleted
          content:
            application/json:
              schema:
                type: object
                properties:
                  ok:
                    type: boolean

  /api/admin/oidc/policy:
    get:
      tags: [Admin — OIDC]
      summary: Get OIDC registration policy
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [policy]
                properties:
                  policy:
                    type: string
                    enum: [open, existing_only, invite_required]
    put:
      tags: [Admin — OIDC]
      summary: Set OIDC registration policy
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [policy]
              properties:
                policy:
                  type: string
                  enum: [open, existing_only, invite_required]
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                type: object
                required: [policy]
                properties:
                  policy:
                    type: string
                    enum: [open, existing_only, invite_required]

  # ── Admin — Notifications ─────────────────────────────────────────────────

  /api/admin/notifications:
    get:
      tags: [Admin — Notifications]
      summary: List notification destinations
      security:
        - sessionCookie: []
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: array
                items:
                  $ref: "#/components/schemas/NotificationDest"
    post:
      tags: [Admin — Notifications]
      summary: Create a notification destination
      security:
        - sessionCookie: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, type, url, events]
              properties:
                name:
                  type: string
                type:
                  type: string
                  enum: [discord, slack, ntfy]
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum: [incident_opened_confirmed, incident_opened_suspected, incident_confirmed, incident_resolved]
                enabled:
                  type: boolean
                  default: true
      responses:
        "201":
          description: Created
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationDest"

  /api/admin/notifications/{id}:
    put:
      tags: [Admin — Notifications]
      summary: Update a notification destination
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                name:
                  type: string
                type:
                  type: string
                  enum: [discord, slack, ntfy]
                url:
                  type: string
                  format: uri
                events:
                  type: array
                  items:
                    type: string
                    enum: [incident_opened_confirmed, incident_opened_suspected, incident_confirmed, incident_resolved]
                enabled:
                  type: boolean
      responses:
        "200":
          description: Updated
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/NotificationDest"
    delete:
      tags: [Admin — Notifications]
      summary: Delete a notification destination
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "204":
          description: Deleted

  /api/admin/notifications/{id}/test:
    post:
      tags: [Admin — Notifications]
      summary: Send a test notification
      security:
        - sessionCookie: []
      parameters:
        - in: path
          name: id
          required: true
          schema:
            type: string
      responses:
        "200":
          description: Test sent
        "404":
          description: Destination not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
        "502":
          description: Delivery failed
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  # ── WebSocket ─────────────────────────────────────────────────────────────

  /api/ws:
    get:
      tags: [WebSocket]
      summary: Real-time event stream
      description: |
        Upgrades the connection to WebSocket. The server pushes JSON messages as events occur.

        **Message types:**

        | type | payload | Description |
        |------|---------|-------------|
        | `entry` | `Entry` | A new entry was ingested |
        | `entry_replaced` | `Entry` | A Docker restart replaced an existing entry |
        | `node_status` | `{online: number, total: number}` | Node connectivity changed |
        | `invalidate_user` | — | Your session was invalidated; re-authenticate |

        The server sends a ping every 30 seconds. Maximum 8 connections per user,
        64 per IP address.
      security:
        - sessionCookie: []
      responses:
        "101":
          description: Switching Protocols
        "401":
          description: Not authenticated