--- module: devices.device-groups title: Device Groups tab: Internal order: 20 audience: internal --- > **Internal · ops/support/admin.** Grounded in `hiveops-devices` backend + `AtmGroups.svelte` (2026-07-01). Owner service: **hiveops-devices** (port 8096, DB `hiveiq_devices`). ## What this module actually is A CRUD screen over the `atm_groups` table plus its `atm_group_members` join table. Groups are named, colored collections of devices used for **filtered device lists** and **bulk hardware-profile pushes**. Deleting a group or dropping a device only touches the join table — no device is ever deleted. ## Roles required - **Read** (list groups, view a group): any authenticated JWT. `GET /api/atm-groups` and `GET /api/atm-groups/{id}` have **no `@PreAuthorize`** — they are readable by any logged-in user, scoped by institution (see below). - **All mutations** (create, update, delete, assign ATMs, add/remove single ATM, assign hardware profile): `@PreAuthorize("hasAnyRole('MSP_ADMIN','BCOS_ADMIN')")`. A non-admin PUT/POST/DELETE returns **403**. ## Institution scoping (why a user "can't see a group") `AtmGroupService` calls `InstitutionContext.resolveScope()` on every read/write: - scope `null` → full access (MSP/BCOS admin) — sees all groups. - scope empty list → sees **zero** groups. - scope non-empty → only groups whose `institution_key` is in scope (`findAllWithAtmsByInstitutionKeyIn`). - On **create**, if the caller is scoped to exactly one institution, the group's `institution_key` is **forced** to that key — the Institution dropdown selection in the form is ignored for single-institution users. First thing to check when "my group vanished / I can't edit it": the caller's JWT institution scope vs the group's `institution_key`. ## Admin-only actions on the screen - **+ New Group** → `POST /api/atm-groups`. - **Edit** → `PUT /api/atm-groups/{id}` (name/description/color/institution/membership; only non-null fields are applied). - **Assign Devices** → `PUT /api/atm-groups/{id}/atms` with a full `[atmId, ...]` list — this **replaces** the whole membership set, it is not additive. - **Assign Profile** → `POST /api/atm-groups/{id}/hardware-profile` `{ "profileKey": "..." }`, then the **frontend** fires a `POST /fleet/tasks` (`CONFIG_UPDATE`, patch `atm.hardware.profile`). - **Delete** → `DELETE /api/atm-groups/{id}`. ## Set Default / Clear Default — NOT server state The **Set Default** / **✕ Clear** buttons are **purely client-side**. The chosen group id is stored in browser `localStorage` under key **`hiveops_defaultGroupId`** (`stores.ts`). It is: - per-browser, per-machine — never synced to the account or DB; - used only to pre-filter the device list (passes `groupId` to `/api/atms/paginated`). "My default group didn't follow me to another machine" is expected behaviour, not a bug. To reset it: clear site data / `localStorage`. ## Common failure modes + fixes | Symptom | Likely cause | Fix | |--------|--------------|-----| | **409 / 500 on create or edit** with terse message | `atm_groups.name` is `UNIQUE` **globally** (not per-institution). A duplicate name — even across institutions — violates the constraint. `server.error.include-message=never` hides the detail. | Pick a unique name; check DB for the colliding row. | | **403 on any save** | Caller lacks `MSP_ADMIN`/`BCOS_ADMIN`. | Use an admin JWT. | | **Group list empty for a real user** | Institution scope is empty, or all groups are tied to institutions outside scope. | Check JWT `institutionKey` claim / scope. | | **"Assign Profile" says N devices but no config lands on ATMs** | Two steps: profile save succeeded but the follow-up fleet task failed, or ATMs are offline/haven't polled. | Check `/fleet/tasks` for the queued `CONFIG_UPDATE`; verify agents are polling. | | **Assigning devices wiped others out** | `Assign Devices` is a full replace, not add. | Re-open, select the complete intended set. | | **Removed a device but it's "back"** | Edit form / Assign panel was opened from stale data. | Reload groups (`GET /api/atm-groups`) and retry. | | **Group shows "All" institution but a scoped admin can still edit it** | `verifyGroupAccess` only blocks when `institution_key` is non-null. Null (All) groups are editable by any in-scope admin. | Intended today; note it before blaming data. | ## Quick checks ```bash # List groups (any authed JWT) curl -s https://api.bcos.cloud/devices/api/atm-groups -H "Authorization: Bearer $JWT" # DB: group + member counts SELECT g.id, g.name, g.institution_key, count(m.atm_id) AS atms FROM atm_groups g LEFT JOIN atm_group_members m ON m.group_id = g.id GROUP BY g.id ORDER BY g.name; ``` See also [devices.device-groups] Overview (customer) and the Architect tab for data flow.