Files
SneakySwole/docs/architecture.md
Phillip Tarrant 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.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.pyget_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