Imported from ndleyton/pe-be (
AGENTS.md). Install upstream withnpx skills add ndleyton/pe-be. Copyright stays with the author.
Development Guidelines
These instructions are for this repository.
- The Python backend lives in
backend/. - The React frontend lives in
pe-be-tracker-frontend/. - Run commands from the relevant subdirectory unless noted otherwise.
Backend Commands
Tests
- Full test suite:
cd backend && uv run pytest - Single test file during iteration:
cd backend && uv run pytest --no-cov tests/test_file.py
Notes:
backend/pytest.inienforces coverage for the full suite with--cov=srcand--cov-fail-under=80.- Focused runs against a single file should usually use
--no-cov, otherwise the global 80% coverage gate will still apply. - Tests load
ENV_FILEif set, otherwisebackend/.env.test. - Test safety checks require
DATABASE_URLto point to a dedicated test database whose name containstest. - Do not call the real
asyncio.run(...)from tests. This repo uses session-scoped pytest-asyncio event loops, and a realasyncio.run(...)inside a test can close the current loop and break later async tests withRuntimeError: There is no current event loop in thread 'MainThread'. - For CLI
main()wrappers that useasyncio.run(...), test the asyncrun(...)function directly for behavior. When testingmain(), monkeypatch the module-localasyncio.runand return a fake result after closing the passed coroutine. - If you add a new sync wrapper around async backend code, follow the existing job-wrapper test pattern instead of exercising the real event-loop boundary inside pytest.
- When renaming or replacing CRUD/service helpers, update import/smoke tests in the same change so the suite does not fail during collection on stale symbols.
Linting
- Run linting:
cd backend && uv run ruff check . - Auto-fix linting issues:
cd backend && uv run ruff check . --fix
Type Checking
- There is no repo-standard mypy setup wired into
backend/pyproject.tomlyet. - Do not treat
uvx mypy srcas a required pre-PR gate unless the typing setup is intentionally being worked on.
Database / Alembic
- Run migrations:
cd backend && uv run alembic upgrade head - Create new migration:
cd backend && uv run alembic revision --autogenerate -m "description" - Downgrade one revision:
cd backend && uv run alembic downgrade -1 - Check migration status:
cd backend && uv run alembic current
Notes:
- Alembic commands require
DATABASE_URLto be set, or a populatedbackend/.env. - Alembic loads models from
backend/srcviabackend/alembic/env.py.
Frontend Commands
- Install dependencies:
cd pe-be-tracker-frontend && corepack enable && pnpm install - Start dev server:
cd pe-be-tracker-frontend && pnpm run dev - Run linting:
cd pe-be-tracker-frontend && pnpm run lint - Run type checking:
cd pe-be-tracker-frontend && pnpm run typecheck - Run unit tests:
cd pe-be-tracker-frontend && pnpm test - Run coverage:
cd pe-be-tracker-frontend && pnpm run test:coverage - Run Playwright E2E:
cd pe-be-tracker-frontend && pnpm run test:e2e
Notes:
- Frontend scripts assume
node_modulesis installed first. VITE_API_BASE_URLis required.- Outside test mode, PostHog env vars are also required:
VITE_PUBLIC_POSTHOG_KEYandVITE_PUBLIC_POSTHOG_HOST.
Architecture Notes
Deployment
-
The frontend auto-deploys through Render's git integration when changes merge to
main. -
Backend releases use the manually dispatched
.github/workflows/deploy-vps.ymlduring supervised maintenance windows. Container recreation can briefly interrupt requests; the GitHub runner remains active while the remote deployment runs. -
Deploy additive backend/API/schema changes before frontend changes that depend on them. Remove obsolete behavior only after callers have migrated; frontend and backend releases are independent.
-
Deployment must pass database readiness at
/health/readythrough origin HTTPS and/api/v1/health/readythrough the frontend proxy./healthremains a database-independent liveness probe. -
Production frontend traffic is served from a static host at
app.example.com. -
Production backend and PostgreSQL run on a VPS behind that public host.
-
The public browser-facing API remains
https://app.example.com/api/...via a frontend-side rewrite/proxy to the VPS origin. -
The current backend origin hostname is
origin-api.example.com. -
When changing backend config, auth redirects, cookie behavior, or API routing, preserve the
app.example.com/api/...public contract unless the task explicitly changes the deployment model. -
Production recurring jobs are scheduled outside Docker Compose via host
systemdtimers, not by the FastAPI app and not bydocker compose up. -
A manual VPS redeploy such as
git pullplusdocker compose -f docker-compose.prod.yml ...does not install, reload, or enablesystemdunits. If a task changes scheduled jobs or depends on them existing, update/etc/systemd/system/, runsystemctl daemon-reload, and verify the timer/service on the host.
Backend
- The backend uses feature slices under
backend/src, includingusers,workouts,exercises,exercise_sets,routines,chat,admin, andhealth. - The API mounts under
/api/v1by default. - User-facing routines are implemented with the
Routine,ExerciseTemplate, andSetTemplatemodels. The legacy backing table remainsrecipes, andexercise_templates.recipe_idremains a legacy column name. - AI-related backend code currently uses
langchain-google-genaiandlangfuse; do not assumeopenaiis the only active integration. - Post-Workout AI Recap: The backend features an automated, evidence-based coaching recap triggered asynchronously upon workout completion.
- Service:
src/workouts/recap.py(WorkoutRecapService) handles metric gathering and Gemini generation. - Data Grounding: The recap is grounded in deterministic metrics (sets, volume, PR detection) and incorporates qualitative feedback from workout/exercise/set notes.
- Trigger: Triggered via
POST /api/v1/workouts/{workout_id}/recapfrom theFinishWorkoutModalin the frontend.
- Service:
- Standalone backend CLIs and scheduled jobs do not get FastAPI app startup imports for free. Before the first ORM query, ensure the SQLAlchemy model registry is loaded so string-based relationships like
"User"and"Workout"resolve correctly.
Frontend
- The frontend uses React 19, TypeScript, Vite, React Router v7, TanStack Query, Tailwind CSS v4, and Zustand.
- Guest/local-first state is managed by a persisted Zustand store in
pe-be-tracker-frontend/src/stores/useGuestStore.ts, not a React context. - That guest-store persistence uses IndexedDB first, with localStorage fallback, via
pe-be-tracker-frontend/src/stores/indexedDBStorage.ts. - Guest-to-authenticated sync logic lives in
pe-be-tracker-frontend/src/utils/syncGuestData.ts. - App-wide providers are configured in
pe-be-tracker-frontend/src/app/providers/AppProviders.tsx. - Prefer thin route/page components. Move feature-specific behavior into feature hooks under
src/features/<feature>/hooks, keep API calls inapi, pure mapping/state helpers inlib, and rendering-heavy sections in smaller components. - When a component mixes rendering with guest/auth branching, optimistic writes, debounced persistence, or nested editor state, treat that as a refactor signal. Split transport/workflow concerns from JSX instead of growing the component further.
- For guest vs authenticated flows, prefer a single hook or adapter boundary that hides the branching from the presentational component.
- If a hook or helper depends on nested server shapes, prefer shared fixtures in
pe-be-tracker-frontend/src/test/fixtures/over large inline objects. Use server-style fixtures for authenticated flows and guest fixtures for local-first flows. - For debounced hook tests, be careful combining fake timers with
waitFor; prefer advancing timers insideact(...)and asserting directly on the resulting state or mock calls. - Preserve progressive rendering. Thin pages should still render stable shells and section-level placeholders/skeletons when possible; avoid replacing an otherwise usable screen with a single full-page "Loading..." state unless the entire route is blocked on first load.
API Conventions
- Prefer frontend endpoint constants in
pe-be-tracker-frontend/src/shared/api/endpoints.tsinstead of hardcoding paths. - Preserve trailing slashes on collection endpoints used by the frontend client to avoid FastAPI
307redirects onPOST. - Do not expose new user-facing API/UI copy as "recipes" unless you are intentionally referring to the backend model/table names. Use "routines" in the product surface.
Backend Performance
- For detail endpoints that return a single entity plus a small fixed relationship graph, prefer
joinedloadover nestedselectinloadto avoid N+1-style multi-query fetches. - For collection endpoints, choose
selectinloadvsjoinedloaddeliberately based on expected row fanout; do not default blindly to one strategy. - When investigating latency, instrument handler-local phases explicitly, especially DB fetch, schema serialization, and any ORM-to-response mapping that would otherwise be hidden inside framework overhead.
- Backend database pooling is configurable through
DATABASE_POOL_PRE_PING,DATABASE_POOL_USE_LIFO,DATABASE_POOL_SIZE,DATABASE_MAX_OVERFLOW,DATABASE_POOL_TIMEOUT, andDATABASE_POOL_RECYCLE; check those settings before assuming per-request connection setup is an application bug.
Useful current route examples:
- Routines API:
/api/v1/routines/ - Workouts list API:
/api/v1/workouts/mine - Workout types API:
/api/v1/workouts/workout-types/ - Exercise types API:
/api/v1/exercises/exercise-types/ - Exercise sets for an exercise:
/api/v1/exercise-sets/exercise/{exercise_id} - Auth session probe:
/api/v1/auth/session
Migration Guidance
Prefer defensive migrations for schema changes that may hit drifted environments, especially when altering or dropping existing objects.
Preferred Patterns
-
Guard column adds and drops with schema inspection.
connection = op.get_bind() inspector = sa.inspect(connection) columns = [col["name"] for col in inspector.get_columns("table_name")] if "column_name" not in columns: op.add_column("table_name", sa.Column("column_name", sa.String(255))) if "column_name" in columns: op.drop_column("table_name", "column_name") -
Guard table drops and creates with inspector checks.
connection = op.get_bind() inspector = sa.inspect(connection) if "table_name" in inspector.get_table_names(): op.drop_table("table_name") -
For Postgres-specific index operations,
IF EXISTS/IF NOT EXISTSviaop.execute(...)is acceptable.op.execute("CREATE INDEX IF NOT EXISTS idx_name ON table_name (column_name)") op.execute("DROP INDEX IF EXISTS idx_name")
Avoid
- Blind destructive operations against existing tables or columns when a simple existence check would make the migration safer.
- Raw SQL for everything by default when Alembic operations plus a guard are clearer.
Validation
- Test migrations against a realistic copy of data when the change is non-trivial.
- Verify both upgrade and downgrade paths when a downgrade is expected to remain usable.
Workflow
- Run
ruffand the relevant backend tests before handoff. - Use focused tests during iteration, then run broader coverage before finalizing backend changes.
- For frontend work, run the relevant pnpm checks from
pe-be-tracker-frontend/. - When extracting significant frontend logic into custom hooks, add dedicated hook tests for the new behavior instead of relying only on page/component tests.
- If a refactor introduces new reusable frontend test data shapes, add them to
pe-be-tracker-frontend/src/test/fixtures/and reuse them instead of copying nested objects between tests. - When refactoring frontend pages, preserve or improve partial rendering behavior. Prefer section loading states and skeletons over coarse route-level loading gates.
- Before merging a feature branch, rebase it onto the current
origin/maininstead of mergingmaininto the branch:git fetch origin git switch my-branch git rebase origin/main - Keep database migrations defensive and easy to reason about.
- Keep changes focused; avoid mixing unrelated work in one PR.
- For backend jobs, scripts, or one-off CLIs, verify the real entrypoint path and not just mocked unit tests. Prefer at least one manual
python -m ...ordocker compose -f docker-compose.prod.yml run --rm ...validation when the change affects standalone execution.