This file provides guidance to AI coding agents (Codex, Cursor, Claude, etc.) when working with code in this repository.
pnpm install # Install dependencies (uses pnpm, not yarn!)
pnpm dev # Start development server (default port 5173)
pnpm build # Build for production (tsc + vite build)
pnpm lint # Run ESLint
pnpm format # Format code with Prettier
pnpm test:unit # Run unit tests (Vitest)
pnpm preview # Preview the production build locally
pnpm generate:types # Generate OpenAPI types: pnpm generate:types -- <base-url>By default, API requests are relative (/api/v1/...). In development, the Vite dev server proxies /api requests to VITE_BACKEND_URL (default http://localhost:8237) so frontend and backend appear on the same origin. For cross-origin deployments, set VITE_API_BASE_URL to an absolute backend origin (for example https://backend.test.com) and ensure backend CORS/cookie settings allow credentialed requests from the frontend origin.
- Framework: Vite 7 + React 19 (SPA)
- Routing: @tanstack/react-router (file-based with auto code-splitting)
- Server state: @tanstack/react-query
- Forms: React Hook Form + Zod validation
- Styling: Tailwind CSS v4 (via
@tailwindcss/viteplugin) - UI primitives: Base UI (
@base-ui/react) + class-variance-authority, with shadcn-style config conventions (components.json) - Icons: @untitledui/icons
- Notifications: Sonner + next-themes
- Type safety: TypeScript (strict mode) with generated types from ZenML OpenAPI spec
- Compiler: React Compiler (via Babel plugin)
- Testing: Vitest
- Pre-commit: Lefthook (ESLint auto-fix + Prettier on staged files)
- The intended module-based structure lives under
src/modules/<module>. - Each module should be split by responsibility using these layers:
- domain/ — module-owned types and actual API/request functions
- business-logic/ — TanStack Query definitions and other module-specific orchestration/business logic
- feature/ — stateful containers, provider composition, and feature entrypoints
- util/ — small module-scoped utilities
- ui/ — stateless presentational components
- Keep modules split by layer from the start rather than growing a flat module and reorganizing later.
src/routes/*— file-based TanStack Router route definitions; these containbeforeLoadlogic for data preloading and redirects, plus page metadatasrc/shared/api/domain/*— transport layer:apiClient, endpoint path constants,FetchErrorclasssrc/shared/api/utils/*— URL builders, querystring helpers, error response handlingsrc/shared/api/types.ts— generated OpenAPI types (do not hand-edit)src/shared/api/*must not import router concerns (notFound, route context, etc.); keep router-aware helpers insrc/shared/router/or module layerssrc/shared/router/utils/*— shared router helpers (e.g.ensureQueryDataOr404)src/shared/ui/*— reusable UI primitives built on Base UI; these are the shadcn-managed surface referenced bycomponents.jsonsrc/shared/utils/*— shared utilities (styles.tsforcn(),build-page-titles.ts)- Root Module bootstrap code (
queryClient, root providers) belongs insrc/modules/root/ - Root Module global resources (server info, session, config) belong in
src/modules/root/domain/* - Assets (icons/images) live in
src/assetsand can be imported as React components via SVGR
Two files are auto-generated and excluded from ESLint. Do not hand-edit them:
src/routeTree.gen.ts— generated by the TanStack Router Vite plugin fromsrc/routes/src/shared/api/types.ts— generated bypnpm generate:typesfrom the ZenML OpenAPI spec
- Prefer composition over inheritance
- Keep components focused; lift state only as needed
- Use component variants (via
cva) for styling variations rather than inline conditionals - Prefer writing Tailwind classes in the
uilayer, but feature/layout shells may use them when intentional - Avoid duplicating code or inventing hyper-generic abstractions: inspect existing flows before writing new components or helpers
- Prefer focused components over catch-all versions; duplicating two purposeful components is often clearer than a single complex abstraction
- Reference existing implementations for similar features
All API interactions follow a consistent pattern. Request definitions belong to the owning module, not generic shared folders.
Request functions — define actual read/write API functions in src/modules/<module>/domain/*.
- Keep these functions focused on transport and response parsing.
- Do not export custom React Query fetcher helpers for reads when
queryOptions(...)can express the query API directly.
Queries — define TanStack Query keys and query collections in src/modules/<module>/business-logic/*.
- Export grouped key factories such as
xQueryKeys. - Export grouped query factories such as
xQueries. - Build read APIs with
queryOptions(...)orinfiniteQueryOptions(...)so they are reusable from both route loaders and components.
Mutations — define and export mutation hooks from src/modules/<module>/business-logic/*.
- Keep the underlying write request function in
domain/. - Wrap
useMutation(...)in a module hook (e.g.useVerifyDevice,useLoginUser). - Return the mutation result plus domain-named aliases for
mutate/mutateAsync(e.g.verifyDevice,verifyDeviceAsync) for ergonomic usage in features. - Do not use
mutationOptions(...)for module mutations.
Components should consume the exported module mutation hooks directly.
Cross-module orchestration — when a mutation needs to chain multiple domain actions (e.g. activate server then login), define that orchestration in the owning module's feature/ layer.
Example query:
// src/modules/device/business-logic/device-queries.ts
import { queryOptions } from "@tanstack/react-query";
import { fetchDevice } from "@/modules/device/domain/fetch-device";
import type { DeviceQueryParams } from "@/modules/device/domain/device-query-params";
export const deviceQueryKeys = {
all: ["device"] as const,
detail: (deviceId: string) => [...deviceQueryKeys.all, deviceId] as const,
};
export const deviceQueries = {
detail: (deviceId: string, queryParams: DeviceQueryParams = {}) =>
queryOptions({
queryKey: [...deviceQueryKeys.detail(deviceId), queryParams],
queryFn: () => fetchDevice(deviceId, queryParams),
}),
};Current data loading flow:
- Route
beforeLoadhandlers callcontext.queryClient.ensureQueryData(...)to preload data - Components use module mutation hooks for write operations
- Global 401 handling:
QueryCache.onErrorinquery-client.tsredirects to/login?next=...onFetchErrorwith status 401 - Mutation errors are handled locally in the form/container that triggered them
The codebase uses @/* as an alias for src/*:
import { apiClient } from "@/shared/api/domain/api-client";
import { deviceQueries } from "@/modules/device/business-logic/device-queries";Configured in both tsconfig.json and vite.config.ts (via vite-tsconfig-paths).
Forms use React Hook Form + Zod:
- Define a Zod schema in
domain/<name>-schema.ts - Use
zodResolver(schema)withuseForm(...)in the form container - Use
Controllerfor controlled field components - Wire submission to the module's exported mutation hook
- Show errors via toast notifications (Sonner)
See ServerActivationFormContainer.tsx and LoginFormContainer.tsx for current examples in the intended naming scheme.
- Reusable UI primitives live in
src/shared/ui/*and are built on Base UI withcvafor variants src/shared/ui/*andsrc/shared/utils/styles.tsare shadcn-managed surfaces referenced bycomponents.json; avoid refactoring them unless explicitly requested- Icons and illustrations live in
src/assetsand can be imported as React components via SVGR - Keep Tailwind utility classes; Prettier (with the Tailwind plugin) auto-sorts them
- Prefer focused components over overly generic abstractions
See DESIGN.md for design-related guidelines.
- Define React components with
functiondeclarations instead of arrow functions - Stick to strict typing: no
any, prefertypealiases, and colocate types near usage - No type casting
- Use PascalCase for React component and context files (e.g.
Dashboard.tsx,DashboardContainer.tsx,AuthContext.tsx) - Use kebab-case for hooks, utilities, API calls, and domain-layer files (e.g.
use-pipeline.tsx,api-client.ts,fetch-device.ts) - Exception: route files should follow TanStack Router naming requirements when those differ (e.g. pathless/layout route conventions)
- define optional props/params like this:
selectedId?: stringinstead ofselectedId: string | nullorselectedId: string | undefined
apiClientsendscredentials: "include"and targets/api/v1/...by default- Optional override: set
VITE_API_BASE_URLto send requests to<origin>/api/v1/... - Default headers:
Content-Type: application/json,Source-Context: dashboard-v2 - Login is a special case that overrides content type to
application/x-www-form-urlencoded - Vite proxies
/apitoVITE_BACKEND_URLin development - Non-OK responses throw
FetchError(seethrow-fetch-error-from-response.ts)
- Place all icons/images in
src/assetsand import them as React components via SVGR; reuse existing assets before adding new ones
Before committing or pushing a PR, check whether README.md and AGENTS.md need updating to reflect your changes. Common triggers:
- New or renamed folders, modules, or routes
- Added/removed dependencies or scripts
- Changes to the data fetching, auth, or networking patterns
- New generated files or build steps
Keep both files accurate — stale docs erode trust faster than missing docs.
- Use plain, descriptive titles without conventional commit prefixes (no
feat:,fix:,ci:, etc.) - Good: "Add workflow to require release label on PRs"
- Bad: "ci: add workflow to require release label on PRs"
GitHub Actions (.github/workflows/build-validation.yml) runs on push to main and on all PRs:
pnpm install --frozen-lockfilepnpm lintpnpm buildpnpm test:unit