---
module: devices.sw-deploy
title: Software Deployment
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Written 2026-07-01 from `hiveops-devices/frontend` + `hiveops-fleet/backend`. Verify against code before relying on specifics.

## Ownership — this feature lives in Fleet, rendered by Devices

The SW Deploy tab is a **Devices SPA view** (`DeviceSoftwareTab.svelte`) but the domain — software download/install tasks — is owned entirely by **hiveops-fleet**. Devices contributes only the device identity and the UI shell. See [platform.service-ownership].

| Concern | Owner |
|---------|-------|
| Tab UI / `atm` object | hiveops-devices (frontend, port 5177) |
| `SOFTWARE` / `SOFTWARE_INSTALL` task lifecycle, storage, dispatch | **hiveops-fleet** (backend, port **8097**, DB `hiveiq_fleet`) |
| Agent auth / device identity source of truth | hiveops-devices backend (`atms`) → mirrored to fleet `device_summary` via Kafka |

## Data flow

### 1. Read path (rendering the tab)
`DeviceSoftwareTab` → `fleetAPI.getTasksPaginated({ atmId, taskKind:['SOFTWARE','SOFTWARE_INSTALL'] })` → gateway `/fleet/tasks/paginated` → `FleetApiController.getFiltered` → `FleetTaskService.getFilteredPaginated` → `FleetTaskSpecification.withFilters` over the **live `fleet_tasks`** table. Frontend derives the two step cards (`latestDownload`, `latestInstall`) and the history table client-side.

### 2. Create path (Schedule Install)
`fleetAPI.createTask({ atmIds:[id], taskKind:'SOFTWARE_INSTALL', scheduledAt? })` → `POST /fleet/tasks` → `FleetTaskService.createTask`:
- Resolves each `atmId` against `device_summary` (`deviceSummaryRepository.findById`); skips if missing, **offline (>15 min since `lastHeartbeat`)**, or out of institution scope.
- `SOFTWARE`/`SOFTWARE_INSTALL` (and `HOTFIX`/`HOTFIX_INSTALL`) are created with initial status **`PENDING_APPROVAL`**.
- If no `scheduledAt` and kind is `SOFTWARE_INSTALL`, defers to the device's assigned **patch window** (`PatchWindowService.nextOccurrence`).
- Persists to `fleet_tasks`, then `FleetTaskEventPublisher.publishCreated` → Kafka.

### 3. Approval → dispatch
`POST /fleet/tasks/{id}/approve` (`FLEET_APPROVER` / `BCOS_ADMIN`) moves the task `PENDING_APPROVAL → PENDING`. The agent (via agent-proxy, role `INTERNAL`) polls `GET /api/internal/fleet/tasks/next?atm={atmId}&kind=...`; `getNextPendingTaskOfKind` returns the oldest `PENDING` task whose `scheduledAt` is null/past, marks it `QUEUED`, and returns artifact metadata (name, version, sha256, CDN url + token) for `SOFTWARE`. `SOFTWARE_INSTALL` carries no artifact — the agent runs `softwareinstall.cmd` against its staged `software/` dir.

### 4. Status reporting → archival (executor → record-store)
Agent reports progress via `POST /api/internal/fleet/tasks/{taskId}/status` (`downloadOffset`, `totalSize`, `status`) → `FleetTaskService.updateTaskStatus`:
- `RUNNING` sets `startedAt`; `COMPLETED`/`FAILED` set `completedAt`; download progress percent is derived from offset/total.
- Every update publishes `FleetTaskEventPublisher.publishStatusUpdate`.
- On **`COMPLETED`**, the row is **archived to `fleet_task_history` and deleted from `fleet_tasks`** (`archiveAndDelete`, also emits `publishArchived`). A `@Scheduled` sweep every 5 min archives stray `COMPLETED`/`CANCELLED`/`REJECTED`; history is purged after 30 days. This is the classic **live-table + archived-record-store** split. See [platform.data-architecture].

## Kafka

| Topic | Direction | Class | Notes |
|-------|-----------|-------|-------|
| `hiveops.fleet.task.events` | **produced** by fleet | `FleetTaskEventPublisher` / `FleetTaskKafkaConfig.FLEET_TASK_EVENTS_TOPIC` | eventTypes `TASK_CREATED`, `TASK_STATUS`, `TASK_ARCHIVED`; keyed per task. Consumed downstream by devices (`FleetTaskEventConsumer` → `device_fleet_tasks` mirror). |
| `hiveops.device.events` | **consumed** by fleet | `DeviceEventConsumer` (group `hiveops-fleet-device-sync`) | Upserts/deletes `device_summary` from devices' `DEVICE_CREATED/UPDATED/DELETED`. This is how fleet knows the device exists, its `institutionKey`, and `lastHeartbeat` seed. |

See [platform.kafka].

## Tables (DB `hiveiq_fleet`)

| Table | Entity | Role in this feature |
|-------|--------|----------------------|
| `fleet_tasks` | `FleetTask` | Live SOFTWARE/SOFTWARE_INSTALL rows — the tab's read source |
| `fleet_task_history` | `FleetTaskHistory` | Archived terminal tasks (completed installs/downloads land here) |
| `device_summary` | `DeviceSummary` | Fleet's mirror of devices; target resolution, online check, institution scope |
| `fleet_artifacts` | `FleetArtifact` | Software package metadata for `SOFTWARE` downloads (enabled flag, CDN url, sha256) |
| patch window tables | `PatchWindow` / `PatchWindowDevice` | Auto-schedule for `SOFTWARE_INSTALL` |

## Key endpoints

| Method + Path | Purpose | Auth |
|---------------|---------|------|
| `GET /api/fleet/tasks/paginated` | Tab read (filter `taskKind`, `atmId`) | USER…BCOS_ADMIN |
| `POST /api/fleet/tasks` | Create install/download task | ADMIN/MSP_ADMIN/BCOS_ADMIN |
| `POST /api/fleet/tasks/{id}/approve` \| `/reject` | Approve gate | FLEET_APPROVER / BCOS_ADMIN |
| `POST /api/fleet/tasks/{id}/cancel` \| `/retry` | Lifecycle | ADMIN/MSP_ADMIN/BCOS_ADMIN |
| `GET /api/fleet/tasks/history` | Archived (completed) tasks | USER…BCOS_ADMIN |
| `GET /api/internal/fleet/tasks/next` | Agent poll/dispatch | INTERNAL |
| `POST /api/internal/fleet/tasks/{id}/status` | Agent status/progress | INTERNAL |

## Architectural note (see uncertainties)

The tab reads the **live** `fleet_tasks` table, but `COMPLETED` tasks are archived+deleted immediately. Completed software history therefore lives only in `fleet_task_history` / the `/tasks/history` endpoint, which this tab does not query. Treat the tab as "in-flight + failed" state; use history for the completed record.

Cross-links: [platform.data-architecture] · [platform.kafka] · [platform.service-ownership]
