---
module: analytics.fleet-health
title: Fleet Health
tab: Claude
order: 40
audience: dev
---

> Act-without-guessing reference. All facts from `FleetHealthController`/`FleetHealthService`/`SecurityConfig` (hiveops-analytics) + `FleetHealthTab.svelte`/`api.ts`.

## Owner
- **hiveops-analytics** owns `GET /api/analytics/fleet-health` (port **8089**, DB `hiveiq_analytics` but **not used by this endpoint**).
- Live synchronous aggregation. No snapshot, no cache, no Kafka, no DB read/write for this tab.

## Endpoints
| Method | Path | Service | Auth |
|--------|------|---------|------|
| GET | `/api/analytics/fleet-health` | analytics | JWT, any authenticated role |
| GET | `/api/atms/summary` | incident | JWT (forwarded) |
| GET | `/api/journal-events/activity?hoursBack=24` | incident | JWT (forwarded) |
| GET | `/api/incidents/status/open` | incident | JWT (forwarded) |
| GET | `/api/journal/gaps?status=OPEN` | journal | JWT (forwarded) |

Frontend call: `analyticsAPI.getFleetHealth()` → `GET {apiBase}/fleet-health`; `apiBase` = `window.__APP_CONFIG__.apiUrl` || `http://localhost:8089/api/analytics`.

## Roles
- Endpoint gate: `.anyRequest().authenticated()` — **no `hasAnyRole`**. Not admin-only.
- `permitAll()`: only `/actuator/health`, `/api/public/**`.
- Role-gated in analytics = **only** `POST /api/analytics/insights` (`hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`) — a different feature.

## Tables / topics
- Tables read/written by this feature: **none**.
- Kafka topics in this feature's path: **none**.
- (Context only, other tabs: DB `analytics_snapshots`, `ai_insights`; topic `analytics.snapshot-ready`.)

## Response fields (`FleetHealthResponse`)
- `totalAtms, connectedAtms, disconnectedAtms, neverConnectedAtms, connectivityPct`
- `openIncidents` (= size of open-incidents list)
- `eventsByCategory: Map<String,Long>` (24h window, always seeded)
- `topProblematicAtms: [{atmId, eventCount}]` (top 5 by 24h event count; `deviceId` alias in frontend)
- `atmsWithOpenGaps` (distinct atmId count, status=OPEN)

## Categories (from `classifyEventType`)
- Emitted set: `HARDWARE, CASH, NETWORK, POWER, AUTH, SYSTEM, OTHER`.
- `POWER_OFF`→POWER · cassette*→CASH · network*→NETWORK · `AUTHENTICATION_FAILURE`→AUTH · reader/dispenser/app-error-hw→HARDWARE · out-of-service/app-error/ejournal-db→SYSTEM · unmatched→OTHER.

## Gotchas
- Downstream failures are **swallowed** (try/catch → log `FleetHealth: failed to fetch …` + empty). **200 with all-zeros = broken dependency, not healthy fleet.**
- Event breakdown + top problematic are a **fixed 24h window** (`hoursBack=24`, hardcoded). Not configurable via request.
- `eventsByCategory` is **always seeded with 7 keys at 0**, so the map is never empty → frontend "No events recorded" empty-state effectively never triggers.
- **No `OPERATIONS` backend category** despite the frontend color map / customer copy mentioning "Operations"; such events land in `OTHER`.
- Journal gaps come from **hiveops-journal** (`/api/journal/gaps`), everything else from **hiveops-incident**.
- Downstream hosts are env-driven: `INCIDENT_SERVICE_URL` (def `http://hiveops-incident-backend:8080`), `JOURNAL_SERVICE_URL` (def `http://hiveops-journal:8087`).

## Do NOT
- Do NOT claim this endpoint needs MSP_ADMIN/BCOS_ADMIN — it only needs a valid JWT.
- Do NOT say Fleet Health reads snapshots / Kafka / the analytics DB — it calls incident+journal live.
- Do NOT add an incident/gap/device write here — analytics owns aggregation only; incident/journal own the records ([platform.service-ownership]).
- Do NOT treat a 200 all-zeros response as "fleet is fine" — check analytics logs first.
- Do NOT assume events older than 24h are counted — they are not.
- Do NOT invent an `OPERATIONS` category in backend logic; unmatched → `OTHER`.
