feat: replace wk1/wk4 targets with 6→8→10→12 rep ladder progression
Simplifies the progression model to a universal rep ladder: every exercise follows 6→8→10→12 reps at current weight, then +5 lbs and reset to 6. Replaces per-user wk1/wk4 rep and weight targets with a single starting_weight field. - Add Alembic migration to drop wk1_reps/wk4_reps/wk1_weight/wk4_weight, add starting_weight (migrated from wk1_weight) - Run Alembic migrations on app startup instead of create_all, with auto-detection and stamping for legacy databases - Include alembic/ and alembic.ini in Docker image - Rewrite progression_service.get_suggestion() with ladder logic: climb, hold, weight_increase, hold_at_top, deload - Replace wk1/wk4 grid in exercise cards with rep ladder progress bar - Add color-coded progression badges by type - Change weight log input from text to number with pre-filled suggestion - Normalize weight input in routes (0→BW, bare number→N lbs) - Remove schedule page (route, template, nav link, tests) - Simplify user_programs.yaml from 4 fields to 1 per exercise - Update all tests for new schema and progression logic Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
54
app/main.py
54
app/main.py
@@ -7,10 +7,12 @@ static files, and structured logging.
|
||||
from pathlib import Path
|
||||
|
||||
import structlog
|
||||
from alembic import command as alembic_command
|
||||
from alembic.config import Config as AlembicConfig
|
||||
from fastapi import FastAPI
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from sqlmodel import SQLModel, Session
|
||||
from sqlmodel import Session
|
||||
|
||||
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
|
||||
from starlette.requests import Request as StarletteRequest
|
||||
@@ -29,7 +31,6 @@ from app.routes.profiles import router as profiles_router
|
||||
from app.routes.logging import router as logging_router
|
||||
from app.routes.workouts import router as workouts_router
|
||||
from app.routes.dashboard import router as dashboard_router
|
||||
from app.routes.schedule import router as schedule_router
|
||||
from app.services.seed_service import SeedService
|
||||
from app.services.user_service import UserService
|
||||
from app.utils.auth import NoProfileSelectedError
|
||||
@@ -64,6 +65,50 @@ class NavContextMiddleware(BaseHTTPMiddleware):
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
def _run_migrations(database_url: str) -> None:
|
||||
"""Run Alembic migrations to bring the DB schema up to date.
|
||||
|
||||
On a fresh DB this creates all tables via the initial migration.
|
||||
On an existing DB created by create_all (no alembic_version table),
|
||||
stamps the last known pre-migration revision, then upgrades.
|
||||
"""
|
||||
from sqlalchemy import create_engine as sa_create_engine, inspect
|
||||
|
||||
project_root = Path(__file__).resolve().parent.parent
|
||||
alembic_cfg = AlembicConfig(str(project_root / "alembic.ini"))
|
||||
alembic_cfg.set_main_option("script_location", str(project_root / "alembic"))
|
||||
alembic_cfg.set_main_option("sqlalchemy.url", database_url)
|
||||
|
||||
engine = sa_create_engine(database_url)
|
||||
inspector = inspect(engine)
|
||||
existing_tables = inspector.get_table_names()
|
||||
|
||||
if existing_tables and "alembic_version" not in existing_tables:
|
||||
# DB was created by create_all — stamp it at the last revision
|
||||
# that matches the current schema so migrations run from there.
|
||||
# Check which schema state we're in by looking at columns.
|
||||
columns = {c["name"] for c in inspector.get_columns("user_exercise_programs")}
|
||||
if "wk1_reps" in columns:
|
||||
# Old schema: stamp at remove-auth migration (before rep ladder)
|
||||
alembic_command.stamp(alembic_cfg, "a1b2c3d4e5f6")
|
||||
logger.info("alembic_stamped", revision="a1b2c3d4e5f6", reason="legacy_db_old_schema")
|
||||
elif "starting_weight" in columns:
|
||||
# New schema already: stamp at head
|
||||
alembic_command.stamp(alembic_cfg, "head")
|
||||
logger.info("alembic_stamped", revision="head", reason="legacy_db_new_schema")
|
||||
else:
|
||||
# Unknown state — stamp at initial and let migrations sort it out
|
||||
alembic_command.stamp(alembic_cfg, "1855836abf6c")
|
||||
logger.info("alembic_stamped", revision="1855836abf6c", reason="legacy_db_unknown")
|
||||
elif not existing_tables:
|
||||
# Fresh DB — create alembic_version table so upgrade starts from scratch
|
||||
logger.info("fresh_database_detected")
|
||||
|
||||
engine.dispose()
|
||||
alembic_command.upgrade(alembic_cfg, "head")
|
||||
logger.info("migrations_applied")
|
||||
|
||||
|
||||
def create_app() -> FastAPI:
|
||||
"""Create and configure the FastAPI application.
|
||||
|
||||
@@ -103,11 +148,10 @@ def create_app() -> FastAPI:
|
||||
app.include_router(profiles_router)
|
||||
app.include_router(workouts_router)
|
||||
app.include_router(dashboard_router)
|
||||
app.include_router(schedule_router)
|
||||
|
||||
# Database setup
|
||||
# Database setup — run Alembic migrations instead of create_all
|
||||
engine = get_engine(settings.database_url)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
_run_migrations(settings.database_url)
|
||||
app.state.engine = engine
|
||||
|
||||
# DB session dependency for routes
|
||||
|
||||
Reference in New Issue
Block a user