---
module: devices.device-list
title: Device List
tab: Architect
order: 30
audience: dev
---

How the Device List is built. Frontend: `hiveops-devices/frontend/src/components/AtmManagement/AtmList.svelte`. Backend of record: **hiveops-devices** (Spring Boot, port **8096**, DB `hiveiq_devices`). See [platform.service-ownership], [platform.data-architecture], [platform.kafka].

## Services involved (for this feature)
- **hiveops-devices** — owns the device inventory and serves every grid query. `AtmController` (`@RequestMapping({"/api/atms","/api/devices"})`) → `AtmService` → `AtmRepository` / `AtmPropertiesRepository` on `hiveiq_devices`.
- **hiveops-incident** — only for **📍 Lookup Coords**: `GeocodingController` (`@RequestMapping("/api/geocode")`) proxies a US Census geocoder. The Device List calls it as a relative `fetch('/api/geocode?address=...')`; nginx must route `/api/geocode` to incident.
- **auth/mgmt (JWT)** — the bearer token carries role + institution keys, consumed by `InstitutionContext` for row-level scoping.

## Data flow — reading the grid
1. `AtmList.fetchAtms()` builds params (page, size, sort, search, status[], model[], agentStatus[], groupId, deviceType, agentVersion[]) and calls `loadAtmsPaginated()` in `stores.ts`.
2. `GET /api/atms/paginated` → `AtmController.getAllAtmsPaginated` → `InstitutionContext.resolveScope()` derives the institution scope from the JWT (scoped for `ROLE_CUSTOMER`/`ROLE_MSP_ADMIN`; null = whole fleet for BCOS_ADMIN) → `atmService.getAllAtmsPaginated(...)`.
3. `AtmService` reads `atms` + `atm_properties`, and per row derives `agentConnectionStatus` via `calculateConnectionStatus(lastHeartbeat)`:
   - `null` → `NEVER_CONNECTED`
   - ≤ 15 min → `CONNECTED`
   - > 15 min → `DISCONNECTED`
4. Returns a Spring `Page<AtmDTO>`; the store maps `page.totalElements / totalPages / number / size`.
5. **Status badge** is computed client-side in the grid: `AGENT_OFFLINE` if disconnected/never-connected, else `IN_SUPERVISOR` if `inSupervisor`, else `serviceState || status`. The `Status` column sorts on backend field `effectiveStatus`.
6. Filter dropdowns are populated by `GET /api/atms/distinct-models` and `GET /api/atms/distinct-agent-versions`; groups by `GET /api/atm-groups`.

## Data flow — Add Device (write) + Kafka mirrors
1. `POST /api/atms` (`@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`) → `AtmService` persists to `atms` (+ `atm_properties`).
2. On create, `deviceEventPublisher.publishCreated(savedAtm)` sends a `DEVICE_CREATED` event to Kafka topic **`hiveops.device.events`** (`KafkaProducerConfig.DEVICE_EVENTS_TOPIC`), keyed by `atmId`. Updates/institution-moves/deletes publish `DEVICE_UPDATED` / `DEVICE_DELETED` the same way.
3. Mirror consumers in **incident / fleet / reports / recon** (and vault) keep their `device_summary` copies in sync from this topic. There is **no reconciler** — every write path must publish or the mirrors go stale.
4. `POST /api/atms/republish-all` (admin) re-emits events to rebuild mirrors.

## DB tables (this feature)
| Table | Read/Write | Role |
|-------|-----------|------|
| `atms` | R (grid) / W (add) | Core device inventory, source of truth |
| `atm_properties` | R (heartbeat, version) / W | Per-device props incl. `lastHeartbeat`, agent version |
| `atm_groups` | R | Group filter |

## Kafka
| Topic | Direction | Producer/Notes |
|-------|-----------|----------------|
| `hiveops.device.events` | produced | `DeviceEventPublisher`; `DEVICE_CREATED/UPDATED/DELETED`, keyed by atmId; consumed by incident/fleet/reports/recon mirrors |

## Key API endpoints
- `GET /api/atms/paginated` — the grid (institution-scoped, server-side filter/sort/page)
- `GET /api/atms/distinct-models`, `GET /api/atms/distinct-agent-versions` — filter options
- `POST /api/atms` — Add Device (admin) → publishes `DEVICE_CREATED`
- `GET /api/geocode?address=` — coord lookup (**incident**, not devices)

## Notes
- `/api/atms*` also answers on the `/api/devices*` Phase-2 alias (ATM→device rename); both must stay routable in nginx.
- Devices backend uses `DB_URL` (full JDBC URL), not split host/port/name vars.
