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>
This commit is contained in:
197
docs/architecture.md
Normal file
197
docs/architecture.md
Normal 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
|
||||
```
|
||||
Reference in New Issue
Block a user