---
module: incident.dashboard
title: Dashboard
tab: Architect
order: 30
audience: dev
---

> **How it's built.** The Dashboard is a thin read-only aggregation view: one Svelte component, three GET endpoints, all served by **hiveops-incident**. There is no dedicated dashboard aggregation service — most of the "analytics" (trend chart, incidents-by-type, device/agent status breakdowns) is computed **client-side** in the component from two data stores. See [platform.service-ownership].

## Owning service

`hiveops-incident` owns every endpoint the Dashboard calls, plus both source tables (`incidents`, `device_summary`). The device data in `device_summary` is a **Kafka-fed mirror** — the source of truth is `hiveops-devices`. See [platform.data-architecture].

## Data flow

```
hiveops-devices ──hiveops.device.events──▶ DeviceEventConsumer ──▶ device_summary (hiveiq_incident)
                                             (group hiveops-incident-device-sync)          │
                                                                                           ▼
Dashboard.svelte ── GET /api/device-summary ─────────────────────────────────────▶ $atms store
                                                                                           │
agent-proxy / ARIA / journal ──▶ IncidentAutoCreationService ──▶ incidents (hiveiq_incident)
                                                                                           ▼
Dashboard.svelte ── GET /api/incidents/paginated (90d, ≤2000) ───────────────────▶ $incidents store
Dashboard.svelte ── GET /api/incident-management/stats/summary ───────────────────▶ incidentStats
```

- **Device Status / Agent Connection panels** and the top "Device Status" cards are derived in-browser from `$atms` (the `device_summary` snapshot): fields `status` / `serviceState`, `inSupervisor`, `deviceType` (ATM vs TCR), `agentConnectionStatus`.
- **Incident Trends** and **Incidents by Type** are derived in-browser from `$incidents` (last 90 days, ≤2000 rows), bucketed by `createdAt` / `type` / `severity` / `actualResolutionTime`.
- **Ticket cards** (Open/Assigned/In Progress/Critical) come from the server-computed `IncidentStatisticsDTO` (all-time, institution-scoped), so they don't inherit the 90-day/2000 cap.

## Kafka

See [platform.kafka].

**Consumed by incident (relevant to this view):**

| Topic | Listener (group) | Effect on Dashboard |
|---|---|---|
| `hiveops.device.events` | `DeviceEventConsumer` (`hiveops-incident-device-sync`) | Upserts/deletes `device_summary` rows → device counts + agent-connection panel; also drives `IncidentAutoCreationService` |
| `hiveops.aria.threats` | `AriaThreatEventConsumer` | Auto-creates incidents → shows up in ticket/trend/type panels |

No topics are produced *for* the Dashboard; incident's lifecycle producers (`hiveops.incidents.created/updated/resolved`) fan out to other services (analytics/reports/recon mirrors) but the Dashboard reads Postgres directly, not those topics.

There is **no reconciler** for `device_summary` — if `hiveops-devices` stops publishing, the mirror (and therefore the device counts) silently goes stale. This is the single most common architectural failure point for this view.

## Tables read

| Table (DB `hiveiq_incident`) | Read by | For |
|---|---|---|
| `device_summary` | `DeviceSummaryController` → `DeviceSummaryRepository` | device + agent-connection stats |
| `incidents` (joins `atm.institution`) | `IncidentController.getIncidentsPaginated`, `IncidentService.getStatistics` | trend/type panels, ticket cards |
| `atm_groups` (via `AtmGroupRepository.findByIdWithAtms`) | `DeviceSummaryController` when `groupId` set | group-scoped device filtering |

The Dashboard **writes nothing** — it is pure read.

## Key API endpoints

| Method + path | Controller | Auth | Scope |
|---|---|---|---|
| `GET /api/device-summary` | `DeviceSummaryController` | authenticated | `InstitutionContext.resolveScope()` |
| `GET /api/device-summary/paginated?groupId&page&size` | `DeviceSummaryController` | authenticated | scope + group ATM-id set |
| `GET /api/incidents/paginated` | `IncidentController` | authenticated (no `@PreAuthorize`) | scope + `dateFrom`/`groupId` |
| `GET /api/incident-management/stats/summary` | `IncidentManagementController` | authenticated (no `@PreAuthorize`) | `resolveScope()` → institution names |

`stats/summary` counts by loading incidents (`findAll` or an `atm.institution` specification) and reducing in memory — it is not a SQL aggregate, so cost grows with incident volume.

## Notable design points

- **Read-open by design:** the Dashboard's four endpoints carry no role gate; only incident *mutations* require `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')`.
- **Two different truth windows on one screen:** ticket cards = all-time server stats; trend/type/"This Week" = client-computed over a capped 90-day window. Expect small mismatches on busy fleets — this is structural, not a bug in the endpoint.
- Institution isolation is enforced at the query layer (`InstitutionContext` + `institutionKeyService.resolveInstitutionNames`), not in the frontend.

Cross-links: [platform.data-architecture] · [platform.kafka] · [platform.service-ownership]
