Description
Everything on the Device Groups page that writes to an existing group is dead. The page sends PUT /device-groups/:id; the router only registers .patch('/:id'). There is no PUT handler, so every one of these 404s at the framework level:
- Editing a group (name, description, type, policy) via the edit modal
- Drag-and-drop device assignment between groups
- Any other caller of the shared
persistGroup helper (DeviceGroupsPage.tsx:468-471)
Live proof
Against a seeded local stack on origin/main (f2d7fda83), authenticated as admin. The discriminator is the body, not the status — both cases return 404 and only the body distinguishes "no such row" from "no such route":
PATCH /api/v1/device-groups/00000000-...-000 -> 404 {"error":"Group not found"}
PUT /api/v1/device-groups/00000000-...-000 -> 404 {"error":"Not Found","path":"/api/v1/device-groups/00000000-...-000"}
GET /api/v1/device-groups -> 200
PATCH reaches the handler and reports a missing row. PUT never reaches a handler — that is Hono's default not-found envelope with the path echoed back.
Route registrations in apps/api/src/routes/groups.ts are get, get, post, patch, delete, get — no put.
UI symptom
persistGroup throws a fixed new Error("Failed to update group") on !response.ok, so the user gets a generic failure with no indication that the request never hit a route. Drag-and-drop appears to do nothing.
Proposed fix
Change the client to PATCH, which is the verb the API actually implements and the correct semantics for a partial update — the payload deliberately omits rules (see the comment at DeviceGroupsPage.tsx:458-459), which is a merge, not a replace.
Do not add a PUT alias: the existing handler treats undefined as "leave this field alone", which is PATCH semantics. A PUT that behaved as a true replace would silently clear filterConditions and rules on every device-assignment write.
While in there, persistGroup should surface the server's message instead of the fixed string — PR #3423 does exactly that for the create/edit submit path, and this helper deserves the same treatment.
Affected files
apps/web/src/components/devices/DeviceGroupsPage.tsx:468-471 (the PUT), :455-475 (persistGroup)
apps/api/src/routes/groups.ts:537 (the PATCH handler that exists)
Context
Found while verifying #3159. Device Groups has four independent write defects right now, which is worth seeing together: create rejected filterConditions: null and silently dropped deviceIds (#3159, fixed in #3423), create 400s for multi-org partners (#3425), edit and drag-drop 404 (this issue), and bulk actions 404 (filed separately). Not a v0.105.0 regression — pre-existing.
Reported By: internal Playwright UI QA sweep 2026-08-11, evidence in docs/testing/FEATURE_TEST_LOG.md.
Description
Everything on the Device Groups page that writes to an existing group is dead. The page sends
PUT /device-groups/:id; the router only registers.patch('/:id'). There is noPUThandler, so every one of these 404s at the framework level:persistGrouphelper (DeviceGroupsPage.tsx:468-471)Live proof
Against a seeded local stack on
origin/main(f2d7fda83), authenticated as admin. The discriminator is the body, not the status — both cases return 404 and only the body distinguishes "no such row" from "no such route":PATCHreaches the handler and reports a missing row.PUTnever reaches a handler — that is Hono's default not-found envelope with thepathechoed back.Route registrations in
apps/api/src/routes/groups.tsareget,get,post,patch,delete,get— noput.UI symptom
persistGroupthrows a fixednew Error("Failed to update group")on!response.ok, so the user gets a generic failure with no indication that the request never hit a route. Drag-and-drop appears to do nothing.Proposed fix
Change the client to
PATCH, which is the verb the API actually implements and the correct semantics for a partial update — the payload deliberately omitsrules(see the comment atDeviceGroupsPage.tsx:458-459), which is a merge, not a replace.Do not add a
PUTalias: the existing handler treatsundefinedas "leave this field alone", which isPATCHsemantics. APUTthat behaved as a true replace would silently clearfilterConditionsandruleson every device-assignment write.While in there,
persistGroupshould surface the server's message instead of the fixed string — PR #3423 does exactly that for the create/edit submit path, and this helper deserves the same treatment.Affected files
apps/web/src/components/devices/DeviceGroupsPage.tsx:468-471(thePUT),:455-475(persistGroup)apps/api/src/routes/groups.ts:537(thePATCHhandler that exists)Context
Found while verifying #3159. Device Groups has four independent write defects right now, which is worth seeing together: create rejected
filterConditions: nulland silently droppeddeviceIds(#3159, fixed in #3423), create 400s for multi-org partners (#3425), edit and drag-drop 404 (this issue), and bulk actions 404 (filed separately). Not a v0.105.0 regression — pre-existing.Reported By: internal Playwright UI QA sweep 2026-08-11, evidence in
docs/testing/FEATURE_TEST_LOG.md.