---
module: recon.device-cycle-mgmt
title: Device Cycle Management
tab: Internal
order: 20
audience: internal
---

> **Internal ops/support.** Grounded in `hiveops-recon` source (2026-07-01). This tab manages the **recon device selection** — which ATMs appear in Cash Reconciliation. It writes one small table; most "bugs" are actually stale device data or institution-scope confusion.

## What this screen actually does

- Frontend component: `DeviceCycleMgmt.svelte` (recon frontend, `recon.bcos.cloud`).
- On load it calls **GET `/api/recon/device-selection`** → `{ selected: [...], all: [...] }`.
  - `selected` = ATM IDs stored in the `recon_device_selection` table for the caller's institution scope.
  - `all` = every ATM ID the caller can see, read from recon's local **`device_summary`** mirror (NOT a live call to incident/devices).
  - Available (left) = `all − selected`, computed in the browser.
- **Save Selection** calls **PUT `/api/recon/device-selection`** with a plain JSON array of ATM IDs, then refreshes the fleet summary.
- Empty selection is intentional: when the saved list is empty, Cash Reconciliation shows **all** ATMs (the summary applies no filter).

## Roles required

| Action | Endpoint | Role gate |
|---|---|---|
| View lists (read) | GET `/api/recon/device-selection` | **None** — any authenticated user (incl. CUSTOMER) can read |
| Save selection | PUT `/api/recon/device-selection` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |
| Enroll one ATM (idempotent) | POST `/api/recon/device-selection/enroll/{atmId}` | `hasAnyRole('MSP_ADMIN','BCOS_ADMIN')` |

- A CUSTOMER/BANK user can open the screen and see it, but **Save** returns 403.
- Institution scope is derived from the JWT (see below), not from a request param.

## Institution scope — read this before "the wrong ATMs show up"

Scope is resolved from the JWT by `InstitutionContext`:

- **BCOS_ADMIN / ADMIN / QDS_ADMIN** → *unrestricted* (scope = null). They read/write the **global** selection rows (`institution IS NULL`) and see **all** ATMs from `device_summary`.
- **CUSTOMER (BANK)** → scoped to their single institution key.
- **MSP_ADMIN** → scoped to their granted institution keys (a list).

Consequences that trip people up:
- **Admin and scoped users keep SEPARATE selection lists.** An admin's global (`NULL`) selection is invisible to a customer, and vice versa. "I saved a selection as admin but the customer still sees everything" is expected — they read different rows.
- The **Available** list for a scoped user is filtered by the `institutionKey` claim in their JWT against `device_summary`. If that claim is missing/blank, they fall through to "all ATMs."

## First things to check when it misbehaves

1. **Available list is empty or missing ATMs** → the `device_summary` mirror is stale or incomplete. It is fed only by Kafka topic `hiveops.device.events`; there is **no reconciler**. Check that devices are still publishing and recon's consumer group `hiveops-recon-device-sync` is healthy. An ATM whose device event arrived **without a `deviceId`** is skipped on insert and will never appear in Available.
2. **Save returns 403** → caller lacks `MSP_ADMIN`/`BCOS_ADMIN`. Confirm the JWT role. GET works for everyone, so "I can see it but can't save" is a role problem, not an outage.
3. **Selection saved but Cash Reconciliation still shows everything** → the saved list is empty (empty = show all), OR the viewer's scope differs from the scope you saved under (admin vs customer). Check `recon_device_selection` rows and their `institution` column.
4. **Wrong ATMs / wrong institution in Available** → almost always a stale `device_summary` row (wrong `institution_key` mirrored in) — the classic cross-service stale-mirror issue, not a recon selection bug.
5. **500 on load** → check recon backend health and DB (`hiveiq_recon`); the GET reads both `recon_device_selection` and `device_summary`.

## Common failure modes + fixes

| Symptom | Likely cause | Fix |
|---|---|---|
| Available list empty | `device_summary` empty/stale; Kafka consumer down | Verify `hiveops.device.events` flowing; restart recon consumer; re-emit device events from devices |
| A known ATM never appears in Available | Its device event had no `deviceId` (skipped insert) | Ensure devices publishes a full event incl. `deviceId` for that ATM |
| Save button 403 | Non-admin role | Use `MSP_ADMIN`/`BCOS_ADMIN` account |
| Customer sees all ATMs despite admin saving a list | Admin wrote global (`NULL`) rows; customer reads their institution rows | Save the selection while scoped to that institution (or leave empty intentionally) |
| MSP_ADMIN with multiple institutions: saved list looks duplicated/stale on reload | Write path stores under only the **first** institution and deletes only that institution's rows; other institutions' rows survive and are re-unioned on read | Verify `recon_device_selection` rows per institution; see uncertainty note |

## Direct DB inspection

DB `hiveiq_recon`, table `recon_device_selection` (`id`, `atm_id`, `institution`, `created_at`; `UNIQUE(atm_id, institution)`):

```sql
-- global (admin) selection
SELECT atm_id FROM recon_device_selection WHERE institution IS NULL ORDER BY atm_id;
-- a specific institution's selection
SELECT atm_id FROM recon_device_selection WHERE institution = 'FSCU' ORDER BY atm_id;
```

Related: `technical/recon/overview.md` for the full service picture; `[architect]` tab for data flow.
