Fitness event tracking app. Athletes upload race results (CSV/JSON), view personal bests, and chat with an AI coach.
| Layer | Tech |
|---|---|
| API | .NET 10, ASP.NET Core (Slim), EF Core 10, PostgreSQL 17 |
| Auth | ASP.NET Identity + JWT Bearer |
| Mediator | Mediator (source-gen) + FluentValidation pipeline |
| Frontend | React 19, Vite, TypeScript, TanStack Query v5, React Router v7, Tailwind v4 |
| AI | Anthropic SDK (claude-haiku-4-5-20251001) via IChatToolHandler pattern |
| Service | URL |
|---|---|
| API (dev) | http://localhost:5291 |
| Frontend (dev) | http://localhost:5173 |
| Nginx proxy | http://localhost:8080 → /apis/pacevite/ → API |
| PostgreSQL | localhost:5432 |
Prerequisite: Podman installed (project uses podman, not docker).
# 1. Start the database
podman compose up -d db
# 2. Start the API (auto-migrates DB on first run)
dotnet run --project src/Pacevite.Api --launch-profile http
# 3. Start the frontend
cd src/Pacevite.Web && npm run devCredentials for the dev DB are in appsettings.Development.json (committed — dev only).
Required environment variables in production (dev uses appsettings.Development.json):
| Variable | Purpose |
|---|---|
ANTHROPIC_API_KEY |
Claude API key for AI coaching endpoint |
DB_USER / DB_PASSWORD |
PostgreSQL credentials |
JWT_SECRET |
HMAC signing key (min 32 chars) |
JWT_ISSUER / JWT_AUDIENCE |
JWT validation values |
Dev credentials are committed in appsettings.Development.json — never use in production.
# .NET unit + integration tests (Testcontainers spins up real Postgres)
# NOTE: TUnit uses Microsoft.Testing.Platform — dotnet test is NOT supported on .NET 10; use dotnet run
dotnet run --project tests/Pacevite.Api.Tests
# Filter by category (.NET only)
dotnet run --project tests/Pacevite.Api.Tests -- --treenode-filter "/*/*/*/*[Category=Unit]"
dotnet run --project tests/Pacevite.Api.Tests -- --treenode-filter "/*/*/*/*[Category=Integration]"
# Frontend unit tests (Vitest + Testing Library + MSW)
cd src/Pacevite.Web && npm test
# E2E tests (Playwright — auto-starts API + frontend if not running)
cd src/Pacevite.Web && npm run test:e2esrc/
Pacevite.Api/
Features/ # Vertical slices — one folder per feature
Auth/ # Register + Login endpoints
Events/ # Upload, GetEvents, GetEventById, GetPersonalBests, DeleteEvent
Infrastructure/
Auth/ # JwtTokenService
Chat/ # IChatToolHandler + ChatToolExecutor (Anthropic AI)
OpenApi/ # ForwardedPrefixTransformer for Scalar UI
Parsing/ # IEventParser (CSV + JSON implementations)
Persistence/ # AppDbContext (EF Core + Identity)
Contracts/ # Request/response DTOs — Requests/ and Responses/
Pipeline/ # ValidationBehavior (Mediator pipeline)
Domain/ # Entities, Enums
Migrations/ # EF Core migrations
Pacevite.Web/
src/
pages/ # LoginPage, RegisterPage, DashboardPage, UploadPage, EventDetailPage
components/ # AuthGuard, ThemeToggle, chart components (ProgressChart, PbPanel, SplitChart, RaceComparison)
context/ # AuthContext (JWT token in memory)
hooks/ # useAuth, useEvents, useEvent
lib/ # api.ts (fetch client), types.ts (shared TS types), chartUtils.ts
test/ # render.tsx (renderWithProviders), handlers.ts (MSW), setup.ts
tests/
Pacevite.Api.Tests/
Unit/ # Unit tests — Parsers/, Chat/, Prediction/
Integration/ # Auth + Event endpoint integration tests (WebApplicationFactory)
Each feature slice owns its own command/query, handler, validator, and endpoint registration. Do not add handler logic to Infrastructure or Pipeline layers.
Use the /add-feature skill — scaffolds the vertical slice (command, handler, validator, endpoint registration) and wires it into Program.cs.
Use the /add-parser skill — scaffolds IEventParser + unit tests + Program.cs registration.
Use the /add-chat-tool skill — scaffolds IChatToolHandler + unit tests + DI registration.
Use the /ef-migrate skill — runs the 3-step migration workflow.
Use the /seed-db skill — registers/logs in via the API and uploads sample-data/my-events.json.
Use the /gen-test skill — routing table for Unit vs Integration vs E2E.
All component tests use renderWithProviders from src/test/render.tsx:
renderWithProviders(<MyComponent />, { authenticated: true, initialEntries: ['/path'] })It wraps with QueryClientProvider + MemoryRouter + a mocked AuthContext. MSW request handlers live in src/test/handlers.ts.
podman, notdocker—docker composewill fail; usepodman compose.- Auto-migration —
Database.Migrate()runs at startup inDevelopment. No manualdotnet ef database updatestep needed locally. - Rate limiting — Auth endpoints are capped at 10 req/min in production.
appsettings.Development.jsonoverrides this to 1000 so tests don't 429. - Multiple
IEventParserregistrations — all parsers are registered asIEventParsersingletons; the upload endpoint iterates them and callsCanParse(contentType)to dispatch. Add new parsers inProgram.csalongside the existing registrations. - Nginx base path — the proxy strips
/apis/pacevite/and forwardsX-Forwarded-Prefixso Scalar's OpenAPI URLs render correctly. TheForwardedPrefixTransformerinInfrastructure/OpenApi/handles this. EventTypeandCompletionmust be uppercased — normalise in the parser, not the handler.- Mediator is source-generated — adding a new
ICommand/IQueryhandler requires a build before the handler is discoverable.
- Specs live in
docs/specs/ - Read
docs/specs/00-overview.mdbefore starting any work on this project
- Read
docs/specs/00-overview.mdbefore starting any work - Read
docs/specs/01-architecture.mdbefore making architectural changes - Read all files in
docs/decisions/before proposing architectural decisions - Add new ADRs to
docs/decisions/using MADR format when making significant decisions
- ADRs live in
docs/decisions/ - Before any architectural decision, read all existing files in
docs/decisions/ - To create a new ADR, find the highest numbered file in
docs/decisions/, increment by 1, and create a new file namedNNNN-short-title-in-kebab-case.md - Use MADR format for every ADR:
- Title — short imperative phrase
- Status — Proposed | Accepted | Deprecated | Superseded by [NNNN]
- Context and Problem Statement — why this decision is needed
- Considered Options — all options evaluated
- Decision Outcome — what was chosen and why
- Consequences — what becomes easier or harder
- If superseding an existing ADR, update its status line to
Superseded by [new NNNN]before creating the replacement
| Level | When to use |
|---|---|
| 🔴 Important | Incorrect logic, auth bypass, data exposure, breaking changes, unhandled exceptions |
| 🟡 Nit | Style, naming, minor refactors — at most 5 per review |
| 💡 Suggestion | Non-blocking improvement worth considering but not required |
If all findings are nits, open the summary with: "No blocking issues."
- Incorrect business logic that would break production behavior
- Unhandled
Taskexceptions —async voidoutside event handlers, fire-and-forget without.ContinueWithor a surroundingtry/catch .Resultor.Wait()called on aTask(deadlock risk in ASP.NET context)catch (Exception)blocks that swallow exceptions without logging or rethrowing
- Missing
[Authorize]on controller actions or endpoints that access user/tenant data - Authorization checks missing after authentication — validate claims, not just identity
- SQL built via string interpolation or concatenation (use parameterized queries / EF Core)
- Hardcoded secrets, API keys, connection strings, or credentials anywhere in the diff
- Sensitive fields (SSN, DOB, passwords, tokens) returned in API responses without masking
- User IDs, emails, or PII appearing in log statements or exception messages
- Backward-incompatible database migrations (column drops, type changes without fallback)
- EF Core N+1 queries —
.ToList()called inside a loop, or missing.Include()on related entities
- HTTP status codes mismatched to semantics (e.g.,
200 OKwith an error body) - Endpoints that return
nullwhere a proper404 NotFoundshould be used - New public endpoints missing request validation (FluentValidation or DataAnnotations)
async/await
- Flag: async void (outside event handlers)
- Flag: .Result or .Wait() on Tasks
- Flag: missing CancellationToken propagation through async call chains
- Flag: missing ConfigureAwait(false) in library/infrastructure code
HttpClient
- Flag: new HttpClient() — must use IHttpClientFactory
Nullable reference types
- Flag: ! null-forgiving operator without an explanatory comment
EF Core
- Flag: N+1 patterns (ToList inside a loop, missing Include)
- Flag: raw SQL via string interpolation
DI / Configuration
- Flag: hardcoded base URLs — use IOptions<T> or IConfiguration
- Flag: service locator pattern (IServiceProvider.GetService inside business logic)
Logging
- Flag: sensitive data (emails, IDs, tokens) in log messages
- Flag: missing structured logging — prefer Log.Information("{Key} {Value}", ...) over string concat
- New endpoints must have at minimum one integration test
- All controller inputs must be validated before use
- Outbound HTTP calls must go through registered
IHttpClientFactoryclients — no rawnew HttpClient() - Cancellation tokens must be threaded through async call chains, not dropped at the controller
- Inter-service communication must not hardcode URLs — externalize to config/environment
*Migrations/*.cs— EF Core migration files (structure, not behavior)*.Designer.cs,*.g.cs,**/obj/**,**/bin/**packages.lock.json,*.lock,**/node_modules/**- Formatting violations — enforced by
dotnet formatin CI - Build warnings and compiler errors — surfaced by CI before review
Do not flag issues in **Tests/** files for production-only patterns (e.g., DI registration style, configuration access).
Post at most 5 🟡 Nit comments per review. If more were found: append "plus N similar style items not individually listed" to the summary.
On subsequent pushes after the first full review:
- Suppress nits already raised or stylistically similar to prior feedback
- Only post 🔴 Important findings or issues newly introduced since the last reviewed commit
- Do not re-litigate resolved threads
Before posting any finding:
- Cite the exact
file:linereference - Read the implementation — do not infer behavior from method or variable naming alone
- If the bug cannot be confirmed from the diff and surrounding context, do not post it
False positives erode trust. When uncertain, use 💡 Suggestion instead.
End every review with:
## Review Summary
- 🔴 Important: <count> issue(s)
- 🟡 Nit: <count> (capped at 5)
- 💡 Suggestions: <count>
Overall: <one sentence verdict>