Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
28c5601
server - update default list value
aditya-arcot Mar 9, 2026
2829406
move admin user config loading to separate function
aditya-arcot Mar 9, 2026
aeb6ae4
client - update imports
aditya-arcot Mar 10, 2026
74ac3d2
fix client watch paths
aditya-arcot Mar 10, 2026
4093532
add docs
aditya-arcot Mar 10, 2026
c695218
fix muscle groups id sequence
aditya-arcot Mar 11, 2026
1dba4f3
server - refactor router definitions
aditya-arcot Mar 11, 2026
eac707d
server - standardize test imports
aditya-arcot Mar 11, 2026
9e8585b
server - add muscle groups endpoint
aditya-arcot Mar 11, 2026
72dea31
refactor http error handling
aditya-arcot Mar 11, 2026
7f6f3a1
server - add exercise endpoints
aditya-arcot Mar 11, 2026
8d3cb7a
server - add updated at column
aditya-arcot Mar 11, 2026
1334d45
fix unique constraint with null column
aditya-arcot Mar 11, 2026
adb6d00
server - format column defs
aditya-arcot Mar 11, 2026
bbbf010
use permanent location for generated pg admin files
aditya-arcot Mar 13, 2026
70758cd
server - add http error tests
aditya-arcot Mar 13, 2026
9c47586
server - add exercise service tests
aditya-arcot Mar 13, 2026
0a69df9
client - update deps
aditya-arcot Mar 13, 2026
1486a44
server - add exercise api tests
aditya-arcot Mar 13, 2026
6e74ba5
update overview file
aditya-arcot Mar 13, 2026
2bac6e1
fix default value
aditya-arcot Mar 13, 2026
6c20e00
verify env matches
aditya-arcot Mar 13, 2026
c4d059c
update default files value
aditya-arcot Mar 13, 2026
9f3f595
client - remove loading timeout
aditya-arcot Mar 13, 2026
1cc2bfa
Merge pull request #91 from arcot-labs/dev
aditya-arcot Mar 13, 2026
f659abc
simplify script env var handling
aditya-arcot Mar 13, 2026
e175f29
fix error message text in tests
aditya-arcot Mar 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
183 changes: 183 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# RepTrack – Copilot Instructions

RepTrack is a full-stack strength-training tracker. The client is React + Vite (TypeScript), the server is FastAPI (Python 3.14+), and the database is PostgreSQL via SQLAlchemy 2.0 + AsyncPG.

---

## Repository Layout

```
client/ React + Vite frontend
server/ FastAPI backend
config/env/ Environment files (.env.example → .env)
config/infra/ Docker Compose for dev/prod
scripts/ Dev bootstrap (dev.sh) and API generation
```

---

## Commands

### Server (`cd server`)

| Task | Command |
| -------------------------- | --------------------------------------------------------------- |
| Start dev server | `make dev` |
| Run all tests | `make test` |
| Run tests with coverage | `make cov` |
| Run a single test file | `uv run pytest app/tests/api/auth/test_login.py -v` |
| Run a single test function | `uv run pytest app/tests/api/auth/test_login.py::test_login -v` |
| Type check | `make check` |
| Generate migration | `make auto_migration msg="description"` |
| Apply migrations | `make migrate` |
| Verify migrations | `make check_migrations` |

### Client (`cd client`)

| Task | Command |
| --------------------- | ---------------------- |
| Start dev server | `npm run dev` |
| Build | `npm run build` |
| Lint (auto-fix) | `npm run lint` |
| Regenerate API client | `npm run generate-api` |

### Root (monorepo)

| Task | Command |
| ----------------- | ------------------ |
| Format all | `npm run format` |
| Lint all | `npm run lint` |
| Type check server | `npm run check:py` |

---

## Dev Environment

```bash
cp config/env/.env.example config/env/.env # fill in values
./scripts/dev.sh # install deps + start Docker Compose
./scripts/dev.sh -s # skip install, just start
./scripts/dev.sh -o # omit client/server containers
```

Docker Compose runs postgres, pgadmin, migrations, server (uvicorn), and client (Vite) with hot reload.

---

## Architecture

### Server

- **Entry point:** `server/app/main.py` — creates the FastAPI app, registers routers, exception handlers, and lifespan
- **Routing:** `server/app/api/router.py` composes all routers under `/api`. Each domain lives in `server/app/api/endpoints/{domain}.py`
- **Services:** `server/app/services/{domain}.py` — business logic, called by endpoints. Keep endpoints thin.
- **Database models:** `server/app/models/database/{entity}.py` — SQLAlchemy 2.0 declarative models
- **Schemas:** `server/app/models/schemas/{domain}.py` — Pydantic request/response models
- **Config:** `server/app/core/config.py` — `Settings` loaded via `pydantic-settings` with `__` as nested delimiter (e.g., `DB__HOST`)
- **Auth:** HTTP-only JWT cookies (`ACCESS_JWT_KEY`, `REFRESH_JWT_KEY`). `get_current_user` and `get_current_admin` are FastAPI dependencies in `server/app/core/security.py`
- **Errors:** Custom `HTTPError` subclasses in `server/app/models/errors.py`; raise them directly (e.g., `raise InvalidCredentials()`)

