Imported from nukehub-dev/nukelab (
frontend/AGENTS.md). Install upstream withnpx skills add nukehub-dev/nukelab --skill frontend. Copyright stays with the author.
Frontend
Purpose
Vite + React 19 single-page application for the NukeLab platform admin dashboard and user interface.
Ownership
All files under frontend/ except generated artifacts (node_modules/, dist/, .tanstack/, test-results/).
Local Contracts
- React 19, TypeScript ~6, Tailwind CSS v4, TanStack Router/Query/Table, Zustand, Playwright for e2e tests.
src/main.tsxis the entry point;src/routeTree.gen.tsis generated by TanStack Router.e2e/owns Playwright end-to-end tests.- Service worker cache injection is handled by
scripts/inject-sw-cache.cjsduringnpm run build.
Work Guidance
Project structure
src/routes/— TanStack Router file-based routes. Each route file exports a default component and may exportRoutemetadata. The route tree is regenerated automatically onnpm run devor vianpx tsr generate.src/components/— reusable UI components. Prefer composition over large monolithic components.src/hooks/— custom React hooks, especially data-fetching wrappers around TanStack Query.src/hooks/use-activity-heartbeat.ts— pingsPOST /servers/:id/activityevery 30s only while the tab is visible AND the user interacted within the input window (src/lib/activity-heartbeat.ts); an open-but-untouched tab must not block idle shutdown.src/hooks/use-page-guard.ts— page-level RBAC guard hook that redirects unauthorized users.src/hooks/use-is-desktop.ts— viewport ≥lg (1024px) check; gates expensive visual effects off mobile and mounts responsive-only content exactly once (e.g.Dialogchildren render in either the mobile sheet or the desktop drawer, never both).src/hooks/use-keyboard-shortcuts.ts— global shortcuts;'mod'modifier = ctrl-or-meta, optionalpermissiongating. Global search palette (src/components/search/command-palette.tsx, backed bysrc/hooks/use-search.ts) opens via theshow-searchwindow event, bound toCtrl+K(mod) and/. Scoped filters: a leading/usersorusers:token narrows the search to one entity group (sent as thegroupAPI param withlimit=10). Static Go-to commands rank above entity results and cover all user and admin pages — each admin command gated by its route guard's permission — withkeywordsaliases, plus an admin-only Grafana action that opens via the monitoring auth redirect.
src/stores/— Zustand stores for client-side state that does not belong in the URL or server cache.src/stores/auth-store.ts— auth store with user state,PERMISSIONSconstants, and permission helpers.src/stores/timezone-store.ts— non-persisted mirror of the backendtimezonepreference ('auto'or an IANA zone →effectiveZone), synced bysrc/hooks/use-timezone-sync.ts. Keep it self-contained;lib/utils.tsimports the store, never the reverse.
src/lib/— pure utility functions and shared constants.src/lib/external-links.ts— single source of truth for external destinations (NukeTalk community, contact page, blog). ImportEXTERNAL_LINKS; never hard-code these URLs.src/lib/utils.ts— backend timestamps are naive UTC (ISO 8601 without Z/offset). Parse and format every API timestamp throughparseUtcDate/formatDate/formatRelativeTimefrom this module; never callnew Date(apiString)directly. Formatters apply the user's timezone preference (default auto/browser, stored in the backendtimezonepreference) and use the browser locale; date-onlyYYYY-MM-DDstrings go throughparseLocalDate/formatDateOnly.
src/api/— generated or hand-written API client code and request/response types.
Adding a route
- Create a route file under
src/routes/matching TanStack Router conventions (e.g.,servers.$serverId.tsxfor/servers/:serverId). - Export
Routefrom@tanstack/react-routerwith path, component, and loader/error boundaries as needed. - Run
npx tsr generateor restartnpm run devto regeneratesrc/routeTree.gen.ts. - Add an e2e test in
e2e/if the route is user-facing.
Fetching data
- Use TanStack Query hooks for server state. Define queries in feature-specific hooks under
src/hooks/rather than inlining them in components. - Keep query keys stable and predictable; include IDs and filter parameters.
- Handle loading and error states explicitly; use skeletons or error boundaries instead of silent failures.
Role-based access control (RBAC)
The frontend mirrors the backend permission model for UX purposes only. The backend remains the ultimate authority on every request.
- Permission constants:
src/stores/auth-store.tsexportsPERMISSIONS. These strings must matchapp/core/permissions.pyin the backend exactly. - Checking permissions: use
useAuthStoreselectors or the helpers it exposes:hasPermission(permission)— single permission.hasAnyPermission([perm1, perm2])— any of the listed permissions.hasAllPermissions([perm1, perm2])— all listed permissions.
- Page guards:
usePageGuardredirects unauthorized users. Use it at the top of route components:
import { usePageGuard } from '../hooks/use-page-guard'
import { PERMISSIONS, useAuthStore } from '../stores/auth-store'
function AdminCreditsPage() {
const allowed = usePageGuard({ permission: PERMISSIONS.CREDITS_READ_ALL })
const canGrant = useAuthStore((state) => state.hasPermission(PERMISSIONS.CREDITS_GRANT))
if (!allowed) return null
return <div>{canGrant && <GrantCreditsButton />}</div>
}
- Route links: in
admin.index.tsxand other navigation lists, userequiredPermissionto hide links the user cannot access.
Adding or changing permissions
- Add the constant to
PERMISSIONSinsrc/stores/auth-store.ts. - Update the role fallback mapping in
checkPermissionif the permission should be derivable from a role when the backend has not sent an explicit permissions list. - Update the admin permissions matrix UI (
src/routes/admin.permissions.tsx) if the permission belongs to a new or existing category. - Hide guarded UI behind
useAuthStorepermission checks; always expect the backend to enforce the same restriction.
Important rules
- Client-side permission checks are for UX only. Never trust the frontend to enforce authorization — the backend must validate every request.
- Keep
PERMISSIONSin sync with the backendPermissionclass. A mismatch will cause false denials or hidden features. - Do not hard-code role names like
role === 'admin'for access decisions unless the semantic truly depends on role identity. Prefer permission checks so dynamic role overrides continue to work.
State management
- Prefer URL state for page-level filters and selections.
- Use Zustand for global UI state that should survive navigation (e.g., sidebar collapse, theme).
- Avoid prop drilling more than two levels deep; use context or Zustand instead.
WebSocket
- One shared connection per app, owned by
src/contexts/websocket-provider.tsx(useWebSocket); consumers useuseSharedWebSocketand room scopes (global,server:<id>,user:<id>). - The connection lifecycle is self-healing: it reconnects on
visibilitychange/online, forces a reconnect when a socket looks open but has been silent past the zombie threshold, and refreshes an expired token on auth failure (4001) instead of dying permanently. Do not add per-feature reconnect logic — fix the shared hook. - Live dashboard metrics (
useDashboardMetrics) depend on theglobalroom; system metrics publish every 60s, container metrics every 5s.
Styling
- Use Tailwind CSS utility classes. Avoid arbitrary values; extend the theme in
tailwind.config.tswhen a value repeats. - Prefer
classNamecomposition withclsxortailwind-mergefor conditional classes.
UI components
- The project does not use a generic component library. Build custom components in
src/components/ui/as needed. - Reuse existing
src/components/ui/*components before writing new ones (e.g.,Tooltip,Button,Modal,Toast). - Do not rely on browser-built-in UI for product UX. For example, use the project's own
Tooltipcomponent (src/components/ui/tooltip.tsx) instead of the nativetitleattribute. - Keep components in
src/components/ui/small, accessible, and styled consistently with Tailwind. Export a clear prop interface and avoid leaking layout concerns into reusable primitives.
Forms and validation
- Use controlled inputs with React state or a form library consistent with the project.
- Validate user input before submission; display field-level errors returned by the backend.
- Token scope options in
src/components/settings/tokens-page.tsx(AVAILABLE_SCOPES) must matchVALID_TOKEN_SCOPESinbackend/app/api/tokens.py; the backend rejects unknown scopes with 422.
Tests
npm run testruns Playwright e2e tests. Write e2e specs for critical user flows.npm run test:unitruns Vitest unit tests (colocated*.test.tsundersrc/). Add unit tests for pure helpers insrc/lib/andsrc/hooks/.
Build and service worker
npm run buildproducesdist/. The service worker is generated and cache injection runs automatically.- Edit
public/sw.js.tplfor service worker behavior;public/sw.jsis generated byscripts/inject-sw-cache.cjsduring build. Do not manually edit generated files indist/or.tanstack/. - Push notification helpers live in
src/lib/register-sw.ts. Enable dev registration withVITE_ENABLE_PUSH_IN_DEV=true. - Route-level code splitting is enabled via the TanStack Router Vite plugin (
autoCodeSplitting: true). Do not setenableRouteGeneration: false: the code splitter only processes files the generator registers, so disabling generation silently produces a single monolithic bundle. src/routeTree.gen.tsis regenerated on every dev/build and is excluded from Prettier; the generator's formatting is authoritative for that file.
Common pitfalls
nukelabctl test frontenddoes not accept path passthrough; scope tests directly viacd frontend && npm run test -- path/to/file.spec.ts.useConfirmDialog'scustomContentis stored as a one-time element snapshot: parent-state-driven props inside it never update. Interactive fields must be self-contained components that own their state and report values through a stable ref (seeBlockRequestFieldsinsrc/components/admin/credits/users-tab.tsx).- Do not import route files directly; rely on the generated route tree.
- Keep environment-specific values in
.env.*files, not hard-coded in source.
Verification
cd frontend
npm run lint
npm run format:check
npm run test
Child NAD Index
- None