---
module: analytics.adoons
title: Adoons Insights
tab: Architect
order: 30
audience: dev
---

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

## Shape of the feature

Adoons Insights is the simplest slice of `hiveops-analytics`: a **single-table CRUD store** (`ai_insights`) exposed over REST, rendered as a card list. Unlike the dashboard/snapshot side of analytics, the insight path is **not** event-driven and does **not** use the executor→Kafka→record-store pattern. It's a straight `Controller → Service → JpaRepository → Postgres` flow.

```
AdoonsTab.svelte ──GET /insights/active──▶ InsightController ──▶ AiInsightService ──▶ AiInsightRepository ──▶ ai_insights (hiveiq_analytics)
publisher (admin JWT) ──POST /insights──▶ InsightController (@PreAuthorize MSP_ADMIN/BCOS_ADMIN) ──▶ save()
```

## Services involved

- **hiveops-analytics** — owns everything for this feature: controller, service, entity, table. Port **8089** (prod host **8012**). Cross-link [platform.service-ownership].
- **Publisher of insights** — *external to this repo.* Analytics has **no insight generator, no Kafka consumer that creates insights, and no scheduled producer** for them. Insight rows arrive only via authenticated `POST`. The intended author is Adoons (`hiveops-ai`), but that producer is **not present in hiveops-analytics** (unverified where it lives / whether it's live).

## Data flow

There is no Kafka in the insight read/write path. Cross-link [platform.kafka] for the surrounding analytics topics (`analytics.snapshot-ready` produced by `SnapshotEventProducer`; `hiveops.incidents.*` and `journal.file-parsed` consumed for snapshots) — **none of these touch `ai_insights`.**

The only coupling to the rest of analytics is a **nullable FK** `ai_insights.snapshot_id → analytics_snapshots.id`. On publish:
- If `snapshotId` is supplied, that snapshot is linked.
- Else `AiInsightService.publish()` links the **latest** snapshot (`findTopByOrderByCreatedAtDesc()`), or `null` if none exist.

So an insight can *reference* the fleet metrics snapshot it was derived from, but the reference is optional and never enforced.

## DB tables

| Table | R/W here | Columns of interest |
|-------|----------|---------------------|
| `ai_insights` | read + write | `id`, `category`, `severity`, `title`, `body`, `snapshot_id` (FK), `published_at`, `expires_at`, `acknowledged`, `acknowledged_at`, `acknowledged_by_user_id` |
| `analytics_snapshots` | read-only (FK lookup on publish) | `id`, `created_at` (used by `findTop...`) |

DB **`hiveiq_analytics`** (PostgreSQL prod / H2 dev). Cross-link [platform.data-architecture]. Flyway manages schema; `ai_insights` is created in the analytics migration set (V1 init + later columns).

**Enums (JPA `EnumType.STRING`):**
- `Category` = `ANOMALY | TREND | RECOMMENDATION | SUMMARY`
- `Severity` = `INFO | WARNING | CRITICAL` (entity default `INFO`)

## Key API endpoints (`InsightController`, base `/api/analytics/insights`)

| Method | Path | Behaviour | Auth |
|--------|------|-----------|------|
| GET | `/api/analytics/insights` | Paged list, `findAllByOrderByPublishedAtDesc` → `Page<AiInsightResponse>` | authenticated |
| GET | `/api/analytics/insights/active` | `findActive(now)`: unack'd + unexpired, newest first | authenticated |
| POST | `/api/analytics/insights` | `publish(req)`, returns `201` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| PATCH | `/api/analytics/insights/{id}/acknowledge` | `acknowledge(id, principal.userId())`; `404` if id unknown; returns `204` | authenticated |
| DELETE | `/api/analytics/insights/active` | `acknowledgeAll(now)`; returns `204` | authenticated |

## Lifecycle / retention

- `published_at` set on `@PrePersist` if unset.
- Active = `acknowledged = false AND (expires_at IS NULL OR expires_at > now)`.
- `acknowledge` is an atomic JPQL `UPDATE`; `acknowledgeAll` clears every unacknowledged row.
- **Nightly purge:** `AiInsightService.purgeOldAcknowledged()` — `@Scheduled(cron = "0 0 3 * * *")`, deletes rows where `acknowledged = true AND acknowledged_at < now-30d`.

## Security model

- `SecurityConfig`: stateless, CSRF disabled; `permitAll` only for `/actuator/health` + `/api/public/**`; `anyRequest().authenticated()`.
- `JwtAuthenticationFilter` reads the token from the **`Authorization: Bearer` header only** (no cookie fallback) and builds an `AnalyticsPrincipal(userId, email, role, institutionKey)`.
- Only publish is role-restricted; read + acknowledge are open to any authenticated principal (note: no institution scoping on insights — they are fleet-global).

Cross-links: [technical.analytics], [platform.data-architecture], [platform.kafka], [platform.service-ownership].
