---
module: devices.device-detail
title: Device Detail
tab: Internal
order: 20
audience: internal
---

Ops/support view of the single-device page (`AtmDetail.svelte`). The page is served by the **hiveops-devices** frontend (port 5177, `devices.bcos.cloud`) and reads/writes through the **hiveops-devices** backend (port 8096, DB `hiveiq_devices`). Several tabs pull from *other* services — see the failure table below.

## Roles required

- **Read** (device detail, tabs, logs): any authenticated HiveIQ user (institution scoping applies via `verifyAtmAccess`).
- **Every mutating action on this page** is gated by `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`. A non-admin sees the tabs but 403s on save/rename/move/reset/delete/acknowledge/apply-template.
- `GET /api/atms/{id}/fleet-tasks` (Task History tab) is also admin-gated — non-admins get 403 on that tab specifically.

## Admin-only actions (Details tab) → what fires

| UI action | Call | Backend effect |
|-----------|------|----------------|
| Edit → Save | `PUT /api/atms/{id}` | Updates the `atms` row, publishes `DEVICE_UPDATED` |
| Rename (ATM ID / display name) | `PUT /api/atms/{id}` | Same update path |
| Move Institution | `PATCH /api/atms/{id}/institution` | Key is **upper-cased + trimmed**; blank rejected; publishes `DEVICE_UPDATED` |
| Reset to In Service | `POST /api/atms/{id}/reset-state` | Sets status `IN_SERVICE`, `serviceState=IN_SERVICE`, `inSupervisor=false`, clears `supervisorEnteredAt` |
| Deactivate / Delete | `DELETE /api/atms/{id}` | **Soft-delete** if active (status → `INACTIVE`); **hard-delete** if already `INACTIVE` (bulk JPQL, DB `ON DELETE CASCADE`) |
| Set hardware profile | `PUT /api/atms/{id}/hardware-profile` **then** `POST /fleet/tasks` (CONFIG_UPDATE) | Two separate calls — see gotcha below |
| Lookup Coords | `fetch('/api/geocode?address=…')` | Geocode proxy fills lat/long |
| Acknowledge hardware change | `POST /api/atms/{id}/hardware/changes/{changeId}/acknowledge` | Clears the review badge |
| Acknowledge config change | `POST /api/atms/{id}/config/changes/{changeId}/acknowledge` | Clears the Config Changes badge |
| Apply / clear cassette template | `POST` / `DELETE /api/atms/{id}/cassette-configuration[/{configId}]` | Applies or clears the cassette template |

## First things to check when it misbehaves

| Symptom | Likely cause | Fix / where to look |
|---------|--------------|---------------------|
| Save/rename/move returns 403 | User lacks `MSP_ADMIN`/`BCOS_ADMIN`, or JWT expired | Confirm role in the JWT; re-login |
| "Delete" left the device visible but INACTIVE | Expected — first delete is a **soft delete** (status→INACTIVE). Delete again to hard-remove | Filter list by INACTIVE to confirm |
| Agent tab "latest log" 404s / errors | `GET /api/atms/{id}/latest-log` reads a file off disk (`atmLogService.getLatestLogPath`); no file yet if the agent never uploaded | Check the agent actually uploaded; nothing to serve = expected error |
| Log Collect download fails | `GET /api/atms/{id}/latest-log-collect` / `.../log-collects/{id}/download` are FileSystemResource reads | Verify the LOG_COLLECT bundle exists on the services VM |
| Journal / Incidents / Transactions / Captures / Modules tab empty or 5xx | **Those tabs are NOT devices** — they call incident, journal, or fleet backends | Check the owning service (table below), not devices |
| Status/institution shown in another app is wrong | Stale **device_summary mirror** — devices must publish on every change | Confirm `DEVICE_UPDATED` fired; no reconciler exists |
| Hardware/Config Changes red badge won't clear | Acknowledge call 403'd or hit wrong service | Re-run as admin; badge clears only after the acknowledge POST 200s |
| Page not refreshing | Auto-refresh (every 60s) is paused while `editing` is true | Cancel the edit form; polling resumes |
| Terse 500 with no message | `server.error.include-message=never` in devices | Pull the real stack from Loki (`localhost:3100` on services VM) |

## Which backend serves each tab

- **hiveops-devices** (`/api/atms/*`, `/api/institution-keys`, `/api/agent-profiles`, `/api/cassette-configurations`, `/api/atm-makes|models|field-definitions`): Details, System Info, Agent, Software, Hardware, Cassettes, Config Changes, Task History.
- **hiveops-incident** (`/incident/journal-events/*`, `/incident/incidents/*`): Journal events, cassette inventory, Incidents history.
- **hiveops-fleet** (`/fleet/*`): Captures, fitted Modules, fleet task creation (CONFIG_UPDATE), Task History artifacts.
- **hiveops-journal** (`/journal/transactions`): Transactions / replenishments.

## Gotchas

- **Hardware profile is a two-step, frontend-orchestrated flow.** The UI first calls `PUT /api/atms/{id}/hardware-profile`, then separately `POST /fleet/tasks` with `taskKind=CONFIG_UPDATE`, `configPatch={ "atm.hardware.profile": <profileKey> }`. Setting the profile via the backend endpoint alone (or any non-UI caller) persists the profile but does **not** queue the CONFIG_UPDATE — the machine won't be told.
- **Move Institution never blanks a key** and always upper-cases; a wrong-token/mis-imaged box keeps its sticky wrong institution until moved here.
- **Soft-delete still emits `DEVICE_DELETED`** — deactivating a device publishes a delete event, so it disappears from all mirror apps even though the `atms` row survives as INACTIVE.
