feat(dashboard): GuruConnect v2 Users admin view
Some checks failed
Build and Test / Build Server (Linux) (push) Failing after 4m43s
Build and Test / Build Agent (Windows) (push) Successful in 8m48s
Build and Test / Security Audit (push) Successful in 4m38s
Build and Test / Build Summary (push) Has been skipped

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>
This commit is contained in:
2026-05-30 14:18:40 -07:00
parent 664f33d5ab
commit 96b4fd7721
17 changed files with 1827 additions and 3 deletions

View File

@@ -7,6 +7,55 @@
export type StatusTone = "ok" | "warn" | "bad" | "neutral";
/** Badge tones available to features (StatusTone plus the brand `accent`). */
export type BadgeTone = StatusTone | "accent";
/**
* Map a user role to a badge tone. `admin` is the elevated, distinct tone and
* gets the brand `accent` so it reads as "privileged" at a glance; `operator`
* is a normal active role (`ok`); `viewer` is the least-privileged, muted
* (`neutral`). An unknown role falls back to `neutral`.
*/
export function roleTone(role: string): BadgeTone {
switch (role) {
case "admin":
return "accent";
case "operator":
return "ok";
case "viewer":
default:
return "neutral";
}
}
/** Title-case label for a role; passes unknown roles through verbatim. */
export function roleLabel(role: string): string {
switch (role) {
case "admin":
return "Admin";
case "operator":
return "Operator";
case "viewer":
return "Viewer";
default:
return role;
}
}
/**
* Map a user's enabled flag to a status tone. An enabled account is healthy
* (`ok`); a disabled one is a deliberate block and reads as `bad` so it stands
* out in the table (a disabled user is an exception worth seeing).
*/
export function userStatusTone(enabled: boolean): StatusTone {
return enabled ? "ok" : "bad";
}
/** Human label for a user's enabled flag. */
export function userStatusLabel(enabled: boolean): string {
return enabled ? "Active" : "Disabled";
}
/** Map a machine `status` string to a tone. */
export function machineTone(status: string): StatusTone {
return status === "online" ? "ok" : "bad";