From 9fa5a7ee9cea0011bae25b48e9a7e9ad7a73bd40 Mon Sep 17 00:00:00 2001 From: Hsin Cheng Date: Tue, 14 Apr 2026 17:05:19 +0200 Subject: [PATCH 1/2] Create CLAUDE.md and README.md --- CLAUDE.md | 95 +++++++++++++++++++++++++++++++++++++++++++++++++++ PLAN.md | 22 +++++------- README.md | 28 +++++++++++++++ app/config.py | 1 - 4 files changed, 131 insertions(+), 15 deletions(-) create mode 100644 CLAUDE.md create mode 100644 README.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..78f0316 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,95 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What This Is + +`taken` is a Python FastAPI backend for duty scheduling at US Basketball Amsterdam (~125 members, ~12 teams, ~126 home games/season). It manages assignment of referee, table official, and hall duty (zaaldienst) roles to members per game, enforcing eligibility rules and optimizing for fairness. A companion Next.js website (separate repo) consumes the public API. + +## Commands + +```bash +# Install +pip install -e ".[dev]" + +# Run dev server +uvicorn app.main:app --reload + +# Run all tests +pytest + +# Run a single test file +pytest tests/test_sync.py + +# Run a single test by name +pytest tests/test_sync.py::test_sync_preview_returns_diff + +# Database migrations +alembic upgrade head +alembic revision --autogenerate -m "description" +alembic downgrade -1 +``` + +Interactive API docs when running locally: `http://localhost:8000/docs` + +> **Alembic note:** `alembic.ini` intentionally has a blank `sqlalchemy.url`. `alembic/env.py` reads `DATABASE_URL` directly from the environment. + +## Architecture + +### Request Flow + +``` +Vercel → api/index.py (re-exports app) → app/main.py (router registration) + → routers/ → services/ → db/models.py (SQLAlchemy ORM) +``` + +### Key Design Points + +**Auth (two-tier):** + +- Admin routes: HS256 JWT via `require_auth()` dependency (`app/middleware/auth.py`), injected per-route with `Depends()` — not global middleware +- `verify_password` accepts plain text if `ADMIN_PASSWORD` doesn't start with `$2b$`/`$2a$` (intentional for local dev; default: `"changeme"`) +- Public routes: Auth0 JWT (`Authorization: Bearer `), verified against Auth0's JWKS endpoint + +**Database:** + +- Production: PostgreSQL via Neon. Both `app/db/engine.py` and `alembic/env.py` independently rewrite `postgres://` → `postgresql://` and append `sslmode=require` for `neon.tech` URLs — keep these in sync. +- Tests: in-memory SQLite with `StaticPool` (required so all connections share the same in-memory DB). The engine factory branches on `url.startswith("sqlite")` to skip PostgreSQL-only pool settings. + +**foys.io sync (two-step):** + +- `POST /seasons/{sid}/games/sync?preview=true` — returns diff (added/updated/removed/conflicts) without writing +- `POST /seasons/{sid}/games/sync?preview=false` — applies the diff; conflicts default to "keep local" unless overridden per `external_id` with `action: "overwrite"` + +**Timeslots:** A `Timeslot` is a unique `(season_id, date, start_time)`. Games link via `GameTimeslot` (many-to-many). Two games are "adjacent" if within 150 minutes on the same day, affecting duty balance counting. + +**Tests:** All tests use per-function in-memory SQLite; `get_db` is overridden via `app.dependency_overrides`. External foys.io HTTP calls are mocked with `unittest.mock.patch`. + +### Configuration (`app/config.py`, pydantic-settings) + +| Variable | Purpose | +| ----------------------------------------- | ----------------------------------------- | +| `DATABASE_URL` | Defaults to `sqlite:///./test.db` locally | +| `ADMIN_PASSWORD` | Plain text (dev) or bcrypt hash (prod) | +| `JWT_SECRET` | HS256 signing key | +| `AUTH0_DOMAIN` / `AUTH0_AUDIENCE` | Verifies website user JWTs | +| `FOYS_FEDERATION_ID` / `FOYS_HOME_ORG_ID` | foys.io defaults (hardcoded) | + +## Implementation Status + +This is a mid-build project. Phases 1–2 are complete: + +- Phase 1: auth, seasons CRUD, DB engine, ORM models, Alembic migration +- Phase 2: foys.io sync service, games listing, timeslot management + +**Phases 3–6 not yet implemented:** members/assignments/balance/solver routers, OR-Tools CP-SAT solver (`app/engine/`), Excel export, Auth0/API key middleware, public endpoints. + +The canonical architecture plan (full API spec, DB schema, solver design) is in `PLAN.md`. + +### Planned Solver (Phase 4) + +OR-Tools CP-SAT with ~63,000 binary decision variables (126 games × 5 duties × 100 eligible members), 30-second time limit, falling back to greedy on timeout. + +## Deployment + +Vercel: `vercel.json` rewrites all traffic to `/api/index.py`. Test files are excluded from the function bundle via `excludeFiles`. diff --git a/PLAN.md b/PLAN.md index 26cc931..06c09eb 100644 --- a/PLAN.md +++ b/PLAN.md @@ -32,7 +32,7 @@ US Basketball Amsterdam (~125 members, 12 teams, ~126 home games/season) needs a | **Database** | PostgreSQL on Neon | Native types, Vercel Marketplace, free tier | | **Optimization** | Google OR-Tools (CP-SAT) | Constraint satisfaction + optimization solver — ideal for duty assignment | | **Auth (admin)** | JWT Bearer tokens (`python-jose`) | Board member login with password | -| **Auth (public)** | API key + Auth0 token verification (`PyJWKClient`) | Dual protection: API key validates the caller is the website, Auth0 token validates the user is a club member | +| **Auth (public)** | Auth0 token verification (`PyJWKClient`) | Verifies the user is an authenticated club member | | **Testing** | pytest + httpx | Standard Python testing | | **Deploy** | Vercel (Fluid Compute) | Same platform as website | @@ -68,7 +68,7 @@ taken/ audit.py # GET /seasons/{sid}/audit export.py # GET /seasons/{sid}/export publish.py # POST /seasons/{sid}/publish - public.py # GET /public/schema (API-key + Auth0 auth) + public.py # GET /public/schema (Auth0 auth) schemas/ # Pydantic request/response models season.py member.py @@ -93,7 +93,6 @@ taken/ middleware/ __init__.py auth.py # JWT verification for admin routes - api_key.py # API key verification for public routes auth0.py # Auth0 token verification for public routes alembic/ # Migration files versions/ @@ -206,7 +205,7 @@ sum(zaal[ts, :]) == 1 ## API Endpoints -All admin routes require `Authorization: Bearer `. Public routes require both `x-api-key` and Auth0 token. +All admin routes require `Authorization: Bearer `. Public routes require `Authorization: Bearer `. ### Auth @@ -278,17 +277,14 @@ GET /seasons/{sid}/export → .xlsx file POST /seasons/{sid}/publish → { ok, published_at } ``` -### Public (for website) — requires both API key + Auth0 token +### Public (for website) — requires Auth0 token ``` GET /public/schema → PublicSchema GET /public/schema?team=H3 → PublicSchema (filtered) ``` -**Dual auth on public routes:** - -1. `x-api-key` header — validates the caller is the club website (not a random client) -2. `Authorization: Bearer ` — the website forwards the user's Auth0 JWT; the taken API verifies it against Auth0's JWKS endpoint (`https:///.well-known/jwks.json`) to confirm the user is an authenticated club member +`Authorization: Bearer ` — the website forwards the user's Auth0 JWT; the taken API verifies it against Auth0's JWKS endpoint (`https:///.well-known/jwks.json`) to confirm the user is an authenticated club member. The taken API needs these env vars for Auth0 verification: @@ -420,15 +416,14 @@ Response shape for `/public/schema`: ### Phase 5: Public API + Publish -- Implement API-key middleware (`app/middleware/api_key.py`) - Implement Auth0 token verification middleware (`app/middleware/auth0.py`) — verifies user JWT against Auth0 JWKS -- Combine both as dependencies on public routes (both must pass) +- Add as dependency on public routes - Implement `GET /public/schema` - Implement `POST /seasons/{sid}/publish` - Implement `services/validation.py` — constraint violation checks -- Write tests for public API (mock both API key and Auth0 token verification) +- Write tests for public API (mock Auth0 token verification) -**Deliverable:** Website can fetch and display duty schedule. Endpoint is protected by both API key (service-level) and Auth0 token (user-level). +**Deliverable:** Website can fetch and display duty schedule. Endpoint is protected by Auth0 token (user must be authenticated club member). ### Phase 6: Export + Polish @@ -465,7 +460,6 @@ from app.main import app # FastAPI instance DATABASE_URL=postgres://... # Auto-provisioned by Neon ADMIN_PASSWORD= JWT_SECRET= -API_KEY= AUTH0_DOMAIN=.auth0.com # For verifying user tokens from the website AUTH0_AUDIENCE= # Auth0 API identifier or client ID FOYS_FEDERATION_ID=52cfa65e-9782-4a81-ab35-e2f981fcb7a9 diff --git a/README.md b/README.md new file mode 100644 index 0000000..a1e9fd1 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# taken + +Python/FastAPI backend for duty scheduling at [US Basketball Amsterdam](https://usbasketball.nl). Manages referee, table official, and hall duty (zaaldienst) assignments across ~125 members and ~126 home games per season. + +## Setup + +```bash +pip install -e ".[dev]" +cp .env.example .env # set DATABASE_URL, ADMIN_PASSWORD, JWT_SECRET +alembic upgrade head +uvicorn app.main:app --reload +``` + +API docs available at `http://localhost:8000/docs`. + +## Tests + +```bash +pytest +``` + +## Deployment + +Deployed on Vercel (Fluid Compute). Set environment variables via Vercel dashboard or `vercel env`. Database is PostgreSQL on Neon (Vercel Marketplace). + +--- + +For architecture, commands, and development notes see [CLAUDE.md](CLAUDE.md). diff --git a/app/config.py b/app/config.py index 43c0ba9..a1ba91c 100644 --- a/app/config.py +++ b/app/config.py @@ -6,7 +6,6 @@ class Settings(BaseSettings): admin_password: str = "changeme" jwt_secret: str = "insecure-dev-secret-change-in-production" jwt_expire_days: int = 7 - api_key: str = "" auth0_domain: str = "" auth0_audience: str = "" foys_federation_id: str = "52cfa65e-9782-4a81-ab35-e2f981fcb7a9" From ad0866524ea3a3b51016eb30ddcbabcd9dbcdff6 Mon Sep 17 00:00:00 2001 From: Hsin Cheng Date: Tue, 14 Apr 2026 17:06:18 +0200 Subject: [PATCH 2/2] Add GitHub Actions CI workflow Runs the full pytest suite on every push to main and every PR targeting main. Uses Python 3.12 with pip caching to keep runs fast. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/ci.yml | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fd7be61 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,25 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + cache: pip + + - name: Install dependencies + run: pip install -e ".[dev]" + + - name: Run tests + run: pytest --tb=short