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>
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 incomponents/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 — follow-up (NOT wired in this pass)
The build uses base: "./" so emitted assets use relative paths. Production
serving means copying dist/ into the GC server's static directory and adding a
catch-all route that returns index.html for non-API, non-asset paths (so deep
links like /machines survive a hard reload under the BrowserRouter).
That Rust-side wiring is a deploy concern and is intentionally left for a later step:
- Copy
dist/→server/static/(or servedist/directly). - Add an Axum fallback route serving
index.htmlfor unmatched GET paths, after the/api/*,/ws/*, and static-asset routes. - If the dashboard is mounted under a sub-path rather than the server root,
switch Vite
baseto that path and pass the samebasenameto<BrowserRouter>.
No server/Rust changes were made in this pass.