Skip to content

[UI][API] Device group bulk actions 404 — web posts to POST /device-groups/bulk, which was never implemented #3429

Description

@ToddHebebrand

Description

Both bulk actions on the Device Groups page post to POST /device-groups/bulk, which does not exist. Select groups, choose a script or a policy, confirm — the request 404s.

  • Run script across selected groupshandleBulkScript, DeviceGroupsPage.tsx:583, body {action:"run-script", scriptId, groupIds}
  • Apply policy across selected groupshandleBulkPolicy, DeviceGroupsPage.tsx:614, body {action:"apply-policy", policyId, groupIds}

Live proof

POST /api/v1/device-groups/bulk  ->  404  {"error":"Not Found","path":"/api/v1/device-groups/bulk"}

Framework-level not-found with the path echoed back — no handler. grep for '/bulk' in apps/api/src/routes/groups.ts returns nothing; the router registers only get, get, post, patch, delete, get.

Why this needs a design call, not just a route

Unlike #3428 (where the handler exists under a different verb), there is no server-side implementation at all — the client invented a contract that was never built. Decisions needed before it can be implemented:

  1. Fan-out semantics. "Run a script across N groups" expands to a device set that can span multiple organizations, since group selection is not org-constrained in the UI. Per the partner-wide fan-out rule, worker-created child rows (script executions) must take the device's org, not the caller's or the group's. Getting this wrong is the eq(table.orgId, device.orgId) class of silent no-op.
  2. Partial failure. If 3 of 10 groups resolve and one script dispatch fails, what does the response look like, and what does the UI show? This is exactly the aggregate/partial-success shape that runActionAllowlist.ts carves out from the standard runAction toast contract.
  3. Whether it should be one endpoint with an action discriminator at all. The client's {action: "run-script" | "apply-policy"} union is a mild smell; two explicit endpoints would type better and keep the authorization checks separate — running a script and assigning a policy are different permissions.
  4. Deduplication. A device in two selected groups should not get the script twice.

Interim consideration

Since there is no backend, the two bulk controls are pure dead UI. If implementing this isn't near-term, hiding or disabling the bulk toolbar is better than leaving controls that always fail — same reasoning as #2789 (built-but-unrendered deployment UI) and #2606 (features with no page). That's a product call.

Affected files

  • apps/web/src/components/devices/DeviceGroupsPage.tsx:579-600 (handleBulkScript), :606-630 (handleBulkPolicy)
  • apps/api/src/routes/groups.ts (no /bulk route)

Context

Found while verifying #3159. Companion to #3428 (edit/drag-drop 404) and #3425 (create 400s for multi-org partners). 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions