Admin-only per-row Remove + multi-select bulk removal on the machines view, plus
per-row purge Remove on the sessions view, wired to the Task-5 admin API
(DELETE /api/machines|sessions/:id?purge=true, POST /api/machines/bulk-remove).
Confirm modals (danger-styled, focus-trapped), TanStack refetch so purged rows
leave the console, structured ApiError surfacing, honest partial-bulk summary,
and admin-gating via useAuth().isAdmin as defense-in-depth over the server 403.
Replaces the legacy all-user delete trigger. typecheck/lint/build clean.
Implements specs/v2-stable-identity/plan.md Task 5 (dashboard portion).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Admin-only user management: list, create, edit role/permissions/status,
reset password, and disable/delete, against the v2 users API.
- Admin-gated three ways: AdminRoute on /users (calm access-denied panel
for non-admins, no redirect loop or data fetch), Sidebar hides the nav
item, and every mutation relies on the server AdminUser 403 as the real
authority. isAdmin is derived from the server-validated user, not the
client token.
- Users table: role badge (admin/operator/viewer), permissions summary,
enabled/disabled status, created, last-login. Sticky header, skeleton,
empty/error states. Self row tagged "You".
- Create/edit use the real roles and permission strings
(view/control/transfer/manage_users/manage_clients); admin permissions
are server-implicit and shown locked. Passwords: typed or Web Crypto
generated (rejection-sampled, copy-once reveal), type=password +
autoComplete=new-password, cleared from state on open/close/success,
never logged/persisted/in-URL; blank on edit means unchanged.
- Self-lockout guards: cannot disable, delete, or demote your own admin
account (controls disabled + submit-handler checks, matched on the
authoritative user id). Server mirrors self-disable/self-delete; the
self-demotion guard is client-side (server todo filed).
- useUpdateUser sequences user-update then permissions-set; invalidates
["users"] on settled so the table reconciles after a partial failure,
with an actionable message if only permissions failed.
Passed Code Review (no blockers after fixes) and local gates
(tsc/lint/build green). Completes the v2 dashboard view set.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Generate, list, and cancel attended-support codes (XXX-XXX-XXX), built
on the v2 codes API and existing UI primitives.
- Codes table: code in mono, status badge (pending+pulse/connected/
completed/cancelled), bound client/machine, created-by, created
(relative + absolute tooltip). Sticky header, skeleton load,
actionable empty/error states.
- Generate opens a focused reveal modal showing the code large in
JetBrains Mono with copy and a read-aloud instruction; the code is
announced character-by-character for screen readers. Mint is ref-
guarded so it creates exactly one code per open (no StrictMode dupe).
- Cancel via confirm dialog (POST /api/codes/:code/cancel), disabled for
non-cancellable statuses; invalidates the codes query. List polls 7s.
- Shared API client now tolerates non-JSON 200 bodies, so the cancel
endpoint's plain-text "Code cancelled" success no longer surfaces as a
failure. Error-envelope handling unchanged.
Passed Code Review (no blockers after fixes) and local gates
(tsc/lint/build green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Active-sessions table with consent-state badges, viewer-token Join,
and disconnect, built on the v2 session API and existing UI primitives.
- Sessions table: machine, mode (managed/attended), consent badge
(granted/pending+pulse/denied/not_required), viewers, started,
duration, status. Sticky header, skeleton load, empty/error states.
- Join action mints a session-scoped viewer token
(POST /api/sessions/:id/viewer-token) and reveals it with the
/ws/viewer relay URL and copy buttons. The static viewer.html is
intentionally not targeted: it sends the raw login JWT, which the v2
viewer plane rejects. In-dashboard web viewer ships in a later pass.
- Authz split mirrors the server mint gate: admin or control permission
gets Control; view permission gets View only; neither hides the action.
Server remains authoritative; the minted token carries the signed
access claim.
- Disconnect via confirm dialog (DELETE /api/sessions/:id), invalidates
the sessions query. List polls every 8s so consent transitions surface.
Passed Code Review (no blockers) and local gates (tsc/lint/build green).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>