### Client

- **API client:** Auto-generated from OpenAPI spec via `@hey-api/openapi-ts`. Run `npm run generate-api` after server changes. Do **not** hand-edit `src/api/`.
- **Auth guards:** `RequireAuth` / `RequireGuest` components wrap routes in `src/router/`
- **Axios interceptors:** Handle 401 auto-refresh with request queueing (`src/lib/axios.ts`)

---

## Key Conventions

### Endpoints

```python
api_router = APIRouter(prefix="/auth", tags=["Auth"])

@api_router.post(
"/login",
operation_id="login", # required — used for OpenAPI client generation
status_code=status.HTTP_204_NO_CONTENT,
responses={status.HTTP_401_UNAUTHORIZED: ErrorResponseModel},
)
async def login_endpoint(
req: LoginRequest,
db: Annotated[AsyncSession, Depends(get_db)],
settings: Annotated[Settings, Depends(get_settings)],
res: Response,
):
result = await login(...) # delegate to service
res.set_cookie(...)
```

Always set `operation_id` — it drives the generated TypeScript client method name.

### Schemas

- Response schemas: `{Entity}Public` (e.g., `UserPublic`)
- Request schemas: `{Action}Request` (e.g., `LoginRequest`, `RegisterRequest`)
- Convert ORM → schema with `Model.model_validate(orm_obj, from_attributes=True)`
- Shared field type aliases (`Name`, `Username`, `Password`, `Email`) live in `server/app/models/schemas/types.py`

### Database Models

```python
class User(Base):
__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
```

- Use SQLAlchemy 2.0 `Mapped` annotations throughout
- All models include `created_at` / `updated_at` with `server_default=func.now()`
- Alembic bulk updates must explicitly set `updated_at` (no ORM trigger)
- Table names: lowercase plural; index names: `ix_{table}_{column}`

### Auth in Routes

```python
# Authenticated route
async def endpoint(user: Annotated[UserPublic, Depends(get_current_user)], ...):

# Admin-only route
async def endpoint(user: Annotated[UserPublic, Depends(get_current_admin)], ...):
```

### Error Handling

Define errors as `HTTPError` subclasses; raise without arguments:

```python
class InvalidCredentials(HTTPError):
status_code = status.HTTP_401_UNAUTHORIZED
code = "invalid_credentials"
detail = "Invalid credentials"

raise InvalidCredentials()
```

### Tests

- Fixtures are in `server/app/tests/fixtures/` and registered in `server/conftest.py`
- Shared helpers go in `server/app/tests/{scope}/utilities.py`, not per-test-file
- Module-private helpers and constants are prefixed with `_`
- Tests use `AsyncClient` from `httpx` against a savepoint-wrapped test DB
- Test settings use `console` email/GitHub backends; admin credentials: `admin` / `admin@example.com` / `password`
- `RegisterRequest` rejects usernames that are email addresses (enforced by `@field_validator`)
- `get_user_by_identifier` resolves login by email OR username

---

## Configuration

Settings use Pydantic nested models with `__` as the env delimiter:

```
DB__HOST, DB__PORT, DB__NAME, DB__USER, DB__PASSWORD
JWT__SECRET_KEY, JWT__ALGORITHM, JWT__ACCESS_TOKEN_EXPIRE_MINUTES
ADMIN__USERNAME, ADMIN__EMAIL, ADMIN__PASSWORD
EMAIL__BACKEND=smtp|local|console|disabled
GH__BACKEND=api|console
```

Production requires `EMAIL__BACKEND=smtp` and `GH__BACKEND=api` (validated at startup).
150 changes: 150 additions & 0 deletions PROJECT_OVERVIEW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
# RepTrack Project Overview

## Purpose

RepTrack is a web app for tracking strength training progress. It is a full-stack application with a React client, a FastAPI server, and a Postgres database, packaged for local development and deployment with Docker Compose.

## High-Level Architecture

- **Client (React + Vite)** talks to the **Server (FastAPI)** over HTTP.
- **Server** handles auth, users/admin, exercises, muscle groups, feedback, and health endpoints.
- **Postgres** stores users, workouts, exercises, sets, and feedback.
- **Background tasks** are used for email notifications and GitHub feedback issue creation.
- **API client** for the frontend is generated from the server OpenAPI spec.

```
Browser
-> Vite React App (client/)
-> Axios (with refresh handling)
-> FastAPI (server/)
-> SQLAlchemy + AsyncPG
-> Postgres
-> Email backend (smtp/local/console/disabled)
-> GitHub backend (api/console)
```

## Repo Layout

- `client/`: React app (Vite, Tailwind). Pages, auth guards, and generated API client.
- `server/`: FastAPI app with SQLAlchemy models, services, and tests.
- `config/infra/`: Docker Compose for local/dev and deployment.
- `config/env/`: `.env` template and PgAdmin config templates.
- `scripts/`: Dev bootstrap and API client generation.

## Client (Frontend)

