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>
198 lines
8.2 KiB
Markdown
198 lines
8.2 KiB
Markdown
# 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
|
|
```
|