Skip to main content

AMS Frontend

React admin dashboard — Vite, TypeScript, TanStack Router, TanStack Query, Shadcn UI, Tailwind, Zustand. GraphQL via OIDC auth (Keycloak).

Key Commands

  • Dev: pnpm run dev (port 5173) — kill all other Vite processes first: ps aux | grep vitekill -9 <PIDs>
  • Build: pnpm run build | Lint: pnpm run lint | Format: pnpm run format
  • Codegen: npx graphql-codegen (after schema changes → updates src/graphql/generated/)

Build errors vs. warnings: See docu/docs/ai/claude/rules/frontend-build.md for what must be fixed, what to ignore (chunk size warnings), known intentional as any patterns, and how to confirm a clean build.

Project Conventions

  • Imports: @/ alias for src/
  • GraphQL: TypedDocumentString from generated types, OIDC token from useAuth().user?.access_token
  • Queries: useGraphQLQuery + centralized keys from src/lib/query-keys.ts
  • Mutations: useGraphQLMutationNew with selector
  • Auth: RequireAuth wraps all routes; authenticated routes under routes/_authenticated/
  • Workspaces: useWorkspace() context for active workspace
  • Tables: Follow src/features/members/ and src/features/applicants/ for CRUD patterns
  • Routing: getRouteApi('/path/').useParams() — no path duplication
  • i18n: src/lib/i18n/translations/ (en.ts, ja.ts, de.ts), use t() hook — all user-facing text must be translated
  • Styling: Tailwind + custom theme variables in src/styles/theme.css
  • Error handling: src/lib/handle-server-error.ts, 401 redirects to sign-in

Layout & UI

  • Page headers: h2 text-2xl font-bold tracking-tight, layout flex flex-wrap items-end justify-between gap-2
  • Full-width pages: <Main fixed fluid>
  • Sidebar layouts: skipMainWrapper={true} in DataTableSimplePage to avoid double Main wrapper
  • ContentSection: lg:max-w-4xl | Table pages: fluid={true} in DataTableSimplePage
  • Sidebar structure: flex flex-1 flex-col space-y-2 overflow-hidden md:space-y-2 lg:flex-row lg:space-y-0 lg:space-x-12

GraphQL & Query Keys

// Centralized query keys
queryKeys.bootstrap() // ['bootstrap']
queryKeys.workspaces() // ['workspaces']
queryKeys.team(teamId) // ['teams', teamId]

// Mutation invalidation pattern
onSuccess: (data, variables) => {
handleServerSuccess(variables.input, data, t('success.message'), [
queryKeys.bootstrap(),
queryKeys.userAppearance()
])
}

Bootstrap & Workspace Management

App Start → useBootstrap → useWorkspaces → setWorkspaces → loadTeams (org only) → selectActive → App Ready
User Switch → WorkspaceSwitcher → setActiveWorkspace → clearTeams → reloadTeams (org only) → UI Updates
  • useBootstrap: one-time init + store orchestration
  • requireActiveWorkspace: type-safe access to guaranteed non-null workspace

Workspace resolution from URL (use-bootstrap.ts):

  1. /o/.../w/{slug}/... → match by workspace slug
  2. /o/{orgSlug}/... (no /w/) → match by entitySlug — covers org-level admin paths (settings, users, teams, etc.). Without this, refreshing an admin page redirects to the personal workspace.
  3. /p/{slug}/... → match by workspace slug

Never remove step 2. It is what makes page refresh work on all org-level paths that have no /w/ segment.

Always read docu/docs/domain/navigation.md before modifying sidebar structure, URLs, or groups.

Key facts:

  • Three sidebar types: Staff (org STAFF/MIXED), Client (org CLIENT), Personal
  • Staff sidebar has a Workspace / Admin context toggle — nav groups are tagged context: 'workspace' | 'admin' | 'always'
  • Configuration group (Documents, Templates, Roles & Permissions) is flat — do not restore the old "Application Setup" collapsible
  • Personal sidebar: section 1 = "Overview" (sidebar.general), section 2 = "Account" (user.sidebar.sectionTitle)
  • Personal billing uses getUserBillingNavItems() — triggered by /settings/billing or /settings/subscription. getUserSettingsNavItems() does not include billing/subscription items
  • Source of truth: src/components/layout/data/sidebar-data.tsx

Routing

// Standard pattern
import { getRouteApi } from '@tanstack/react-router'
const route = getRouteApi('/_authenticated/w/$workspaceSlug/teams/$teamSlug/members/')
export function Members() {
const { workspaceSlug, teamSlug } = route.useParams()
}

Route folder structure:

src/routes/_authenticated/
├── p/$workspaceSlug/ # Personal workspace
└── o/$workspaceSlug/ # Organization workspace
├── members/
├── applicants/
│ ├── index.tsx
│ └── $applicantId/
│ ├── index.tsx
│ ├── route.tsx
│ └── applications/index.tsx
├── settings/
└── teams/$teamSlug/
├── index.tsx
├── members/
└── route.tsx
  • Workspace slugs: unique within a user
  • Team slugs: unique within a workspace
  • URL: /p/$workspaceSlug/... (personal) or /o/$orgSlug/... (organization)

Specialist Guides

TaskAgent/Rule file
Notification systemrules/notifications.md
CRUD table / listing featureagents/frontend-table-feature.md
Sidebar navigation (settings-style)agents/frontend-sidebar-feature.md
Context providers / shared dataagents/frontend-context-patterns.md
Application detail pageagents/application-detail.md
Full CRUD feature (listing + detail)agents/complete-feature.md
Listing-only tableagents/simple-table.md
Multi-step wizard formagents/create-application-process.md
Viewport-locked split panelagents/frontend-split-panel.md
Messages inboxagents/messages-feature.md
Template statuses / ReactFlow workflow.claude/agents/workflow-statuses-feature.md