- **Framework**: React 19 with Vite and TypeScript.
- **Routing**: `react-router-dom` with guarded routes (`RequireAuth`, `RequireGuest`).
- **API Layer**: Generated client via `@hey-api/openapi-ts`, configured in `client/src/api/axios.ts`.
- **Auth State**: `SessionProvider` loads current user and tracks authentication.
- **UI**: Tailwind CSS + Radix UI + shadcn-derived components in `client/src/components/ui/`.

Key entry points:

- `client/src/main.tsx`: Initializes API client, routes, session provider.
- `client/src/AppRoutes.tsx`: Route map with auth/admin gating.
- `client/src/api/axios.ts`: Axios instance with refresh-token queueing.

## Server (Backend)

- **Framework**: FastAPI, async endpoints.
- **DB**: SQLAlchemy 2.0 + AsyncPG.
- **Config**: Pydantic settings with env nested delimiter `__`.
- **Auth**: JWT access + refresh tokens stored as HTTP-only cookies.
- **Services**: Separated business logic in `server/app/services/`.
- **API**: Routers under `server/app/api/endpoints/` with `/api` prefix.

Key files:

- `server/app/__init__.py`: `create_app` and middleware setup.
- `server/app/api/router.py`: API router composition.
- `server/app/core/config.py`: Settings and environment wiring.
- `server/app/core/dependencies.py`: DB session + auth dependencies.

## Domain Model (Database)

Core tables (SQLAlchemy models in `server/app/models/database/`):

- `users`: accounts with `is_admin` flag.
- `access_requests`: request/approval flow for onboarding.
- `registration_tokens` and `password_reset_tokens`: hashed token storage with expirations.
- `workouts`, `workout_exercises`, `sets`: workout logging data.
- `exercises`, `muscle_groups`, `exercise_muscle_groups`: exercise catalog and tagging.
- `feedbacks`: user feedback, optional file attachments stored as JSON payload.

Basic relationships:

- User -> Workouts -> WorkoutExercises -> Sets.
- Exercises optionally owned by a user and tagged with muscle groups.
- AccessRequest -> RegistrationToken for invite-based registration.

## Core Flows

- **Request Access**: Public endpoint creates access request, notifies admins.
- **Admin Approval**: Admin updates access request status, generates registration token.
- **Registration**: User registers using token, creates account.
- **Login/Refresh**: JWT cookies, client handles refresh on 401s.
- **Password Reset**: Request reset, token emailed, token-based reset endpoint.
- **Exercise Library**: Authenticated users can browse system exercises, create their own exercises, and tag them with muscle groups.
- **Feedback**: Authenticated users submit feedback; server stores entry and can open a GitHub issue.

## API Surface (Current)

- `POST /api/auth/*`: request-access, register, login, refresh-token, logout, forgot/reset-password
- `GET /api/users/current`: current user
- `GET/PATCH /api/admin/*`: access request management and user list
- `GET/POST /api/exercises`, `GET/PATCH/DELETE /api/exercises/{id}`: exercise library CRUD
- `GET /api/muscle-groups`: system muscle group reference data
- `POST /api/feedback`: feedback submission
- `GET /api/health`, `GET /api/health/db`: health checks

## API Surface (Planned — Workouts)

**Workouts**

- `GET /api/workouts` — list current user's workouts
- `POST /api/workouts` — create a workout
- `GET /api/workouts/{id}` — get workout with exercises and sets
- `PATCH /api/workouts/{id}` — update workout (started_at, ended_at, notes)
- `DELETE /api/workouts/{id}` — delete workout

**Workout Exercises (nested)**

- `POST /api/workouts/{id}/exercises` — add an exercise to a workout
- `DELETE /api/workouts/{id}/exercises/{workout_exercise_id}` — remove exercise from workout

**Sets (nested)**

- `POST /api/workouts/{id}/exercises/{workout_exercise_id}/sets` — log a set
- `PATCH /api/workouts/{id}/exercises/{workout_exercise_id}/sets/{set_id}` — update a set
- `DELETE /api/workouts/{id}/exercises/{workout_exercise_id}/sets/{set_id}` — delete a set

## Infrastructure & Deployment

- **Docker Compose** in `config/infra/` provides:
- Postgres 18
- PgAdmin
- Server container
- Client container
- Migrations job
- **Local Dev** uses `docker compose --profile include-client-server up --watch` via `scripts/dev.sh`.
- **Traefik** labels are present for routing in deployed environments.

## Local Development Workflow

1. Copy env: `cp config/env/.env.example config/env/.env`
2. Run `./scripts/dev.sh` to install deps, watch API spec generation, and start compose.
3. `scripts/generate_api.sh` keeps the client OpenAPI SDK in sync.

## Testing

- Server tests under `server/app/tests/`.
- Uses pytest, pytest-asyncio, testcontainers.
- Coverage configured in `server/pyproject.toml`.

## Notes for Future Agents

- **OpenAPI SDK**: Generated files live under `client/src/api/generated/`.
- **Auth**: Cookies are HTTP-only; the client relies on `/users/current` to resolve session state.
- **Email/GitHub**: Backends are configurable via settings; production expects SMTP + GitHub API.
- **Migrations**: Alembic runs in Docker (service `migrations`).
Loading
Loading