fix: resolve template errors, orphaned sessions, and auth redirects

- Fix exercise_id undefined error in log_form.html by using scalar
  exercise_id instead of exercise.id object reference
- Clean up orphaned WorkoutSession records when all logs are deleted
- Filter empty sessions from dashboard stats (sessions, volume, streak)
- Replace broken HTTPException auth redirect with custom exception
  handler that properly returns 302 to /login

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 14:00:34 -06:00
parent 134542b66f
commit 215ce90404
6 changed files with 42 additions and 13 deletions

View File

@@ -17,6 +17,7 @@ from sqlmodel import SQLModel, Session
from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint
from starlette.requests import Request as StarletteRequest from starlette.requests import Request as StarletteRequest
from starlette.responses import Response from starlette.responses import Response
from fastapi.responses import RedirectResponse
from app.models.user import User from app.models.user import User
from app.config import get_settings from app.config import get_settings
@@ -35,7 +36,7 @@ from app.routes.schedule import router as schedule_router
from app.services.seed_service import SeedService from app.services.seed_service import SeedService
from app.services.auth_service import AuthService from app.services.auth_service import AuthService
from app.services.user_service import UserService from app.services.user_service import UserService
from app.utils.auth import SESSION_COOKIE_NAME from app.utils.auth import SESSION_COOKIE_NAME, NotAuthenticatedError
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
@@ -91,6 +92,11 @@ def create_app() -> FastAPI:
version="0.1.0", version="0.1.0",
) )
# Redirect unauthenticated requests to login
@app.exception_handler(NotAuthenticatedError)
async def _not_authenticated_handler(request, exc):
return RedirectResponse(url="/login", status_code=302)
# Mount static files # Mount static files
app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")

View File

@@ -48,23 +48,28 @@ class AnalyticsService:
Dict with keys: total_sessions, total_volume, total_sets, Dict with keys: total_sessions, total_volume, total_sets,
current_streak, last_workout_date. current_streak, last_workout_date.
""" """
sessions = self._session.exec( all_sessions = self._session.exec(
select(WorkoutSession) select(WorkoutSession)
.where(WorkoutSession.user_id == user_id) .where(WorkoutSession.user_id == user_id)
.order_by(WorkoutSession.date.desc()) .order_by(WorkoutSession.date.desc())
).all() ).all()
total_sessions = len(sessions)
# Only count sessions that still have log entries
sessions = []
total_volume = 0.0 total_volume = 0.0
total_sets = 0 total_sets = 0
for ws in sessions: for ws in all_sessions:
logs = self._session.exec( logs = self._session.exec(
select(WorkoutLog).where(WorkoutLog.session_id == ws.id) select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
).all() ).all()
if not logs:
continue
sessions.append(ws)
for log_entry in logs: for log_entry in logs:
total_sets += 1 total_sets += 1
weight = _weight_to_float(log_entry.weight_used) weight = _weight_to_float(log_entry.weight_used)
total_volume += log_entry.reps_completed * weight total_volume += log_entry.reps_completed * weight
total_sessions = len(sessions)
current_streak = 0 current_streak = 0
if sessions: if sessions:
@@ -162,6 +167,9 @@ class AnalyticsService:
select(WorkoutLog).where(WorkoutLog.session_id == ws.id) select(WorkoutLog).where(WorkoutLog.session_id == ws.id)
).all() ).all()
if not logs:
continue
day_volume = sum( day_volume = sum(
log_entry.reps_completed * _weight_to_float(log_entry.weight_used) log_entry.reps_completed * _weight_to_float(log_entry.weight_used)
for log_entry in logs for log_entry in logs

View File

@@ -149,6 +149,8 @@ class LogService:
def delete_log(self, log_id: int) -> None: def delete_log(self, log_id: int) -> None:
"""Delete a log entry. """Delete a log entry.
Removes the log and cleans up the parent session if no logs remain.
Args: Args:
log_id: The log entry ID. log_id: The log entry ID.
@@ -159,10 +161,22 @@ class LogService:
if log is None: if log is None:
raise ValueError(f"WorkoutLog with id {log_id} not found") raise ValueError(f"WorkoutLog with id {log_id} not found")
session_id = log.session_id
self._session.delete(log) self._session.delete(log)
self._session.commit() self._session.commit()
logger.info("log_deleted", log_id=log_id) logger.info("log_deleted", log_id=log_id)
# Clean up orphaned session if no logs remain
remaining = self._session.exec(
select(WorkoutLog).where(WorkoutLog.session_id == session_id)
).first()
if remaining is None:
ws = self._session.get(WorkoutSession, session_id)
if ws:
self._session.delete(ws)
self._session.commit()
logger.info("session_deleted_empty", session_id=session_id)
def get_latest_logs_for_exercise( def get_latest_logs_for_exercise(
self, self,
user_id: int, user_id: int,

View File

@@ -38,6 +38,7 @@
{% set next_set = logs|length + 1 %} {% set next_set = logs|length + 1 %}
{% include "partials/log_entry.html" %} {% include "partials/log_entry.html" %}
{% else %} {% else %}
{% set exercise_id = exercise.id %}
{% set next_set = 1 %} {% set next_set = 1 %}
{% include "partials/log_form.html" %} {% include "partials/log_form.html" %}
{% endif %} {% endif %}

View File

@@ -1,9 +1,9 @@
<!-- Inline logging form, included inside each exercise_card.html --> <!-- Inline logging form, included inside each exercise_card.html -->
<form hx-post="/log" <form hx-post="/log"
hx-target="#logs-exercise-{{ exercise.id }}" hx-target="#logs-exercise-{{ exercise_id }}"
hx-swap="innerHTML" hx-swap="innerHTML"
style="margin-bottom:0;"> style="margin-bottom:0;">
<input type="hidden" name="exercise_id" value="{{ exercise.id }}"> <input type="hidden" name="exercise_id" value="{{ exercise_id }}">
<input type="hidden" name="workout_day_id" value="{{ workout_day_id }}"> <input type="hidden" name="workout_day_id" value="{{ workout_day_id }}">
<input type="hidden" name="set_number" value="{{ next_set|default(1) }}"> <input type="hidden" name="set_number" value="{{ next_set|default(1) }}">

View File

@@ -17,6 +17,10 @@ from app.services.auth_service import AuthService
logger = structlog.get_logger(__name__) logger = structlog.get_logger(__name__)
class NotAuthenticatedError(Exception):
"""Raised when a request lacks valid authentication."""
# Cookie name for the admin session # Cookie name for the admin session
SESSION_COOKIE_NAME = "session" SESSION_COOKIE_NAME = "session"
@@ -77,11 +81,7 @@ def _login_redirect():
"""Create a redirect exception to the login page. """Create a redirect exception to the login page.
Returns: Returns:
An HTTPException-compatible RedirectResponse. A NotAuthenticatedError handled by a registered exception handler
in main.py that sends a 302 redirect to /login.
""" """
from fastapi import HTTPException return NotAuthenticatedError()
response = RedirectResponse(url="/login", status_code=303)
exc = HTTPException(status_code=303, detail="Not authenticated")
exc.response = response
return exc