From 5584022a23e529b3c9ad9afb5acdc4e2031b28bc Mon Sep 17 00:00:00 2001 From: Jacob Hinkle Date: Mon, 29 Jun 2026 10:50:01 -0400 Subject: [PATCH] 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 --- AGENTS.md | 16 ++-- Dockerfile | 22 +++-- README.md | 67 +++++++++++++ app/config.py | 1 + app/main.py | 3 +- app/routers/agent_api.py | 143 +++++++++++++++++++++++++++ app/routers/chat.py | 47 ++++++++- app/routers/checkins.py | 3 +- app/routers/dashboard.py | 8 +- app/routers/workouts.py | 6 ++ app/templates/checkin_new.html | 16 +++- app/templates/checkins.html | 9 +- app/templates/dashboard.html | 19 +++- app/templates/workout_detail.html | 3 + docker-compose.yml | 16 +--- opencode/fitness-trainer.md | 154 ++++++++++++++++-------------- 16 files changed, 419 insertions(+), 114 deletions(-) create mode 100644 app/routers/agent_api.py diff --git a/AGENTS.md b/AGENTS.md index 650b08c..b6c422f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,7 +12,7 @@ Replaces the fitness-agent markdown-based training repo. - **FastAPI** — async Python web framework - **Jinja2 + Pico.css** — server-rendered templates with minimal CSS framework (CDN) - **SQLite + SQLAlchemy 2.0 async** — database with aiosqlite driver -- **opencode serve** — AI coach service (Big Pickle model, free). Runs as a separate process/container. +- **opencode serve** — AI coach service (Big Pickle model, free). Runs alongside uvicorn in the same container, managed by a shell entrypoint. ## Key Files - `app/main.py` — App factory, route registration, lifespan @@ -20,6 +20,7 @@ Replaces the fitness-agent markdown-based training repo. - `app/auth.py` — Password hashing, session management, `get_current_user` dependency - `app/models/` — SQLAlchemy ORM models - `app/routers/` — Route handlers (one per feature) +- `app/routers/agent_api.py` — JSON API for the AI coach to create workouts/check-ins - `scripts/schema.py` — DB initialization - `scripts/seed.py` — Seed data (exercises, phases) - `opencode/fitness-trainer.md` — Agent config for AI coach (copied from fitness-agent) @@ -38,13 +39,14 @@ uv run uvicorn app.main:app --reload # Dev server on :8000 - SSE streaming for chat responses - Pico.css from CDN (no build step) - Chat messages stored in DB per session for history -- Docker Compose for deployment (opencode serve service commented out until ready) +- Docker Compose for deployment (opencode serve runs as background process in same container) ## Next Steps / TODOs 1. Seed exercises and phases (done via `python scripts/seed.py`) 2. Add exercise progress chart (matplotlib or chart.js) -3. Enable opencode serve integration (uncomment docker-compose service) -4. Migrate existing markdown logs from fitness-agent repo into DB -5. Migrate Juggernaut training xlsx data into DB -6. Add calendar view for training history -7. PWA manifest + service worker for offline-capable mobile use +3. Enable opencode serve integration (Dockerfile + entrypoint done, single-container approach) +4. Add agent API for DB writes (endpoints + agent config done) +5. Migrate existing markdown logs from fitness-agent repo into DB +6. Migrate Juggernaut training xlsx data into DB +7. Add calendar view for training history +8. Update `fitness-trainer.md` agent config to work with DB-backed context instead of markdown files (done) diff --git a/Dockerfile b/Dockerfile index 385ccd3..4e5b876 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,20 @@ +FROM node:22-alpine AS opencode-build +RUN apk add --no-cache curl && \ + curl -fsSL https://opencode.ai/install | sh + FROM python:3.12-slim - +RUN apt-get update && apt-get install -y --no-install-recommends curl && \ + rm -rf /var/lib/apt/lists/* +RUN pip install --no-cache-dir uv WORKDIR /app - COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt - +RUN uv pip install --system --no-cache-dir -r requirements.txt +COPY --from=opencode-build /usr/local/bin/opencode /usr/local/bin/opencode COPY . . - EXPOSE 8000 - -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] +ENV OPENCODE_SERVE_URL=http://127.0.0.1:4096 +ENV AGENT_API_KEY=dev-agent-key-change-in-production +RUN mkdir -p /root/.config && \ + ln -s /app/opencode /root/.config/opencode && \ + printf '#!/bin/sh\nopencode serve --host 127.0.0.1 --port 4096 &\nsleep 1\nexec uvicorn app.main:app --host 0.0.0.0 --port 8000\n' > /entrypoint.sh && chmod +x /entrypoint.sh +ENTRYPOINT ["/entrypoint.sh"] diff --git a/README.md b/README.md index 29be9f9..3326f2c 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,7 @@ Track workouts, log daily check-ins, explore exercise history, and chat with an - **AI Coach** — Chat interface backed by opencode (Big Pickle model, free) - **Multi-user** — Login-based, each user has independent data - **Calendar view** — See your training history at a glance +- **NixOS-ready** — Docker-based deploy with provided NixOS module ## Quick Start @@ -35,6 +36,72 @@ Open http://localhost:8000, register a user, and you're ready. docker compose up -d ``` +## NixOS Deployment + +A single container runs both the web app and the AI coach (opencode-serve) together, sharing the SQLite database on the same filesystem. + +### 1. Build image + +```bash +docker build -t fitness-web:latest . +``` + +### 2. NixOS module + +Add `machines/cj/fitness-web.nix` to your nix_config: + +```nix +{ serverIP, serverIP6 }: { + fitness-web = { + image = "fitness-web:latest"; + ports = [ "8688:8000" ]; + environment = { + TZ = "America/New_York"; + SESSION_SECRET = "change-me-in-production"; + }; + volumes = [ + "/serverdata/fitness-web/data:/app/data" + "/serverdata/fitness-web/opencode:/root/.config/opencode" + ]; + }; +} +``` + +Wire it into `configuration.nix`: + +```nix +virtualisation.oci-containers.containers = let + ips = { ... }; +in { + # ... existing containers ... + fitness-web = (import ./fitness-web.nix ips).fitness-web; +}; +``` + +### 3. Nginx reverse proxy + +In `nginx.nix`, add a vhost: + +```nix +"fitness.jhink.org" = simpleProxy 8688; +``` + +### 4. Firewall + +Add to `allowedTCPPorts` in `firewall.nix`: + +```nix +8688 # fitness-web +``` + +### 5. Deploy + +```bash +nixos-rebuild switch --flake .#cj +``` + +Then access at https://fitness.jhink.org (or your chosen domain). + ## Architecture ``` diff --git a/app/config.py b/app/config.py index f352e26..37e7c15 100644 --- a/app/config.py +++ b/app/config.py @@ -6,3 +6,4 @@ DATA_DIR = Path(os.environ.get("DATA_DIR", PROJECT_ROOT / "data")) DATABASE_PATH = str(DATA_DIR / "fitness.db") SESSION_SECRET = os.environ.get("SESSION_SECRET", "dev-secret-change-in-production") OPENCODE_SERVE_URL = os.environ.get("OPENCODE_SERVE_URL", "http://localhost:4096") +AGENT_API_KEY = os.environ.get("AGENT_API_KEY", "") diff --git a/app/main.py b/app/main.py index 53eb072..5cedd85 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 +from app.routers import auth, dashboard, workouts, exercises, checkins, profile, chat, agent_api from scripts.schema import init_db @@ -27,3 +27,4 @@ app.include_router(exercises.router) app.include_router(checkins.router) app.include_router(profile.router) app.include_router(chat.router) +app.include_router(agent_api.router) diff --git a/app/routers/agent_api.py b/app/routers/agent_api.py new file mode 100644 index 0000000..ea4b242 --- /dev/null +++ b/app/routers/agent_api.py @@ -0,0 +1,143 @@ +from fastapi import APIRouter, Header, HTTPException, Depends +from pydantic import BaseModel +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.checkin import Checkin + +router = APIRouter(prefix="/api/agent", tags=["agent"]) + + +async def verify_agent(x_api_key: str = Header("")): + if AGENT_API_KEY and x_api_key != AGENT_API_KEY: + raise HTTPException(status_code=403, detail="invalid api key") + + +class CreateWorkoutRequest(BaseModel): + username: str + name: str + date: str + phase_id: int | None = None + notes: str = "" + + +class CreateSetRequest(BaseModel): + exercise: str + set_number: int + reps: int | None = None + weight: float | None = None + rpe: float | None = None + notes: str = "" + + +class CreateCheckinRequest(BaseModel): + username: str + date: str + feeling: str | None = None + weight_lb: float | None = None + calories: int | None = None + steps: int | None = None + sleep_hours: float | None = None + notes: str = "" + + +@router.post("/workouts") +async def agent_create_workout( + body: CreateWorkoutRequest, + _=Depends(verify_agent), +): + async with async_session() as session: + result = await session.execute( + select(User).where(User.username == body.username) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="user not found") + + workout = Workout( + user_id=user.id, + phase_id=body.phase_id, + name=body.name, + date=body.date, + notes=body.notes, + status="plan", + ) + session.add(workout) + await session.commit() + await session.refresh(workout) + + return {"id": workout.id, "name": workout.name, "date": workout.date, "status": workout.status} + + +@router.post("/workouts/{workout_id}/sets") +async def agent_add_set( + workout_id: int, + body: CreateSetRequest, + _=Depends(verify_agent), +): + async with async_session() as session: + ws = WorkoutSet( + workout_id=workout_id, + exercise=body.exercise, + set_number=body.set_number, + reps=body.reps, + weight=body.weight, + rpe=body.rpe, + notes=body.notes, + ) + session.add(ws) + await session.commit() + await session.refresh(ws) + + return {"id": ws.id, "exercise": ws.exercise, "set_number": ws.set_number} + + +@router.put("/workouts/{workout_id}/complete") +async def agent_complete_workout( + workout_id: int, + _=Depends(verify_agent), +): + async with async_session() as session: + result = await session.execute( + select(Workout).where(Workout.id == workout_id) + ) + workout = result.scalar_one_or_none() + if not workout: + raise HTTPException(status_code=404, detail="workout not found") + workout.status = "complete" + await session.commit() + + return {"id": workout.id, "status": "complete"} + + +@router.post("/checkins") +async def agent_create_checkin( + body: CreateCheckinRequest, + _=Depends(verify_agent), +): + async with async_session() as session: + result = await session.execute( + select(User).where(User.username == body.username) + ) + user = result.scalar_one_or_none() + if not user: + raise HTTPException(status_code=404, detail="user not found") + + checkin = Checkin( + user_id=user.id, + date=body.date, + feeling=body.feeling, + weight_lb=body.weight_lb, + calories=body.calories, + steps=body.steps, + sleep_hours=body.sleep_hours, + notes=body.notes, + ) + session.add(checkin) + await session.commit() + await session.refresh(checkin) + + return {"id": checkin.id, "date": checkin.date} diff --git a/app/routers/chat.py b/app/routers/chat.py index 06d5320..0dbf510 100644 --- a/app/routers/chat.py +++ b/app/routers/chat.py @@ -8,6 +8,8 @@ from sqlalchemy import select, desc from app.models.base import async_session from app.models.user import User from app.models.chat import ChatMessage +from app.models.workout import Workout +from app.models.checkin import Checkin from app.auth import get_current_user from app.services.opencode_proxy import query_opencode @@ -48,12 +50,55 @@ async def chat_send( message: str = Form(), ): session_id = request.cookies.get("chat_session_id") or str(uuid.uuid4()) + + async with async_session() as session: + result = await session.execute( + select(Workout) + .where(Workout.user_id == user.id) + .order_by(desc(Workout.date)) + .limit(5) + ) + recent_workouts = result.scalars().all() + + result = await session.execute( + select(Checkin) + .where(Checkin.user_id == user.id) + .order_by(desc(Checkin.date)) + .limit(5) + ) + recent_checkins = result.scalars().all() + + workout_lines = [] + for w in recent_workouts: + workout_lines.append(f" {w.date} — {w.name} ({w.status})") + checkin_lines = [] + for c in recent_checkins: + parts = [] + if c.feeling: + parts.append(f"feeling={c.feeling}") + if c.weight_lb: + parts.append(f"weight={c.weight_lb}lb") + if c.calories: + parts.append(f"cal(yesterday)={c.calories}") + if c.steps: + parts.append(f"steps(yesterday)={c.steps}") + if c.sleep_hours: + parts.append(f"sleep={c.sleep_hours}h") + checkin_lines.append(f" {c.date} — {' | '.join(parts)}") + user_context = ( f"Username: {user.username}. " f"Weight: {user.weight_lb} lb. " f"Goals: {user.goals or 'Not specified'}. " - f"Medical: {user.medical_notes or 'None'}" + f"Equipment: {user.equipment or 'Not specified'}. " + f"Medical: {user.medical_notes or 'None'}. " + f"Calorie goal: {user.calorie_goal or 'Not set'}. " + f"Step goal: {user.step_goal or 'Not set'}. " ) + if recent_workouts: + user_context += "Recent workouts:\n" + "\n".join(workout_lines) + ". " + if recent_checkins: + user_context += "Recent check-ins:\n" + "\n".join(checkin_lines) + ". " async def stream(): async with async_session() as session: diff --git a/app/routers/checkins.py b/app/routers/checkins.py index 623521e..c9fc010 100644 --- a/app/routers/checkins.py +++ b/app/routers/checkins.py @@ -1,4 +1,4 @@ -from datetime import datetime, timezone +from datetime import date, datetime, timezone from fastapi import APIRouter, Request, Depends, Form from fastapi.responses import HTMLResponse, RedirectResponse from fastapi.templating import Jinja2Templates @@ -32,6 +32,7 @@ async def checkin_list(request: Request, user: User = Depends(get_current_user)) async def new_checkin_page(request: Request, user: User = Depends(get_current_user)): return templates.TemplateResponse(request, "checkin_new.html", { "user": user, + "today": date.today().isoformat(), }) diff --git a/app/routers/dashboard.py b/app/routers/dashboard.py index 32ab6f1..c645b18 100644 --- a/app/routers/dashboard.py +++ b/app/routers/dashboard.py @@ -5,7 +5,7 @@ from sqlalchemy import select, desc from app.models.base import async_session from app.models.user import User -from app.models.workout import Workout +from app.models.workout import Phase, Workout from app.models.checkin import Checkin from app.auth import get_current_user @@ -38,8 +38,14 @@ 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) + ) + current_phase = result.scalar_one_or_none() + return templates.TemplateResponse(request, "dashboard.html", { "user": user, "recent_workouts": recent_workouts, "latest_checkin": latest_checkin, + "current_phase": current_phase, }) diff --git a/app/routers/workouts.py b/app/routers/workouts.py index bcfe488..17c5ead 100644 --- a/app/routers/workouts.py +++ b/app/routers/workouts.py @@ -89,10 +89,16 @@ async def workout_detail( ) 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, }) diff --git a/app/templates/checkin_new.html b/app/templates/checkin_new.html index edd0554..aa2ea70 100644 --- a/app/templates/checkin_new.html +++ b/app/templates/checkin_new.html @@ -1,37 +1,45 @@ {% extends "base.html" %} -{% block title %}New Check-in{% endblock %} +{% block title %}Morning Check-in{% endblock %} {% block content %} -

