.agents/rules/frontend/admin.md
clients/admin)Operator/SuperAdmin-facing console. Read frontend/shared.md first; this file is only the divergences.
http://localhost:5030 (HTTP) · localStorage prefix fsh.admin.* · login header X-FSH-App: admin.src/env.ts): { apiBase, defaultTenant, dashboardUrl }. dashboardUrl is used for the one-way impersonation handoff into the dashboard app.Admin is the only app with react-hook-form + zod + @hookform/resolvers. Use them:
const form = useForm<Schema>({ resolver: zodResolver(schema) });
Form layout primitives live in src/components/list/ (PageHeader, Field, FormShell, FormSection, FormActions, Pagination, ErrorBand).
GET /api/v1/identity/permissions (getMyPermissions), cached under fsh.admin.permissions.AuthProvider hydrates them in an effect keyed on subject change and exposes permissionsHydrated to avoid a 403 flash on first paint.<RouteGuard perms={[IdentityPermissions.Users.View]}>…</RouteGuard>. It renders a "Resolving permissions" state while !permissionsHydrated, else <ForbiddenView missing={…}/>. (ProtectedRoute also accepts a permissions? prop.)src/lib/permissions.ts (IdentityPermissions, MultitenancyPermissions, … frozen objects + PERMISSION_CATALOG driving the role editor). There is intentionally no runtime catalog fetch — when the server adds a permission, mirror the constant here.<RouteGuard perms={…}> (no per-route Suspense wrapper).RealtimeProvider is mounted in App.tsx and wires only ["NotificationCreated"].Cool-cast neutrals (hue 240, small non-zero chroma — not chroma 0), a single fixed chartreuse "signal" accent (--accent-signal / --signal-500), Geist / Geist Mono fonts. Defined in src/styles/globals.css.
src/lib/permissions.ts (and PERMISSION_CATALOG if it belongs in the role editor) and wrap the route in <RouteGuard perms={[…]}>.seedAuthedSession here also pre-seeds fsh.admin.permissions so RouteGuard passes on first paint.