# 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 ```bash 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 ` 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: 1. Copy `dist/` → `server/static/` (or serve `dist/` directly). 2. Add an Axum fallback route serving `index.html` for unmatched GET paths, *after* the `/api/*`, `/ws/*`, and static-asset routes. 3. If the dashboard is mounted under a sub-path rather than the server root, switch Vite `base` to that path and pass the same `basename` to ``. No server/Rust changes were made in this pass.