New Check-in

+

Morning Check-in

+

All fields are optional — fill in whatever you have each morning.

diff --git a/app/templates/checkins.html b/app/templates/checkins.html index be49b1f..7794248 100644 --- a/app/templates/checkins.html +++ b/app/templates/checkins.html @@ -1,7 +1,8 @@ {% extends "base.html" %} {% block title %}Check-ins{% endblock %} {% block content %} -

Check-ins

+

Morning Check-ins

+

Log what's on your mind and what happened yesterday — all fields optional.

New Check-in {% if checkins %} @@ -11,8 +12,8 @@ Date Feeling Weight - Calories - Steps + Calories (yesterday) + Steps (yesterday) Sleep @@ -30,6 +31,6 @@ {% else %} -

No check-ins yet.

+

No check-ins yet. Start your first morning check-in.

{% endif %} {% endblock %} diff --git a/app/templates/dashboard.html b/app/templates/dashboard.html index 34e52db..bb19893 100644 --- a/app/templates/dashboard.html +++ b/app/templates/dashboard.html @@ -18,11 +18,20 @@ +{% if current_phase %} +
+

Current Phase: {{ current_phase.name }}

+

{{ current_phase.description }}

+ {% if current_phase.notes %}

{{ current_phase.notes }}

{% endif %} + {% if current_phase.start_date %}Started {{ current_phase.start_date }}{% endif %} +
+{% endif %} + {% if latest_checkin %}
-

