# CLAUDE.md This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. ## Commands ### Backend (ASP.NET Core) ```bash # Run API (must listen on port 5000 for the Vite proxy to work) cd src/Budget.Api ASPNETCORE_URLS=http://localhost:5000 dotnet run # Build dotnet build src/Budget.Api/Budget.Api.csproj # Add an EF Core migration dotnet ef migrations add --project src/Budget.Infrastructure --startup-project src/Budget.Api # Remove last migration (before it has been applied) dotnet ef migrations remove --project src/Budget.Infrastructure --startup-project src/Budget.Api ``` ### Frontend (Vite/React) ```bash cd src/Budget.Client npm run dev # dev server at http://localhost:5173 npm run build # tsc + vite build (what CI and Docker use) npm run lint # eslint ``` ### Docker ```powershell .\deploy.ps1 # builds image, pushes to docker.stwaddle.com/budget:latest, and deploys to prod via SSH ``` ## Architecture The app is a single Docker container: the .NET API serves the compiled React SPA as static files and also handles all `/api` requests. In development the two processes run separately; Vite proxies `/api` to `http://localhost:5000`. ### Project layout ``` src/ Budget.Core/ # Domain models, DTOs, FrequencyCalculator — no EF, no ASP.NET Budget.Infrastructure/# EF Core DbContext, migrations, BudgetAuthorizationService Budget.Api/ # ASP.NET Core controllers, middleware, Program.cs Budget.Client/ # Vite/React SPA ``` ### Data model - **Budget** (owner, name) → owns **Incomes**, **Outgos**, **BudgetShares** - All entities use **soft delete** (`IsDeleted` / `DeletedAt`) with a global EF query filter — hard deletes are not used - **Budget** uses PostgreSQL's `xmin` shadow property as an EF concurrency token - **BudgetShare** links a budget to another user by email; `IsPending = true` until the target user logs in (resolved by `KnownUserMiddleware`) - **Frequency** is a C# enum (`Monthly`, `Biweekly`, `Annually`, etc.); `FrequencyCalculator` converts any frequency to monthly/annual equivalents - The 50/30/20 rule is hardcoded in `SummaryController`: Need=50%, Want=30%, Save=20% ### API conventions - All controllers require `[Authorize(Roles = "admin,user")]` — tokens that lack a `role` claim matching either value get a 403 - `RoleClaimType = "role"` and `NameClaimType = "sub"` are set in JWT options; role checks work with `User.IsInRole()` - `BudgetAuthorizationService` centralises read/write/owner checks against the budget's owner and any accepted shares — always call `CanReadAsync` / `CanWriteAsync` / `IsOwnerAsync` before touching budget data - The `TryGetUserId` helper pattern (returns an `IActionResult?` error or out-params the user id) is used consistently across all controllers - **Enum serialisation**: `JsonStringEnumConverter` is registered globally, so all C# enums are sent/received as strings (e.g. `"Monthly"`, not `5`) - Write endpoints use the `"writes"` rate-limit policy (30 req/min per user); reads use the global limiter (120 req/min) - EF migrations run automatically via `MigrateAsync()` at startup ### Frontend conventions - **All server state** goes through TanStack Query (`src/api/`). Query keys follow the pattern `['budgets']`, `['budgets', id]`, `['budgets', id, 'summary']`, etc. - **All forms** use `react-hook-form` + `zod` schemas defined in `src/schemas/index.ts`. Never use uncontrolled inputs outside of RHF. - **CSS design system** lives entirely in `src/index.css` as CSS custom properties — no Tailwind, no CSS modules. Use the existing variables (`--color-primary`, `--green-*`, etc.) rather than hardcoded hex values. `App.css` is intentionally near-empty. - **Icons** come from `lucide-react` (tree-shaken per-import). Use `btn-icon` class for icon-only buttons; always add a `title` attribute for accessibility. - **Inline table editing**: rows show read-only data; clicking a cell enters edit mode in-place using a `useState(editing)` toggle. New rows use a separate input row appended to `` (not a modal). - **Sorting**: `SortableHeader` component and `SortState` type in `src/components/SortableHeader.tsx`. Sorting is client-side only and derives a view from the drag-ordered `displayItems` array — clearing sort restores drag order. Drag handles are disabled while a sort is active. - **Frequency calculations** for real-time previews are in `src/utils/frequency.ts`, mirroring the C# `FrequencyCalculator` exactly. - **Drag-and-drop ordering**: `@dnd-kit` is used on income/outgo rows. Row order is persisted to the server. Sort handles are hidden while a column sort is active. - **Auth**: `react-oidc-context` wraps the whole app; `AuthGuard` protects all budget routes; `TokenSync` wires the OIDC access token into the API client on every auth state change. After OIDC callback, navigation uses `window.location.replace('/')` (not `history.replaceState`) so React Router picks up the URL change. ### Local development environment Required env vars when running the API locally (set in `appsettings.Development.json` or shell): - `ConnectionStrings__DefaultConnection` — Postgres connection string - `Oidc__Audience` — `budget_api` - `OTEL_EXPORTER_OTLP_ENDPOINT` — optional; omit or set to skip telemetry export ### Auth / OIDC - Authority: `https://auth.stwaddle.com/` (OpenIddict-based, at `src.Auth.Server` in a sibling repo) - API audience claim: `budget_api` (underscore — must match `Oidc__Audience` env var) - Scope: `budget_api` (underscore, not hyphen) - Roles `admin` and `user` are granted per-user on the client registration in the auth server admin panel. A user with no role gets `access_denied` at the authorize endpoint. - Frontend OIDC config is baked into the build via `VITE_*` env vars; override locally in `src/Budget.Client/.env.local`