2026-05-31

Roles stopped being a two-value admin/viewer flag and became a full capability matrix you can build custom roles from; the workspace-tab cache learned to stop resurrecting tabs you had closed; ingest grew a guard against the reserved e2e device UUID; and BusymateHelper 1.0 (build 164) went in for App Store review. Dashboard builds 271 → 278; Supabase migrations 190 → 191; iOS builds 159 → 164; docs builds 150 → 152.

What changed

Capability-based RBAC (dashboard 276 / supabase 190)

  • A new roles table replaces the binary role: each role is a per-section × action capability matrix plus a grants_all escape hatch. profiles.role is now an FK to roles.key, and a has_capability(section, action) SQL function backs every gate.
  • RLS swapis_admin() is gone from the policies; fleet writes (settings_global, service_groups, tags) now require the matching capability, and reads on devices / entries / settings_device / push / JWTs / PAC / breakpoints are owner-or-capability.
  • Built-in roles are immutable: admin (all) and viewer (own-only). A protection trigger blocks deleting or renaming admin and stops anyone stripping its grants_all. Changes fan out live over a roles:all Realtime broadcast.
  • ClientuseCapabilities() hook, a SECTION_GATES map, and a RequireCapability guard (forbidden card) wrap every gated section; nav is filtered by can(section, 'view'). A new Roles section (matrix editor) lets admins build custom roles, and the Users role picker is now populated from the roles table.
  • Edge Functions (admin user create/delete, password update, TestFlight admin, snapshot) gate on has_capability via a shared helper. MCP gained 5 role tools (count 61 → 66).

RBAC test coverage (dashboard / supabase)

  • @rbac e2e specs cover the admin Roles UI (nav entry, role list, capability matrix per section, admin locked full-access, New-role dialog) and a @mcp @rbac suite exercises list / create / update / delete_role + set_user_role against live mcp.busymate.net, including the confirm-gate, a full CRUD round-trip, and the built-in-protection trigger.

Tab-persistence fixes (dashboard 277 → 278)

  • Closed tabs reappearing after reload is fixed (277), and 278 adds full dead-mode cleanup and stops the warm-cache tab resurrection — a closed tab no longer comes back when the cache rehydrates.

Ingest hardening (supabase 191)

  • Ingest to the reserved e2e device UUID is now blocked at the database, and the accumulated backlog from that fixture device was drained.

App Store 1.0 + TestFlight (ios 159 → 164 / dashboard 271 → 275 / docs 150 → 152)

  • BusymateHelper 1.0 (build 164) was submitted for App Store review. Submission prep closed the last gates: the App Privacy nutrition label was settled, a submit Fastlane lane (submit-only deliver, manual release, export/IDFA pre-answered) was added, and the screenshot set was reordered app-flow-first (home, settings, network, detail, account, login) with a matched iPhone + iPad light/dark set.
  • Every TestFlight upload now always distributes to external testers — internal-only ships are no longer possible.
  • iOS picked up Dynamic Type accessibility scaling (164), a redesigned Account screen with a live device list (163), an optional in-app cert setup video in the wizard (160 → 162), and Live Activity only while connected (159).
  • The /helper listing preview was redeployed to reflect the reordered, theme-aware screenshot set, and the docs site gained a narrated certificate setup walkthrough page (docs 150 → 152).

Where it's documented