Compare commits

..

3 Commits

Author SHA1 Message Date
77d1bc4a25 chore: add uv.lock for reproducible dependency resolution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:20:39 -06:00
89d70fcae7 Merge branch 'docs/cleanup-implementation-files' 2026-02-24 14:19:54 -06:00
53e62f694f docs: replace implementation plans with architecture and API reference docs
Remove phase implementation plans, design notes, and source spreadsheet
that are no longer needed. Add architecture.md, API_REFERENCE.md, and
database_schema.md for ongoing development and debugging reference.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 14:19:49 -06:00
8 changed files with 714 additions and 159 deletions

278
docs/API_REFERENCE.md Normal file
View File

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

View File

@@ -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.** **No application code belongs here.**
@@ -8,11 +8,12 @@ This folder contains business planning, architecture decisions, and documentatio
| Document | Purpose | | Document | Purpose |
|----------|---------| |----------|---------|
| `code_guidelines.md` | Code creation guidelines and Git strategy | | `architecture.md` | App architecture, layers, patterns, request lifecycle |
| `security.md` | Python focused security code suggestions | | `API_REFERENCE.md` | All 23 endpoints with params, auth, and response details |
| `roadmap.md` | 5-phase development roadmap | | `database_schema.md` | All 8 tables, columns, relationships, migrations |
| `plans/` | Design documents and implementation plans | | `code_guidelines.md` | Coding standards and Git strategy |
| `security.md` | Python-focused security guidance |
| `roadmap.md` | Feature status and future plans |
## Guidelines ## Guidelines

197
docs/architecture.md Normal file
View File

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

202
docs/database_schema.md Normal file
View File

@@ -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.

View File

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

View File

@@ -1,85 +1,30 @@
# SneakySwole Roadmap # 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) ### Phase 2: Data Layer & Seeding
- Dockerfile + docker-compose.yaml (SQLite volume, port 8000, `.env` support) 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.
- 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 ### 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 ### Phase 5: Progression & Analytics
- Tables: users, exercises, warmups, workout_days, user_exercise_programs, sets, progress_log Auto-progression engine (+reps/+weight/deload rules), 4-week schedule calendar, progress dashboard with Chart.js charts, per-exercise progress pages with suggestions.
- `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 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. - Multi-user login (replace profile switcher with individual logins)
- REST API for mobile clients
**Key files:** - Exercise video/image attachments
- `app/services/auth_service.py` — AuthService (bcrypt auth + itsdangerous session tokens) - Custom workout program builder
- `app/utils/auth.py``get_current_admin_user` (303 redirect to /login), `get_active_profile_id` - Export/import workout data (CSV/JSON)
- `app/routes/auth.py` — login/logout routes - Notifications/reminders
- `app/routes/profiles.py` — profile list, switch, edit routes - Social features (sharing workouts)
- `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)

8
uv.lock generated Normal file
View File

@@ -0,0 +1,8 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "sneakyswole"
version = "0.1.0"
source = { editable = "." }

Binary file not shown.