---
module: fleet.dashboard
title: Fleet Dashboard
tab: Architect
order: 30
audience: dev
---

How the **Fleet Stats** dashboard is built. It is a thin read-only view over hiveops-fleet's own database — no cross-service HTTP at request time. Cross-link [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Services involved

| Concern | Owner | Role for this screen |
|---------|-------|----------------------|
| Dashboard UI | `hiveops-fleet/frontend` (Svelte, port 5176) | `FleetDashboard.svelte`, aggregates ATMs client-side |
| Stats + device list API | `hiveops-fleet/backend` (Spring Boot 3.4, port 8097) | `FleetApiController` → `FleetTaskService.getStats(...)` + `DeviceSummaryRepository` |
| Device state feed | `hiveops-devices` | Publishes `hiveops.device.events`; fleet mirrors it |

Only hiveops-fleet is on the request path. hiveops-devices contributes **asynchronously** via Kafka, never a live call. This is the deliberate no-cross-service-HTTP pattern for fleet.

## Data flow

**Task/artifact counters (Task Statistics + Fleet Overview bars):**

```
FleetDashboard onMount / 60s timer
  → GET /api/fleet/tasks/stats
    → FleetTaskService.getStats(InstitutionContext.resolveScope())
      → fleet_tasks   (count / countByTaskKindAndStatus / findByStatus)
      → fleet_artifacts (count)
    → FleetDashboardStats DTO
```

**ATMs by Model / by Institution tables:**

```
FleetDashboard onMount (once, not on the 60s timer)
  → GET /api/fleet/devices
    → InstitutionContext.resolveScope()  (null = unrestricted; else List<institutionKey>)
    → deviceSummaryRepository.findAll()  OR  findByInstitutionKeyIn(scope)
    → List<Map> {atmId, institution(Key), status, model, ...}
  → frontend groups by model & institution, counts operational/offline
```

The device rows are populated out-of-band by the Kafka consumer, so the dashboard reads a **local mirror**, decoupled from device-service availability.

## Kafka (async, off the request path) — [platform.kafka]

**Consumed by fleet (feeds this screen's device tables):**

| Topic | Consumer | Effect |
|-------|----------|--------|
| `hiveops.device.events` | `DeviceEventConsumer` (`@KafkaListener`, groupId `hiveops-fleet-device-sync`, `auto-offset-reset=earliest`) | Upserts `device_summary` rows read by `GET /api/fleet/devices` |

**Produced by fleet (NOT read by this screen — listed for completeness):**

| Topic | Producer | Notes |
|-------|----------|-------|
| `hiveops.fleet.task.events` | `FleetTaskEventPublisher` via `FleetTaskService` | created/status/archived events for downstream consumers; the dashboard reads `fleet_tasks` directly, not this topic |

There is **no reconciler** — if devices stops publishing, `device_summary` (and therefore the ATM tables) goes stale silently. Cross-link [platform.data-architecture].

## DB tables (all in `hiveiq_fleet`)

| Table | Access for this screen | Notes |
|-------|------------------------|-------|
| `fleet_tasks` | READ (counts) | active tasks (PENDING/QUEUED/RUNNING) |
| `fleet_artifacts` | READ (count) | uploadable/CDN-imported artifacts |
| `device_summary` | READ | Kafka-synced mirror of device state; the only source for the ATM tables |

`fleet_task_history`, `patch_windows`, `atm_captures` exist in fleet but are **not** touched by the dashboard.

## Key API endpoints (this feature)

| Method Path | Handler | Auth |
|-------------|---------|------|
| `GET /api/fleet/tasks/stats` (alias `/api/fleet/stats`) | `FleetApiController.getStats` → `FleetTaskService.getStats` | `hasAnyRole('USER','CUSTOMER','ADMIN','MSP_ADMIN','BCOS_ADMIN')` |
| `GET /api/fleet/devices` | `FleetApiController.listDevices` | same |

Base default in frontend: `http://localhost:8097/api/fleet`; prod `https://api.bcos.cloud/fleet`.

## Executor → Kafka → record-store pattern (context)

The broader fleet task lifecycle follows executor → `hiveops.fleet.task.events` → devices record-store, and terminal tasks archive to `fleet_task_history`. **This dashboard sits outside that loop** — it only aggregates current-state reads (task/artifact counts + the device mirror). Worth knowing so you don't wire the dashboard into the task-event pipeline.

## Notable implementation facts (verify before relying on displayed numbers)

- `getStats` currently hard-codes `completedTasks`/`failedTasks` to `0`, scopes `pendingTasks` to `AUTO_UPDATE`+`PENDING` only, and **ignores** its `institutionKeys` argument for the global `fleet_tasks`/`fleet_artifacts` counts.
- The `FleetDashboardStats` DTO has no `totalModulesReporting` field, so the frontend's "Modules Reporting" always renders its 0 default.
- Device status mapping is client-side: `IN_SERVICE`→operational; `OUT_OF_SERVICE`/`DOWN`/`INACTIVE`→offline.
