---
module: devices.map
title: Map
tab: Architect
order: 30
audience: dev
---

> **Internal · architecture.** Grounded in `AtmMap.svelte`, `frontend/src/lib/{api,stores}.ts`, and the devices/incident controllers, 2026-07-01.

## Shape of the feature

The Map is a **pure read/aggregate view** with **zero server-side state of its own**. There is no `devices.map` controller, table, executor, or Kafka topic. The Svelte component orchestrates two backends and does all clustering/colour/filter logic in the browser. See [platform.service-ownership].

```
AtmMap.svelte (devices SPA, port 5177)
   ├─ loadAtms()       → devicesApi   GET /api/atms            → hiveops-devices (8096)  → atms table
   ├─ loadIncidents()  → incidentApi  GET /incident/incidents  → hiveops-incident (8080) → incidents table
   └─ loadAtmGroups()  → devicesApi   GET /api/atm-groups      → hiveops-devices (8096)  → atm_groups table
        (Leaflet circleMarkers, client-side grouping + colour + filter)
```

## Services involved & how they interact

- **hiveops-devices** — system of record for the pins. `AtmController` (`@RequestMapping({"/api/atms","/api/devices"})`) serves the ATM inventory incl. `latitude`/`longitude`/`status`/`inSupervisor`/`agentConnectionStatus`. `AtmGroupController` (`/api/atm-groups`) serves group definitions for the Group filter. See [technical.devices].
- **hiveops-incident** — supplies open incidents. `IncidentController.getAllIncidents()` (`GET /api/incidents`, institution-scoped via `InstitutionContext.resolveScope()`). The map keeps only `status === 'OPEN'`, indexes them by `atmId`, and uses that to (a) colour a pin orange, (b) build the Incident Type filter, (c) render incident tags in the pin popup.

The two calls are **independent** — an incident-backend outage degrades colour/type-filter but still renders every pin.

### Frontend API wiring (`lib/api.ts`)

- `devicesApi` baseURL = `window.__APP_CONFIG__.apiUrl` (prod `https://api.bcos.cloud/devices`, dev `http://localhost:8096`).
- `incidentApi` baseURL = `${gatewayUrl}/incident`, where `gatewayUrl` = devices URL with the trailing `/devices` stripped. So `incidentAPI.getAll()` hits `…/incident/incidents`; nginx rewrites that to the controller's `/api/incidents`.

## Data flow (no Kafka in the read path)

The map does **not** consume Kafka. It reads live from both services on each refresh. Note the platform-level relationship for context: `hiveops-devices` publishes `hiveops.device.events` (`DEVICE_CREATED/UPDATED/DELETED`) which mirrors device data into incident/fleet/reports/recon `device_summary` tables — but **this map bypasses those mirrors** and queries devices + incident directly, so it is never stale-mirror-affected. See [platform.kafka], [platform.data-architecture].

Auto-refresh is a browser `setInterval`; each tick calls `loadAtms()` + `loadIncidents()` (interval = `settings.dashboardRefreshIntervalSeconds`).

## DB tables (read-only)

| Table | DB | Service | Used for |
|-------|----|---------|----------|
| `atms` (cols incl. `latitude`, `longitude`, `status`, `in_supervisor`, `agent_connection_status`) | `hiveiq_devices` | devices | Pin position, colour, popup |
| `atm_groups` | `hiveiq_devices` | devices | Group filter options |
| `incidents` | `hiveiq_incident` | incident | Open-incident colour + Incident Type filter + popup tags |

No table is written by this view.

## Key API endpoints

| Method + Path | Service | Role | Purpose |
|---------------|---------|------|---------|
| `GET /api/atms` | devices | authenticated (no `@PreAuthorize`) | Pins. Falls back to `GET /api/atms/paginated?size=9999&groupId=<gid>` when a default group is set |
| `GET /api/atm-groups` | devices | authenticated | Group filter |
| `GET /api/incidents` | incident | authenticated, institution-scoped | Open incidents |
| `PUT /api/atms/{id}` | devices | `MSP_ADMIN`/`BCOS_ADMIN` | Set lat/long to un-hide an ATM (done from ATM Detail, not the map) |

## Client-side logic worth knowing

- **Clustering:** ATMs are grouped by exact `"${latitude},${longitude}"` string key; a group of >1 renders one `circleMarker` + a count badge `divIcon`, coloured by the worst member (`STATUS_PRIORITY`: DOWN < MAINTENANCE < INACTIVE < OUT_OF_CASH < IN_SERVICE).
- **Pin colour** (`getPinColor`): not IN_SERVICE/OUT_OF_CASH → red; open incident → amber; OUT_OF_CASH → amber; `inSupervisor` → blue; else green.
- **Persistence** is entirely `localStorage`: `hiveops_map_filters_v2` (current filters + presets), `devices_map_autoRefresh`, `hiveops_defaultGroupId`.

## Cross-links

- [platform.data-architecture] · [platform.kafka] · [platform.service-ownership] · [technical.devices]
