From 1a2509ab3486c6616c87171a8c3f599a545805b1 Mon Sep 17 00:00:00 2001 From: Jacob Hinkle Date: Mon, 29 Jun 2026 10:51:28 -0400 Subject: [PATCH] Add Plan page with phase timeline and agent phase management API - New /plan page shows all phases in order with current phase highlighted - Add GET/POST/PUT /api/agent/phases endpoints for the AI coach - Plan link added to navigation bar - Agent config updated with phase management instructions --- app/main.py | 3 +- app/routers/agent_api.py | 63 ++++++++++++++++++++++++++++++++++++- app/routers/dashboard.py | 2 +- app/routers/plan.py | 26 +++++++++++++++ app/templates/base.html | 1 + app/templates/plan.html | 29 +++++++++++++++++ opencode/fitness-trainer.md | 30 ++++++++++++++++++ 7 files changed, 151 insertions(+), 3 deletions(-) create mode 100644 app/routers/plan.py create mode 100644 app/templates/plan.html diff --git a/app/main.py b/app/main.py index 5cedd85..c87e768 100644 --- a/app/main.py +++ b/app/main.py @@ -4,7 +4,7 @@ from fastapi.staticfiles import StaticFiles from contextlib import asynccontextmanager from app.config import DATA_DIR -from app.routers import auth, dashboard, workouts, exercises, checkins, profile, chat, agent_api +from app.routers import auth, dashboard, workouts, exercises, checkins, profile, chat, agent_api, plan from scripts.schema import init_db @@ -28,3 +28,4 @@ app.include_router(checkins.router) app.include_router(profile.router) app.include_router(chat.router) app.include_router(agent_api.router) +app.include_router(plan.router) diff --git a/app/routers/agent_api.py b/app/routers/agent_api.py index ea4b242..6241397 100644 --- a/app/routers/agent_api.py +++ b/app/routers/agent_api.py @@ -5,7 +5,7 @@ from sqlalchemy import select from app.config import AGENT_API_KEY from app.models.base import async_session from app.models.user import User -from app.models.workout import Workout, WorkoutSet +from app.models.workout import Phase, Workout, WorkoutSet from app.models.checkin import Checkin router = APIRouter(prefix="/api/agent", tags=["agent"]) @@ -16,6 +16,14 @@ async def verify_agent(x_api_key: str = Header("")): raise HTTPException(status_code=403, detail="invalid api key") +class CreatePhaseRequest(BaseModel): + name: str + description: str = "" + start_date: str | None = None + end_date: str | None = None + notes: str = "" + + class CreateWorkoutRequest(BaseModel): username: str name: str @@ -141,3 +149,56 @@ async def agent_create_checkin( await session.refresh(checkin) return {"id": checkin.id, "date": checkin.date} + + +@router.get("/phases") +async def agent_list_phases(_=Depends(verify_agent)): + async with async_session() as session: + result = await session.execute(select(Phase).order_by(Phase.start_date.nulls_last())) + phases = result.scalars().all() + return [ + {"id": p.id, "name": p.name, "description": p.description, + "start_date": p.start_date, "end_date": p.end_date, "notes": p.notes} + for p in phases + ] + + +@router.post("/phases") +async def agent_create_phase( + body: CreatePhaseRequest, + _=Depends(verify_agent), +): + async with async_session() as session: + phase = Phase( + name=body.name, + description=body.description, + start_date=body.start_date, + end_date=body.end_date, + notes=body.notes, + ) + session.add(phase) + await session.commit() + await session.refresh(phase) + + return {"id": phase.id, "name": phase.name} + + +@router.put("/phases/{phase_id}") +async def agent_update_phase( + phase_id: int, + body: CreatePhaseRequest, + _=Depends(verify_agent), +): + async with async_session() as session: + result = await session.execute(select(Phase).where(Phase.id == phase_id)) + phase = result.scalar_one_or_none() + if not phase: + raise HTTPException(status_code=404, detail="phase not found") + phase.name = body.name + phase.description = body.description + phase.start_date = body.start_date + phase.end_date = body.end_date + phase.notes = body.notes + await session.commit() + + return {"id": phase.id, "name": phase.name} diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index c645b18..9b4bc47 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -39,7 +39,7 @@ async def dashboard(request: Request, user: User = Depends(get_current_user)): latest_checkin = result.scalar_one_or_none() result = await session.execute( - select(Phase).order_by(desc(Phase.start_date)).limit(1) + select(Phase).order_by(Phase.start_date.desc().nulls_last()).limit(1) ) current_phase = result.scalar_one_or_none() diff --git a/app/routers/plan.py b/app/routers/plan.py new file mode 100644 index 0000000..0e54c46 --- /dev/null +++ b/app/routers/plan.py @@ -0,0 +1,26 @@ +from fastapi import APIRouter, Request, Depends +from fastapi.responses import HTMLResponse +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 +from app.auth import get_current_user + +router = APIRouter() +templates = Jinja2Templates(directory="app/templates") + + +@router.get("/plan", response_class=HTMLResponse) +async def plan_page(request: Request, user: User = Depends(get_current_user)): + async with async_session() as session: + result = await session.execute( + select(Phase).order_by(Phase.start_date.nulls_last()) + ) + phases = result.scalars().all() + + return templates.TemplateResponse(request, "plan.html", { + "user": user, + "phases": phases, + }) diff --git a/app/templates/base.html b/app/templates/base.html index 8be2cf8..47f6b67 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -14,6 +14,7 @@