Web Dashboard Architecture
nestr intermediate 5 min read
ELI5
The Web app is a single-tablet kiosk in front of the Engine: it groups screens by login status (auth vs app), uses a shared messenger (axios) that knocks with a key card on every door, and a query cache that whispers “ask again in 30 seconds” so the screens stay fresh.
Technical Deep Dive
Next.js App Router under web/src/app:
src/app/├── layout.tsx # root├── (auth)/login, /signup # public group, shared auth layout├── (app)/dashboard|pellets|cache|metrics|graph|chat└── api/auth/[...all] # BetterAuth proxyData Flow
flowchart LR UI[Page / Component] -->|hook| TQ[TanStack Query] TQ -->|cache miss / refetch| AX[axios client.ts] AX -->|Bearer token + retry x3| ENG[Engine REST] ENG -.WebSocket.-> WSH[useWebSocket] WSH -->|invalidate keys| TQ AX -->|401| LOGIN["/login redirect"]axios Client (web/src/api/client.ts)
| Concern | Implementation |
|---|---|
| Base URL | process.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080' |
| Auth header | request interceptor injects Authorization: Bearer ${getAuthToken()} |
| 401 handling | response interceptor → window.location = '/login' |
| Retry | up to 3 attempts on retryable errors with exponential backoff 1s * N |
| Typed methods | getWorkspace, getServices, runOperation, syncWorkspace, assembleWorkspace, listPellets, getPellet, compressPellet, extractPellet, getCacheStats, prunePellets |
TanStack Query Hooks (hooks/useNestrAPI.ts)
| Hook | Cadence | Notes |
|---|---|---|
usePellets | refetchInterval 30 s | invalidated on pellet:compressed / pellet:pruned events |
usePellet(id) | enabled when id truthy | |
useCompressPellet | mutation | invalidates pelletKeys.lists() on success |
useCacheStats | refetchInterval 10 s | feeds useCacheHealth (nestr-004) |
Component Surface
CompressForm, CacheViewer, OperationDashboard, MetricsPanel, WorkspaceOverview, GraphView, plus a chat module (ChatView, ToolCallCard, AgentStatus) that overlays the same data with an LLM tool-use panel.
Tests
Playwright config (web/playwright.config.ts): test dir ./tests/e2e, 30 s per-test timeout, browsers Chromium/Firefox/WebKit + mobile, base URL process.env.BASE_URL ?? 'http://localhost:5173', web server yarn start. Specs cover health, data-loading, navigation, operations and workspace APIs.
Key Terms
- BetterAuth proxy →
/api/auth/[...all]route forwarding to the auth provider; isolates session handling from the Engine’s JWT validation. - Query key family →
pelletKeys.lists()is a structured key prefix used to invalidate all pellet-list queries at once. - Retryable error → axios-classified network failures and 5xx — 4xx other than 401 are not retried.
Q&A
Q: How does the dashboard discover that a pellet was compressed in another tab? A: It doesn’t. The WS hook is per browser tab; cross-tab fan-out would require a BroadcastChannel layer that is not present in this revision.
Q: What is the cost of the 30 s refetch on usePellets?
A: One GET /api/pellets. With WS invalidation in place this is essentially a safety net for missed events; a longer interval is safe if the WS hook is reliable.
Q: Why does the 401 interceptor hard-redirect instead of refreshing the token? A: The Engine validates against an external JWKS, the Web app does not own a refresh token in this revision; the cleanest recovery is to send the user back through BetterAuth.
Examples
On the /pellets page, mounting triggers usePellets() → axios.get('/api/pellets') with a Bearer token; a pellet:compressed WS frame arrives 1.2 s later → queryClient.invalidateQueries(pelletKeys.lists()) → immediate refetch shows the new pellet without waiting for the 30 s tick.
neighbors on the map
- purr-api Layered Architecture adding a new feature to purr-api
- LORE Route Structure & Data Model implementing a new LORE page or route
- Choco Factory Architecture Map onboarding to a new choco service repo