diff --git a/docs/API_REFERENCE.md b/docs/API_REFERENCE.md new file mode 100644 index 0000000..0911a50 --- /dev/null +++ b/docs/API_REFERENCE.md @@ -0,0 +1,278 @@ +# API Reference + +All endpoints return HTML (full pages or HTMX partials) unless noted otherwise. Protected routes require a valid admin session cookie. + +--- + +## Authentication + +### `GET /login` + +Render the login form. + +- **Auth:** None +- **Response:** Full page (`pages/login.html`) + +### `POST /login` + +Authenticate admin credentials and start a session. + +- **Auth:** None +- **Content-Type:** `application/x-www-form-urlencoded` +- **Form fields:** + - `username` (string) — admin username + - `password` (string) — admin password +- **Success:** 303 redirect to `/`, sets `session` cookie (httponly, samesite=lax, 24h TTL) +- **Failure:** 200 with login page re-rendered and error message + +### `GET /logout` + +End the admin session. + +- **Auth:** None (clears cookies regardless) +- **Response:** 303 redirect to `/login`, deletes `session` and `active_profile_id` cookies + +--- + +## Health + +### `GET /health` + +Application health check for monitoring and readiness probes. + +- **Auth:** None +- **Response:** JSON + ```json + { + "app": "sneakyswole", + "version": "0.1.0", + "status": "ok" + } + ``` + +--- + +## Pages + +### `GET /` + +Home page. + +- **Auth:** None +- **Response:** Full page (`pages/home.html`) + +--- + +## Profiles + +All profile routes require admin authentication. + +### `GET /profiles` + +List all user profiles (excludes admin). + +- **Auth:** Admin session +- **Response:** Full page (`pages/profiles.html`) +- **Template context:** `profiles`, `active_profile_id`, `admin` + +### `POST /profiles/switch` + +Switch the active user profile. + +- **Auth:** Admin session +- **Content-Type:** `application/x-www-form-urlencoded` +- **Form fields:** + - `profile_id` (string) — ID of the profile to activate +- **Response:** 303 redirect to referring page, sets `active_profile_id` cookie + +### `GET /profiles/{profile_id}/edit` + +Render the profile edit form. + +- **Auth:** Admin session +- **Path params:** `profile_id` (int) +- **Response:** Full page (`pages/profile_edit.html`) + +### `POST /profiles/{profile_id}/edit` + +Update a user profile. + +- **Auth:** Admin session +- **Path params:** `profile_id` (int) +- **Content-Type:** `application/x-www-form-urlencoded` +- **Form fields:** + - `display_name` (string) + - `height` (string) + - `weight` (string) + - `goals` (string) +- **Response:** 303 redirect to `/profiles` + +--- + +## Workouts + +All workout routes require admin authentication. + +### `GET /workouts` + +List all workout days as clickable cards. + +- **Auth:** Admin session +- **Response:** Full page (`pages/workout_days.html`) +- **Template context:** `days`, `admin` + +### `GET /workouts/{day_name}` + +Display a full workout day with warmups, exercises, programming targets, and inline logging. + +- **Auth:** Admin session +- **Path params:** `day_name` (string) — URL-friendly name, e.g., "push", "pull", "lower", "full-body" +- **Response:** Full page (`pages/workout_day.html`) +- **Template context:** `day_name`, `warmups`, `exercises`, `programs`, `active_profile`, `existing_logs`, `suggestions`, `workout_day_id`, `admin` +- **Notes:** Day name is normalized (e.g., "full-body" → "Full Body"). If an active profile is set, includes programming targets, progression suggestions, and today's existing logs. + +--- + +## Exercises + +All exercise routes require admin authentication. + +### `GET /exercises` + +Render the exercise browser with filter dropdowns. + +- **Auth:** Admin session +- **Response:** Full page (`pages/exercise_browser.html`) +- **Template context:** `exercises`, `workout_days`, `muscle_groups`, `admin` + +### `GET /exercises/search` + +HTMX partial — return filtered exercise list. + +- **Auth:** Admin session +- **Query params:** + - `workout_day` (string, optional) — filter by workout day name + - `muscle_group` (string, optional) — filter by muscle group +- **Response:** HTMX partial (`partials/exercise_list.html`) +- **Usage:** Called via `hx-get` from exercise browser filter dropdowns + +--- + +## Workout Logging + +All logging routes require admin authentication. Responses are HTMX partials that update inline. + +### `POST /log` + +Log a single set for an exercise. Auto-creates today's workout session if needed. + +- **Auth:** Admin session +- **Content-Type:** `application/x-www-form-urlencoded` +- **Form fields:** + - `exercise_id` (int) + - `workout_day_id` (int) + - `set_number` (int, default=1) + - `reps` (int) + - `weight` (string) — e.g., "30 lbs", "BW" + - `felt_easy` (checkbox, "on" = true) +- **Response:** HTMX partial (`partials/log_entry.html`) with updated logs for this exercise +- **Error:** If no active profile selected, returns `partials/flash_message.html` with error + +### `POST /log/{log_id}/edit` + +Edit an existing log entry. + +- **Auth:** Admin session +- **Path params:** `log_id` (int) +- **Content-Type:** `application/x-www-form-urlencoded` +- **Form fields:** + - `reps` (int) + - `weight` (string) + - `felt_easy` (checkbox) + - `notes` (string, optional) +- **Response:** HTMX partial (`partials/log_entry.html`) with updated logs + +### `POST /log/{log_id}/delete` + +Delete a log entry. + +- **Auth:** Admin session +- **Path params:** `log_id` (int) +- **Response:** HTMX partial (`partials/log_entry.html`) with remaining logs, or empty HTML if log not found + +--- + +## History + +All history routes require admin authentication. + +### `GET /history` + +Display log history for the active profile — list of past sessions, most recent first. + +- **Auth:** Admin session +- **Response:** Full page (`pages/log_history.html`) +- **Template context:** `sessions`, `days_by_id`, `active_profile`, `admin` +- **Notes:** Requires active profile to show sessions + +### `GET /history/{session_id}` + +Display detailed logs for a specific workout session, grouped by exercise. + +- **Auth:** Admin session +- **Path params:** `session_id` (int) +- **Response:** Full page (`pages/session_detail.html`) +- **Template context:** `workout_session`, `logs_by_exercise`, `exercises_by_id`, `days_by_id`, `admin` + +--- + +## Dashboard + +All dashboard routes require admin authentication. + +### `GET /dashboard` + +Render the progress dashboard with summary stats, volume chart, and exercise links. + +- **Auth:** Admin session +- **Response:** Full page (`pages/dashboard.html`) +- **Template context:** `stats`, `volume_data_json`, `exercises`, `active_profile`, `admin` +- **Notes:** Stats and volume data require an active profile. Chart.js renders client-side charts from JSON embedded in the template. + +### `GET /dashboard/exercise/{exercise_id}` + +Per-exercise progress page with charts and progression suggestions. + +- **Auth:** Admin session +- **Path params:** `exercise_id` (int) +- **Response:** Full page (`pages/exercise_progress.html`) +- **Template context:** `exercise`, `progress_data_json`, `suggestion`, `active_profile`, `admin` + +--- + +## Schedule + +### `GET /schedule` + +4-week calendar view showing which workout day maps to which date. + +- **Auth:** Admin session +- **Response:** Full page (`pages/schedule.html`) +- **Template context:** `weeks`, `active_profile`, `admin` +- **Notes:** Calendar starts from Monday of the current week. Days with completed sessions are highlighted. Requires active profile for session completion data. + +--- + +## Authentication Details + +### Cookies + +| Cookie | Purpose | Flags | TTL | +|--------|---------|-------|-----| +| `session` | Admin session token (itsdangerous signed) | httponly, samesite=lax | 24 hours | +| `active_profile_id` | Currently selected user profile ID | httponly, samesite=lax | 24 hours | + +### Auth Failure Behavior + +- Missing or invalid session cookie → 302 redirect to `/login` +- Handled via `NotAuthenticatedError` exception + registered handler in `app/main.py` diff --git a/docs/CLAUDE.md b/docs/CLAUDE.md index 3311b62..69410bb 100644 --- a/docs/CLAUDE.md +++ b/docs/CLAUDE.md @@ -1,6 +1,6 @@ -# Documentation Folder +# Documentation -This folder contains business planning, architecture decisions, and documentation. +This folder contains architecture, API reference, and development documentation. **No application code belongs here.** @@ -8,15 +8,16 @@ This folder contains business planning, architecture decisions, and documentatio | Document | Purpose | |----------|---------| -| `code_guidelines.md` | Code creation guidelines and Git strategy | -| `security.md` | Python focused security code suggestions | -| `roadmap.md` | 5-phase development roadmap | -| `plans/` | Design documents and implementation plans | - +| `architecture.md` | App architecture, layers, patterns, request lifecycle | +| `API_REFERENCE.md` | All 23 endpoints with params, auth, and response details | +| `database_schema.md` | All 8 tables, columns, relationships, migrations | +| `code_guidelines.md` | Coding standards and Git strategy | +| `security.md` | Python-focused security guidance | +| `roadmap.md` | Feature status and future plans | ## Guidelines - Keep documents focused and concise - Update docs when architecture decisions change - Use markdown tables for structured information -- Include links to external documentation where relevant \ No newline at end of file +- Include links to external documentation where relevant diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d35fa3e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,197 @@ +# Architecture + +## Overview + +SneakySwole is a workout tracking and programming app built with FastAPI, HTMX, and SQLite. It follows a layered architecture with strict separation of concerns: routes handle HTTP, services handle business logic and data access, and models define the schema. + +All user interactions return HTML (full pages or HTMX partials) — there are no JSON APIs except the health check. + +--- + +## Technology Stack + +| Layer | Technology | Notes | +|-------|-----------|-------| +| Web framework | FastAPI + Uvicorn | Async, Python 3.12 | +| Frontend | Jinja2 + HTMX + Pico CSS | Dark theme, no JS framework | +| Database | SQLite3 + SQLModel ORM | Single file at `data/sneakyswole.db` | +| Migrations | Alembic | Schema versioning, auto-generated DDL | +| Auth | bcrypt + itsdangerous | Hashed passwords, signed session cookies | +| Logging | structlog | Structured JSON logging | +| Config | pydantic-settings | Typed `.env` loader with validation | +| Container | Docker + docker-compose | Single service, port 8000, named volume | +| Testing | pytest | 30+ test modules | +| Dependencies | uv + requirements.txt | Pinned versions | + +--- + +## Application Layers + +### 1. Routes (`app/routes/`) + +HTTP handlers that parse requests, call services, and return rendered templates. Each file covers one domain. + +| File | Prefix | Purpose | +|------|--------|---------| +| `auth.py` | `/login`, `/logout` | Login form, credential verification, session cookies | +| `pages.py` | `/` | Home page | +| `profiles.py` | `/profiles` | Profile list, switch, edit | +| `workouts.py` | `/workouts` | Workout day list and detail viewer | +| `exercises.py` | `/exercises` | Exercise browser with HTMX search | +| `logging.py` | `/log` | Inline set logging (create/edit/delete) | +| `history.py` | `/history` | Past session list and detail | +| `dashboard.py` | `/dashboard` | Progress stats, volume charts, per-exercise progress | +| `schedule.py` | `/schedule` | 4-week calendar view | +| `health.py` | `/health` | JSON health check | + +**Rules:** +- Routes never query the database directly — all data access goes through services +- All routes (except `/login`, `/logout`, `/health`) require admin authentication via `@Depends(get_current_admin_user)` +- Routes return `TemplateResponse` (HTML), not JSON + +### 2. Services (`app/services/`) + +Business logic and all database access. Each service is instantiated with a SQLModel `Session`. + +| Service | File | Responsibility | +|---------|------|---------------| +| `AuthService` | `auth_service.py` | bcrypt password verification, session token creation/validation (itsdangerous) | +| `UserService` | `user_service.py` | User CRUD, list profiles, update stats | +| `ExerciseService` | `exercise_service.py` | Exercise queries (by day, muscle group), warmup listing, workout day listing | +| `LogService` | `log_service.py` | Workout log CRUD (create/read/update/delete sets) | +| `WorkoutSessionService` | `workout_session_service.py` | Session management (get_or_create, list, lookup) | +| `ProgressionService` | `progression_service.py` | Auto-progression suggestions (+reps/+weight/deload) | +| `AnalyticsService` | `analytics_service.py` | User stats, volume by day, per-exercise progress data | +| `SeedService` | `seed_service.py` | YAML-driven database initialization from `config/` files | + +### 3. Models (`app/models/`) + +SQLModel ORM classes defining 8 database tables. See `docs/database_schema.md` for full details. + +### 4. Utils (`app/utils/`) + +Shared utilities — currently just auth dependencies: + +- **`auth.py`** — `get_current_admin_user()` (FastAPI dependency), `get_active_profile_id()`, `NotAuthenticatedError`, `SESSION_COOKIE_NAME` + +### 5. Templates (`app/templates/`) + +Jinja2 templates split into full pages and reusable HTMX partials. + +- **`base.html`** — Master layout with Pico CSS dark theme, HTMX script, nav bar +- **`pages/`** — 12 full-page templates (login, home, dashboard, workout_day, etc.) +- **`partials/`** — 13 HTMX fragment templates (exercise_card, log_form, nav, stats_card, etc.) + +### 6. Configuration + +- **`app/config.py`** — Typed `Settings` class (pydantic-settings), singleton via `@lru_cache` +- **`.env`** — Runtime secrets (gitignored), referenced by docker-compose +- **`.env.example`** — Template documenting required variables +- **`config/exercises.yaml`** — Exercise library (name, muscle group, sets, tempo, form cues) +- **`config/user_programs.yaml`** — Per-user programming targets (week 1/4 reps and weights) + +--- + +## Key Patterns + +### Authentication Flow + +1. Admin submits credentials via `POST /login` +2. `AuthService.authenticate()` verifies bcrypt hash +3. On success, `AuthService.create_session_token()` creates a signed token (itsdangerous) +4. Token stored in httponly cookie (`session`), samesite=lax, 24h TTL +5. `get_current_admin_user()` dependency validates token on every protected route +6. Invalid/missing token raises `NotAuthenticatedError` → 302 redirect to `/login` + +### Profile Switching + +- Admin selects active profile via `POST /profiles/switch` +- Profile ID stored in separate httponly cookie (`active_profile_id`) +- `get_active_profile_id()` extracts it from the request +- Workout logging happens under the active profile + +### NavContextMiddleware + +Starlette middleware (`app/main.py:NavContextMiddleware`) runs on every request: +1. Reads session cookie and validates token +2. If valid admin: loads `admin`, `profiles` list, and `active_profile` into `request.state` +3. Templates read from `request.state` to render the nav bar (profile switcher, etc.) + +### HTMX Partial Pattern + +All dynamic updates use HTMX with HTML fragment responses: +- Filter dropdowns trigger `hx-get` to `/exercises/search` → returns `partials/exercise_list.html` +- Log form submits `hx-post` to `/log` → returns `partials/log_entry.html` +- No JSON APIs, no fetch calls, no vanilla JS + +### Database Startup + +1. `create_app()` creates SQLModel engine and calls `SQLModel.metadata.create_all()` +2. `@app.on_event("startup")` triggers `SeedService.seed_all()` +3. Seed service reads `config/exercises.yaml` + `config/user_programs.yaml` +4. Inserts exercises, warmups, workout days, user profiles, and programming targets +5. Creates admin user from `.env` credentials (bcrypt-hashed) +6. Skips seeding if data already exists + +--- + +## Request Lifecycle + +``` +Client Request + ↓ +NavContextMiddleware (injects admin/profiles/active_profile into request.state) + ↓ +FastAPI Router (matches route) + ↓ +Auth Dependency (get_current_admin_user — validates session cookie) + ↓ +Route Handler (parses request, calls service(s)) + ↓ +Service Layer (business logic, DB queries via SQLModel Session) + ↓ +Jinja2 TemplateResponse (renders full page or HTMX partial) + ↓ +Client Response (HTML) +``` + +--- + +## Docker Setup + +- **`Dockerfile`** — Slim Python 3.12 base, copies app + config, installs deps, runs Uvicorn +- **`docker-compose.yaml`** — Single `app` service, port 8000, named volume for `data/`, `.env` file +- Static files and templates are baked into the image +- SQLite DB persists via Docker volume mount at `data/` + +--- + +## Directory Structure + +``` +SneakySwole/ +├── app/ +│ ├── __init__.py +│ ├── config.py # Typed settings (pydantic-settings) +│ ├── database.py # Engine factory + session dependency +│ ├── logging_config.py # structlog setup +│ ├── main.py # App factory, middleware, router registration +│ ├── models/ # SQLModel ORM (8 tables) +│ ├── routes/ # FastAPI handlers (10 modules) +│ ├── services/ # Business logic + DB access (8 services) +│ ├── static/css/ # Pico CSS overrides +│ ├── templates/ # Jinja2 (pages/ + partials/) +│ └── utils/ # Auth dependencies +├── config/ # YAML seed data +│ ├── exercises.yaml +│ └── user_programs.yaml +├── alembic/ # Database migrations +├── tests/ # pytest test suite +├── data/ # SQLite DB (gitignored, Docker volume) +├── docs/ # Documentation +├── Dockerfile +├── docker-compose.yaml +├── pyproject.toml +├── requirements.txt +└── .env.example +``` diff --git a/docs/database_schema.md b/docs/database_schema.md new file mode 100644 index 0000000..8f7f9a8 --- /dev/null +++ b/docs/database_schema.md @@ -0,0 +1,202 @@ +# Database Schema + +SQLite database at `data/sneakyswole.db`, managed by SQLModel ORM with Alembic migrations. + +--- + +## Tables + +### `users` + +User profiles (admin and regular). Admin has login credentials; regular profiles are managed by the admin. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `username` | VARCHAR | UNIQUE, indexed | Login identifier | +| `password_hash` | VARCHAR | default="" | bcrypt hash (admin only) | +| `display_name` | VARCHAR | default="" | Name shown in UI | +| `height` | VARCHAR | nullable | e.g., "6'0\"" | +| `weight` | VARCHAR | nullable | e.g., "260 lbs" | +| `goals` | VARCHAR | nullable | Free-text training goals | +| `is_admin` | BOOLEAN | default=False | Admin privileges flag | +| `created_at` | DATETIME | auto | Record creation time | +| `updated_at` | DATETIME | auto | Last update time | + +**Model:** `app/models/user.py:User` + +--- + +### `exercises` + +Exercise library catalog. Each exercise belongs to a workout day. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `name` | VARCHAR | indexed | e.g., "DB Chest Press (Floor)" | +| `muscle_group` | VARCHAR | default="" | e.g., "Chest", "Shoulders" | +| `workout_day` | VARCHAR | indexed | "Push", "Pull", "Lower", "Full Body" | +| `sets` | INTEGER | default=3 | Default number of sets | +| `tempo` | VARCHAR | default="" | e.g., "3-1-2" (eccentric-pause-concentric) | +| `form_cues` | VARCHAR | default="" | Detailed form instructions | +| `created_at` | DATETIME | auto | Record creation time | + +**Model:** `app/models/exercise.py:Exercise` + +--- + +### `warmups` + +Standardized warmup routine displayed before every workout. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `name` | VARCHAR | indexed | e.g., "Cat / Cow" | +| `type` | VARCHAR | default="" | Category: "Thoracic Mob", "Hip Mobility", etc. | +| `reps` | VARCHAR | default="" | e.g., "8 reps", "8 each side" | +| `form_cues` | VARCHAR | default="" | Detailed form instructions | +| `sort_order` | INTEGER | default=0 | Display order in warmup sequence | +| `created_at` | DATETIME | auto | Record creation time | + +**Model:** `app/models/warmup.py:Warmup` + +--- + +### `workout_days` + +The 4-day training split definition. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `name` | VARCHAR | UNIQUE, indexed | "Push", "Pull", "Lower", "Full Body" | +| `day_number` | INTEGER | UNIQUE | Order in rotation (1-4) | +| `description` | VARCHAR | default="" | Brief focus description | + +**Model:** `app/models/workout_day.py:WorkoutDay` + +--- + +### `user_exercise_programs` + +Per-user programming targets linking users to exercises with week 1/4 goals. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `user_id` | INTEGER | FK → `users.id`, indexed | Profile this applies to | +| `exercise_id` | INTEGER | FK → `exercises.id`, indexed | Exercise being programmed | +| `wk1_reps` | VARCHAR | default="" | Week 1 target reps (e.g., "10", "30 sec") | +| `wk4_reps` | VARCHAR | default="" | Week 4 target reps | +| `wk1_weight` | VARCHAR | default="" | Week 1 target weight (e.g., "30 lbs", "BW") | +| `wk4_weight` | VARCHAR | default="" | Week 4 target weight | +| `created_at` | DATETIME | auto | Record creation time | +| `updated_at` | DATETIME | auto | Last update time | + +**Model:** `app/models/user_exercise_program.py:UserExerciseProgram` + +--- + +### `workout_sessions` + +A completed workout session — ties a user to a workout day on a date. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `user_id` | INTEGER | FK → `users.id`, indexed | Who did the workout | +| `workout_day_id` | INTEGER | FK → `workout_days.id` | Which day was trained | +| `date` | DATE | default=today | Date the workout was performed | +| `notes` | VARCHAR | nullable | Free-text session notes | +| `created_at` | DATETIME | auto | Record creation time | + +**Model:** `app/models/workout_session.py:WorkoutSession` + +--- + +### `workout_logs` + +Individual set logs within a session. Each row = one set of one exercise. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `session_id` | INTEGER | FK → `workout_sessions.id`, indexed | Parent session | +| `exercise_id` | INTEGER | FK → `exercises.id` | Which exercise | +| `set_number` | INTEGER | default=1 | Set number (1, 2, 3...) | +| `reps_completed` | INTEGER | default=0 | Actual reps performed | +| `weight_used` | VARCHAR | default="" | Weight used (e.g., "30 lbs", "BW") | +| `felt_easy` | BOOLEAN | default=False | Progression signal | +| `notes` | VARCHAR | nullable | Per-set notes | +| `created_at` | DATETIME | auto | Record creation time | + +**Model:** `app/models/workout_log.py:WorkoutLog` + +--- + +### `progress_log` + +Progression tracking — what the engine suggested vs what the user actually did. + +| Column | Type | Constraints | Description | +|--------|------|------------|-------------| +| `id` | INTEGER | PK, auto-increment | | +| `user_id` | INTEGER | FK → `users.id`, indexed | Profile being tracked | +| `exercise_id` | INTEGER | FK → `exercises.id` | Exercise being tracked | +| `date` | DATE | default=today | Date of progression entry | +| `suggested_reps` | INTEGER | nullable | Engine recommendation | +| `suggested_weight` | VARCHAR | nullable | Engine recommendation | +| `actual_reps` | INTEGER | nullable | What user actually did | +| `actual_weight` | VARCHAR | nullable | What user actually used | +| `progression_applied` | VARCHAR | nullable | Type: "reps_increase", "weight_increase", "deload" | +| `created_at` | DATETIME | auto | Record creation time | + +**Model:** `app/models/progress_log.py:ProgressLog` + +--- + +## Relationships + +``` +users + ├── user_exercise_programs (1:N via user_id) + ├── workout_sessions (1:N via user_id) + └── progress_log (1:N via user_id) + +exercises + ├── user_exercise_programs (1:N via exercise_id) + ├── workout_logs (1:N via exercise_id) + └── progress_log (1:N via exercise_id) + +workout_days + └── workout_sessions (1:N via workout_day_id) + +workout_sessions + └── workout_logs (1:N via session_id) +``` + +--- + +## Migrations + +Managed by Alembic. Config at `alembic.ini`, migration scripts at `alembic/versions/`. + +- **Initial migration:** `1855836abf6c_initial_schema_8_tables.py` — creates all 8 tables + +To create a new migration: +```bash +alembic revision --autogenerate -m "description" +alembic upgrade head +``` + +--- + +## Seeding + +On first startup, `SeedService.seed_all()` reads: +- `config/exercises.yaml` — exercise catalog + warmups + workout days +- `config/user_programs.yaml` — per-user week 1/4 targets + +Admin user is created from `ADMIN_USERNAME` / `ADMIN_PASSWORD` env vars with bcrypt hash. Seeding is skipped if data already exists. diff --git a/docs/plans/2026-02-23-roadmap-and-data-design.md b/docs/plans/2026-02-23-roadmap-and-data-design.md deleted file mode 100644 index 3e0b201..0000000 --- a/docs/plans/2026-02-23-roadmap-and-data-design.md +++ /dev/null @@ -1,76 +0,0 @@ -# Design: Roadmap, Exercise Data, and Auth Model - -**Date:** 2026-02-23 -**Status:** Approved - -## Overview - -This document captures the design decisions for the SneakySwole roadmap structure, exercise data management, and authentication model. - -## Roadmap Structure - -Five phases, each producing a testable milestone: - -1. **Scaffold & Infrastructure** — project structure, Docker, base template, logging -2. **Data Layer & Seeding** — SQLite schema, Alembic migrations, YAML-based exercise/program seeding -3. **Workout UI** — admin auth, profile management, workout viewer, exercise browser -4. **Logging & Tracking** — per-exercise logging, session tracking, log history -5. **Progression & Analytics** — auto-progression, schedule view, progress dashboard - -Full details in `docs/roadmap.md`. - -## Exercise Data Design - -### Separated YAML Files - -Exercise data is split into two config files for clean separation of concerns: - -- **`config/exercises.yaml`** — Universal exercise library - - Exercise name, muscle group, workout day, sets, tempo, form cues - - Warmup exercises (name, type, reps/time, form cues) - - Rarely changes; defines the available exercise catalog - -- **`config/user_programs.yaml`** — Per-user programming - - References exercises by name - - Week 1 and week 4 reps and weights per user per exercise - - Changes when users adjust their programming or new users are added - -### Seeding Flow - -1. On first run (empty DB), the seed script reads both YAML files -2. Exercises and warmups are inserted into their respective tables -3. User profiles are created (admin from `.env`, others from user_programs.yaml) -4. User-specific programming (reps/weights) is linked to exercises and users -5. Subsequent runs skip seeding if data already exists - -### Why YAML Over Spreadsheet - -- Human-readable and version-controllable -- Easy to update without special tools -- Can be validated with schema checks -- Spreadsheet remains source of truth for initial data extraction only - -## Authentication Model - -### Phase 1: Simple Admin Auth - -- Single admin user with credentials stored in `.env` (`ADMIN_USERNAME`, `ADMIN_PASSWORD`) -- On first run, admin user is created in DB with bcrypt-hashed password -- Admin logs in via a simple login form (session-based auth) -- Admin creates user profiles through the UI (no login for profiles) -- Profile switcher in navigation allows admin to select the active profile -- All workout logging happens under the selected profile - -### Security Considerations - -- Passwords hashed with bcrypt (never stored plaintext) -- `.env` file is gitignored and never committed -- `.env.example` documents required variables without real values -- Session tokens with reasonable expiry -- Admin-only routes protected by auth middleware - -### Future Upgrade Path - -- Phase 1 auth is designed to be replaceable -- If multi-user login is needed later, add login credentials to the user profiles table -- Profile switcher becomes unnecessary when each user logs in independently diff --git a/docs/roadmap.md b/docs/roadmap.md index bc5cd00..3fb78c0 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,85 +1,30 @@ # SneakySwole Roadmap -## Phase 1: Scaffold & Infrastructure +## Completed -Set up the project foundation — everything needed before writing features. +### Phase 1: Scaffold & Infrastructure +FastAPI project structure, Dockerfile + docker-compose.yaml, Pico CSS dark theme base template, `.env` config, structlog logging, health check endpoint. -- FastAPI project structure (`app/` with routes, services, models, templates, static, utils) -- Dockerfile + docker-compose.yaml (SQLite volume, port 8000, `.env` support) -- Pico CSS dark theme base template (Jinja2 with `data-theme="dark"`) -- `.env` / `.env.example` with admin credentials (`ADMIN_USERNAME`, `ADMIN_PASSWORD`) -- `uv` for dependency management, `requirements.txt` with pinned versions -- Structlog logging setup -- Basic health check route (`/health`) +### Phase 2: Data Layer & Seeding +8-table SQLite schema via SQLModel + Alembic migrations. YAML-driven seed script (`config/exercises.yaml`, `config/user_programs.yaml`). Service layer for all DB access. Admin user auto-created from `.env` with bcrypt. -## Phase 2: Data Layer & Seeding +### Phase 3: Workout UI +Admin login (bcrypt + signed session cookies), profile switcher, workout day viewer with warmups and exercise cards, HTMX-powered exercise browser with search/filter. NavContextMiddleware for automatic template context. -Build the database schema and seed it from YAML config files. +### Phase 4: Logging & Tracking +Inline set logging from the workout day view via HTMX. Auto-created workout sessions. Log history with per-session detail view. Edit and delete log entries. -- SQLite schema via Alembic migrations - - Tables: users, exercises, warmups, workout_days, user_exercise_programs, sets, progress_log -- `config/exercises.yaml` — exercise library (name, muscle group, workout day, sets, tempo, form cues) -- `config/user_programs.yaml` — per-user week 1/4 reps and weights for each exercise -- Seed script that loads YAML into DB on first run -- Admin user auto-created from `.env` credentials on startup (password hashed with bcrypt) -- Two initial user profiles seeded: Phillip and Daughter -- Service layer for all DB access (no direct queries from routes) +### Phase 5: Progression & Analytics +Auto-progression engine (+reps/+weight/deload rules), 4-week schedule calendar, progress dashboard with Chart.js charts, per-exercise progress pages with suggestions. -### Phase 3: Workout UI ✅ +--- -**Completed:** 2026-02-24 +## Future Ideas -**Summary:** Built the core user-facing experience — admin login with bcrypt + signed session cookies, profile switcher, workout day viewer with warmups and exercise cards, and HTMX-powered exercise browser with search/filter. Added Jinja2 context processor middleware for automatic nav context injection. - -**Key files:** -- `app/services/auth_service.py` — AuthService (bcrypt auth + itsdangerous session tokens) -- `app/utils/auth.py` — `get_current_admin_user` (303 redirect to /login), `get_active_profile_id` -- `app/routes/auth.py` — login/logout routes -- `app/routes/profiles.py` — profile list, switch, edit routes -- `app/routes/workouts.py` — workout day list + detail viewer -- `app/routes/exercises.py` — exercise browser with HTMX search -- `app/templates/partials/nav.html` — profile switcher dropdown (reads from request.state) -- `app/main.py` — NavContextMiddleware, secret_key, all routers registered - -**Endpoints created:** -- `GET /login` — render login form -- `POST /login` — authenticate and set session cookie -- `GET /logout` — clear session, redirect to /login -- `GET /profiles` — list user profiles -- `POST /profiles/switch` — set active profile cookie -- `GET /profiles/{id}/edit` — profile edit form -- `POST /profiles/{id}/edit` — update profile -- `GET /workouts` — workout day cards -- `GET /workouts/{day_name}` — warmups + exercises + programming targets -- `GET /exercises` — exercise browser with filter dropdowns -- `GET /exercises/search` — HTMX partial for filtered exercise list - -**Key details:** -- Auth uses 303 redirect to /login (not 401) for browser UX -- Nav context injected via `NavContextMiddleware` into `request.state` (admin, profiles, active_profile) -- Session cookie: httponly=True, samesite="lax", max_age=86400 (24h) -- `itsdangerous>=2.2.0` added to requirements.txt -- `python-multipart>=0.0.20` added for form data parsing -- 66 tests pass (12 new Phase 3 tests + 54 existing) - -## Phase 4: Logging & Tracking - -Enable workout logging so users can track what they actually did. - -- Per-exercise logging: sets completed, reps, weight used, "felt easy?" toggle -- Workout session model (date, user profile, workout day) -- HTMX inline logging — log directly from the workout day view -- Log history view per user profile -- Edit/delete past log entries - -## Phase 5: Progression & Analytics - -Smart suggestions and visual progress tracking. - -- Auto-progression engine based on log history - - +1-2 reps/week, +5 lbs every 2 weeks - - Deload detection at week 5 (-20% weight) -- 4-week schedule view (calendar-style, shows which day maps to which date) -- Progress dashboard per user profile -- Per-exercise progress history and trends -- Summary stats (total volume, streak tracking) +- Multi-user login (replace profile switcher with individual logins) +- REST API for mobile clients +- Exercise video/image attachments +- Custom workout program builder +- Export/import workout data (CSV/JSON) +- Notifications/reminders +- Social features (sharing workouts) diff --git a/workout_plan_v2.xlsx b/workout_plan_v2.xlsx deleted file mode 100644 index 2bfa387..0000000 Binary files a/workout_plan_v2.xlsx and /dev/null differ