openapi: 3.1.0
info:
  title: Fetch Laika Web Control API
  description: |
    REST API for remote control of the Fetch Laika NDI multiviewer.

    Enable via Settings > API > Enable Web API. Default port 1928.
    All responses include `Access-Control-Allow-Origin: *` for browser access.
  version: 1.0.0
  contact:
    name: Fetch Media Tools
servers:
  - url: http://localhost:1928
    description: Default local instance

paths:
  /api/status:
    get:
      summary: Get current application status
      operationId: getStatus
      responses:
        "200":
          description: Current multiviewer state
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Status"
              example:
                layout: "2x2"
                launchpad: "Studio A"
                viewer_count: 4
                version: "1.0.57"

  /api/sources:
    get:
      summary: List discovered NDI sources
      operationId: getSources
      responses:
        "200":
          description: All NDI sources currently visible on the network
          content:
            application/json:
              schema:
                type: object
                required: [sources]
                properties:
                  sources:
                    type: array
                    items:
                      $ref: "#/components/schemas/Source"
              example:
                sources:
                  - name: "CAMERA-1 (Studio)"
                    url: "ndi://192.168.1.10/CAMERA-1"
                  - name: "GRAPHICS (Control Room)"
                    url: "ndi://192.168.1.20/GRAPHICS"

  /api/viewers:
    get:
      summary: List viewer assignments
      operationId: getViewers
      responses:
        "200":
          description: Current viewer slots with source assignments and connection state
          content:
            application/json:
              schema:
                type: object
                required: [viewers]
                properties:
                  viewers:
                    type: array
                    items:
                      $ref: "#/components/schemas/Viewer"
              example:
                viewers:
                  - index: 0
                    source: "CAMERA-1 (Studio)"
                    connected: true
                  - index: 1
                    source: null
                    connected: false

  /api/viewers/{index}:
    post:
      summary: Assign a source to a viewer
      operationId: assignViewer
      parameters:
        - $ref: "#/components/parameters/ViewerIndex"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [source]
              properties:
                source:
                  type: string
                  description: NDI source name (must match a name from /api/sources)
            example:
              source: "CAMERA-1 (Studio)"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          description: Invalid viewer index or missing source field
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                ok: false
                error: "Viewer index out of range"
        "404":
          description: Source not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                ok: false
                error: "Source not found"
    delete:
      summary: Clear a viewer
      operationId: clearViewer
      parameters:
        - $ref: "#/components/parameters/ViewerIndex"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "400":
          description: Invalid viewer index
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"

  /api/layout:
    post:
      summary: Switch layout
      operationId: setLayout
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Layout name (built-in or custom)
            example:
              name: "2x2"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "404":
          description: Layout not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                ok: false
                error: "Layout not found"

  /api/launchpads:
    get:
      summary: List available launchpads
      operationId: getLaunchpads
      responses:
        "200":
          description: All configured launchpad presets
          content:
            application/json:
              schema:
                type: object
                required: [launchpads]
                properties:
                  launchpads:
                    type: array
                    items:
                      $ref: "#/components/schemas/Launchpad"
              example:
                launchpads:
                  - name: "Studio A"
                    layout: "2x2"
                    active: true
                  - name: "Studio B"
                    layout: "1+5"
                    active: false

  /api/launchpad:
    post:
      summary: Switch to a launchpad preset
      operationId: selectLaunchpad
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name]
              properties:
                name:
                  type: string
                  description: Launchpad name (must match a name from /api/launchpads)
            example:
              name: "Studio B"
      responses:
        "200":
          $ref: "#/components/responses/Ok"
        "404":
          description: Launchpad not found
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
              example:
                ok: false
                error: "Launchpad not found"

components:
  parameters:
    ViewerIndex:
      name: index
      in: path
      required: true
      description: Zero-based viewer slot index
      schema:
        type: integer
        minimum: 0

  schemas:
    Status:
      type: object
      required: [layout, launchpad, viewer_count, version]
      properties:
        layout:
          type: string
          description: Current layout name
        launchpad:
          type: string
          description: Active launchpad name (empty string if none)
        viewer_count:
          type: integer
          description: Number of viewer slots in the current layout
        version:
          type: string
          description: Application version

    Source:
      type: object
      required: [name, url]
      properties:
        name:
          type: string
          description: NDI source display name
        url:
          type: string
          description: NDI source URL

    Viewer:
      type: object
      required: [index, source, connected]
      properties:
        index:
          type: integer
          description: Zero-based viewer slot index
        source:
          type: string
          nullable: true
          description: Assigned source name, or null if empty
        connected:
          type: boolean
          description: Whether the viewer is actively receiving frames

    Launchpad:
      type: object
      required: [name, layout, active]
      properties:
        name:
          type: string
          description: Launchpad preset name
        layout:
          type: string
          description: Layout used by this launchpad
        active:
          type: boolean
          description: Whether this is the currently active launchpad

    Error:
      type: object
      required: [ok, error]
      properties:
        ok:
          type: boolean
          const: false
        error:
          type: string
          description: Human-readable error message

    Success:
      type: object
      required: [ok]
      properties:
        ok:
          type: boolean
          const: true

  responses:
    Ok:
      description: Operation succeeded
      content:
        application/json:
          schema:
            $ref: "#/components/schemas/Success"
          example:
            ok: true
