openapi: "3.0.3" info: title: OTA Manifest — Daemon API description: | Device-side REST API for the OTA update platform. Each daemon (`mcd`) calls these endpoints to register, heartbeat, poll for updates, download artefacts, and report progress. **10 Mbps bandwidth design points:** - Downloads are **rate-limited** (~25 KB/s per connection) and **gated** (max 50 concurrent). - The download endpoint supports **HTTP Range** (`206 Partial Content`) for resumable transfers. - `check-update` returns a delta patch when available to minimise bytes on the wire. version: "1.0.0" contact: name: Quanta OTA Team servers: - url: http://localhost:8080/api/v1 description: Local development server tags: - name: Device description: Registration, heartbeat, and update checking - name: Package description: Artefact download - name: Deployment description: Lifecycle status reporting # ───────────────────────────────────────────────────────────── # Paths # ───────────────────────────────────────────────────────────── paths: # ═══ Device ═══ /devices/register: post: tags: [Device] summary: Register or refresh a device description: | Called by the daemon on startup. If the `deviceId` already exists this is an upsert — all fields are updated and `lastHeartbeat` is refreshed. operationId: registerDevice requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/RegisterDeviceRequest" example: deviceId: "mcd-7a3f-001" ipAddress: "192.168.1.42" hostname: "gateway-east-01" os: "Debian 12" currentVersion: "1.2.0" groupName: "east-region" responses: "200": description: Device registered or updated content: application/json: schema: $ref: "#/components/schemas/Device" example: id: 1 deviceId: "mcd-7a3f-001" ipAddress: "192.168.1.42" hostname: "gateway-east-01" os: "Debian 12" currentVersion: "1.2.0" status: "online" groupName: "east-region" lastHeartbeat: "2026-07-03T10:30:00Z" createdAt: "2026-07-03T10:30:00Z" updatedAt: "2026-07-03T10:30:00Z" "400": $ref: "#/components/responses/BadRequest" /devices/heartbeat: post: tags: [Device] summary: Send periodic heartbeat description: | Called by the daemon every 30–60 s. Keeps the device marked `online` and updates the `currentVersion` so the server always knows which version each device is running. operationId: heartbeat requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/HeartbeatRequest" example: deviceId: "mcd-7a3f-001" currentVersion: "1.2.0" status: "online" responses: "200": description: Heartbeat acknowledged content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" /devices/check-update: get: tags: [Device] summary: Check whether an update is pending for this device description: | Polled by the daemon on a configurable interval (e.g. every 5 min). When `hasUpdate` is `true` the daemon should proceed to download the referenced artefact and then POST to `/deployments/{id}/status` as the install progresses. operationId: checkUpdate parameters: - name: deviceId in: query required: true schema: type: string description: The device's unique identifier example: "mcd-7a3f-001" responses: "200": description: Update status for the device content: application/json: schema: $ref: "#/components/schemas/CheckUpdateResponse" examples: hasUpdate: summary: Update available value: hasUpdate: true artefactId: 5 version: "1.3.0" fileSize: 4194304 sha256Hash: "a1b2c3d4e5f6..." downloadUrl: "/api/v1/packages/download/5" noUpdate: summary: No update needed value: hasUpdate: false "400": $ref: "#/components/responses/BadRequest" # ═══ Package ═══ /packages/download/{id}: get: tags: [Package] summary: Download an artefact (full or delta patch) description: | Serves the artefact file with **per-connection bandwidth throttling** (~25 KB/s). Supports **HTTP Range** requests so interrupted downloads can be resumed via `Range: bytes=-`. **Headers returned:** - `Content-Disposition: attachment; filename=""` - `X-Checksum-SHA256: ` — validate the file after download - `Accept-Ranges: bytes` - `Content-Length: ` **Query parameters** are optional but recommended — they let the server track each device's download progress. operationId: downloadPackage parameters: - name: id in: path required: true schema: type: integer format: int64 description: Artefact ID (from `check-update` response) example: 5 - name: deviceId in: query required: false schema: type: string description: Device identifier for progress tracking example: "mcd-7a3f-001" - name: deploymentId in: query required: false schema: type: integer format: int64 description: Deployment ID for progress tracking example: 3 responses: "200": description: Full artefact file headers: Content-Disposition: schema: type: string description: Attachment filename X-Checksum-SHA256: schema: type: string description: SHA-256 hex digest of the file Accept-Ranges: schema: type: string example: bytes content: application/octet-stream: schema: type: string format: binary "206": description: Partial content (resumed download) headers: Content-Range: schema: type: string example: "bytes 1048576-4194303/4194304" content: application/octet-stream: schema: type: string format: binary "404": $ref: "#/components/responses/NotFound" "503": description: Server at download capacity — retry later content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" example: error: "Server at capacity. Please retry later." # ═══ Deployment ═══ /deployments/{id}/status: post: tags: [Deployment] summary: Report deployment status for a single device description: | Called by the daemon at each lifecycle transition: 1. `downloading` — started the download 2. `downloaded` — download complete (may be omitted for small files) 3. `verifying` — checking SHA-256 hash 4. `installing` — applying the update 5. `completed` — update applied, device running new version 6. `failed` — unrecoverable error (set `error` field) The server tracks these transitions per-device so operators can monitor rollout progress. operationId: updateDeploymentStatus parameters: - name: id in: path required: true schema: type: integer format: int64 description: Deployment ID example: 3 requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/DeploymentStatusRequest" examples: downloading: value: deviceId: "mcd-7a3f-001" status: "downloading" completed: value: deviceId: "mcd-7a3f-001" status: "completed" failed: value: deviceId: "mcd-7a3f-001" status: "failed" error: "SHA256 mismatch: expected a1b2.. got x9y0.." responses: "200": description: Status recorded content: application/json: schema: $ref: "#/components/schemas/StatusResponse" "400": $ref: "#/components/responses/BadRequest" # ───────────────────────────────────────────────────────────── # Component Schemas # ───────────────────────────────────────────────────────────── components: schemas: # -- Requests -- RegisterDeviceRequest: type: object required: [deviceId] properties: deviceId: type: string maxLength: 256 description: Unique device identifier (set by daemon config) ipAddress: type: string description: Device IP at registration time hostname: type: string description: OS hostname os: type: string description: Operating system name and version currentVersion: type: string description: Currently installed client version groupName: type: string description: Logical group for targeted rollouts (defaults to "default") HeartbeatRequest: type: object required: [deviceId] properties: deviceId: type: string maxLength: 256 currentVersion: type: string description: Currently running version status: type: string enum: [online, offline, upgrading] default: online DeploymentStatusRequest: type: object required: [deviceId, status] properties: deviceId: type: string description: Device identifier status: type: string enum: - downloading - downloaded - verifying - installing - completed - failed error: type: string description: Error description (only meaningful when status = "failed") # -- Responses -- Device: type: object properties: id: type: integer format: int64 deviceId: type: string ipAddress: type: string hostname: type: string os: type: string currentVersion: type: string status: type: string enum: [online, offline, upgrading] groupName: type: string lastHeartbeat: type: string format: date-time createdAt: type: string format: date-time updatedAt: type: string format: date-time CheckUpdateResponse: type: object properties: hasUpdate: type: boolean description: Whether an update is pending artefactId: type: integer format: int64 description: Artefact ID to pass to the download endpoint version: type: string description: Target version string fileSize: type: integer format: int64 description: Total file size in bytes sha256Hash: type: string description: Expected SHA-256 hex digest (validate after download) downloadUrl: type: string description: Download path relative to the server StatusResponse: type: object properties: status: type: string example: "acknowledged" ErrorResponse: type: object properties: error: type: string description: Human-readable error message # -- Reusable responses -- responses: BadRequest: description: Invalid request body or parameters content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" example: error: "deviceId is required" NotFound: description: Resource not found content: application/json: schema: $ref: "#/components/schemas/ErrorResponse" example: error: "Artefact not found" # ───────────────────────────────────────────────────────────── # Daemon update flow (non-normative) # ───────────────────────────────────────────────────────────── x-daemon-flow: polling: interval: "5m (configurable)" description: | 1. **Startup** → `POST /devices/register` 2. **Every N s** → `POST /devices/heartbeat` 3. **Every 5 min** → `GET /devices/check-update?deviceId=xxx` 4. **If hasUpdate==true** → a. `POST /deployments/{id}/status` → `downloading` b. `GET /packages/download/{artefactId}?deviceId=xxx&deploymentId={id}` - Use `Range: bytes=-` to resume interrupted transfers c. Verify SHA-256 hash against `sha256Hash` from step 3 d. `POST /deployments/{id}/status` → `verifying` e. `POST /deployments/{id}/status` → `installing` f. Restart the Java process, confirm health g. `POST /deployments/{id}/status` → `completed` h. `POST /devices/heartbeat` with the new `currentVersion` - **On any failure** → `POST /deployments/{id}/status` → `failed`