---
module: technical.analytics
title: Analytics — fleet dashboard aggregation & snapshots
tab: Overview
order: 10
audience: dev
---

> **DRAFT · internal.** Written 2026-07-01 from hiveops-analytics source. Verify against code before relying on specifics.

## Responsibility

`hiveops-analytics` computes fleet dashboard analytics by aggregating data from **hiveops-incident** and **hiveops-journal** (and **hiveops-mgmt**). It exposes read-oriented dashboard endpoints (fleet health, uptime, incident trends, cash intelligence, journal coverage, agent insights) and persists periodic **snapshots** plus **AI insights** ("Adoons"-style callouts) in its own database. Runs on **port 8089** (`PORT` env override). Cross-link [platform.service-ownership].

Note: the in-repo `CLAUDE.md` still describes the service as "stateless — no database," but the code has since gained its own DB (`analytics_snapshots`, `ai_insights`), a snapshot scheduler, and Kafka consumers. Treat it as stateful with an aggregation cache.

## Data

Database: **`hiveiq_analytics`** (PostgreSQL in `prod`, H2 in-memory in `dev`). Cross-link [platform.data-stores].

Prod connection uses split env vars **`DB_HOST` / `DB_PORT` / `DB_NAME` / `DB_USERNAME` / `DB_PASSWORD`** (JDBC URL assembled in `application.yml`), not a single `DB_URL`. `DB_NAME` defaults to `hiveiq_analytics`.

| Table | Purpose |
|-------|---------|
| `analytics_snapshots` | Point-in-time fleet metrics: atm counts (total/connected/disconnected/never-connected), incidents by severity (open/critical/high/medium/low), tasks (pending/in-progress/completed-today), transactions today + failed, plus `opened_incidents_today` / `closed_incidents_today` (V2). Unique on `(snapshot_date, snapshot_hour)`. |
| `ai_insights` | Generated insight cards: `category`, `severity`, `title`, `body`, optional `snapshot_id` FK, `expires_at`, `acknowledged` / `acknowledged_at` / `acknowledged_by_user_id`. |

Flyway migrations in `src/main/resources/db/migration/` (V1 init, V2 open/close incident columns, V3 snapshot_hour type fix). Cache TTL for aggregations via `ANALYTICS_CACHE_TTL` (default 300s).

## Kafka

Topic constants in `kafka/AnalyticsKafkaConfig.java`. Cross-link [platform.kafka].

**Produced**

| Topic | Producer class | Notes |
|-------|----------------|-------|
| `analytics.snapshot-ready` | `SnapshotEventProducer` | Emitted (key `"snapshot"`, `SnapshotReadyEvent{snapshotDate, snapshotHour}`) after a snapshot is collected by `SnapshotCollectionService`. |

**Consumed**

| Topic | Listener class |
|-------|----------------|
| `hiveops.incidents.created` | `IncidentEventConsumer` |
| `hiveops.incidents.updated` | `IncidentEventConsumer` |
| `hiveops.incidents.resolved` | `IncidentEventConsumer` |
| `journal.file-parsed` | `JournalFileParsedConsumer` |

Consumer group `hiveops-analytics`, `auto-offset-reset: latest`. Incident/journal events trigger real-time snapshot `collect()`; a `@Scheduled(cron = "0 0 */6 * * *")` job in `SnapshotCollectionService` is the 6-hour fallback.

## API surface

Most controllers sit under base **`/api/analytics`**; snapshots and insights have their own sub-paths; there is one unauthenticated public path.

| Method | Path | Purpose |
|--------|------|---------|
| GET | `/api/analytics/dashboard/overview` | Fleet overview: atm counts, incidents, tasks, transactions |
| GET | `/api/analytics/fleet-health` | Fleet health summary |
| GET | `/api/analytics/uptime` | Uptime metrics |
| GET | `/api/analytics/incident-trends` | Incident trends over a period |
| GET | `/api/analytics/cash-intelligence` | Cash intelligence metrics |
| GET | `/api/analytics/journal-coverage` | Journal coverage/reporting metrics |
| GET | `/api/analytics/agents` | Agent insights |
| GET | `/api/analytics/snapshots/latest` | Most recent snapshot |
| GET | `/api/analytics/snapshots` | List snapshots |
| GET | `/api/analytics/snapshots/range` | Snapshots for a date range |
| GET | `/api/analytics/insights` | All AI insights |
| GET | `/api/analytics/insights/active` | Active (unacknowledged, unexpired) insights |
| POST | `/api/analytics/insights` | Create an insight — `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| GET | `/api/public/stats` | Public stats (no auth) |

Not exhaustive.

## Gotchas

- **`/api/public/**` and `/actuator/health` are `permitAll()`** in `SecurityConfig`; everything else requires an authenticated JWT. `/api/public/stats` is intentionally unauthenticated — do not put sensitive fleet data there.
- **Only `POST /api/analytics/insights` is role-gated** (`hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`); the many GET endpoints are `.anyRequest().authenticated()` with no role restriction.
- **Service-to-service auth forwards the caller's JWT** to incident/journal/mgmt (`WebClientConfig`); there is no separate service account. Downstream calls fail if the user token lacks access.
- **Downstream URLs are env-configured** (`INCIDENT_SERVICE_URL`, `JOURNAL_SERVICE_URL`, `MGMT_SERVICE_URL`), defaulting to in-cluster hostnames (`hiveops-incident-backend:8080`, `hiveops-journal:8087`, `hiveops-mgmt:8080`).
- **Snapshots are event-driven with a 6h cron fallback**; if incident/journal Kafka events stop flowing, dashboard freshness degrades to the 6-hour cadence.
- **Prod uses split DB_HOST/DB_PORT/DB_NAME env vars** (unlike hiveops-journal, which expects a single `DB_URL`).
- **`dev` profile uses H2 with `ddl-auto: create-drop` and Flyway disabled**; `prod` uses PostgreSQL with `ddl-auto: validate` and Flyway enabled — schema drift surfaces only in prod validation.
