Files
guru-connect/dashboard
Mike Swanson 96b4fd7721
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
feat(dashboard): GuruConnect v2 Users admin view
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>
2026-05-30 14:18:40 -07:00
..

GuruConnect Operator Dashboard (v2)

React + Vite + TypeScript SPA — the operator console for GuruConnect v2. A dark "operations terminal" UI for managing the remote-support fleet.

Pass 1 scope. This pass ships the scaffold, design system, app shell, auth, the typed API client, and the Machines view. Sessions, Codes, and Users are nav stubs only (disabled in the sidebar) and arrive in later passes.

Stack

  • React 18 + React Router 6 (client-side routing)
  • Vite 5 (dev server + build)
  • TypeScript (strict)
  • @tanstack/react-query (server-state, polling, cache invalidation)
  • @fontsource — Hanken Grotesk (UI) + JetBrains Mono (technical data)

No component/icon libraries — primitives and icons are hand-built to keep the console aesthetic and the bundle lean.

Scripts

npm install
npm run dev        # Vite dev server (proxies /api + /ws to the local GC server)
npm run build      # tsc -b && vite build  ->  dist/
npm run preview     # serve the production build locally
npm run typecheck  # tsc --noEmit
npm run lint       # eslint

Project layout

src/
  api/            Typed API client + response interfaces (source of truth: server/src/api/*.rs)
    client.ts       fetch wrapper: base URL, bearer token, dual error-envelope normalization
    types.ts        TS mirrors of the Rust response structs
    auth.ts         login / me / logout
    machines.ts     list / get / history / delete + admin key endpoints
    stubs.ts        sessions / codes / users — scaffolds for later passes
  auth/           AuthProvider (token in memory + sessionStorage), context, ProtectedRoute
  components/
    ui/             Reusable primitives: Button, Badge/StatusDot, Table, Panel,
                    Modal, ConfirmDialog, Input/Field, Spinner, States, Toast
    layout/         AppShell, Sidebar, Topbar, PageHeader, inline SVG icons
  features/
    auth/           LoginPage
    machines/       MachinesPage + detail / delete / admin-keys modals + hooks
  lib/            time formatting, clipboard, relay-status probe
  styles/         tokens.css (design tokens)

Design system — "operations terminal"

Dark control-room console. Tokens live in src/styles/tokens.css; primitive styles in src/components/ui/*.css.

  • Surfaces: --bg #0b0f14, --panel #141b22, --panel-2 #0e1419
  • Accent (signal cyan): --accent #22d3bf — primary actions + live state
  • Status language (dot + label, used everywhere): ok/online --ok, pending --warn (soft pulse), denied/offline/error --bad, neutral --neutral. Mapping centralised in components/ui/status.ts.
  • Type: Hanken Grotesk for UI; JetBrains Mono for all technical data (agent IDs, support codes, IPs, versions, timestamps, key fingerprints).
  • Motion (restrained): staggered row fade-in, the consent pulse, the live relay pip, hover transitions. All disabled under prefers-reduced-motion.

Auth

POST /api/auth/login{ token, user }. The token is held in an in-memory ref and mirrored to sessionStorage (never localStorage), so it clears when the tab closes. GET /api/auth/me restores the session on reload; POST /api/auth/logout revokes it server-side. The client attaches Authorization: Bearer <token> to every request and bounces to /login on any 401. Admin-only UI (per-agent key management) is gated on role === "admin".

The API uses two error envelopes — { error } and { detail, error_code, status_code }. api/client.ts extracts a message from whichever is present (and falls back to plain-text bodies that some routes return), so callers see one normalized ApiError.

Dev proxy

vite.config.ts proxies /api and /ws to the local GC server (http://localhost:3002). Run the Rust server locally, then npm run dev — same-origin requests reach the backend with no CORS setup.

To develop the UI against a remote backend instead, set VITE_API_URL (see .env.example).

Production serving — WIRED

The SPA is served by the GC Axum server from the server root. No manual copy step: vite.config.ts sets build.outDir to ../server/static/app/, so the build lands exactly where the server serves it.

Build & deploy flow

# from dashboard/
npm run build      # tsc -b && vite build  ->  ../server/static/app/

That single command refreshes the served SPA. emptyOutDir clears only server/static/app/ (the dedicated SPA subdir), so the v1 portal files in the static root are never touched.

How the server serves it (server/src/main.rs)

  • base is / (absolute asset paths). The SPA uses BrowserRouter, so a hard reload of a deep link (/machines) must still load /assets/*; relative (./) paths would resolve against the deep-link path and 404. Absolute is required.
  • The Router's fallback_service is ServeDir::new("static/app") with .fallback(ServeFile::new("static/app/index.html")). Real files under /assets/* are served from disk; any other unmatched path returns index.html (HTTP 200) so React Router resolves the route.
  • Precedence / safety: the fallback runs only after every explicit /api/*, /ws/*, /health, /metrics route and the /downloads nest. Two catch-all routes — /api/*rest and /ws/*rest — return a JSON 404 for unrouted API/WS paths, so the SPA fallback never answers an API/WS path with HTML (which would break this client's error-envelope parsing).
  • Caching: /assets/* (content-hashed) → immutable, one year; index.html and everything else → no-cache, must-revalidate.

Build output in git

server/static/app/ is a build artifact. Whether to commit it or .gitignore it depends on the deploy model (server-side npm run build vs shipping the repo's static dir). Decide at commit time. The old dashboard/dist/ path is no longer used.

Sub-path mounting (not used)

The dashboard is mounted at the server root. If it is ever moved under a sub-path, switch Vite base to that path and pass the same basename to <BrowserRouter>.