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>
139 lines
6.0 KiB
Markdown
139 lines
6.0 KiB
Markdown
# 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 <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
|
|
|
|
```bash
|
|
# 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>`.
|