Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
1d51727
client - standardize form field components
aditya-arcot Mar 16, 2026
608a268
client - left justify updated at column
aditya-arcot Mar 16, 2026
c2dd6df
client - update column widths
aditya-arcot Mar 17, 2026
0b0312d
client - add tooltip wrapping
aditya-arcot Mar 17, 2026
f172c81
add agents symlink
aditya-arcot Mar 17, 2026
3f42b50
update readme, add db erd
aditya-arcot Mar 17, 2026
32f8dbd
server - centralize public model conversions
aditya-arcot Mar 18, 2026
9b93b8d
server - update model relationships
aditya-arcot Mar 18, 2026
178e9b6
server - refactor exercise logic
aditya-arcot Mar 18, 2026
8fb85d8
Merge branch 'main' into dev
aditya-arcot Mar 19, 2026
e68f9d0
server - add workout schemas & endpoints
aditya-arcot Mar 19, 2026
3c005d4
server - refactor exercise api tests
aditya-arcot Mar 19, 2026
e411811
server - add workout api tests
aditya-arcot Mar 19, 2026
3fcc4b0
server - add service tests
aditya-arcot Mar 20, 2026
94eeeb8
server - fix import ignore comments
aditya-arcot Mar 20, 2026
da7bbe0
server - allow null values in update endpoints
aditya-arcot Mar 20, 2026
9e4663e
Merge pull request #99 from arcot-labs/dev
aditya-arcot Mar 20, 2026
cca2f5b
client - update deps
aditya-arcot Mar 20, 2026
fdd907d
Bump flatted in /client in the npm_and_yarn group across 1 directory
dependabot[bot] Mar 21, 2026
37ba2de
server - update tags
aditya-arcot Mar 21, 2026
abc3948
server - implement workout exercise endpoints
aditya-arcot Mar 22, 2026
7d04cee
server - rename test folders
aditya-arcot Mar 22, 2026
8133c04
server - add workout exercise api tests
aditya-arcot Mar 22, 2026
21c99e0
server - add workout exercise service tests
aditya-arcot Mar 22, 2026
3a7ccfd
Merge pull request #100 from arcot-labs/dependabot/npm_and_yarn/clien…
aditya-arcot Mar 22, 2026
5ad476a
Merge branch 'stage' into dev
aditya-arcot Mar 22, 2026
f01f54d
Merge pull request #101 from arcot-labs/dev
aditya-arcot Mar 22, 2026
f693680
server - add set service & tests
aditya-arcot Mar 23, 2026
41e1015
client - update deps
aditya-arcot Mar 23, 2026
b4bc9a1
server - add set endpoints
aditya-arcot Mar 23, 2026
c44a294
Bump the server-dependencies group in /server with 3 updates
dependabot[bot] Mar 23, 2026
44be2b5
Merge pull request #103 from arcot-labs/dependabot/uv/server/stage/se…
aditya-arcot Mar 23, 2026
3787ddf
chore: format & lint after PR merge
github-actions[bot] Mar 23, 2026
3a941ae
server - fix eager loading bug
aditya-arcot Mar 23, 2026
278c385
server - add api conflict tests
aditya-arcot Mar 23, 2026
2db2efe
update overview
aditya-arcot Mar 23, 2026
3124d93
Merge branch 'stage' into dev
aditya-arcot Mar 23, 2026
737c321
server - replace set unit with enum
aditya-arcot Mar 23, 2026
79bf74a
server - clean up db models
aditya-arcot Mar 23, 2026
b2d617a
server - add conflict response
aditya-arcot Mar 23, 2026
eb5349d
server - update set weight type to match db
aditya-arcot Mar 23, 2026
91bea85
server - fix type issues
aditya-arcot Mar 23, 2026
28de1df
Merge pull request #104 from arcot-labs/dev
aditya-arcot Mar 23, 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
212 changes: 50 additions & 162 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,183 +1,71 @@
# RepTrack – Copilot Instructions
# RepTrack – Agent 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.
RepTrack is a full-stack strength-training tracker:

---
- `client/`: React 19 + Vite + TypeScript
- `server/`: FastAPI + SQLAlchemy 2.0 async stack
- DB: PostgreSQL (AsyncPG)

## Repository Layout
## Build, Test, and Lint Commands

```
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
```

---
### Root (monorepo)

## Commands
- Format all: `npm run format`
- Lint/typecheck all configured checks: `npm run lint`
- Server typecheck only: `npm run check:py`

### 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` |
- Dev server: `make dev`
- Typecheck: `make check`
- Test suite: `make test`
- Tests with coverage: `make cov`
- Single test file: `uv run pytest app/tests/api/auth/test_login.py -v`
- Single test function: `uv run pytest app/tests/api/auth/test_login.py::test_login -v`
- Alembic check: `make check_migrations`
- Apply migrations: `make migrate`

