Merge origin/master: integrate auto-populate suggestions and set renumbering
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 36s

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 14:43:14 -05:00
4 changed files with 75 additions and 4 deletions

View File

@@ -14,6 +14,7 @@ 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
@@ -25,7 +26,7 @@ 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.
'0' or '' -> 'BW', bare number -> '{n} lbs', already formatted -> pass through.
"""
raw = raw.strip()
if not raw or raw == "0":
@@ -39,6 +40,30 @@ def _normalize_weight(raw: str) -> str:
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,
@@ -80,6 +105,9 @@ async def log_set(
# 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", {
@@ -89,6 +117,8 @@ async def log_set(
"workout_day_id": workout_day_id,
"next_set": next_set,
"session_id": ws.id,
"suggested_reps": suggested_reps,
"suggested_weight": suggested_weight,
})
@@ -115,6 +145,13 @@ async def edit_log(
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,
@@ -123,6 +160,8 @@ async def edit_log(
"workout_day_id": 0,
"next_set": next_set,
"session_id": log.session_id,
"suggested_reps": suggested_reps,
"suggested_weight": suggested_weight,
})
@@ -145,6 +184,13 @@ async def delete_log(
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,
@@ -153,6 +199,8 @@ async def delete_log(
"workout_day_id": 0,
"next_set": next_set,
"session_id": session_id,
"suggested_reps": suggested_reps,
"suggested_weight": suggested_weight,
})
return HTMLResponse("")

View File

@@ -149,7 +149,8 @@ class LogService:
def delete_log(self, log_id: int) -> None:
"""Delete a log entry.
Removes the log and cleans up the parent session if no logs remain.
Removes the log, renumbers remaining sets, and cleans up the
parent session if no logs remain.
Args:
log_id: The log entry ID.
@@ -162,15 +163,32 @@ class LogService:
raise ValueError(f"WorkoutLog with id {log_id} not found")
session_id = log.session_id
exercise_id = log.exercise_id
self._session.delete(log)
self._session.commit()
logger.info("log_deleted", log_id=log_id)
# Clean up orphaned session if no logs remain
# Renumber remaining sets so they stay sequential (1, 2, 3...)
remaining = self._session.exec(
select(WorkoutLog)
.where(
WorkoutLog.session_id == session_id,
WorkoutLog.exercise_id == exercise_id,
)
.order_by(WorkoutLog.set_number)
).all()
for i, remaining_log in enumerate(remaining, start=1):
if remaining_log.set_number != i:
remaining_log.set_number = i
self._session.add(remaining_log)
if remaining:
self._session.commit()
# Clean up orphaned session if no logs remain for ANY exercise
any_remaining = self._session.exec(
select(WorkoutLog).where(WorkoutLog.session_id == session_id)
).first()
if remaining is None:
if any_remaining is None:
ws = self._session.get(WorkoutSession, session_id)
if ws:
self._session.delete(ws)

View File

@@ -42,6 +42,10 @@
<!-- Inline logging (Phase 4) -->
{% if active_profile %}
<div id="logs-exercise-{{ exercise.id }}">
{% if suggestions and suggestions[exercise.id] %}
{% set suggested_reps = suggestions[exercise.id].suggested_reps %}
{% set suggested_weight = suggestions[exercise.id].suggested_weight %}
{% endif %}
{% if existing_logs and existing_logs[exercise.id] %}
{% set suggested_reps = existing_logs[exercise.id][-1].reps_completed %}
{% set suggested_weight = existing_logs[exercise.id][-1].weight_used %}

View File

@@ -11,6 +11,7 @@
<small style="white-space:nowrap; opacity:0.7;">Set {{ next_set|default(1) }}</small>
<input type="number" name="reps" placeholder="Reps"
min="0" max="100" required
{% if suggested_reps %}value="{{ suggested_reps }}"{% endif %}
style="width:5rem; margin-bottom:0;">
<input type="number" name="weight" placeholder="Weight (lbs)"
min="0" max="999" step="0.5" required