{
  "openapi": "3.0.3",
  "info": {
    "title": "Stellar Link API",
    "description": "REST + WebSocket API for the Stellar Link platform (v1). Refer to ADR 0003 for the contract and versioning policy.",
    "license": {
      "name": ""
    },
    "version": "0.1.0"
  },
  "paths": {
    "/api/v1/campaign-runs": {
      "get": {
        "tags": [
          "campaigns"
        ],
        "operationId": "list_runs",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CampaignStateDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaign-runs/{id}": {
      "get": {
        "tags": [
          "campaigns"
        ],
        "operationId": "get_run",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Execution id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStateDRO"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaign-runs/{id}/pause": {
      "post": {
        "tags": [
          "campaigns"
        ],
        "operationId": "pause",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Execution id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Pause requested — it takes effect at the next step boundary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStateDRO"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaign-runs/{id}/resume": {
      "post": {
        "tags": [
          "campaigns"
        ],
        "operationId": "resume",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Execution id",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStateDRO"
                }
              }
            }
          },
          "409": {
            "description": "Finished, or another campaign is running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaign-runs/{id}/steps/{step}/rerun": {
      "post": {
        "tags": [
          "campaigns"
        ],
        "operationId": "rerun_step",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Execution id",
            "required": true,
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "step",
            "in": "path",
            "description": "Step id, as the campaign names it",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStateDRO"
                }
              }
            }
          },
          "409": {
            "description": "The campaign is running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaigns": {
      "get": {
        "tags": [
          "campaigns"
        ],
        "operationId": "list",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/CampaignSummaryDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaigns/active": {
      "get": {
        "tags": [
          "campaigns"
        ],
        "operationId": "active",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignActiveDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaigns/{name}": {
      "get": {
        "tags": [
          "campaigns"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Campaign identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The campaign document"
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/campaigns/{name}/run": {
      "post": {
        "tags": [
          "campaigns"
        ],
        "operationId": "run",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Campaign to run",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CampaignStateDRO"
                }
              }
            }
          },
          "404": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          },
          "409": {
            "description": "Another campaign is running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chain": {
      "get": {
        "tags": [
          "chain"
        ],
        "summary": "The complete conditioning verdict, in one call.",
        "operationId": "status",
        "parameters": [
          {
            "name": "target",
            "in": "query",
            "description": "`air`, `device-loopback` or `pl-loopback`. Defaults to what the board is\nalready wired for.",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ChainStateDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chain/condition": {
      "post": {
        "tags": [
          "chain"
        ],
        "summary": "Run the missing conditioning steps, in the one order that works.",
        "description": "WHY AN ENDPOINT AND NOT A RUNBOOK. The order is not a convention, it is a\nconstraint: `rf tune` resets the sample rate and drives `dac_data_sel` to PN\nfor its sweep, a mode switch rewrites the ingress gain, and a profile apply\nrewrites the datapath registers and the LOs. Each step therefore undoes part\nof the ones before it if taken out of turn — which is exactly the mistake\nthat produced five empty RF passes on 2026-09-09, from a note that stated the\nrule correctly. A sequence encoded in a function cannot be read in the wrong\norder.\n\nIT CALLS THE EXISTING HANDLERS, deliberately, rather than repeating what they\ndo. Duplicating `rf_mode_air`'s body here would let the two drift, and the\npart that would drift is the ordering inside it — the destination is\nconfigured fully before the path is switched to it, because mux-first\nmeasured 31.28 % against 99.98 %.",
        "operationId": "condition",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ConditionRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ConditionResultDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/chain/recover": {
      "post": {
        "tags": [
          "chain"
        ],
        "summary": "Pulse the receiver's CLEAR bits — the recovery that is not a reboot.",
        "description": "WHY THIS IS A ROUTE AND NOT A NOTE. The carrier loop on this bench gets\nstuck: symbols flow, lock reads true, and the deframer never finds its ASM.\nThe recorded remedy is a CLEAR pulse, after which the RF link frames at\n~100 %. Three blocks decode `CTRL[1] = clear`, the RTL consumes it, and the\nPS wrote none of them — so the remedy existed only as a `reg set` typed by\nhand, and the ordinary answer to a stuck loop was a reboot.\n\nIT REPORTS WHAT IT PULSED, NOT THAT IT WORKED. The reply carries an explicit\ncaveat, because a frame counter that starts moving is not evidence: the ASM\nis inserted downstream of the payload in the PL, so it counts clean frames\nover entirely wrong bytes (STE-402). The verdict is a byte-exact loopback.",
        "operationId": "recover",
        "responses": {
          "200": {
            "description": "What was pulsed, and what the registers read back"
          },
          "503": {
            "description": "No PL driver",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/datapath": {
      "get": {
        "tags": [
          "datapath"
        ],
        "operationId": "status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatapathStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/datapath/capture": {
      "post": {
        "tags": [
          "datapath"
        ],
        "summary": "Collect `blocks` raw DMA blocks and return them as `application/octet-stream`.",
        "operationId": "capture",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/CaptureRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Raw DMA blocks, concatenated"
          },
          "503": {
            "description": "No datapath in this daemon"
          },
          "504": {
            "description": "The chain did not sustain the requested blocks"
          }
        }
      }
    },
    "/api/v1/datapath/disable": {
      "post": {
        "tags": [
          "datapath"
        ],
        "operationId": "disable",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatapathStateDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/datapath/enable": {
      "post": {
        "tags": [
          "datapath"
        ],
        "operationId": "enable",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DatapathStateDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/datapath/flush": {
      "post": {
        "tags": [
          "datapath"
        ],
        "summary": "Transmit whatever is buffered, padded out to a full block.",
        "description": "A caller pushing at a low rate MUST call this: the assembler only emits\nFULL blocks, so a handful of packets sit there until the datapath is\nreleased. Measured on hardware 2026-09-12 — a scenario injected one 8-byte\nCSP packet and `tx.framer.frame_cnt` never moved.\n\nThere is deliberately no timer doing it automatically: the padding goes on\nthe air, and spending it is the caller's decision rather than a hidden\nlatency policy.",
        "operationId": "flush",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/FlushRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlushedDRO"
                }
              }
            }
          },
          "404": {
            "description": "no datapath in this daemon"
          }
        }
      }
    },
    "/api/v1/datapath/loopback": {
      "post": {
        "tags": [
          "datapath"
        ],
        "summary": "Feed a payload while capturing: the whole loopback measurement in one call.",
        "description": "The payload is the RAW BODY, so it stays byte-identical from the host file to\nthe DAC. Query parameters carry the rest.",
        "operationId": "loopback",
        "requestBody": {
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "Raw DMA blocks captured while feeding"
          },
          "504": {
            "description": "The chain did not sustain the requested blocks"
          }
        }
      }
    },
    "/api/v1/datapath/measure": {
      "post": {
        "tags": [
          "datapath"
        ],
        "summary": "Feed a payload, capture, and COMPARE — the whole measurement, verdict included.",
        "description": "WHY THE COMPARISON LIVES HERE. `satlink-measure` exists because its\npredecessor, an untested python script, twice declared a healthy chain dead\n(lane extraction applied to one side only; a single aggregate hiding a\nper-block truth). Both defects are replayed as tests in that crate. A second\nimplementation in a UI — untested, and reading the same raw bytes — would\nreintroduce exactly the class of failure the crate was written to end. So the\ncaller receives the verdict, never the raw bytes to interpret itself.\n\n`/datapath/loopback` still returns the raw capture: an operator archiving a\ncampaign needs the bytes, byte-identical. This route is for a client that\nwants the answer.",
        "operationId": "measure",
        "requestBody": {
          "content": {
            "application/octet-stream": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MeasureReportDRO"
                }
              }
            }
          },
          "504": {
            "description": "The chain did not sustain the requested blocks"
          }
        }
      }
    },
    "/api/v1/datapath/transmit": {
      "post": {
        "tags": [
          "datapath"
        ],
        "summary": "Queue packets for transmission.",
        "description": "Returns once QUEUED, not once transmitted: the DAC paces the datapath, and\nblocking the caller on it would export the modem's back-pressure to HTTP.\nFor a sustained feed use `/ws/tx` instead — one HTTP request per packet\nreintroduces the per-block `open`/`close` cost measured on 2026-09-08.",
        "operationId": "transmit",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/TransmitRequest"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/TransmitAcceptedDRO"
                }
              }
            }
          },
          "400": {
            "description": "the framing cannot hold its alignment, or a packet does not fit its slot"
          },
          "404": {
            "description": "no datapath in this daemon"
          },
          "409": {
            "description": "the chain cannot carry the emission; the body names the first missing step and the command that clears it (STE-995). Pass `force: true` to transmit anyway"
          }
        }
      }
    },
    "/api/v1/events": {
      "get": {
        "tags": [
          "events"
        ],
        "operationId": "list",
        "parameters": [
          {
            "name": "kind",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          },
          {
            "name": "severity",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "nullable": true
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "nullable": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventDRO"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "events"
        ],
        "operationId": "purge",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/PurgeQueryDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurgeResultDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/events/latest": {
      "get": {
        "tags": [
          "events"
        ],
        "operationId": "latest",
        "parameters": [
          {
            "name": "n",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/frames": {
      "get": {
        "tags": [
          "frames"
        ],
        "operationId": "list",
        "parameters": [
          {
            "name": "protocol",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          },
          {
            "name": "profile",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "nullable": true
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "nullable": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FramesPageDRO"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "frames"
        ],
        "operationId": "purge",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PurgeResultDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/frames/{id}": {
      "get": {
        "tags": [
          "frames"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Frame id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FrameDRO"
                }
              }
            }
          },
          "404": {
            "description": "not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/health": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "health",
        "responses": {
          "200": {
            "description": "Process is alive",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/HealthDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/iq/captures": {
      "get": {
        "tags": [
          "iq"
        ],
        "operationId": "list",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/IqCaptureDRO"
                  }
                }
              }
            }
          }
        }
      },
      "post": {
        "tags": [
          "iq"
        ],
        "operationId": "start",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StartCaptureDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "201": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IqCaptureDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/iq/captures/{id}": {
      "get": {
        "tags": [
          "iq"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Capture id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/IqCaptureDRO"
                }
              }
            }
          },
          "404": {
            "description": "not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "iq"
        ],
        "operationId": "delete_one",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Capture id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "204": {
            "description": "Deleted"
          },
          "404": {
            "description": "not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/iq/captures/{id}/download": {
      "get": {
        "tags": [
          "iq"
        ],
        "operationId": "download",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "Capture id",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Raw IQ bytes (little-endian i16 I+Q pairs)"
          }
        }
      }
    },
    "/api/v1/iq/streams": {
      "get": {
        "tags": [
          "iq"
        ],
        "operationId": "iq_streams",
        "responses": {
          "501": {
            "description": "WS live streams ship with L7b",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "list",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/JobDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/jobs/{job_id}": {
      "get": {
        "tags": [
          "jobs"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "job_id",
            "in": "path",
            "description": "Job identifier",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/JobDRO"
                }
              }
            }
          },
          "404": {
            "description": "Job not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/logs": {
      "get": {
        "tags": [
          "logs"
        ],
        "operationId": "list",
        "parameters": [
          {
            "name": "level",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "nullable": true
            }
          },
          {
            "name": "until",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "format": "date-time",
              "nullable": true
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/EventDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/metrics": {
      "get": {
        "tags": [
          "system"
        ],
        "summary": "The Prometheus registry, as JSON, on the API port.",
        "description": "WHY IT EXISTS ALONGSIDE `:9090/metrics`. The exposition is served on the\nobservability port, which is right for a scraper and unusable from the\ndashboard: the frontend talks to the API port, a second origin needs CORS\nthe metrics server does not set, and a browser has no business splitting\nexposition text by hand.\n\n404 WHEN THERE IS NO EXPORTER, rather than an empty list. \"This daemon has\nno metrics\" and \"every metric reads zero\" are different facts and a caller\ncannot tell them apart from `[]`.",
        "operationId": "metrics",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/MetricFamilyDRO"
                  }
                }
              }
            }
          },
          "404": {
            "description": ""
          }
        }
      }
    },
    "/api/v1/monitor/fft": {
      "get": {
        "tags": [
          "monitor"
        ],
        "operationId": "fft",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FftCaptureDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitor/power": {
      "get": {
        "tags": [
          "monitor"
        ],
        "operationId": "power",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PowerDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/monitor/rssi": {
      "get": {
        "tags": [
          "monitor"
        ],
        "operationId": "rssi",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RssiDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pl/aux": {
      "get": {
        "tags": [
          "pl"
        ],
        "operationId": "aux",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuxDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pl/interrupts": {
      "get": {
        "tags": [
          "pl"
        ],
        "operationId": "interrupts",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterruptsDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pl/interrupts/ack": {
      "post": {
        "tags": [
          "pl"
        ],
        "operationId": "ack_interrupts",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AckIrqDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/InterruptsDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pl/profile-sequencer": {
      "get": {
        "tags": [
          "pl"
        ],
        "operationId": "profile_sequencer",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileSequencerDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pl/registers/{addr}": {
      "get": {
        "tags": [
          "pl"
        ],
        "operationId": "read_register",
        "parameters": [
          {
            "name": "addr",
            "in": "path",
            "description": "Register address, hex with 0x prefix",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterDRO"
                }
              }
            }
          },
          "400": {
            "description": "invalid address",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/pl/status": {
      "get": {
        "tags": [
          "pl"
        ],
        "operationId": "status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/PlStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profiles": {
      "get": {
        "tags": [
          "profiles"
        ],
        "operationId": "list",
        "responses": {
          "200": {
            "description": "List of all loaded profiles",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProfileSummaryDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profiles/active": {
      "get": {
        "tags": [
          "profiles"
        ],
        "operationId": "active",
        "responses": {
          "200": {
            "description": "The active profile and whether it still describes the board",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveProfileDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profiles/validate": {
      "post": {
        "tags": [
          "profiles"
        ],
        "operationId": "validate",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ProfileDRO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profiles/{name}": {
      "get": {
        "tags": [
          "profiles"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Profile identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ProfileDRO"
                }
              }
            }
          },
          "404": {
            "description": "Profile not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      },
      "put": {
        "tags": [
          "profiles"
        ],
        "summary": "PUT /profiles/:name — push a profile the daemon does not have.",
        "description": "WHY THIS EXISTS. Until now a profile could only reach the board inside a\nfirmware build: `apply` takes a NAME out of the set loaded from\n`profiles_dir` at startup. So trying a profile meant a release cycle, and an\nexternal client — a control centre, an example script — holding a perfectly\ngood profile had no way to use it.\n\nTHE BODY IS PARSED AS YAML, WHICH ALSO ACCEPTS JSON, because YAML is a\nsuperset of it. One parse path, and a client may send whichever it has;\nprofiles are written as YAML files, so a script that reads one off disk can\nforward the bytes unchanged instead of converting them.\n\nIT IS TRANSIENT, AND THE REPLY SAYS SO. Nothing is written to `profiles_dir`\n— the rootfs is a ramdisk in any case. The client keeps the file and stays\nthe source of truth; a copy quietly persisted on the board would be a second\none, free to drift from the first.",
        "operationId": "register",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Name to register the profile under",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "requestBody": {
          "description": "The profile, as YAML or JSON",
          "content": {
            "text/plain": {
              "schema": {
                "type": "string"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RegisterResultDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profiles/{name}/apply": {
      "post": {
        "tags": [
          "profiles"
        ],
        "operationId": "apply",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Profile to apply",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApplyResultDRO"
                }
              }
            }
          },
          "404": {
            "description": "Profile not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/profiles/{name}/revisions": {
      "get": {
        "tags": [
          "profiles"
        ],
        "operationId": "list_revisions",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Profile identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Revision history sorted newest-first",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ProfileRevisionDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/frequency": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "set_frequency",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetFrequencyDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/gain": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "set_gain",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SetGainDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/loopback": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "enable_loopback",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnableLoopbackDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/reset": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "reset",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ResetDTO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          },
          "400": {
            "description": "invalid scope",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/rf/calibrate": {
      "post": {
        "tags": [
          "radio"
        ],
        "summary": "Back to the PL loopback: the AD9361 is bypassed entirely.",
        "operationId": "rf_calibrate",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfStateDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/rf/kill": {
      "post": {
        "tags": [
          "radio"
        ],
        "summary": "EMERGENCY STOP: take RF off the connector, now.",
        "description": "WHAT ACTUALLY REMOVES RF, and what only looks like it. The TX attenuation\nbottoms out at -89.75 dB and still emits a carrier. An empty DAC still leaks\nthe LO. Closing the modem's egress stops the samples without touching the\nmixers — measured on this bench, that leaves the LO leakage on the analyser.\nOnly the transceiver's own state machine takes the transmitter down, so\n`ensm_mode = alert` is the first thing this does and the one that decides\nwhether the button worked.\n\nThe digital steps follow rather than lead: they stop the chain feeding a\npart that is already off, which matters for what the board is doing when it\ncomes back, not for whether it is emitting.\n\nEVERY STEP IS READ BACK and reported by name. An emergency stop that reports\nsuccess without checking is worse than no button: it converts \"I am not\ntransmitting\" from something you verified into something you were told.\n\nIt is deliberately NOT a toggle. Coming back is the ordinary conditioning\nsequence (`satlinkctl chain up`), which re-establishes the ENSM, the tuning\nthe part lost by leaving FDD, the profile and the path — in that order.",
        "operationId": "rf_kill",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfKillDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/rf/mode/air": {
      "post": {
        "tags": [
          "radio"
        ],
        "summary": "ON AIR. Identical to `mode/rf` except the part's internal loopback stays",
        "description": "OPEN, so the signal reaches the mixers and the TX port.\n\nThe distinction is not cosmetic and it cost an evening: `mode/rf` closes the\nAD9361's DIGITAL loopback, which returns TX to RX BEFORE the mixers. In that\nmode the LOs, the gains and the TX attenuation have no effect whatsoever, so\nnothing reaches an antenna and none of those settings explains why.",
        "operationId": "rf_mode_air",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RfStateDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/rx{chan}/arm": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "rx_arm",
        "parameters": [
          {
            "name": "chan",
            "in": "path",
            "description": "RX channel (1/2)",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/rx{chan}/disarm": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "rx_disarm",
        "parameters": [
          {
            "name": "chan",
            "in": "path",
            "description": "channel number",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/status": {
      "get": {
        "tags": [
          "radio"
        ],
        "operationId": "status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/tx{chan}/start": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "tx_start",
        "parameters": [
          {
            "name": "chan",
            "in": "path",
            "description": "channel number",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/radio/tx{chan}/stop": {
      "post": {
        "tags": [
          "radio"
        ],
        "operationId": "tx_stop",
        "parameters": [
          {
            "name": "chan",
            "in": "path",
            "description": "channel number",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/RadioStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reports": {
      "get": {
        "tags": [
          "reports"
        ],
        "operationId": "list",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "description": "Maximum number of rows to return (default 100).",
            "required": false,
            "schema": {
              "type": "integer",
              "minimum": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Restrict to a specific status (`passed`, `failed`, `cancelled`).\nRunning rows are always filtered out of `/reports`.",
            "required": false,
            "schema": {
              "type": "string",
              "nullable": true
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ReportSummaryDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/reports/{run_id}": {
      "get": {
        "tags": [
          "reports"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "description": "Run identifier",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReportDetailDRO"
                }
              }
            }
          },
          "404": {
            "description": "Report not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenario-runs": {
      "get": {
        "tags": [
          "scenarios"
        ],
        "operationId": "list_runs",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ScenarioRunDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenario-runs/{run_id}": {
      "get": {
        "tags": [
          "scenarios"
        ],
        "operationId": "get_run",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "description": "Run identifier",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScenarioRunDRO"
                }
              }
            }
          },
          "404": {
            "description": "Run not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenario-runs/{run_id}/events": {
      "get": {
        "tags": [
          "scenarios"
        ],
        "operationId": "get_run_events",
        "parameters": [
          {
            "name": "run_id",
            "in": "path",
            "description": "Run identifier",
            "required": true,
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Execution event timeline (empty while the run is still in-flight)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ExecutionEventDRO"
                  }
                }
              }
            }
          },
          "404": {
            "description": "Run not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenarios": {
      "get": {
        "tags": [
          "scenarios"
        ],
        "operationId": "list",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "type": "array",
                  "items": {
                    "$ref": "#/components/schemas/ScenarioSummaryDRO"
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenarios/active": {
      "get": {
        "tags": [
          "scenarios"
        ],
        "operationId": "active",
        "responses": {
          "200": {
            "description": "The run in flight, or null",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ActiveRunDRO"
                    }
                  ],
                  "nullable": true
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenarios/active/cancel": {
      "post": {
        "tags": [
          "scenarios"
        ],
        "operationId": "cancel_active",
        "responses": {
          "200": {
            "description": "Stop requested — returns the run it asked",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ActiveRunDRO"
                }
              }
            }
          },
          "409": {
            "description": "Nothing is running",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenarios/validate": {
      "post": {
        "tags": [
          "scenarios"
        ],
        "operationId": "validate",
        "requestBody": {
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ScenarioDRO"
              }
            }
          },
          "required": true
        },
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ValidationDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenarios/{name}": {
      "get": {
        "tags": [
          "scenarios"
        ],
        "operationId": "get_one",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Scenario identifier",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScenarioDRO"
                }
              }
            }
          },
          "404": {
            "description": "Scenario not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/scenarios/{name}/run": {
      "post": {
        "tags": [
          "scenarios"
        ],
        "operationId": "run",
        "parameters": [
          {
            "name": "name",
            "in": "path",
            "description": "Scenario to run",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Run started — returns a run summary",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ScenarioRunDRO"
                }
              }
            }
          },
          "404": {
            "description": "Scenario not found",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/crate.error.ApiErrorDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/system/capabilities": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "capabilities",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CapabilitiesDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/system/clocks": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "clocks",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ClocksDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/system/status": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "status",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/SystemStatusDRO"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/version": {
      "get": {
        "tags": [
          "system"
        ],
        "operationId": "version",
        "responses": {
          "200": {
            "description": "",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/VersionDRO"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "AckIrqDTO": {
        "type": "object",
        "required": [
          "bits"
        ],
        "properties": {
          "bits": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "ActiveDRO": {
        "type": "object",
        "required": [
          "id",
          "campaign",
          "cursor",
          "total",
          "pausing"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "campaign": {
            "type": "string"
          },
          "cursor": {
            "type": "integer",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "pausing": {
            "type": "boolean"
          }
        }
      },
      "ActiveProfileDRO": {
        "type": "object",
        "description": "The active profile, AND whether it still describes the board.\n\n`name` alone was a claim that outlived its truth: moving the frequency, a\ngain or a register by hand leaves the profile reported as applied while the\nboard no longer matches it. Same idea the RF readiness already applies to\nthe digital tuning, generalised — a status that asserts something no longer\ntrue is worse than no status, because it is believed.",
        "required": [
          "dirty"
        ],
        "properties": {
          "name": {
            "type": "string",
            "nullable": true
          },
          "dirty": {
            "type": "boolean",
            "description": "A manual change landed AFTER the profile was applied."
          },
          "dirty_reason": {
            "type": "string",
            "description": "What made it dirty — the route or register. \"dirty\" alone sends the\nreader looking through everything.",
            "nullable": true
          }
        }
      },
      "ApiErrorDRO": {
        "type": "object",
        "description": "Error payload returned to clients.",
        "required": [
          "code",
          "message"
        ],
        "properties": {
          "code": {
            "type": "string",
            "description": "Machine-readable error code (`not_found`, `validation_failed`, …)."
          },
          "message": {
            "type": "string",
            "description": "Human-readable message."
          }
        }
      },
      "ApplyResultDRO": {
        "type": "object",
        "description": "Result of `POST /profiles/{id}/apply`.",
        "required": [
          "profile_name",
          "cfg_epoch",
          "tuning"
        ],
        "properties": {
          "profile_name": {
            "type": "string"
          },
          "cfg_epoch": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "tuning": {
            "type": "string",
            "description": "What the apply did to the RADIO, in one sentence — tuned, or why not.\n\nApplying a profile used to leave the AD9361 exactly where it was while\nthe reply said nothing about it (STE-994), so a UHF profile could be\n\"applied\" onto a part listening in S-band and every field in this\nstruct stayed green. A tuning that did not happen has to be visible\nwhere the apply is."
          }
        }
      },
      "AssertionResultDRO": {
        "type": "object",
        "description": "One assertion's outcome, surfaced in the report detail endpoint.",
        "required": [
          "name",
          "passed",
          "verified",
          "message"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "passed": {
            "type": "boolean"
          },
          "verified": {
            "type": "boolean",
            "description": "Whether this assertion read the LINK or only the scenario's own\ntimeline.\n\nIt was missing from this DTO while `scenario_passed` required\n`passed && verified` on every assertion — so the field that decides the\nverdict was the one field a caller could not see, and a green run was\nindistinguishable from a run that had checked the script against\nitself. That distinction is the entire reason the flag exists."
          },
          "message": {
            "type": "string"
          }
        }
      },
      "AttemptDRO": {
        "type": "object",
        "required": [
          "step",
          "outcome",
          "detail",
          "at_uptime_ms"
        ],
        "properties": {
          "step": {
            "type": "string"
          },
          "outcome": {
            "type": "string"
          },
          "run_id": {
            "type": "string",
            "nullable": true
          },
          "detail": {
            "type": "string"
          },
          "at_uptime_ms": {
            "type": "integer",
            "format": "int64",
            "description": "MILLISECONDS SINCE THE DAEMON STARTED, not since the epoch. The board\nhas no RTC; a client subtracting this from its own wall clock would be\nout by decades, which has already cost this project a UI feature.",
            "minimum": 0
          }
        }
      },
      "AuxDRO": {
        "type": "object",
        "required": [
          "available",
          "regs"
        ],
        "properties": {
          "available": {
            "type": "boolean"
          },
          "reason": {
            "type": "string",
            "description": "Set when nothing could be mapped — typically satlinkd not running as\nroot, or /dev/mem absent.",
            "nullable": true
          },
          "regs": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuxRegDRO"
            }
          }
        }
      },
      "AuxRegDRO": {
        "type": "object",
        "required": [
          "name",
          "addr",
          "doc"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "addr": {
            "type": "string"
          },
          "value": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          },
          "doc": {
            "type": "string",
            "description": "What the value means; present even when the read failed, so a client can\nshow the contract without the number."
          },
          "error": {
            "type": "string",
            "description": "Why `value` is absent. Never left implicit: a missing number that does\nnot say why is indistinguishable from a zero.",
            "nullable": true
          }
        }
      },
      "BlockMatchDRO": {
        "type": "object",
        "required": [
          "index",
          "matched",
          "total",
          "rotation",
          "ratio"
        ],
        "properties": {
          "index": {
            "type": "integer",
            "minimum": 0
          },
          "matched": {
            "type": "integer",
            "minimum": 0
          },
          "total": {
            "type": "integer",
            "minimum": 0
          },
          "rotation": {
            "type": "integer",
            "minimum": 0
          },
          "ratio": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "CampaignActiveDRO": {
        "type": "object",
        "required": [
          "board_provider"
        ],
        "properties": {
          "active": {
            "allOf": [
              {
                "$ref": "#/components/schemas/ActiveDRO"
              }
            ],
            "nullable": true
          },
          "board_provider": {
            "type": "string",
            "description": "Who would take a board action, in words. \"the orchestrator power-cycled\nit\" and \"nobody did, the step was refused\" must not read the same\nafterwards."
          }
        }
      },
      "CampaignStateDRO": {
        "type": "object",
        "required": [
          "id",
          "campaign",
          "status",
          "cursor",
          "total_steps",
          "pause_requested",
          "attempts",
          "started_at_uptime_ms"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "campaign": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "cursor": {
            "type": "integer",
            "minimum": 0
          },
          "total_steps": {
            "type": "integer",
            "minimum": 0
          },
          "verdict": {
            "type": "boolean",
            "description": "`true` / `false` / **null** — and null is the common case.\n\nA CAMPAIGN THAT DID NOT FINISH HAS NO VERDICT. One paused at step 3 of\n15 must not report a pass on twelve steps nobody ran, and a client that\nrendered `false` there would be accusing the board of failures it was\nnever asked to produce.",
            "nullable": true
          },
          "pause_requested": {
            "type": "boolean",
            "description": "A pause is asked for and has not yet taken effect."
          },
          "attempts": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AttemptDRO"
            }
          },
          "started_at_uptime_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        }
      },
      "CampaignSummaryDRO": {
        "type": "object",
        "required": [
          "name",
          "description",
          "steps",
          "tags"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string"
          },
          "steps": {
            "type": "integer",
            "minimum": 0
          },
          "cron": {
            "type": "string",
            "description": "The cron expression, when this campaign schedules itself.",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "CapabilitiesDRO": {
        "type": "object",
        "required": [
          "modulations",
          "rx_channels",
          "tx_channels",
          "mock",
          "symbol_rates"
        ],
        "properties": {
          "modulations": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Supported modulations (strings matching the profile enum)."
          },
          "rx_channels": {
            "type": "integer",
            "format": "int32",
            "description": "Number of RX channels exposed by the platform.",
            "minimum": 0
          },
          "tx_channels": {
            "type": "integer",
            "format": "int32",
            "description": "Number of TX channels exposed by the platform.",
            "minimum": 0
          },
          "mock": {
            "type": "boolean",
            "description": "`true` when the PL driver is a mock (no real HW attached)."
          },
          "symbol_rates": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/SymbolRateDRO"
            },
            "description": "THE SYMBOL RATES THIS MODEM CAN PRODUCE, and the two it cannot.\n\nNot a menu someone chose: the rate is `ad9361_rate / (sps * interp)`\nwith sps fixed at 4 and interp decoded only over 1..32, so the set is\nfixed by the hardware. Clients build their forms from this rather than\noffering a free number — `phy.symbol_rate_baud` accepted anything and\nreached no register at all, so a profile declaring 9600 baud transmitted\nat about 2 MBaud (measured 2026-09-13).\n\nThe unreachable entries are PRESENT, with the bound they hit, rather\nthan silently absent: a list that quietly drops what it cannot do reads\nas a complete list of what exists."
          }
        }
      },
      "CaptureRequest": {
        "type": "object",
        "required": [
          "blocks"
        ],
        "properties": {
          "blocks": {
            "type": "integer",
            "description": "Number of RAW DMA blocks to collect. Obtaining them in a row is the\nproof of a sustained regime; a short capture is refused, not truncated.",
            "minimum": 0
          },
          "warmup": {
            "type": "integer",
            "description": "Blocks to obtain and DISCARD before the measured window starts.\n\nEvery capture re-arms the chain, so the leading blocks carry the\nacquisition transient. Skipping them is legitimate; skipping them\nSILENTLY, or guessing which ones look like transient, would throw away\nreal head-of-capture errors and call the result clean. Hence explicit,\nand zero by default.",
            "minimum": 0
          },
          "timeout_ms": {
            "type": "integer",
            "format": "int64",
            "description": "Give up after this many milliseconds.",
            "minimum": 0
          },
          "release": {
            "type": "boolean",
            "description": "Release the IIO devices afterwards so bring-up tools can open them."
          }
        }
      },
      "ChainStateDRO": {
        "type": "object",
        "required": [
          "target",
          "reaches_the_connector",
          "stage",
          "ready",
          "conditions"
        ],
        "properties": {
          "target": {
            "type": "string",
            "description": "The target these conditions were computed for."
          },
          "current_target": {
            "type": "string",
            "description": "What the board is wired for right now; `null` when the two loopback\nswitches could not be read.",
            "nullable": true
          },
          "reaches_the_connector": {
            "type": "boolean",
            "description": "Does anything leave the board in this target? False for both loopbacks —\nthe AD9361's internal one returns TX to RX BEFORE the mixers, so no LO,\ngain or attenuation setting can explain an empty analyser."
          },
          "stage": {
            "type": "string"
          },
          "ready": {
            "type": "boolean"
          },
          "next": {
            "type": "string",
            "description": "The first unmet condition and the command to run. `null` when ready.",
            "nullable": true
          },
          "conditions": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ConditionDRO"
            },
            "description": "Every condition, in the order the bring-up needs them."
          }
        }
      },
      "ClockDRO": {
        "type": "object",
        "required": [
          "name",
          "enable",
          "prepare",
          "rate_hz"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "enable": {
            "type": "integer",
            "format": "int32",
            "description": "Authoritative: non-zero means the clock is enabled.",
            "minimum": 0
          },
          "prepare": {
            "type": "integer",
            "format": "int32",
            "description": "Authoritative: non-zero means it is prepared.",
            "minimum": 0
          },
          "rate_hz": {
            "type": "integer",
            "format": "int64",
            "description": "NOT authoritative. Computed by the clock framework from the parent and\nthe dividers, and printed unchanged with the MMCM off (STE-943). Present\nfor display; never judge on it.",
            "minimum": 0
          }
        }
      },
      "ClocksDRO": {
        "type": "object",
        "required": [
          "available",
          "source",
          "clocks"
        ],
        "properties": {
          "available": {
            "type": "boolean",
            "description": "`None` when debugfs is not mounted — distinct from \"no clocks\", which\nwould read as a healthy empty set."
          },
          "source": {
            "type": "string"
          },
          "clocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ClockDRO"
            }
          }
        }
      },
      "ConditionDRO": {
        "type": "object",
        "required": [
          "key",
          "stage",
          "source",
          "required",
          "observed",
          "verdict",
          "breaks",
          "fix"
        ],
        "properties": {
          "key": {
            "type": "string",
            "description": "Stable key — `dac-source`, `tx-egress`, …"
          },
          "stage": {
            "type": "string",
            "description": "The stage this must be satisfied to leave."
          },
          "source": {
            "type": "string",
            "description": "`board` when the value was re-read from a register or an IIO attribute,\n`session` when it is this daemon's memory and a restart forgets it."
          },
          "required": {
            "type": "string"
          },
          "observed": {
            "type": "string",
            "description": "What it is, or `could not be read`."
          },
          "verdict": {
            "type": "string",
            "description": "`met` / `unmet` / `unknown`. An `unknown` BLOCKS: a register we failed to\nread is not a register holding the right value."
          },
          "breaks": {
            "type": "string",
            "description": "What goes wrong when it is unmet, as the symptom the operator will see."
          },
          "fix": {
            "type": "string",
            "description": "The command that clears it."
          }
        }
      },
      "ConditionRequest": {
        "type": "object",
        "properties": {
          "target": {
            "type": "string",
            "description": "Where the samples should go. Defaults to `air` — the only target of the\nthree that puts anything on a connector, and the one an operator at a\nspectrum analyser is asking for.",
            "nullable": true
          },
          "profile": {
            "type": "string",
            "description": "Which profile to apply. Defaults to the one currently applied; with none\napplied the request is refused rather than guessing, because the profile\ndecides the frame length, the coding and the frequency.",
            "nullable": true
          },
          "rate_sps": {
            "type": "integer",
            "format": "int64",
            "description": "AD9361 rate for the tuning step, when one is needed.",
            "minimum": 0
          }
        }
      },
      "ConditionResultDRO": {
        "type": "object",
        "required": [
          "steps",
          "detail",
          "state"
        ],
        "properties": {
          "steps": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "The steps actually run, in order."
          },
          "detail": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What each step reported, keyed in the same order as `steps`."
          },
          "state": {
            "$ref": "#/components/schemas/ChainStateDRO"
          }
        }
      },
      "DatapathStateDRO": {
        "type": "object",
        "required": [
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "DatapathStatusDRO": {
        "type": "object",
        "required": [
          "acquired"
        ],
        "properties": {
          "acquired": {
            "type": "boolean",
            "description": "True while the daemon holds the modem's IIO devices."
          }
        }
      },
      "EnableLoopbackDTO": {
        "type": "object",
        "required": [
          "enabled"
        ],
        "properties": {
          "enabled": {
            "type": "boolean"
          }
        }
      },
      "EventDRO": {
        "type": "object",
        "description": "Event record returned by `/events`.",
        "required": [
          "kind",
          "source",
          "severity",
          "message",
          "timestamp"
        ],
        "properties": {
          "id": {
            "type": "integer",
            "format": "int64",
            "nullable": true
          },
          "kind": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "severity": {
            "type": "string"
          },
          "message": {
            "type": "string"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "ExecutionEventDRO": {
        "type": "object",
        "description": "One entry in a run's execution timeline. Mirrors\n`satlink_scenario_engine::report::ExecutionEvent` with a stringified\n`kind` for wire stability (the engine enum is free to add variants).",
        "required": [
          "elapsed_ms",
          "kind",
          "description"
        ],
        "properties": {
          "elapsed_ms": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "event_id": {
            "type": "string",
            "nullable": true
          },
          "kind": {
            "type": "string"
          },
          "description": {
            "type": "string"
          }
        }
      },
      "FftCaptureDRO": {
        "type": "object",
        "required": [
          "size",
          "source",
          "bins_db",
          "synthetic",
          "channel",
          "enabled",
          "sample_rate_hz"
        ],
        "properties": {
          "size": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "source": {
            "type": "integer",
            "format": "int32",
            "description": "`monitor.fft.SRC_SEL` — which tap the core is pointed at.",
            "minimum": 0
          },
          "bins_db": {
            "type": "array",
            "items": {
              "type": "number",
              "format": "double"
            },
            "description": "Frequency-domain magnitude samples."
          },
          "synthetic": {
            "type": "boolean",
            "description": "TRUE while these bins are MANUFACTURED HERE and do not come from the PL.\n\nThe field exists because nothing in the response distinguished a\nmeasurement from an invention: a spectrum served by `/api/v1/...` reads\nas signal, and the UI displayed it as such. The client now labels itself\nfrom this value rather than from a hard-coded string, which would have\ngone stale the day the real path landed — and a stale note is this\nproject's dominant failure mode.\n\nSet it to `false` IN THE SAME COMMIT as the real sample read, not\nbefore: a premature `false` is worse than today's `true`, because it\nmakes the invention undetectable."
          },
          "channel": {
            "type": "string",
            "description": "Which end of the link this spectrum describes (`\"rx\"` / `\"tx\"`)."
          },
          "enabled": {
            "type": "boolean",
            "description": "Whether analysis is switched on for that channel. Off by default: a\nframe costs a DMA block plus a transform, and nothing should pay that\nbecause a dashboard tab happens to be open."
          },
          "sample_rate_hz": {
            "type": "integer",
            "format": "int64",
            "description": "Sample rate the block was taken at, 0 when there is no block.",
            "minimum": 0
          },
          "unavailable": {
            "allOf": [
              {
                "$ref": "#/components/schemas/satlink_spectrum.Unavailable"
              }
            ],
            "nullable": true
          }
        }
      },
      "FlushRequest": {
        "type": "object",
        "properties": {
          "blocks": {
            "type": "integer",
            "description": "How many blocks to emit. **Use 2 whenever you intend to see the result\ncome back.** An RX block is returned only when ENTIRELY full, and the\nreceive side is always short of what was sent — the first frame of a\nfreshly-armed chain is lost to acquisition, so 4096 useful bytes out\ncome back as 4080. One block therefore stops just before the line and\nwaits for traffic a one-shot sender never sends.\n\nMeasured on hardware 2026-09-12: one flushed block never surfaced in\n60 s (with `tx.framer.frame_cnt` confirming all 256 frames had\ncrossed); two came back in 2 s.",
            "minimum": 0
          }
        }
      },
      "FlushedDRO": {
        "type": "object",
        "required": [
          "useful_bytes"
        ],
        "properties": {
          "useful_bytes": {
            "type": "integer",
            "description": "Useful bytes HANDED TO THE DMA, padding included. Zero means nothing\nwas pending — a block of pure padding would put silence on the air.\n\nHanded to the DMA is not \"on the air\". This field used to say it was,\nand it was wrong: with the TX egress closed the modem discards the\nblock and this number is exactly the same. `tx_chain_running` is the\nonly field here that tells the two apart.",
            "minimum": 0
          },
          "tx_chain_running": {
            "type": "boolean",
            "description": "Whether the modem's TX egress is open. `null` when there is no PL\ndriver to ask, which is not the same as `false`.",
            "nullable": true
          }
        }
      },
      "FrameDRO": {
        "type": "object",
        "required": [
          "id",
          "timestamp",
          "protocol",
          "fec_status",
          "payload_hex"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          },
          "profile": {
            "type": "string",
            "nullable": true
          },
          "protocol": {
            "type": "string"
          },
          "fec_status": {
            "type": "string"
          },
          "crc_ok": {
            "type": "boolean",
            "nullable": true
          },
          "payload_hex": {
            "type": "string"
          }
        }
      },
      "FramesPageDRO": {
        "type": "object",
        "description": "One page of the ring, plus what the caller cannot see from the rows.\n\n`[]` used to be the whole answer, and it conflated two very different\nfacts: \"the link carried nothing\" and \"nothing is reading the modem\". The\nfirst is information about the RADIO, the second about the DAEMON, and an\noperator reads the empty array as the first. Same shape as `/pl/aux`, which\nreports `available: false` WITH its reason.",
        "required": [
          "granularity",
          "clock",
          "frames"
        ],
        "properties": {
          "granularity": {
            "type": "string",
            "description": "What ONE entry is. `\"dma_block\"` today: the producer pushes one entry\nper DMA block, so an entry is 4096 useful bytes cut at a boundary that\nhas no relation to any protocol frame. Calling that a frame is the\ndefect STE-989 is about; naming it is the honest half of the fix.\nFor packet granularity use `/ws/frames?framing=slot&slot_len=N`."
          },
          "clock": {
            "type": "string",
            "description": "How to read `timestamp`. `\"boot_relative\"`: the board has no RTC and no\nNTP, so times start at the epoch on every boot. A control centre would\notherwise take them for wall-clock."
          },
          "empty_reason": {
            "type": "string",
            "description": "`null` when the page is non-empty. Otherwise WHY it is empty.",
            "nullable": true
          },
          "frames": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FrameDRO"
            }
          }
        }
      },
      "HealthDRO": {
        "type": "object",
        "required": [
          "status"
        ],
        "properties": {
          "status": {
            "type": "string"
          }
        }
      },
      "InterruptsDRO": {
        "type": "object",
        "required": [
          "status",
          "enable",
          "bits"
        ],
        "properties": {
          "status": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "enable": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "bits": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "IqAnnotationDTO": {
        "type": "object",
        "required": [
          "offset_samples",
          "label"
        ],
        "properties": {
          "offset_samples": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "label": {
            "type": "string"
          },
          "note": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "IqCaptureDRO": {
        "type": "object",
        "required": [
          "id",
          "created_at",
          "samples",
          "sample_rate_hz",
          "center_frequency_hz",
          "path",
          "size_bytes",
          "status",
          "tags",
          "annotations",
          "synthetic"
        ],
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "samples": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "sample_rate_hz": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "center_frequency_hz": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "path": {
            "type": "string"
          },
          "size_bytes": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "status": {
            "type": "string"
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "annotations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IqAnnotationDTO"
            }
          },
          "profile_ref": {
            "type": "string",
            "nullable": true
          },
          "synthetic": {
            "type": "boolean",
            "description": "TRUE when the samples were MANUFACTURED rather than captured. Without\nit a fabricated capture is indistinguishable from a measurement: it\ncarries the true centre frequency and the active profile, and opens in\nany viewer showing a clean carrier."
          }
        }
      },
      "JobDRO": {
        "type": "object",
        "description": "A job descriptor returned when kicking off an async operation\n(apply profile, run scenario, capture IQ, …).",
        "required": [
          "job_id",
          "kind",
          "status",
          "created_at",
          "updated_at"
        ],
        "properties": {
          "job_id": {
            "type": "string",
            "format": "uuid"
          },
          "kind": {
            "type": "string"
          },
          "status": {
            "$ref": "#/components/schemas/JobStatusDRO"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          },
          "updated_at": {
            "type": "string",
            "format": "date-time"
          },
          "progress": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "MeasureReportDRO": {
        "type": "object",
        "required": [
          "verdict",
          "rotation_slip",
          "blocks",
          "overall_ratio",
          "worst_block_ratio",
          "capture_bytes",
          "warmup"
        ],
        "properties": {
          "verdict": {
            "type": "string",
            "description": "`BytePerfect` | `Degraded` | `NoCorrelation`."
          },
          "rotation_slip": {
            "type": "boolean",
            "description": "A byte was lost or inserted at a block boundary. NOT a demodulation\nerror — reporting it separately is what stops it being read as one."
          },
          "blocks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/BlockMatchDRO"
            },
            "description": "Read this before `overall_ratio`: an aggregate is exactly what hid the\n2026-09-08 defect, where the whole capture scored 49.99 % while every\nblock taken separately scored 99.98 %."
          },
          "overall_ratio": {
            "type": "number",
            "format": "double"
          },
          "worst_block_ratio": {
            "type": "number",
            "format": "double"
          },
          "capture_bytes": {
            "type": "integer",
            "minimum": 0
          },
          "warmup": {
            "type": "integer",
            "description": "Blocks obtained and discarded before the measured window. Never hidden:\na window that was moved must say so, or the number stops meaning what\nthe reader thinks it means.",
            "minimum": 0
          }
        }
      },
      "PlStatusDRO": {
        "type": "object",
        "required": [
          "global_ctrl",
          "version",
          "build_id",
          "cfg_epoch",
          "irq_status",
          "irq_enable"
        ],
        "properties": {
          "global_ctrl": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "version": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "build_id": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "cfg_epoch": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "irq_status": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "irq_enable": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "PowerDRO": {
        "type": "object",
        "required": [
          "tap",
          "power_dbfs"
        ],
        "properties": {
          "tap": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "power_dbfs": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "ProfileDRO": {
        "allOf": [
          {
            "type": "object"
          },
          {
            "type": "object"
          }
        ],
        "description": "Full profile payload — serialized as the opaque `satlink.profile/v1` shape."
      },
      "ProfileRevisionDRO": {
        "type": "object",
        "description": "One row of a profile's revision history.",
        "required": [
          "version",
          "cfg_epoch",
          "applied_at"
        ],
        "properties": {
          "version": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "cfg_epoch": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "applied_at": {
            "type": "string",
            "format": "date-time"
          },
          "applied_by": {
            "type": "string",
            "nullable": true
          },
          "message": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "ProfileSequencerDRO": {
        "type": "object",
        "required": [
          "state",
          "status",
          "err_code",
          "profile_id",
          "state_name"
        ],
        "properties": {
          "state": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "status": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "err_code": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "profile_id": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "state_name": {
            "type": "string"
          }
        }
      },
      "ProfileSummaryDRO": {
        "type": "object",
        "description": "Summary of a profile for list endpoints.",
        "required": [
          "name",
          "modulation",
          "symbol_rate_baud",
          "center_frequency_hz"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "nullable": true
          },
          "modulation": {
            "type": "string"
          },
          "symbol_rate_baud": {
            "type": "number",
            "format": "double"
          },
          "center_frequency_hz": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          }
        }
      },
      "PurgeQueryDTO": {
        "type": "object",
        "required": [
          "older_than_hours"
        ],
        "properties": {
          "older_than_hours": {
            "type": "integer",
            "format": "int64",
            "description": "Purge events older than this many hours."
          }
        }
      },
      "PurgeResultDRO": {
        "type": "object",
        "required": [
          "purged"
        ],
        "properties": {
          "purged": {
            "type": "integer",
            "minimum": 0
          }
        }
      },
      "RadioStatusDRO": {
        "type": "object",
        "required": [
          "rx_state",
          "tx_state",
          "center_frequency_hz",
          "rx_gain_db",
          "tx_gain_db",
          "loopback",
          "timing_lock",
          "carrier_lock",
          "rx_nco_offset_hz",
          "tx_nco_offset_hz",
          "profile_center_frequency_hz"
        ],
        "properties": {
          "rx_state": {
            "type": "string"
          },
          "tx_state": {
            "type": "string"
          },
          "center_frequency_hz": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "rx_gain_db": {
            "type": "integer",
            "format": "int32"
          },
          "tx_gain_db": {
            "type": "integer",
            "format": "int32"
          },
          "loopback": {
            "type": "boolean"
          },
          "timing_lock": {
            "type": "boolean"
          },
          "carrier_lock": {
            "type": "boolean"
          },
          "rx_lo_hz": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "rx_nco_offset_hz": {
            "type": "integer",
            "format": "int64",
            "description": "The DDC's digital offset from the RX LO, signed, in Hz."
          },
          "tx_nco_offset_hz": {
            "type": "integer",
            "format": "int64",
            "description": "The DUC's digital offset from the TX LO, signed, in Hz."
          },
          "profile_center_frequency_hz": {
            "type": "integer",
            "format": "int64",
            "description": "What the applied PROFILE declares as its centre, read from the inert\n`chan_cfg` shadow. It describes the configuration, not the radio, and\nthe two diverge the moment anyone tunes by hand.",
            "minimum": 0
          },
          "tx_lo_hz": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "rx_rf_gain_db": {
            "type": "number",
            "format": "double",
            "description": "The part's own gain, in dB at the connector. TX is an ATTENUATION.",
            "nullable": true
          },
          "tx_rf_gain_db": {
            "type": "number",
            "format": "double",
            "nullable": true
          },
          "rx_bandwidth_hz": {
            "type": "integer",
            "format": "int64",
            "description": "The part's ANALOG filter width, per direction, read from IIO.\n\nNOT a PL register — the AD9361 has none in this address space, which is\nexactly why the datapath diagram could not show it. `None` means the\nattribute could not be read, never a default: this is the value that sat\nat 18 MHz for a 53 kHz signal (STE-998), and a plausible-looking number\nhere would be worse than an absence.\n\nThe part QUANTISES what is written, so this read-back legitimately differs\nfrom any requested width. It is the truth; the request is not.\n\nSERIALISED EVEN WHEN NULL, unlike its neighbours above, and the difference\nis deliberate. An ABSENT field cannot be told apart from an older daemon\nthat never had it; an explicit `null` says \"this daemon knows about the\nbandwidth and could not read it\". For a field whose whole point is that\nnobody could see it before, that distinction is the feature.",
            "nullable": true,
            "minimum": 0
          },
          "tx_bandwidth_hz": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "sample_rate_sps": {
            "type": "integer",
            "format": "int64",
            "description": "The part's sample rate. Also on `/radio/rf-state`; carried here so that\none call answers \"how is the front end configured\" instead of two that\nhave to be reconciled.",
            "nullable": true,
            "minimum": 0
          }
        }
      },
      "RegisterDRO": {
        "type": "object",
        "required": [
          "addr",
          "value"
        ],
        "properties": {
          "addr": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          },
          "value": {
            "type": "integer",
            "format": "int32",
            "minimum": 0
          }
        }
      },
      "RegisterResultDRO": {
        "type": "object",
        "required": [
          "name",
          "replaced",
          "persisted",
          "note"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "replaced": {
            "type": "boolean",
            "description": "True when a profile of this name already existed and was replaced."
          },
          "persisted": {
            "type": "boolean",
            "description": "Always false today, and present so a client never has to assume."
          },
          "note": {
            "type": "string"
          }
        }
      },
      "ReportDetailDRO": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ReportSummaryDRO"
          },
          {
            "type": "object",
            "required": [
              "has_report",
              "verified",
              "aborted",
              "events",
              "assertions",
              "metric_samples"
            ],
            "properties": {
              "has_report": {
                "type": "boolean",
                "description": "`true` if the run had a persisted report (completed/failed/cancelled)."
              },
              "verified": {
                "type": "boolean",
                "description": "Whether ANY assertion in this run read the link rather than the\nscenario's own script. A report that observed nothing is not a pass, and\nthe reader has to be able to tell the two apart."
              },
              "aborted": {
                "type": "boolean",
                "description": "The run did not reach the end of its timeline. Its assertions were\nwritten for a whole one, so neither verdict applies."
              },
              "events": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/ExecutionEventDRO"
                }
              },
              "assertions": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/AssertionResultDRO"
                }
              },
              "metric_samples": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/MetricSampleDRO"
                },
                "description": "The board's own trace across the run. EMPTY IS NOT \"NOTHING HAPPENED\":\nit means no sampler was attached, which `sample_interval_ms == None`\nis how you tell apart."
              },
              "sample_interval_ms": {
                "type": "integer",
                "format": "int64",
                "description": "The sampler's nominal tick, beside the samples — a series without its\ncadence cannot distinguish a slow poll from a stalled board.",
                "nullable": true,
                "minimum": 0
              },
              "source": {
                "type": "string",
                "description": "Where this run's numbers came from: `\"pl\"`, `\"stub\"`, `\"mixed\"`, `\"none\"`.\n\nSERVED, not kept internal. The point of the field is that a report\ndeclares its own provenance to whoever reads it — and the reader of a\nqualification document is usually not the person who ran it."
              }
            }
          }
        ],
        "description": "Detailed view returned by `GET /reports/{run_id}`. Embeds the parsed\n`satlink.report/v1` payload (events + assertion results)."
      },
      "ReportSummaryDRO": {
        "type": "object",
        "description": "One row of the reports list.",
        "required": [
          "run_id",
          "scenario_name",
          "status",
          "started_at"
        ],
        "properties": {
          "run_id": {
            "type": "string",
            "format": "uuid"
          },
          "scenario_name": {
            "type": "string"
          },
          "profile_ref": {
            "type": "string",
            "description": "Profile referenced by the scenario at the time the run finished, if\nstill known to the daemon's in-memory scenarios map.",
            "nullable": true
          },
          "status": {
            "type": "string"
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "duration_ms": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "steps_executed": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          },
          "assertions_passed": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          },
          "assertions_failed": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          }
        }
      },
      "ResetDTO": {
        "type": "object",
        "required": [
          "scope"
        ],
        "properties": {
          "scope": {
            "type": "string"
          }
        }
      },
      "RfKillDRO": {
        "type": "object",
        "required": [
          "ensm_mode",
          "stopped",
          "failed",
          "rf_off",
          "recover"
        ],
        "properties": {
          "ensm_mode": {
            "type": "string",
            "description": "What the part's ENSM reports AFTER the stop. `alert` is RF off."
          },
          "stopped": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Steps that took effect, in the order they were applied."
          },
          "failed": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Steps that did NOT. Present even when RF is off, because a stop that\nhalf-worked and says nothing is the failure mode this exists against."
          },
          "rf_off": {
            "type": "boolean",
            "description": "Is RF actually off at the connector?"
          },
          "recover": {
            "type": "string",
            "description": "How to come back."
          }
        }
      },
      "RssiDRO": {
        "type": "object",
        "required": [
          "average_dbm",
          "instant_dbm",
          "threshold_dbm"
        ],
        "properties": {
          "average_dbm": {
            "type": "number",
            "format": "double"
          },
          "instant_dbm": {
            "type": "number",
            "format": "double"
          },
          "threshold_dbm": {
            "type": "number",
            "format": "double"
          }
        }
      },
      "ScenarioDRO": {
        "allOf": [
          {
            "type": "object"
          },
          {
            "type": "object"
          }
        ]
      },
      "ScenarioRunDRO": {
        "type": "object",
        "description": "Public view of a scenario execution (summary).",
        "required": [
          "run_id",
          "scenario_name",
          "status",
          "started_at"
        ],
        "properties": {
          "run_id": {
            "type": "string",
            "format": "uuid"
          },
          "scenario_name": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "description": "`pending`, `running`, `passed`, `failed`, `cancelled`."
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "duration_ms": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "steps_executed": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          },
          "assertions_passed": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          },
          "assertions_failed": {
            "type": "integer",
            "format": "int32",
            "nullable": true,
            "minimum": 0
          }
        }
      },
      "ScenarioSummaryDRO": {
        "type": "object",
        "required": [
          "name",
          "tags"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "nullable": true
          },
          "description": {
            "type": "string",
            "nullable": true
          },
          "profile_ref": {
            "type": "string",
            "nullable": true
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "ServiceStatusDRO": {
        "type": "object",
        "required": [
          "name",
          "healthy"
        ],
        "properties": {
          "name": {
            "type": "string"
          },
          "healthy": {
            "type": "boolean"
          }
        }
      },
      "SetFrequencyDTO": {
        "type": "object",
        "required": [
          "center_frequency_hz"
        ],
        "properties": {
          "center_frequency_hz": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "channel": {
            "type": "string",
            "description": "Which side to tune: `\"rx\"`, `\"tx\"` or `\"both\"` (default).\n\nRX AND TX ARE INDEPENDENT LOs. Measured on this bench 2026-09-12: the RX\nLO sat at 2.400 GHz while the TX LO was still at its 2.450 GHz default,\nso an analyser on TX1A at 2.4 GHz saw nothing and the API reported one\n\"center_frequency_hz\" that described neither."
          },
          "tune_lo": {
            "type": "boolean",
            "description": "`true` (default) also drives the AD9361's analogue LO. `false` writes\nonly the PL's digital NCO (`chan_cfg::FREQ_HZ`).\n\nThe distinction is not pedantry: until today this route wrote ONLY the\nNCO, so `/radio/status` reported a centre frequency the transmitter had\nnever been told about."
          }
        }
      },
      "SetGainDTO": {
        "type": "object",
        "properties": {
          "rx_gain_db": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "tx_gain_db": {
            "type": "integer",
            "format": "int32",
            "nullable": true
          },
          "domain": {
            "$ref": "#/components/schemas/GainDomain"
          }
        }
      },
      "StartCaptureDTO": {
        "type": "object",
        "required": [
          "samples"
        ],
        "properties": {
          "samples": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "sample_rate_hz": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "center_frequency_hz": {
            "type": "integer",
            "format": "int64",
            "nullable": true,
            "minimum": 0
          },
          "tags": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "annotations": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/IqAnnotationDTO"
            }
          },
          "profile_ref": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "SymbolRateDRO": {
        "type": "object",
        "required": [
          "baud",
          "label",
          "available"
        ],
        "properties": {
          "baud": {
            "type": "number",
            "format": "double",
            "description": "The exact rate in baud. Fractional at the bottom of the ladder: the\nhardware produces 39 062.5, and rounding it would publish a rate nobody\ncan select."
          },
          "label": {
            "type": "string",
            "description": "As an operator writes it, e.g. \"39.063 ksym/s\"."
          },
          "available": {
            "type": "boolean",
            "description": "False when this rate cannot be produced at all."
          },
          "ad9361_rate_sps": {
            "type": "integer",
            "format": "int64",
            "description": "What the AD9361 must be clocked at to realise it. `null` when unavailable.",
            "nullable": true,
            "minimum": 0
          },
          "interp": {
            "type": "integer",
            "format": "int32",
            "description": "The DUC interpolation that realises it. `null` when unavailable.",
            "nullable": true,
            "minimum": 0
          },
          "unavailable_reason": {
            "type": "string",
            "description": "Which bound it hits. Present exactly when `available` is false.",
            "nullable": true
          }
        }
      },
      "SystemStatusDRO": {
        "type": "object",
        "required": [
          "uptime_seconds",
          "pl_driver",
          "services"
        ],
        "properties": {
          "uptime_seconds": {
            "type": "integer",
            "format": "int64",
            "minimum": 0
          },
          "pl_driver": {
            "type": "string",
            "description": "`mock` or `uio`."
          },
          "services": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ServiceStatusDRO"
            }
          }
        }
      },
      "TransmitAcceptedDRO": {
        "type": "object",
        "required": [
          "packets",
          "useful_bytes",
          "wire_bytes",
          "boundaries_preserved",
          "block_useful_bytes"
        ],
        "properties": {
          "packets": {
            "type": "integer",
            "minimum": 0
          },
          "useful_bytes": {
            "type": "integer",
            "description": "Useful bytes queued (before the `le:u8/32` lane expansion).",
            "minimum": 0
          },
          "wire_bytes": {
            "type": "integer",
            "description": "Bytes actually written to the DMA block.",
            "minimum": 0
          },
          "boundaries_preserved": {
            "type": "boolean",
            "description": "True when packet boundaries survive the crossing. False in stream mode\n— stated rather than left for the caller to discover from a receiver\nthat returns bytes at the wrong offsets."
          },
          "tx_chain_running": {
            "type": "boolean",
            "description": "Whether the modem's TX egress is open.\n\n`false` means these bytes reach the DMA and the modem DISCARDS them —\nnothing goes on the air, and no other number in this reply changes.\n`null` when there is no PL driver to ask.",
            "nullable": true
          },
          "block_useful_bytes": {
            "type": "integer",
            "description": "Useful bytes that fill one DMA block.\n\nNot a measurement of what is buffered — the assembler's fill level\nlives in the service and is not queried here, and reporting a guessed\none would be the `snr_db` mistake of STE-977. It is the quantum that\nmatters to a caller: a packet does NOT go on the air until its block\nis complete, so this is the latency granularity.",
            "minimum": 0
          },
          "chain": {
            "allOf": [
              {
                "$ref": "#/components/schemas/crate.routes.chain.ChainStateDRO"
              }
            ],
            "nullable": true
          }
        }
      },
      "TransmitRequest": {
        "type": "object",
        "required": [
          "packets"
        ],
        "properties": {
          "packets": {
            "type": "array",
            "items": {
              "type": "object"
            },
            "description": "Packets to transmit, tagged by protocol:\n`{\"protocol\":\"raw\",\"payload_hex\":\"..\"}` for bytes forged elsewhere,\n`{\"protocol\":\"csp\",\"header\":{..},\"payload_hex\":\"..\"}` to have SatLink\nbuild the packet, likewise `\"ccsds\"`."
          },
          "framing": {
            "type": "object",
            "description": "How packets are laid out in the byte stream.\n\n`{\"mode\":\"stream\"}` packs them end to end: maximum throughput, and NO\npacket boundary survives — the receiver gets a byte stream cut at the\nPL's `FRAME_LEN`, which has no relation to what was pushed.\n\n`{\"mode\":\"slot\",\"slot_len\":256,\"pad\":0}` gives one packet per slot, so\nthe framer's cut lands on the boundary. `slot_len` must equal the PL's\n`FRAME_LEN` and divide the block's useful bytes, or the request is\nrefused."
          },
          "force": {
            "type": "boolean",
            "description": "Transmit even though the chain cannot carry it (STE-995).\n\nThe refusal is a guard, not a wall: probing a closed-egress branch or an\nunconditioned front-end is legitimate bench work, and\n`dsl/scenarios/bringup/tx_egress_probe.yaml` exists to do exactly that.\nIt is deliberately not the default, and the reply carries the chain state\neither way so no report can claim the chain was healthy."
          }
        }
      },
      "ValidationDRO": {
        "type": "object",
        "description": "Validation result — DRO returned by `POST /profiles/validate`\n(and `POST /scenarios/validate` in the scenarios module).",
        "required": [
          "ok",
          "errors"
        ],
        "properties": {
          "ok": {
            "type": "boolean"
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            }
          }
        }
      },
      "VersionDRO": {
        "type": "object",
        "required": [
          "firmware",
          "api",
          "release"
        ],
        "properties": {
          "firmware": {
            "type": "string",
            "description": "satlinkd crate version."
          },
          "api": {
            "type": "string",
            "description": "API contract version (`v1`)."
          },
          "fpga": {
            "type": "string",
            "description": "PL bitstream identifier read from the shell — `None` in mock mode.\n\nThis is the shell's `BUILD_ID` register, read live over `/dev/uio0`. It\nsays which bitstream is LOADED, which no host-side manifest can.",
            "nullable": true
          },
          "release": {
            "type": "string",
            "description": "Release bundle this daemon was built for, or `\"unreleased\"`.\n\nCompare it with the manifest of the bundle you deployed: they must be\nequal. On a ramdisk rootfs, deploying and running are two different\nthings, and this field is what separates them (STE-985)."
          }
        }
      }
    }
  },
  "tags": [
    {
      "name": "system",
      "description": "Health, version, capabilities"
    },
    {
      "name": "profiles",
      "description": "Radio profile discovery and apply"
    },
    {
      "name": "scenarios",
      "description": "Scenario CRUD and runs"
    },
    {
      "name": "radio",
      "description": "Radio runtime (RX/TX FSM) — planned L6"
    },
    {
      "name": "events",
      "description": "Runtime events — planned L3"
    },
    {
      "name": "jobs",
      "description": "Long-running jobs — L3"
    },
    {
      "name": "monitor",
      "description": "RSSI, power, FFT — planned L7"
    },
    {
      "name": "frames",
      "description": "Decoded frames — planned L7"
    },
    {
      "name": "iq",
      "description": "IQ captures and streams — planned L7"
    },
    {
      "name": "pl",
      "description": "PL debug (registers, sequencer) — planned L7"
    },
    {
      "name": "logs",
      "description": "Structured log query — planned L7"
    },
    {
      "name": "reports",
      "description": "Scenario reports (`satlink.report/v1`)"
    }
  ]
}