Axum now serves the v2 React/Vite dashboard SPA at / with a client-side routing fallback, and the dead v1 HTML portal is removed (nothing was live on the server to preserve). - SPA served from server/static/app via ServeDir with a fallback to index.html, so deep links (/machines, /sessions) resolve to the SPA. - /api/*rest and /ws/*rest return JSON 404 so unrouted API/WS paths never leak index.html to clients; real /api, /ws, /health, /metrics, and the /downloads nest keep precedence (matchit static-over-wildcard). - Path-aware Cache-Control: hashed /assets immutable, index.html no-cache. - Vite builds to server/static/app (base /); the artifact is gitignored and rebuilt at deploy time (npm ci && npm run build). - Removed v1 portal files (login/dashboard/users/index/viewer .html) and their dead serve_* handlers; the SPA owns /, /login, /dashboard, /users. Verified locally: server boots, / and deep links serve the SPA, unknown /api path returns JSON 404 (not HTML), /health and /downloads intact. cargo build + clippy -D warnings green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
6.0 KiB
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 — 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)
baseis/(absolute asset paths). The SPA usesBrowserRouter, 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_serviceisServeDir::new("static/app")with.fallback(ServeFile::new("static/app/index.html")). Real files under/assets/*are served from disk; any other unmatched path returnsindex.html(HTTP 200) so React Router resolves the route. - Precedence / safety: the fallback runs only after every explicit
/api/*,/ws/*,/health,/metricsroute and the/downloadsnest. Two catch-all routes —/api/*restand/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.htmland 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>.