CRUMB a card from devarno-cloud

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 proxy

Data 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)

ConcernImplementation
Base URLprocess.env.NEXT_PUBLIC_API_URL ?? 'http://localhost:8080'
Auth headerrequest interceptor injects Authorization: Bearer ${getAuthToken()}
401 handlingresponse interceptor → window.location = '/login'
Retryup to 3 attempts on retryable errors with exponential backoff 1s * N
Typed methodsgetWorkspace, getServices, runOperation, syncWorkspace, assembleWorkspace, listPellets, getPellet, compressPellet, extractPellet, getCacheStats, prunePellets

TanStack Query Hooks (hooks/useNestrAPI.ts)

HookCadenceNotes
usePelletsrefetchInterval 30 sinvalidated on pellet:compressed / pellet:pruned events
usePellet(id)enabled when id truthy
useCompressPelletmutationinvalidates pelletKeys.lists() on success
useCacheStatsrefetchInterval 10 sfeeds 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 familypelletKeys.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