---
module: analytics.agents
title: Agents
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Grounded in hiveops-analytics + hiveops-devices source (2026-07-01). Cross-link [platform.data-architecture], [platform.kafka], [platform.service-ownership], [technical.analytics].

## Shape of the feature

The Agents tab is a **thin read-through aggregation**. No dedicated persistence, no Kafka, no snapshot. On each page load the analytics backend fans out **one** HTTP call to the devices backend, computes distributions in-memory, and returns a single DTO. This is the exception to the rest of the analytics app, which is snapshot/Kafka-driven (see [technical.analytics]).

## Services involved

| Service | Role for this feature | Port |
|---------|-----------------------|------|
| hiveops-analytics | Serves `GET /api/analytics/agents`; computes version distribution, connection counts, latest-semver, outdated list | 8089 |
| hiveops-devices | **Source of truth** for ATM/agent data; serves `GET /api/atms/paginated` | 8096 |

Analytics reaches devices via a `WebClient` bean named **`incidentWebClient`** (`WebClientConfig`), base URL from `services.incident.url` (`INCIDENT_SERVICE_URL`, default `http://hiveops-incident-backend:8080`). Historically `/api/atms/*` lived in incident; those endpoints were **extracted into hiveops-devices ~2026-06-18** (`AtmController`, `@RequestMapping({"/api/atms","/api/devices"})`). The analytics bean name and default URL still say "incident" — in a correct deploy that URL must resolve to the backend actually serving `/api/atms/paginated` (devices). See [platform.service-ownership].

## Data flow

```
Browser (AgentsTab.svelte)
   │  GET /api/analytics/agents   (Authorization: Bearer <userJWT>)
   ▼
hiveops-analytics
   AgentInsightController.agents()
      → extracts raw token from Authorization header
   AgentInsightService.buildAgentInsight(token)
      → incidentWebClient GET /api/atms/paginated?size=500&page=0
        (forwards "Bearer <userJWT>")
   ▼
hiveops-devices
   AtmController.getAllAtmsPaginated(...)
      → AtmService.getAllAtmsPaginated(...) → Spring Data Page<AtmDTO>
      → reads atms + atm_properties (agentVersion, lastHeartbeat)
      → agentConnectionStatus derived by calculateConnectionStatus(lastHeartbeat)
   ◀ Page JSON: { content: [AtmDTO...], page: {...} }
   ▼
hiveops-analytics (in-memory aggregation)
   - versionDistribution  = groupingBy(agentVersion), desc by count
   - latestVersion        = findLatestSemver(distinct versions)   [highest x.y.z present]
   - connected/disconnected/neverConnectedCount = filter on agentConnectionStatus
   - outdatedAtms         = every ATM where version blank or != latestVersion
   ◀ AgentInsightResponse
   ▼
Browser renders bars / status counts / outdated table
```

### Fields consumed from each `AtmDTO`
`atmId`, `agentVersion`, `lastHeartbeat` (ISO string after JSON serialization of `LocalDateTime`), `agentConnectionStatus` (`CONNECTED` | `DISCONNECTED` | `NEVER_CONNECTED`). Analytics reads them as loosely-typed `Map<String,Object>` and defensively coerces to String.

## Kafka

**None for this feature.** The Agents tab is a synchronous read-through — it does **not** consume or produce any topic, and does **not** read the analytics `analytics_snapshots` table. Contrast with the rest of analytics, which consumes `hiveops.incidents.*` / `journal.file-parsed` and produces `analytics.snapshot-ready` (see [technical.analytics], [platform.kafka]). Device changes propagate to *other* services via devices' `hiveops.device.events`, but analytics' Agents tab bypasses that and hits devices' REST API live.

## Databases

| DB | Owner | Read/written by this feature |
|----|-------|------------------------------|
| `hiveiq_devices` → `atms`, `atm_properties` | hiveops-devices | **Read** (indirectly, via devices REST). `agentVersion`/`lastHeartbeat` live on `atm_properties`. |
| `hiveiq_analytics` | hiveops-analytics | **Not touched** by the Agents tab. |

Connection status is computed, not stored: `calculateConnectionStatus(lastHeartbeat)` → `NEVER_CONNECTED` (null), `CONNECTED` (≤15 min), else `DISCONNECTED`.

## Key endpoints

| Method | Path | Service | Auth |
|--------|------|---------|------|
| GET | `/api/analytics/agents` | hiveops-analytics | authenticated JWT (no role gate) |
| GET | `/api/atms/paginated?size=500&page=0` | hiveops-devices | authenticated JWT (no role gate); JWT forwarded from analytics |

## Design notes & consequences

- **Read-through, not executor→Kafka→record-store.** Unlike the snapshot pipeline in [technical.analytics], this tab has no scheduler, no producer, no record store — it trades freshness-guarantees for simplicity and is always live-consistent with devices at request time.
- **Fail-soft aggregation:** `fetchAtmsPaginated` catches everything and returns `List.of()`, so upstream failures surface as an empty (not errored) tab.
- **Single page, size 500:** the aggregation is bounded to the first 500 ATMs — a scaling limit as fleets grow.
- **"Latest" is fleet-relative:** `findLatestSemver` = max semver present among devices, independent of the CDN `latest.json`. Strict `\d+\.\d+\.\d+` regex means suffixed versions collapse "latest" to `"unknown"`.
