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

4.4 KiB
Raw Blame History

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Commands

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.