### 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
```
- Dev server: `npm run dev`
- Build: `npm run build`
- Lint (auto-fix): `npm run lint`
- Regenerate OpenAPI client: `npm run generate-api`

Docker Compose runs postgres, adminer, migrations, server (uvicorn), and client (Vite) with hot reload.
## High-Level Architecture

---
### Server request path

## Architecture
- `server/app/__init__.py` owns app assembly (`create_app`): logging setup, global exception handlers, `/api` router mounting, Swagger UI install in non-prod-like envs, CORS wrapping.
- `server/app/main.py` just instantiates and exports `fastapi_app, app`.
- `server/app/api/router.py` composes domain routers from `server/app/api/endpoints/*.py`.
- Endpoints are intentionally thin and delegate business logic to `server/app/services/*.py`.

### Server
### Auth/session architecture across backend + frontend

- **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()`)
- Backend auth uses HTTP-only cookies (`access_token`, `refresh_token`) from `auth` endpoints.
- JWT verification and current-user/admin dependencies are in `server/app/core/dependencies.py` and `server/app/core/security.py`.
- Frontend API client (`client/src/api/axios.ts`) queues concurrent 401s and performs one refresh-token request, then retries queued calls.
- `SessionProvider` loads `/api/users/current` to determine authenticated state; `RequireAuth` / `RequireGuest` gate routes in `client/src/AppRoutes.tsx`.

### Client
### OpenAPI client generation pipeline

- **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`)

---
- Server OpenAPI spec is generated by `scripts/generate_api.sh`.
- Client generation uses `client/openapi-ts.config.ts` with `@hey-api/openapi-ts`.
- SDK generation strategy is `byTags` + `operationId` nesting, so endpoint `operation_id` values drive generated method names.
- Generated files live in `client/src/api/generated/` and should not be hand-edited.

## 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).
- Always set explicit `operation_id` on FastAPI endpoints. Missing or unstable IDs cause churn/breakage in generated TS SDK method names.
- Route handlers keep orchestration only; core business logic belongs in `server/app/services/`.
- Error handling uses `HTTPError` subclasses in `server/app/models/errors.py`; raise typed errors directly (e.g., `raise InvalidCredentials()`), not ad-hoc `HTTPException` payloads.
- Settings come from nested env vars with `__` delimiter (see `server/app/core/config.py`, e.g. `DB__HOST`, `JWT__SECRET_KEY`). In prod-like envs, config validators enforce `EMAIL__BACKEND=smtp` and `GH__BACKEND=api`.
- SQLAlchemy models use `Mapped[...]` + `mapped_column(...)`; models include `created_at`/`updated_at` timestamps. Alembic/bulk updates must set `updated_at` explicitly.
- Client lint rules enforce wrapper usage over direct imports:
- Prefer app override components like `@/components/ui/overrides/button` instead of base shadcn paths.
- Prefer `@/` alias imports over deep relative paths.
- Prefer `@/lib/notify` over direct `toast` import from `sonner`.
- Test stack uses pytest + async fixtures with a Postgres Testcontainers instance (`postgres:18`) and savepoint-based rollback per test (`server/app/tests/fixtures/database.py`).
- Shared test helpers belong in `utilities.py` under the relevant test scope; module-private test helpers/constants are prefixed with `_`.
- Auth schema rule: usernames cannot be email-shaped strings (`RegisterRequest` validator in `server/app/models/schemas/auth.py`); login accepts username or email identifier.
1 change: 1 addition & 0 deletions AGENTS.md
21 changes: 10 additions & 11 deletions PROJECT_OVERVIEW.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ RepTrack is a web app for tracking strength training progress. It is a full-stac
## 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.
- **Server** handles auth, users/admin, workouts, workout exercises, sets, 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.
Expand Down Expand Up @@ -91,7 +91,8 @@ Basic relationships:
## Current Implementation Status

- ✅ Exercise API + client CRUD flow is implemented (`/api/exercises`, `/api/muscle-groups`, `/exercises` page).
- 🚧 Workout API + client workflow is the primary remaining feature area.
- ✅ Workout API (workouts, workout-exercises, sets) is implemented on the server with tests.
- 🚧 Workout client workflow is the primary remaining feature area.

## API Surface (Current)

Expand All @@ -103,8 +104,6 @@ Basic relationships:
- `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
Expand All @@ -113,16 +112,16 @@ Basic relationships:
- `PATCH /api/workouts/{id}` — update workout (started_at, ended_at, notes)
- `DELETE /api/workouts/{id}` — delete workout

**Workout Exercises (nested)**
**Workout Exercises**

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

**Sets (nested)**
**Sets**

- `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
- `POST /api/sets/{workout_id}/exercises/{workout_exercise_id}/sets` — log a set
- `PATCH /api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — update a set
- `DELETE /api/sets/{workout_id}/exercises/{workout_exercise_id}/sets/{set_id}` — delete a set

## Infrastructure & Deployment

Expand Down
24 changes: 15 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

# RepTrack

## Local Development
## Development

Copy `.env.example` to `.env` & populate variables

Expand All @@ -27,7 +27,7 @@ Start containers:
./scripts/dev.sh
```

## Local GitHub Actions Testing
## Testing GitHub Actions Workflows

Use `act` to run workflows locally for quick validation.

Expand All @@ -43,13 +43,7 @@ Run a specific job:
act -j {job-id}
```

## Database Conventions

All writes should go through SQLAlchemy

Alembic updates & bulk SQLAlchemy updates must explicitly set `updated_at`

## Shadcn Component Conventions
## Shadcn Components

shadcn adds components under `client/src/components/ui/`

Expand All @@ -58,3 +52,15 @@ To ensure custom styles & behavior survive component updates, follow these conve
- Create custom component overrides under `client/src/components/ui/overrides/`
- Import override components in app code instead of generated shadcn components
- Add ESLint rules to prevent direct imports of generated components & point to override paths

## Database

### Conventions

All writes should go through SQLAlchemy

Alembic updates & bulk SQLAlchemy updates must explicitly set `updated_at`

### Database ER Diagram

![diagram](db_erd.png 'Database ER Diagram')
5 changes: 5 additions & 0 deletions client/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ export default defineConfig([
message:
'Use Button from @/components/ui/overrides/button',
},
{
name: '@/components/ui/tooltip',
message:
'Use Tooltip from @/components/ui/overrides/tooltip',
},
{
name: 'sonner',
importNames: ['toast'],
Expand Down
Loading
Loading