Latest Check-in

+

Latest Morning Check-in

{{ latest_checkin.date }} — {{ latest_checkin.feeling or 'No feeling recorded' }}

-

Weight: {{ latest_checkin.weight_lb or '—' }} lb | Calories: {{ latest_checkin.calories or '—' }} | Steps: {{ latest_checkin.steps or '—' }} | Sleep: {{ latest_checkin.sleep_hours or '—' }}h

+

Weight: {{ latest_checkin.weight_lb or '—' }} lb | Calories (yesterday): {{ latest_checkin.calories or '—' }} | Steps (yesterday): {{ latest_checkin.steps or '—' }} | Sleep: {{ latest_checkin.sleep_hours or '—' }}h

{% endif %} @@ -49,12 +58,12 @@ {% else %} -

No workouts yet. Plan one now.

+

No workouts yet. Ask the AI Coach to plan one.

{% endif %}
- New Workout - New Check-in AI Coach + All Workouts + All Check-ins
{% endblock %} diff --git a/app/templates/workout_detail.html b/app/templates/workout_detail.html index a7abc33..dd13455 100644 --- a/app/templates/workout_detail.html +++ b/app/templates/workout_detail.html @@ -3,6 +3,9 @@ {% block content %}

{{ workout.name }}

Date: {{ workout.date }} | Status: {{ workout.status }}

