--- 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=` 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]