Files
bikeslop/CLAUDE.md
T
ryan c6d9a3b8b6 Add multi-user support: Authelia OIDC, SQLite, Garage S3
- Auth via Authelia OIDC (public client + PKCE) with signed session cookies
- SQLite DB for users, rides, and insights (SQLAlchemy)
- Garage S3 for GPX file storage (boto3)
- All API endpoints scoped to authenticated user
- Dockerfile + docker-compose.yml with Garage service
- Frontend auth check: redirects to /auth/login on 401, shows user badge

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-08-06 22:12:23 -04:00

72 lines
4.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Commands
```bash
uv run uvicorn app:app --reload # start dev server at http://localhost:8000
uv run --env-file .env uvicorn app:app --reload # load env vars from .env file
uv sync # install/sync dependencies
uv add <package> # add a new dependency
```
Set `LLAMACPP_BASE_URL=http://host:port/v1` to point at a local LLM (inline, exported, or via `.env`).
## Environment variables
```
# Auth (Authelia OIDC — public client with PKCE, no secret)
OIDC_ISSUER=https://auth.example.com
OIDC_CLIENT_ID=bikeslop
APP_BASE_URL=https://bikeslop.example.com
SECRET_KEY=<64-char random hex> # signs session cookies
# S3 (Garage or any S3-compatible)
S3_ENDPOINT=https://s3.example.com
S3_BUCKET=bikeslop-gpx
S3_ACCESS_KEY=<key>
S3_SECRET_KEY=<secret>
# Optional
DATABASE_URL=sqlite:///bikeslop.db # default
LLAMACPP_BASE_URL=http://localhost:8080/v1
```
## Architecture
Four-file app: `app.py` (FastAPI backend), `db.py` (SQLAlchemy models), `auth.py` (OIDC + session), `storage.py` (S3 wrapper), and `static/index.html` (SPA frontend).
**`db.py`** defines SQLAlchemy models (`User`, `Ride`, `Insight`) backed by SQLite (or any SQLAlchemy-compatible DB). Tables are created at startup via `create_tables()`. Ride IDs are UUIDs generated at insert time.
**`auth.py`** handles Authelia OIDC login via `authlib`. Session is a signed cookie (itsdangerous) containing the user UUID — no server-side session storage. Routes: `GET /auth/login`, `GET /auth/callback`, `GET /auth/logout`. The `require_user` FastAPI dependency reads the cookie and raises 401 if not authenticated.
**`storage.py`** wraps boto3 for Garage S3. GPX files are stored at `users/{user_id}/{filename}.gpx`. Raises `RuntimeError` on first use if env vars are not configured.
**`app.py`** — all API endpoints require auth (`Depends(require_user)`) and are scoped to the current user. GPX metadata is stored in the DB at upload time; raw GPX bytes live in S3 and are fetched on demand for the `/points` endpoint and insight generation. In-memory caches: `_user_rides_cache` (ride metadata per user) and `_points_cache` (LRU, 50 entries) to avoid repeated S3 fetches. API routes:
- `GET /api/me` — current user info
- `GET /api/rides` — summary list for current user
- `GET /api/rides/{id}` — ride metadata + insight from DB
- `GET /api/rides/{id}/points` — downloads GPX from S3, parses, returns lat/lon/ele/hr array
- `POST /api/rides/{id}/insights` — streams AI coaching, saves to DB
- `POST /api/insights/generate-all` — SSE stream, generates insights for all rides without one
- `POST /api/fetch-rides` — SSE stream, fetches from Garmin → uploads to S3 → inserts into DB
- `POST /api/mfa` — submits MFA code for an in-progress Garmin session
**AI integration** uses the OpenAI Python SDK pointed at a local LLM endpoint (`LLAMACPP_BASE_URL` env var, defaults to `http://localhost:8080/v1`). The model name is auto-discovered from `GET /v1/models` at startup (falls back to `"local-model"`). The chunk reader handles both `content` and `reasoning_content` delta fields for reasoning models.
**`static/index.html`** is a self-contained SPA (no build step). On load it calls `GET /api/me`; a 401 redirects to `/auth/login`. Leaflet.js draws the route as per-segment polylines colored by HR intensity zone. Chart.js renders elevation and HR profiles. AI insights stream via `fetch` + `ReadableStream`. Units (km/mi) toggle and resting HR are persisted in `localStorage`.
**`main.py`** is a standalone CLI script for bulk-downloading Garmin Connect GPX files (single-user, pre-auth era — largely superseded by the in-app Garmin sync).
## GPX data notes
- Garmin extension namespace for HR: `http://www.garmin.com/xmlschemas/TrackPointExtension/v1` (`ns3:hr`)
- Activities with an empty `<trkseg/>` (e.g. indoor rides) get `has_gps: false` — frontend shows no map or charts
- HR zone boundaries (fixed, used for API summary): Z1 <120, Z2 120140, Z3 140160, Z4 160180, Z5 >180 bpm
- Karvonen zones (used in the UI and AI prompt) are computed from resting HR input + lifetime max HR observed across all rides
## Logging
httpx, httpcore, and openai loggers are set to WARNING to suppress connection-level debug noise. The `bikeslop` logger runs at DEBUG.