+{% if phase %} +

Phase: {{ phase.name }}

+{% endif %} {% if workout.notes %}

{{ workout.notes }}

{% endif %} diff --git a/docker-compose.yml b/docker-compose.yml index 09610d6..509b277 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,21 +1,11 @@ services: - web: + fitness-web: build: . ports: - "8000:8000" volumes: - ./data:/app/data + - ./opencode:/root/.config/opencode environment: - - DATABASE_PATH=/app/data/fitness.db - SESSION_SECRET=change-me-in-production - - OPENCODE_SERVE_URL=http://opencode-serve:4096 - - # opencode-serve: - # image: opencode:latest - # entrypoint: ["opencode", "serve", "--host", "0.0.0.0", "--port", "4096"] - # ports: - # - "4096:4096" - # volumes: - # - ./opencode:/root/.config/opencode - # environment: - # - OPENCODE_SERVER_PASSWORD=your-password-here + - AGENT_API_KEY=dev-agent-key-change-in-production diff --git a/opencode/fitness-trainer.md b/opencode/fitness-trainer.md index 78d9ce0..be47448 100644 --- a/opencode/fitness-trainer.md +++ b/opencode/fitness-trainer.md @@ -1,23 +1,28 @@ --- description: > Your personal fitness trainer. Plans workouts, tracks progress, adapts to how - you're feeling, and logs everything to markdown. Use this agent for daily - check-ins, workout reviews, and programming discussions. + you're feeling, and logs everything via the web app API. Use this agent for + daily check-ins, workout reviews, and programming discussions. mode: primary color: "#4ade80" --- You are an experienced, adaptable personal trainer. Your client (the user) has -provided their equipment, goals, and medical history in `inputs/`. Their -historical lifting data is in `inputs/Juggernaut training.xlsx`. +provided their equipment, goals, and medical history through the fitness web +app. Their training data is stored in the app's database — past workouts, +daily check-ins, exercise history, and current stats are all available from +there. Your job is to guide them through their fitness journey. Be encouraging but honest. You are their single point of contact for training chat. ## Guidelines -- Always consider their medical history (especially the distal radius fracture) - and available equipment when programming +- Always consider their medical history (especially the distal radius + fracture) and available equipment when programming +- The web app passes your client's current stats (weight, goals, medical + notes, recent workouts, recent check-ins) alongside each message. Use this + context to understand their situation. - **Periodic research check:** Roughly once per week (or every few check-ins), do a brief web search on current best practices relevant to their situation — e.g., distal radius fracture return-to-training, tendinopathy prevention, @@ -30,9 +35,14 @@ honest. You are their single point of contact for training chat. - If they're interested in a specific program methodology (Juggernaut, Stronglifts 5x5, etc.), use their training history to pick up where they left off or start a new cycle -- If they want something new, design intelligently using the programming - principles in the `fitness-workout` skill -- **Left hand grip limitation:** Client is doing grip/rehab exercises 3x/day with their PT. In our workouts, minimize left hand grip demand (use straps for any pulling, avoid goblet squats, keep DB loads light). Check in early next week (Mon/Tue) about whether they feel ready to add more grip work back into sessions — they have weekly PT appointments each Wednesday for the next 3 weeks and will update accordingly. +- If they want something new, design intelligently using sound programming + principles +- **Left hand grip limitation:** Client is doing grip/rehab exercises 3x/day + with their PT. In our workouts, minimize left hand grip demand (use straps + for any pulling, avoid goblet squats, keep DB loads light). Check in early + next week (Mon/Tue) about whether they feel ready to add more grip work back + into sessions — they have weekly PT appointments each Wednesday for the next + 3 weeks and will update accordingly. - **During the reintroduction period (weeks 1-4 after a layoff or injury),** always program movements with **limited range of motion** — avoid end-range positions (stretch at the bottom/top of any lift) for all exercises. This @@ -40,70 +50,74 @@ honest. You are their single point of contact for training chat. Mid-range movements only (e.g., Bulgarian split squats instead of deep squats, landmine press instead of full ROM OHP, step-ups instead of deep lunges). Apply this to squat, hinge, push, pull, and core movements alike. -- High-level training context lives in `plans/`. Read the relevant plan before - designing individual workouts and keep it up to date as context evolves - (progressing phases, new constraints, notable decisions). The plan describes - current phase, timeline, constraints, and progression criteria. Workout - Analysis sections should briefly reference the broader plan to show how the - session fits. -- Logs are written to `logs/workouts/` and `logs/checkins/`. Use - `grep`/`read`/`glob` to search past logs when they ask questions like "when - was the last time I did farmer's carry?" -## Check-in Workflow +## API — Creating Workouts and Check-ins -When they want to check in, follow this structure: +You write workout plans and check-in logs directly to the database via the +web app's internal API. The API key is in the environment variable +`AGENT_API_KEY`. All endpoints are at `http://localhost:8000/api/agent/`. + +Use `curl` with the API key header: + +```bash +curl -s http://localhost:8000/api/agent/workouts \ + -H "X-API-Key: $AGENT_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"username": "jacob", "name": "Upper Body A", "date": "2026-06-30", "notes": "..."}' +``` + +### Endpoints + +**Create a workout plan:** +``` +POST /api/agent/workouts +Body: { username, name, date, phase_id?, notes? } +``` + +**Add a set to a workout:** +``` +POST /api/agent/workouts/{id}/sets +Body: { exercise, set_number, reps?, weight?, rpe?, notes? } +``` + +**Mark a workout complete:** +``` +PUT /api/agent/workouts/{id}/complete +``` + +**Create a check-in log:** +``` +POST /api/agent/checkins +Body: { username, date, feeling?, weight_lb?, calories?, steps?, sleep_hours?, notes? } +``` + +Always use the username from the context provided with each message. + +## Check-in Flow + +When the user wants to check in or discuss their training: 1. **Status check** — Ask how they're feeling: soreness, energy, injuries, - sleep, weight, motivation -2. **Nutrition & Steps** — Ask if they'd like to review/adjust their LoseIt! - calorie goal, and if they can report their average daily steps from their - Google Pixel phone. Log these numbers in the check-in entry. If they're - comfortable, suggest a small calorie goal adjustment based on their weight - trend and activity level. -3. **Review** — Check the last planned workout log. Did they complete it? How - did each exercise feel? Update the log with results if needed -4. **Adjust** — Based on feedback + programming guidelines + history, adjust - the next session (weight, volume, exercise selection, or rest day) -5. **Plan** — Write a new workout plan to `logs/workouts/-.md` - with exercises, sets, reps, weights, and any notes. Present it to them -6. **Log** — Write a brief check-in entry to `logs/checkins/-checkin.md` - summarizing the conversation and any decisions made. This includes calorie - goal, steps, weight, and workout results. -7. **Commit** — After logging, use `git add logs/` and `git commit` with a - descriptive message summarizing the check-in, any plan changes, and key - metrics (weight, calories, steps). Do NOT push unless asked. + sleep, weight, motivation. Reference trends from the context. +2. **Nutrition & Steps** — Ask if they'd like to review or adjust their + calorie goal and daily steps. Suggest adjustments based on weight trend + and activity level. +3. **Review** — Look at their recent workouts (from context). Did they + complete them? How did each exercise feel? +4. **Adjust** — Based on feedback + programming guidelines + history, suggest + adjustments for the next session (weight, volume, exercise selection, or + rest day) +5. **Plan & Save** — Design the next workout with exercises, sets, reps, + weights, and notes. **Create it in the database** using the API + (`POST /api/agent/workouts` followed by `POST /api/agent/workouts/{id}/sets` + for each exercise). Present the plan to the user. +6. **Log the check-in** — After the conversation wraps, **create a check-in + entry** via the API (`POST /api/agent/checkins`) summarizing the key + decisions, metrics, and any adjustments made. -## Log format +## Session Analysis -Workout plan log: -```markdown -# Workout: -**Date:** -**Program:** -**Status:** Plan - -## Analysis - - -## Exercises -- : x @ -- ... - -## Notes - -``` - -After completion, update the **Status** to "Complete" and add results inline: -```markdown -- Bench Press: 3x5 @ 185lb — completed (RPE 8) -``` - -Check-in log: -```markdown -# Check-in: -**Feeling:** -**Review:** -**Adjustments:** -**Next session:** -``` +When discussing a specific workout, briefly note muscles targeted, the +overall goal for the session, and how it fits into their broader training +context. Use the workout history from the context to reference progression +and past performance.