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 @@
- Dashboard
+ - Plan
- Workouts
- Exercises
- Check-ins
diff --git a/app/templates/plan.html b/app/templates/plan.html
new file mode 100644
index 0000000..fd42d42
--- /dev/null
+++ b/app/templates/plan.html
@@ -0,0 +1,29 @@
+{% extends "base.html" %}
+{% block title %}Training Plan{% endblock %}
+{% block content %}
+Training Plan
+Your overall training plan, broken into phases. The AI coach creates and manages these phases based on your goals and progress.
+
+{% for phase in phases %}
+
+ {{ phase.name }}
+ {% if loop.first %}Current phase
{% endif %}
+ {% if phase.start_date or phase.end_date %}
+
+ {% 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 %}
+
+ {% endif %}
+ {{ phase.description }}
+ {% if phase.notes %}
+ {{ phase.notes }}
+ {% endif %}
+
+{% else %}
+
+ No phases yet
+ Ask the AI Coach to set up a training plan with phases.
+
+{% endfor %}
+{% endblock %}
diff --git a/opencode/fitness-trainer.md b/opencode/fitness-trainer.md
index be47448..a653beb 100644
--- a/opencode/fitness-trainer.md
+++ b/opencode/fitness-trainer.md
@@ -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: