.agents/rules/frontend/dashboard.md
clients/dashboard)Tenant-facing application. Read frontend/shared.md first; this file is only the divergences.
https://localhost:7030 (HTTPS, with ws: true for the SignalR hub) · localStorage prefix fsh.dashboard.* · login header X-FSH-App: dashboard.src/env.ts): { apiBase, defaultTenant, demoMode }.Authorization header.The dashboard does not depend on react-hook-form or zod. Use controlled inputs + local state. Don't add those deps to match admin.
The JWT carries only role names. auth-context.tsx fetches the effective permission list from GET /api/v1/identity/permissions (getMyPermissions() in src/api/identity.ts), caches it in tokenStore under fsh.dashboard.permissions, and exposes a permissionsHydrated flag so gated UI doesn't flash while the fetch is in flight (re-fetched on login/impersonation swaps; refreshPermissions() for role changes). Nav items are gated via perm/anyPerm in src/components/layout/nav-data.ts. ProtectedRoute is still auth-only (no per-route permission gating) — don't add RouteGuard-style gating here.
withSuspense(node) (per-route skeleton fallback). No per-route permission guards.RealtimeProvider and SseProvider are mounted inside AppShell (authenticated routes only), under a CommandPaletteProvider (cmdk).src/sse/, dashboard-only): two-step token — POST /api/v1/sse/token, then GET /api/v1/sse/stream?token=<guid> consumed via fetch streaming (parseSseStream async generator; EventSource can't send auth headers). Two split contexts: useSseStatus() (stable, for status dots) vs useSseEvents() (mutates per event) to avoid cascading re-renders; useSse() is the composite.token-store.ts has beginImpersonation / endImpersonationWithFreshTokens / restoreStashedActor that stash the operator's tokens under fsh.dashboard.impersonation.*. AuthProvider exposes beginImpersonation/stopImpersonation and derives ImpersonationInfo from act_sub / act_tenant / act_name claims. Admin triggers the handoff one-way via its dashboardUrl.
@tanstack/react-virtual for long lists — use it for any large collection (chat history, big tables).cmdk powers the command palette.Chroma-0 neutrals (--neutral-*: oklch(L 0 0) — untinted; the warm-paper tint was deliberately removed). Rose default brand with swappable accent themes via .accent-{rose,indigo,violet,sky,emerald,amber} classes that override the --brand-* oklch stops; saffron secondary; Figtree font. Defined in src/styles/globals.css. Keep neutrals at chroma 0.
withSuspense(<X/>); no permission guard.useRealtimeEvent("EventName", handler) (register the name in realtime-context.tsx) or useSseEvents() for SSE.react-virtual for long lists; keep neutrals chroma 0.