---
module: incident.incident-tracker
title: Incident Tracker
tab: Architect
order: 30
audience: dev
---

> How the Incident Tracker is built. Grounded in `hiveops-incident` frontend + backend as of 2026-07-01. See [platform.data-architecture], [platform.kafka], [platform.service-ownership].

## Shape of the feature

The tracker is a **pure read view with client-side aggregation**. There is no server-side "tracker" projection, endpoint, or table — the Svelte component fans out to two existing controllers and assembles the board in the browser. All state (pipeline stages, severity rollup, sort, "recently closed" toggle) is computed in `IncidentTracker.svelte`.

Everything is owned by a **single service, `hiveops-incident`** ([platform.service-ownership]). The only cross-service dependency is the `device_summary` mirror, which is fed from **hiveops-devices** over Kafka.

## Services involved

| Service | Role for this feature |
|---------|-----------------------|
| `hiveops-incident` (backend, port 8080) | Serves `/api/device-summary` and `/api/incidents/paginated`; owns `incidents` + `device_summary` tables |
| `hiveops-incident` (frontend, Vite+Svelte SPA) | `IncidentTracker.svelte` — does all joining/aggregation |
| `hiveops-devices` | Source of truth for device data; publishes `hiveops.device.events` |
| NGINX | Routes `/api/incident/` → incident backend `:8080` |

## Data flow (per 60s refresh)

```
IncidentTracker.svelte  ──(3 parallel GETs)──►  hiveops-incident @ 8080
  1. deviceSummaryAPI.getAll()      → GET /api/device-summary            → DeviceSummaryController.getAll()    → device_summary
  2. incidentAPI.getPaginated(open) → GET /api/incidents/paginated       → IncidentController.getIncidentsPaginated() → incidents
  3. incidentAPI.getPaginated(closed)→ GET /api/incidents/paginated (…)  → same handler                        → incidents
```

Client-side assembly:
1. Build `dsMap` keyed by `device_summary.atm_id` (string natural key).
2. Concatenate active + closed incidents; group by `incident.atmName ?? String(incident.atmId)`.
3. Join each incident group to `dsMap` for location; if no match → stub row (`location: ''`).
4. Per ATM: `hasAction` if any active; `currentStage = min(stage index)` over active; `highestSeverity` = first of `[CRITICAL,HIGH,MEDIUM,LOW]` present; `closedStage = max(stage index)` over closed.
5. Filter to rows that `hasAction || recentlyClosed`; sort action-first, then by severity, then closed-most-recent, then atmId.

`incident.atmId` (integer FK) is retained only for the `viewAtmIncidents` dispatch that drills into the filtered `IncidentList`.

## How `device_summary` stays current (Kafka) — [platform.kafka]

The left column is **not** incident's own device data — it's a Kafka-synced mirror:

| Topic | Consumer (class / group) | Effect |
|-------|--------------------------|--------|
| `hiveops.device.events` | `DeviceEventConsumer` / `hiveops-incident-device-sync` | Upsert (`repo.save`) or delete rows in `device_summary`; also updates `atms` and can trigger `IncidentAutoCreationService` |

Topic + group are constants in `config/DeviceEventKafkaConfig` (`TOPIC = "hiveops.device.events"`, `CONSUMER_GROUP = "hiveops-incident-device-sync"`). No reconciler — if devices stops publishing, the mirror silently ages. This is the same `device_summary` mirror pattern used across incident/fleet/reports/recon ([platform.data-architecture]).

Incident lifecycle itself produces `hiveops.incidents.created/updated/resolved` (via `IncidentLifecycleProducer`), but **the tracker does not consume these** — it re-polls REST every 60s instead.

## DB tables

| Table | Access | Notes |
|-------|--------|-------|
| `incidents` (`@Table(name="incidents")`) | read | status, severity, atmName, atmId, resolvedAt, updatedAt |
| `device_summary` (`@Table(name="device_summary")`) | read | Kafka-fed mirror; keyed by `atm_id` string |

Both live in DB `hiveiq_incident`. Institution scoping is applied server-side in each controller via `InstitutionContext.resolveScope()` (repository `findByInstitutionKeyIn(scope)` when scoped, `findAll` when unrestricted).

## Key API endpoints (this feature)

| Method + path | Controller | Auth |
|---------------|-----------|------|
| `GET /api/device-summary` | `DeviceSummaryController.getAll` | any JWT; institution-scoped |
| `GET /api/incidents/paginated` | `IncidentController.getIncidentsPaginated` | any JWT; institution-scoped |
| `GET /api/incidents` | `IncidentController.getAllIncidents` | any JWT; institution-scoped (not used by tracker) |

Note this feature has **no executor→Kafka→record-store write path** — it is entirely read-side. The write/lifecycle path (create/assign/transition → `hiveops.incidents.*`) belongs to the broader incident domain, not to the tracker view.

## Cross-links

- [platform.data-architecture] — device_summary mirror pattern
- [platform.kafka] — topic/consumer conventions
- [platform.service-ownership] — incident owns incidents + journal-event store
