---
module: analytics.fleet-health
title: Fleet Health
tab: Internal
order: 20
audience: internal
---

> **Ops / Support / Admin view.** How to keep the Fleet Health screen honest and what to check when it lies. Grounded in `hiveops-analytics` source (`FleetHealthController`, `FleetHealthService`, `SecurityConfig`).

## What actually serves this screen

- Endpoint: **`GET /api/analytics/fleet-health`** on **hiveops-analytics** (port **8089**).
- It is **computed live on every request** — no analytics DB, no cache, no snapshot for this tab. Each load fans out to hiveops-incident and hiveops-journal and aggregates in-memory.
- The user's **JWT is forwarded** to those downstream services. If the caller's token can't see a device/incident, neither can this screen. Institution scoping is inherited from downstream, not enforced here.

## Roles required

- **Any authenticated JWT** can read fleet-health. `SecurityConfig` only has `permitAll()` on `/actuator/health` + `/api/public/**`; everything else is `.anyRequest().authenticated()`.
- There is **no `@PreAuthorize` / role gate** on this endpoint — it is *not* admin-only. Do not tell a customer "you need MSP_ADMIN to see it." (Role gates in analytics apply only to `POST /api/analytics/insights`.)

## First things to check when it misbehaves

1. **Whole screen empty / zeros everywhere** → the caller's JWT likely can't reach downstream, or a downstream service is down. Every fetch in `FleetHealthService` is wrapped in try/catch that **logs a warning and returns empty** — so a dead dependency produces silent zeros, not an error. Check analytics logs for:
   - `FleetHealth: failed to fetch ATM summary`
   - `FleetHealth: failed to fetch activity events`
   - `FleetHealth: failed to fetch open incidents`
   - `FleetHealth: failed to fetch open gaps`
2. **Connectivity donut wrong / total = 0** → `GET /api/atms/summary` on hiveops-incident is failing or returning `{}`. Curl it directly with the same JWT.
3. **Event Breakdown / Top Problematic empty** → `GET /api/journal-events/activity?hoursBack=24` on hiveops-incident. Note the **fixed 24-hour window** — anything older than 24h never appears here, by design.
4. **"ATMs with Journal Gaps" = 0 but you expect gaps** → `GET /api/journal/gaps?status=OPEN` on hiveops-journal. Only gaps with status `OPEN` are counted; distinct `atmId` values.
5. **Open Incidents count off** → `GET /api/incidents/status/open` on hiveops-incident. The number shown is simply `openIncidents.size()` of that list.

## Common failure modes → fixes

| Symptom | Likely cause | Fix |
|---------|-------------|-----|
| All cards zero, no error toast | A downstream call threw and was swallowed | Check analytics logs for the four `FleetHealth: failed to fetch…` warnings; restart/repair the named service |
| Donut total looks stale | incident `/api/atms/summary` cached/stale upstream | Investigate hiveops-incident, not analytics — analytics holds no state here |
| Event breakdown shows categories with **0** events | Backend seeds HARDWARE/CASH/NETWORK/POWER/AUTH/SYSTEM/OTHER at 0 (see architect tab) | Expected; not a data loss. The "No events recorded" empty-state effectively never fires |
| An event type shows under **OTHER** unexpectedly | `classifyEventType` switch has no case for it | Add the mapping in `FleetHealthService.classifyEventType` (see architect tab) |
| 401/403 on the endpoint | Missing/expired JWT | Re-auth; confirm `Authorization: Bearer` reaches analytics through NGINX |
| Downstream URL wrong after infra change | `INCIDENT_SERVICE_URL` / `JOURNAL_SERVICE_URL` env misconfigured | Fix env on the analytics container; defaults are `http://hiveops-incident-backend:8080` and `http://hiveops-journal:8087` |

## Admin actions

- There are **no admin mutation actions** on this screen. It is read-only. Nothing to "reset," acknowledge, or re-run — a browser refresh re-computes from live sources.
- To change what counts as Hardware/Cash/Network/etc., that is a **code change** in `classifyEventType`, not a config toggle.

## Smoke test

```bash
curl -s https://analytics.bcos.cloud/api/analytics/fleet-health \
  -H "Authorization: Bearer $JWT" | jq '{totalAtms,connectedAtms,openIncidents,atmsWithOpenGaps,eventsByCategory}'
```
Expect HTTP 200 and a parseable body. Zeros with 200 = a swallowed downstream failure, not success — check logs.
