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 contextlib import asynccontextmanager
|
||||||
|
|
||||||
from app.config import DATA_DIR
|
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
|
from scripts.schema import init_db
|
||||||
|
|
||||||
|
|
||||||
@ -28,3 +28,4 @@ app.include_router(checkins.router)
|
|||||||
app.include_router(profile.router)
|
app.include_router(profile.router)
|
||||||
app.include_router(chat.router)
|
app.include_router(chat.router)
|
||||||
app.include_router(agent_api.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.config import AGENT_API_KEY
|
||||||
from app.models.base import async_session
|
from app.models.base import async_session
|
||||||
from app.models.user import User
|
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
|
from app.models.checkin import Checkin
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/agent", tags=["agent"])
|
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")
|
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):
|
class CreateWorkoutRequest(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
name: str
|
name: str
|
||||||
@ -141,3 +149,56 @@ async def agent_create_checkin(
|
|||||||
await session.refresh(checkin)
|
await session.refresh(checkin)
|
||||||
|
|
||||||
return {"id": checkin.id, "date": checkin.date}
|
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()
|
latest_checkin = result.scalar_one_or_none()
|
||||||
|
|
||||||
result = await session.execute(
|
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()
|
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>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="/dashboard">Dashboard</a></li>
|
<li><a href="/dashboard">Dashboard</a></li>
|
||||||
|
<li><a href="/plan">Plan</a></li>
|
||||||
<li><a href="/workouts">Workouts</a></li>
|
<li><a href="/workouts">Workouts</a></li>
|
||||||
<li><a href="/exercises">Exercises</a></li>
|
<li><a href="/exercises">Exercises</a></li>
|
||||||
<li><a href="/checkins">Check-ins</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? }
|
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.
|
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
|
## Check-in Flow
|
||||||
|
|
||||||
When the user wants to check in or discuss their training:
|
When the user wants to check in or discuss their training:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user