---
module: analytics.uptime
title: Uptime
tab: Architect
order: 30
audience: dev
---

> **How Uptime is built.** Grounded in `hiveops-analytics` source (2026-07-01). Cross-link [platform.data-architecture], [platform.kafka], [platform.service-ownership].

## Shape of the feature

Uptime is a **read-over-snapshots + one live overlay** feature. `hiveops-analytics` owns the endpoint and the persisted history; the "currently down" list is a live pass-through to the incident service. Nothing about Uptime is computed on the ATM or by any other frontend.

```
UptimeTab.svelte ──GET /api/analytics/uptime──▶ UptimeController ──▶ UptimeService
                                                                        │
                             ┌──────────────────────────────────────────┤
                             ▼                                          ▼
              analytics_snapshots (DB, today)          incident: GET /api/atms/paginated
              → NOW / 7d / 30d / trend                 ?agentStatus=DISCONNECTED,NEVER_CONNECTED
                                                        → currentlyDown + neverConnected + table
```

## Services involved

| Service | Role for Uptime |
|---|---|
| **hiveops-analytics** (8089) | Owns `GET /api/analytics/uptime`, owns `analytics_snapshots`, runs the snapshot collector. |
| **hiveops-incident** (8080) | Serves the **live** down/never-connected ATM list via `/api/atms/paginated`. Token forwarded from caller. |
| **hiveops-devices** (8096) | Source of ATM health **counts** written into snapshots (via `/api/atms/summary`). |
| **hiveops-fleet** (8097) | Task counts in the same snapshot (not shown on Uptime, collected together). |
| **hiveops-journal** (8087) | Transaction counts in the same snapshot; `journal.file-parsed` also triggers snapshot collection. |

Downstream base URLs are env-configured in `WebClientConfig` (`services.incident.url`, `services.devices.url`, `services.fleet.url`, `services.journal.url`, `services.mgmt.url`) with in-cluster defaults.

## Data flow — write path (snapshots)

`SnapshotCollectionService.collect()` is the executor. It runs on two triggers:

- **Scheduled fallback:** `@Scheduled(cron = "0 0 */6 * * *")` — every 6 hours.
- **Event-driven (real-time):** Kafka consumers call `collect()` on each relevant event.

On each run it:
1. Mints an **internal service JWT** (`InternalTokenGenerator` — `sub=0`, `role=ADMIN`, 300 s TTL).
2. Calls `FleetOverviewService.buildOverview(token, "internal")`, which fans out to devices `/api/atms/summary`, incident `/api/incident-management/stats/summary`, fleet `/api/fleet/tasks/stats`, journal `/api/journal/transactions/summary` (each failure is caught → zeros for that section; `@Cacheable("fleetOverview")`).
3. **Upserts** one row into `analytics_snapshots` keyed by `(snapshot_date, snapshot_hour)` — same hour replaces, else insert.
4. Emits `analytics.snapshot-ready` via `SnapshotEventProducer`.

This is the platform **executor → Kafka → record-store** pattern: events fan in to trigger a collect, the collect persists a record, and a "ready" event fans back out. See [platform.kafka].

## Data flow — read path (the Uptime endpoint)

`UptimeService.buildUptime(callerToken)`:
1. Loads `analytics_snapshots` for **today** (`findBySnapshotDateBetween...`, `from = to = today`).
2. `currentUptimePct` = latest row's `connectedAtms/totalAtms`.
3. `last24Hours` = last 24 snapshot rows (hourly points).
4. `dailyTrend` = today's rows averaged into a single daily point.
5. `sevenDayAvgPct` and `thirtyDayAvgPct` = `average(dailyTrend)` (see Caveats — both are today's average today).
6. Overlays the **live** incident call `/api/atms/paginated?size=500&agentStatus=DISCONNECTED,NEVER_CONNECTED` (caller's token) for `currentlyDownCount`, `neverConnectedCount`, and the `atmsCurrentlyDown` table (sorted longest-down-first, capped at 50).

## Kafka

Cross-link [platform.kafka].

| Direction | Topic | Class / group |
|---|---|---|
| Consume | `hiveops.incidents.created`, `hiveops.incidents.updated`, `hiveops.incidents.resolved` | `IncidentEventConsumer`, group `hiveops-analytics-incidents` → `collect()` |
| Consume | `journal.file-parsed` | `JournalFileParsedConsumer`, group `hiveops-analytics-journal` → `collect()` |
| Produce | `analytics.snapshot-ready` | `SnapshotEventProducer` (`SnapshotReadyEvent{snapshotDate, snapshotHour}`) |

Uptime freshness therefore degrades gracefully: if incident/journal events stop flowing, the NOW/trend data falls back to the 6-hour cron cadence; the live down-list is unaffected (it doesn't read snapshots).

## DB tables

Cross-link [platform.data-architecture].

| Table | R/W for Uptime | Notes |
|---|---|---|
| `analytics_snapshots` | **Read** by `UptimeService`; **written** by `SnapshotCollectionService` | Unique `(snapshot_date, snapshot_hour)`; `snapshot_hour` INT (V3 fix). Fields used: `total_atms`, `connected_atms`, `disconnected_atms`, `never_connected_atms`. |

No Uptime-specific table; it reuses the shared fleet snapshot. `ai_insights` is unrelated to Uptime.

## Key endpoints

| Method | Path | Service | Purpose |
|---|---|---|---|
| GET | `/api/analytics/uptime` | analytics | The Uptime screen payload |
| GET | `/api/atms/paginated?size=500&agentStatus=DISCONNECTED,NEVER_CONNECTED` | incident | Live down/never-connected list |
| GET | `/api/atms/summary` | devices | ATM counts written into snapshots |
| GET | `/api/incident-management/stats/summary` | incident | Incident counts in snapshot |
| GET | `/api/fleet/tasks/stats` | fleet | Task counts in snapshot |
| GET | `/api/journal/transactions/summary?dateFrom=&dateTo=` | journal | Transaction counts in snapshot |

## Caveats / architectural notes

- **Snapshot vs live split:** NOW/7d/30d/trend are snapshot-derived (≤6 h stale possible); the down-list is live. Intentional but can look inconsistent.
- **Today-only window:** the read path only loads today's snapshots, so 7-day and 30-day averages resolve to the same value and the trend chart has a single point. See [analytics.uptime] Internal tab and the module's overview for the intended (advertised) behaviour vs. what ships today.
- **Cross-service ownership drift:** snapshot ATM **counts** come from devices, but the live down-list still calls **incident** `/api/atms/paginated`. If ATM detail ownership fully moves to devices, this call is the thing to re-point. See [platform.service-ownership].
