Files
SneakySwole/app/routes/logging.py
Phillip Tarrant c5a7728818
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 36s
Merge origin/master: integrate auto-populate suggestions and set renumbering
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 14:43:14 -05:00

207 lines
6.3 KiB
Python

"""Workout logging routes for inline set tracking.
Handles creating, editing, and deleting individual set logs.
All responses are HTMX partials that update in place.
"""
from datetime import date
import structlog
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlmodel import Session
from app.database import get_db_session
from app.models.user import User
from app.services.log_service import LogService
from app.services.progression_service import ProgressionService
from app.services.workout_session_service import WorkoutSessionService
from app.utils.auth import require_active_profile, get_active_profile_id
logger = structlog.get_logger(__name__)
router = APIRouter(prefix="/log", tags=["logging"])
def _normalize_weight(raw: str) -> str:
"""Convert numeric weight input to display format.
'0' or '' -> 'BW', bare number -> '{n} lbs', already formatted -> pass through.
"""
raw = raw.strip()
if not raw or raw == "0":
return "BW"
try:
num = float(raw)
if num == int(num):
return f"{int(num)} lbs"
return f"{num} lbs"
except ValueError:
return raw
def _get_prefill_values(
logs: list,
session: Session,
profile_id: int,
exercise_id: int,
) -> tuple:
"""Get pre-fill values for the next set form.
If sets have already been logged this session, use the last logged
set's values (users typically repeat the same reps/weight across sets).
Otherwise, use the progression engine's suggestion.
Returns:
(suggested_reps, suggested_weight) tuple.
"""
if logs:
last = logs[-1]
return last.reps_completed, last.weight_used
progression = ProgressionService(session)
suggestion = progression.get_suggestion(profile_id, exercise_id)
return suggestion.get("suggested_reps"), suggestion.get("suggested_weight")
@router.post("", response_class=HTMLResponse)
async def log_set(
request: Request,
session: Session = Depends(get_db_session),
profile: User = Depends(require_active_profile),
):
"""Log a single set for an exercise.
Creates the workout session if it doesn't exist yet (auto-create).
Returns the updated log entries partial for this exercise.
"""
form = await request.form()
exercise_id = int(form.get("exercise_id", 0))
workout_day_id = int(form.get("workout_day_id", 0))
set_number = int(form.get("set_number", 1))
reps = int(form.get("reps", 0))
weight = _normalize_weight(form.get("weight", ""))
felt_easy = form.get("felt_easy") == "on"
# Get or create today's session
ws_service = WorkoutSessionService(session)
ws = ws_service.get_or_create_session(
user_id=profile.id,
workout_day_id=workout_day_id,
session_date=date.today(),
)
# Create the log entry
log_service = LogService(session)
log_service.create_log(
session_id=ws.id,
exercise_id=exercise_id,
set_number=set_number,
reps_completed=reps,
weight_used=weight,
felt_easy=felt_easy,
)
# Return updated logs for this exercise
logs = log_service.list_logs_for_exercise(ws.id, exercise_id)
next_set = len(logs) + 1
suggested_reps, suggested_weight = _get_prefill_values(
logs, session, profile.id, exercise_id,
)
templates = request.app.state.templates
return templates.TemplateResponse("partials/log_entry.html", {
"request": request,
"logs": logs,
"exercise_id": exercise_id,
"workout_day_id": workout_day_id,
"next_set": next_set,
"session_id": ws.id,
"suggested_reps": suggested_reps,
"suggested_weight": suggested_weight,
})
@router.post("/{log_id}/edit", response_class=HTMLResponse)
async def edit_log(
log_id: int,
request: Request,
session: Session = Depends(get_db_session),
profile: User = Depends(require_active_profile),
):
"""Edit an existing log entry."""
form = await request.form()
log_service = LogService(session)
log_service.update_log(
log_id,
reps_completed=int(form.get("reps", 0)),
weight_used=_normalize_weight(form.get("weight", "")),
felt_easy=form.get("felt_easy") == "on",
notes=form.get("notes"),
)
log = log_service.get_log_by_id(log_id)
logs = log_service.list_logs_for_exercise(log.session_id, log.exercise_id)
next_set = len(logs) + 1
active_profile_id = get_active_profile_id(request)
suggested_reps, suggested_weight = None, None
if active_profile_id:
suggested_reps, suggested_weight = _get_prefill_values(
logs, session, active_profile_id, log.exercise_id,
)
templates = request.app.state.templates
return templates.TemplateResponse("partials/log_entry.html", {
"request": request,
"logs": logs,
"exercise_id": log.exercise_id,
"workout_day_id": 0,
"next_set": next_set,
"session_id": log.session_id,
"suggested_reps": suggested_reps,
"suggested_weight": suggested_weight,
})
@router.post("/{log_id}/delete", response_class=HTMLResponse)
async def delete_log(
log_id: int,
request: Request,
session: Session = Depends(get_db_session),
profile: User = Depends(require_active_profile),
):
"""Delete a log entry."""
log_service = LogService(session)
log = log_service.get_log_by_id(log_id)
if log:
exercise_id = log.exercise_id
session_id = log.session_id
log_service.delete_log(log_id)
logs = log_service.list_logs_for_exercise(session_id, exercise_id)
next_set = len(logs) + 1
active_profile_id = get_active_profile_id(request)
suggested_reps, suggested_weight = None, None
if active_profile_id:
suggested_reps, suggested_weight = _get_prefill_values(
logs, session, active_profile_id, exercise_id,
)
templates = request.app.state.templates
return templates.TemplateResponse("partials/log_entry.html", {
"request": request,
"logs": logs,
"exercise_id": exercise_id,
"workout_day_id": 0,
"next_set": next_set,
"session_id": session_id,
"suggested_reps": suggested_reps,
"suggested_weight": suggested_weight,
})
return HTMLResponse("")