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>
8.2 KiB
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 barpages/— 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— TypedSettingsclass (pydantic-settings), singleton via@lru_cache.env— Runtime secrets (gitignored), referenced by docker-compose.env.example— Template documenting required variablesconfig/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
- Admin submits credentials via
POST /login AuthService.authenticate()verifies bcrypt hash- On success,
AuthService.create_session_token()creates a signed token (itsdangerous) - Token stored in httponly cookie (
session), samesite=lax, 24h TTL get_current_admin_user()dependency validates token on every protected route- 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:
- Reads session cookie and validates token
- If valid admin: loads
admin,profileslist, andactive_profileintorequest.state - Templates read from
request.stateto render the nav bar (profile switcher, etc.)
HTMX Partial Pattern
All dynamic updates use HTMX with HTML fragment responses:
- Filter dropdowns trigger
hx-getto/exercises/search→ returnspartials/exercise_list.html - Log form submits
hx-postto/log→ returnspartials/log_entry.html - No JSON APIs, no fetch calls, no vanilla JS
Database Startup
create_app()creates SQLModel engine and callsSQLModel.metadata.create_all()@app.on_event("startup")triggersSeedService.seed_all()- Seed service reads
config/exercises.yaml+config/user_programs.yaml - Inserts exercises, warmups, workout days, user profiles, and programming targets
- Creates admin user from
.envcredentials (bcrypt-hashed) - 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 Uvicorndocker-compose.yaml— Singleappservice, port 8000, named volume fordata/,.envfile- 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