fitness-web/app/routers/workouts.py
Jacob Hinkle 5584022a23 Single-container AI coach with agent API endpoints and UI polish
- Merge opencode-serve into the web container via entrypoint script
- Add /api/agent/* JSON endpoints for workouts, sets, checkins
- Rewrite fitness-trainer.md to use API instead of markdown files
- Pass recent workouts and check-ins as chat context to the coach
- Show current training phase on dashboard
- Clarify check-ins as morning check-ins (calories/steps = yesterday)
- Add NixOS deployment section to README
- Make all check-in fields explicitly optional in UI
2026-06-29 10:50:01 -04:00

161 lines
4.9 KiB
Python

from datetime import datetime, timezone
from fastapi import APIRouter, Request, Depends, Form
from fastapi.responses import HTMLResponse, RedirectResponse
from fastapi.templating import Jinja2Templates
from sqlalchemy import select, desc
from app.models.base import async_session
from app.models.user import User
from app.models.workout import Phase, Workout, WorkoutSet
from app.auth import get_current_user
router = APIRouter()
templates = Jinja2Templates(directory="app/templates")
@router.get("/workouts", response_class=HTMLResponse)
async def workout_list(request: Request, user: User = Depends(get_current_user)):
async with async_session() as session:
result = await session.execute(
select(Workout)
.where(Workout.user_id == user.id)
.order_by(desc(Workout.date))
)
workouts = result.scalars().all()
return templates.TemplateResponse(request, "workouts.html", {
"user": user,
"workouts": workouts,
})
@router.get("/workouts/new", response_class=HTMLResponse)
async def new_workout_page(request: Request, user: User = Depends(get_current_user)):
async with async_session() as session:
result = await session.execute(select(Phase))
phases = result.scalars().all()
return templates.TemplateResponse(request, "workout_new.html", {
"user": user,
"phases": phases,
})
@router.post("/workouts/new")
async def new_workout(
request: Request,
user: User = Depends(get_current_user),
name: str = Form(),
date: str = Form(),
phase_id: int = Form(default=None),
notes: str = Form(default=""),
):
async with async_session() as session:
workout = Workout(
user_id=user.id,
phase_id=phase_id,
name=name,
date=date,
notes=notes,
status="plan",
)
session.add(workout)
await session.commit()
await session.refresh(workout)
workout_id = workout.id
return RedirectResponse(url=f"/workouts/{workout_id}", status_code=303)
@router.get("/workouts/{workout_id}", response_class=HTMLResponse)
async def workout_detail(
request: Request,
workout_id: int,
user: User = Depends(get_current_user),
):
async with async_session() as session:
result = await session.execute(
select(Workout).where(
Workout.id == workout_id,
Workout.user_id == user.id,
)
)
workout = result.scalar_one_or_none()
if not workout:
return templates.TemplateResponse(request, "404.html", status_code=404)
result = await session.execute(
select(WorkoutSet)
.where(WorkoutSet.workout_id == workout_id)
.order_by(WorkoutSet.exercise, WorkoutSet.set_number)
)
sets = result.scalars().all()
phase = None
if workout.phase_id:
result = await session.execute(select(Phase).where(Phase.id == workout.phase_id))
phase = result.scalar_one_or_none()
return templates.TemplateResponse(request, "workout_detail.html", {
"user": user,
"workout": workout,
"sets": sets,
"phase": phase,
})
@router.post("/workouts/{workout_id}/add-set")
async def add_set(
request: Request,
workout_id: int,
user: User = Depends(get_current_user),
exercise: str = Form(),
set_number: int = Form(),
reps: int = Form(default=None),
weight: float = Form(default=None),
rpe: float = Form(default=None),
notes: str = Form(default=""),
):
async with async_session() as session:
result = await session.execute(
select(Workout).where(
Workout.id == workout_id,
Workout.user_id == user.id,
)
)
workout = result.scalar_one_or_none()
if not workout:
return RedirectResponse(url="/workouts", status_code=303)
ws = WorkoutSet(
workout_id=workout_id,
exercise=exercise,
set_number=set_number,
reps=reps,
weight=weight,
rpe=rpe,
notes=notes,
)
session.add(ws)
await session.commit()
return RedirectResponse(url=f"/workouts/{workout_id}", status_code=303)
@router.post("/workouts/{workout_id}/complete")
async def complete_workout(
workout_id: int,
user: User = Depends(get_current_user),
):
async with async_session() as session:
result = await session.execute(
select(Workout).where(
Workout.id == workout_id,
Workout.user_id == user.id,
)
)
workout = result.scalar_one_or_none()
if workout:
workout.status = "complete"
await session.commit()
return RedirectResponse(url=f"/workouts/{workout_id}", status_code=303)