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
This commit is contained in:
parent
5584022a23
commit
1a2509ab34
@ -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)
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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()
|
||||
|
||||
|
||||
26
app/routers/plan.py
Normal file
26
app/routers/plan.py
Normal file
@ -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,
|
||||
})
|
||||
@ -14,6 +14,7 @@
|
||||
</ul>
|
||||
<ul>
|
||||
<li><a href="/dashboard">Dashboard</a></li>
|
||||
<li><a href="/plan">Plan</a></li>
|
||||
<li><a href="/workouts">Workouts</a></li>
|
||||
<li><a href="/exercises">Exercises</a></li>
|
||||
<li><a href="/checkins">Check-ins</a></li>
|
||||
|
||||
29
app/templates/plan.html
Normal file
29
app/templates/plan.html
Normal file
@ -0,0 +1,29 @@
|
||||
{% extends "base.html" %}
|
||||
{% block title %}Training Plan{% endblock %}
|
||||
{% block content %}
|
||||
<h1>Training Plan</h1>
|
||||
<p><small>Your overall training plan, broken into phases. The AI coach creates and manages these phases based on your goals and progress.</small></p>
|
||||
|
||||
{% for phase in phases %}
|
||||
<article{% if loop.first %} style="border-left: 4px solid var(--primary);"{% endif %}>
|
||||
<h3>{{ phase.name }}</h3>
|
||||
{% if loop.first %}<p><small>Current phase</small></p>{% endif %}
|
||||
{% if phase.start_date or phase.end_date %}
|
||||
<p><small>
|
||||
{% if phase.start_date %}{{ phase.start_date }}{% endif %}
|
||||
{% if phase.start_date and phase.end_date %} — {% endif %}
|
||||
{% if phase.end_date %}{{ phase.end_date }}{% endif %}
|
||||
</small></p>
|
||||
{% endif %}
|
||||
<p>{{ phase.description }}</p>
|
||||
{% if phase.notes %}
|
||||
<p><small>{{ phase.notes }}</small></p>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% else %}
|
||||
<article>
|
||||
<h3>No phases yet</h3>
|
||||
<p>Ask the <a href="/chat">AI Coach</a> to set up a training plan with phases.</p>
|
||||
</article>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
||||
@ -91,8 +91,38 @@ POST /api/agent/checkins
|
||||
Body: { username, date, feeling?, weight_lb?, calories?, steps?, sleep_hours?, notes? }
|
||||
```
|
||||
|
||||
**List all phases:**
|
||||
```
|
||||
GET /api/agent/phases
|
||||
```
|
||||
|
||||
**Create a new phase:**
|
||||
```
|
||||
POST /api/agent/phases
|
||||
Body: { name, description?, start_date?, end_date?, notes? }
|
||||
```
|
||||
|
||||
**Update a phase:**
|
||||
```
|
||||
PUT /api/agent/phases/{id}
|
||||
Body: { name, description?, start_date?, end_date?, notes? }
|
||||
```
|
||||
|
||||
Always use the username from the context provided with each message.
|
||||
|
||||
## Managing the Training Plan
|
||||
|
||||
You maintain a training plan broken into phases. The plan lives in the
|
||||
database as a series of Phase records. Each phase has a name, description,
|
||||
start/end dates, and notes where you can store the plan details.
|
||||
|
||||
- Create phases for the overall training arc (e.g., Tendon Adaptation →
|
||||
Progressive Loading → Strength Building)
|
||||
- Update phase descriptions and notes as the plan evolves
|
||||
- Assign workouts to a phase by including `phase_id` when creating them
|
||||
- The Plan page in the web UI shows all phases in order so the user can see
|
||||
their training context
|
||||
|
||||
## Check-in Flow
|
||||
|
||||
When the user wants to check in or discuss their training:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user