--- module: technical.analytics title: Analytics — fleet dashboard aggregation & snapshots tab: Overview order: 10 audience: internal --- > **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.