# Track 1: Wire Frontend to Real API — Implementation Plan > 2026-05-09 | Daedalus subagent (planning only, no execution) --- ## Current State Assessment ### Network Architecture ``` User Browser (HTTPS) │ ▼ hoffdesk.com:443 (Cloudflare) │ ▼ Port 8000 (HoffDesk main_v2.py — uvicorn) │ ├── /rtsport/* → rtsport_mock.py (serves static HTML/CSS/JS) ├── /api/v1/dashboard/coach/* → rtsport_coach_api.py (IN-MEMORY MOCK) 🟡 ├── /api/v1/dashboard/parent/* → rtsport_parent_api.py (IN-MEMORY MOCK) 🟡 ├── /api/v1/messages/* → rtsport_coach_api.py or parent_api.py (IN-MEMORY MOCK) 🟡 └── /api/v1/{other} → rtsport_api_proxy.py → port 8001 ✅ │ ▼ Port 8001 (RTSport API — real SQLite DB, JWT, role guards) ``` **Key issue:** The running server at port 8000 still has mock coach/parent routes loaded. The source `main_v2.py` has them commented out, but the process hasn't been restarted since the change. ### Data Flow Discrepancies #### Coach Dashboard | Field | Mock API (`rtsport_coach_api.py`) | Real API (`dashboard.py` on 8001) | Frontend Expects | |-------|------|------|------| | `status_summary` | `{out, modified, cleared}` with `total_athletes` | N/A — uses `stats` key | `data.status_summary.out/.modified/.cleared` | | `injury_breakdown` | `[{body_part, count}]` sorted desc | N/A — no equivalent endpoint | `data.injury_breakdown[].body_part/.count` | | `recovery_timeline` | `[{athlete_name, status, phase, days_remaining}]` | N/A — no equivalent endpoint | `data.recovery_timeline[].athlete_name/.status/.phase/.days_remaining` | | `teams` | N/A | `{Varsity: [{id, name, grade, position, current_status, status_label, case_title}]}` | Not used (roster tab pulls from separate `/roster` endpoint) | | `recent_activity` | 5 hardcoded entries with `type, athlete_name, from, to, timestamp` | Built from `Event` table, different schema | `data.recent_activity[].type/.athlete_name/.from/.to/.timestamp` | | `sport` | String | String (same) | Used in header | **Bottom line:** The Coach frontend renders a **different data structure** than what the real API provides. The real API's `/dashboard/coach` endpoint returns `stats` + `teams` (roster organized by team), but the frontend needs `status_summary` + `injury_breakdown` + `recovery_timeline`. #### Parent Dashboard | Field | Mock API | Real API | Frontend Expects | |-------|---------|----------|-----------------| | `athlete` | `{id, first_name, last_name, sport, team, grade, jersey_number, current_status}` | `{id, first_name, last_name, sport, team, grade, initials}` | `data.athlete.id/.first_name/.last_name` | | `case` | Has `restrictions[]`, `next_appointment`, `care_team[]`, `milestones[]`, `phases` | Has `restrictions`, `next_appointment`, `care_team`, `milestones`, `timeline` — DIFFERENT FIELD NAMES | `data.case.restrictions`, `data.case.next_appointment`, `data.case.care_team`, `data.case.phase_label` | | `activity_feed` | `[{type, title, date, note}]` | `recent_activity: [{type, text, time_ago}]` | `data.activity_feed[].type/.title/.date/.note` | | `/parent/children` | Hardcoded `PARENT_CHILDREN` list | Queries DB by `parent_ids` | `data.children[].id/.first_name/.last_name/.sport` | #### Messages | Endpoint | Mock | Real API | |----------|------|----------| | `GET /messages/coach` | Per-athlete conversations from in-memory dict | **DOES NOT EXIST** — no messages table | | `GET /messages/coach/{id}` | Message threads | **DOES NOT EXIST** | | `POST /messages/coach/{id}` | Creates in-memory message | **DOES NOT EXIST** | | `GET /messages/parent` | Per-contact conversations | **DOES NOT EXIST** | | `GET /messages/parent/{id}` | Message threads by contact | **DOES NOT EXIST** | | `POST /messages/parent/{id}` | Creates + mirrors to coach | **DOES NOT EXIST** | ### Auth Gaps | Issue | Detail | |-------|--------| | Auto-login | Hardcoded creds in each dashboard HTML (`coach@preble.k12.wi.us`, `parent@example.com`, both with password `testpass123`) | | Auth guard | Missing — direct dashboard access shows errors instead of redirecting to login | | Mock token validation | Mock APIs accept any non-empty token; real API validates JWT properly | | Session expiry | Frontend doesn't handle 401 redirect properly | ### Seed Data Gaps | Issue | Detail | Impact | |-------|--------|--------| | Parent-child link | `parent_ids` is empty for all athletes | Parent dashboard returns empty children list / 403 | | No recent events | `events` table empty (or no parent-visible events) | Parent/Coach dashboards show empty activity | --- ## Implementation Plan ### Phase 1: Enable Proxy Routing (10 minutes) **Goal:** Get the running server to forward coach/parent dashboard requests to the real API. 1. **Restart HoffDesk uvicorn process** - The source `main_v2.py` already has coach/parent mock routes commented out - Running process is stale — restart it - `systemctl restart hoffdesk-api` or kill + restart the uvicorn process - **Verify:** `curl /api/v1/dashboard/coach?school_id=schl_001&sport=Football` returns proxy response instead of "Route is handled locally" 2. **Update proxy file docstring** (optional, not blocking) - File: `/home/hoffmann_admin/.openclaw/workspace-socrates/hoffdesk-api/rtsport_api_proxy.py` - Remove the outdated comment about coach endpoints being excluded ### Phase 2: Fix Seed Data — Link Parent to Children (30 minutes) **Goal:** `parent@example.com` can see their children. 1. **Run SQL to link parent user to athletes** - File: `/home/hoffmann_admin/rtsport/backend/rtsport.db` - User `usr_parent001` (parent@example.com) → link to `ath_001` (Jake Larson) and `ath_007` (Emma Larson) - SQL: `UPDATE athletes SET parent_ids = '["usr_parent001"]' WHERE id IN ('ath_001', 'ath_007');` - **Verify:** Parent login → calls `/dashboard/parent/children` → returns 2 children 2. **Seed some events** for activity feed - Insert a few `Event` records for case_001 with "parent" visibility so the activity feed shows content ### Phase 3: Update Frontend JS to Match Real API Shapes (4-6 hours) **Goal:** Both Coach and Parent dashboards render data from the real API. #### 3a: Coach Dashboard Status Tab **File:** `/home/hoffmann_admin/.openclaw/shared/current/frontend/static/js/dashboard-tabs.js` **Changes needed in `renderStatusData()` function:** Replace the mock-expected data structure handling with real API data mapping: ```javascript // Real API returns: { sport, stats: {out, modified, cleared}, teams, recent_activity } function renderStatusData(data, container) { if (!data || !data.stats) { renderStatusEmpty(container); return; } const { sport, stats, teams, recent_activity } = data; // Stats → status_pills (already uses status_summary.{out,modified,cleared}) // Change: data.status_summary → data.stats // Teams → Compute injury_breakdown and recovery_timeline from teams // The real API doesn't have injury_breakdown or recovery_timeline as separate fields // We need to EITHER: // Option A: Add injury_breakdown + recovery_timeline to the real API // Option B: Compute them client-side from the teams/athlete data // Option C: Create new real API endpoints that match what the frontend expects // recent_activity: Real API returns ActivityItem[{type, text, time_ago}] // Mock returned: [{type, athlete_name, from, to, timestamp}] // The renderer expects: act.type, act.athlete_name, act.from, act.to, act.timestamp // This needs to be updated to handle the new shape } ``` **Three approaches for the shape mismatch:** **Option A (Recommended — Server-side): Add `injury_breakdown` and `recovery_timeline` to the real `/dashboard/coach` response.** - File: `/home/hoffmann_admin/rtsport/backend/app/api/dashboard.py` - Add computation of `injury_breakdown` (aggregate body parts from active cases) and `recovery_timeline` (athletes with active cases + phase + estimated days) to the coach dashboard endpoint - Add to the `CoachDashboardResponse` Pydantic model - This preserves the frontend JS with minimal changes **Option B (Client-side only):** Map `teams[]` athletes into the shapes the renderer expects. More complex JS but no backend changes. Riskier due to needing to compute phases from milestones client-side. **Option C (New endpoint):** Create `/dashboard/coach/status` on the real API that exactly matches the mock shape. **Recommendation: Option A** — server-side change, 30-50 lines of Python, zero JS changes needed for the status section. #### 3b: Parent Dashboard **File:** `/home/hoffmann_admin/.openclaw/shared/current/frontend/static/js/parent-tabs.js` **Changes needed:** The mock returns: ```javascript data.case.restrictions // array of {text: "..."} data.case.next_appointment // {date, title, provider} data.case.care_team // [{name, role, initials, email}] data.case.phase_label // "Active Rehab" data.case.phase_number // 2 data.case.total_phases // 4 data.case.milestones // [{label, status, completed_date, target_date}] data.activity_feed // [{type, title, date, note}] ``` The real API returns: ```javascript data.case.restrictions // [{event_id, text, added_at, timestamp}] — different shape data.case.next_appointment // {date, title, provider} — same shape ✅ data.case.care_team // [{name, role}] — no initials/email ❌ data.case.phase_label // "Active Rehab" — same ✅ data.case.phase_number // computed — same ✅ data.case.total_phases // computed — same ✅ data.case.milestones // [{id, title, status, target_date, achieved_at}] — no label field ❌ data.recent_activity // [{type, text, time_ago}] — different from activity_feed ``` **Changes:** 1. `renderRestrictionsCard(data.case)` — Map `restriction.text` or compute from restriction shape 2. `renderCareTeam(data.case)` — Care team no longer has `initials` or `email`; update renderer to generate initials from name 3. Activity feed — Change from `data.activity_feed` to `data.recent_activity` with new field mapping (`title`→`text`, `note`→`text`) 4. Add `initials` and `email` to the real API's `ParentCareTeamMember` schema (or compute initials client-side) #### 3c: AT Dashboard **Check AT dashboard response shape vs frontend expectations:** - Real API returns: `{stats: {full_count, modified_count, out_count}, active_cases: [{...}], recent_activity: [{...}]}` - Frontend (`at/dashboard.html`): needs verification — likely uses different endpoint path #### 3d: AD Dashboard **Check AD dashboard shape:** - Real API returns: `{school_id, total_athletes, active_cases, out_today, modified, sports, recent_activity}` - Frontend needs: verify against mock expectations ### Phase 4: Add Messages to Real API (1-2 days) **Goal:** Messages persist in the database instead of in-memory dicts. 1. **Create Messages table in SQLAlchemy models** - File: `/home/hoffmann_admin/rtsport/backend/app/models.py` - New model: `Message` with fields: `id`, `school_id`, `athlete_id`, `sender_id`, `sender_role`, `text`, `created_at`, `read_at` - Thread support via `thread_id` or `parent_message_id` 2. **Create Messages API endpoints** - File: `/home/hoffmann_admin/rtsport/backend/app/api/messages.py` (new file) - `GET /api/v1/messages/coach?school_id=XXX&sport=YYY` — conversations per athlete - `GET /api/v1/messages/coach/{athlete_id}` — thread messages - `POST /api/v1/messages/coach/{athlete_id}` — send message - `GET /api/v1/messages/parent?athlete_id=XXX` — parent conversations - `GET /api/v1/messages/parent/{contact_id}` — parent thread - `POST /api/v1/messages/parent/{contact_id}` — send parent message 3. **Register router in `main.py`** - File: `/home/hoffmann_admin/rtsport/backend/app/main.py` - Add `app.include_router(messages_router)` 4. **Seed some initial messages** - Insert messages matching the mock conversation data for demo purposes ### Phase 5: Auth Hardening (2-3 hours) 1. **Remove auto-login from all dashboard HTML files** - Files: - `/home/hoffmann_admin/.openclaw/shared/current/frontend/templates/coach/dashboard.html` - `/home/hoffmann_admin/.openclaw/shared/current/frontend/templates/parent/dashboard.html` - `/home/hoffmann_admin/.openclaw/shared/current/frontend/templates/at/dashboard.html` - `/home/hoffmann_admin/.openclaw/shared/current/frontend/templates/ad/dashboard.html` - Remove the `checkAuth()` auto-login block that hardcodes credentials - Keep the `getAuthToken()` → `apiRequest()` flow 2. **Add auth guard to each dashboard** - Add early return in `checkAuth()`: if no token and no auto-login → redirect to `/rtsport/login` - In `apiRequest()`: on 401 → redirect to login 3. **Login flow should work end-to-end** - Login form posts to real API → receives JWT → stores in localStorage → redirects to role-specific dashboard ### Phase 6: Clean Up Mock Files (after everything works) 1. **Delete mock API files** (or keep as reference) - `/home/hoffmann_admin/.openclaw/workspace-socrates/hoffdesk-api/rtsport_coach_api.py` - `/home/hoffmann_admin/.openclaw/workspace-socrates/hoffdesk-api/rtsport_parent_api.py` - The mock JSON data files in `/static/data/` if no longer needed 2. **Mark as complete** in `progress.md` and roadmap --- ## Execution Order (Recommended) | Step | What | Who | Effort | Depends On | |------|------|-----|--------|------------| | 1 | Restart HoffDesk proxy | Socrates/Wadsworth | 5 min | — | | 2 | Link parent to children in DB | Socrates | 15 min | — | | 3 | Add injury_breakdown + recovery_timeline to real API | Socrates | 1-2 hrs | — | | 4 | Update frontend coach dashboard JS | Daedalus | 2-3 hrs | Step 3 | | 5 | Update frontend parent dashboard JS | Daedalus | 2-3 hrs | Step 2 | | 6 | Create Messages table + endpoints | Socrates | 1-2 days | — | | 7 | Update frontend message JS | Daedalus | 2-4 hrs | Step 6 | | 8 | Auth guard + remove auto-login | Daedalus | 2-3 hrs | — | | 9 | Clean up mock files | Either | 30 min | Steps 1-8 working | | 10 | End-to-end verification | Both | 2-4 hrs | Steps 1-9 | **Estimated total effort:** 5-8 days (not continuous; actual work time) --- ## File Change Summary ### Backend (Socrates) | File | Change | Priority | |------|--------|----------| | `rtsport/backend/rtsport.db` | SQL UPDATE to link parent→children | 🔴 Phase 2 | | `rtsport/backend/app/api/dashboard.py` | Add injury_breakdown + recovery_timeline to coach response | 🔴 Phase 3 | | `rtsport/backend/app/models.py` | Add Message model | 🟡 Phase 4 | | `rtsport/backend/app/api/messages.py` (new) | Message CRUD endpoints | 🟡 Phase 4 | | `rtsport/backend/app/main.py` | Register messages router | 🟡 Phase 4 | ### Proxy Layer (Socrates) | File | Change | Priority | |------|--------|----------| | `hoffdesk-api/main_v2.py` | Already done (coach/parent commented out) — just restart | 🔴 Phase 1 | | `hoffdesk-api/rtsport_api_proxy.py` | Update docstring | 🟢 Cleanup | ### Frontend (Daedalus) | File | Change | Priority | |------|--------|----------| | `shared/current/frontend/static/js/dashboard-tabs.js` | `renderStatusData()` — adapt for real API shape | 🔴 Phase 3 | | `shared/current/frontend/static/js/parent-tabs.js` | Adapt for real API shapes (restrictions, care_team, activity_feed) | 🔴 Phase 3 | | `shared/current/frontend/static/js/mock-data.js` | Evaluate if still needed | 🟢 Phase 6 | | `shared/current/frontend/templates/coach/dashboard.html` | Remove auto-login, add auth guard | 🟡 Phase 5 | | `shared/current/frontend/templates/parent/dashboard.html` | Remove auto-login, add auth guard | 🟡 Phase 5 | | `shared/current/frontend/templates/at/dashboard.html` | Remove auto-login, check API shape | 🟡 Phase 3/5 | | `shared/current/frontend/templates/ad/dashboard.html` | Remove auto-login, check API shape | 🟡 Phase 3/5 | ### Config | File | Change | Priority | |------|--------|----------| | Systemd | Restart `hoffdesk-api.service` | 🔴 Phase 1 | --- ## Appendix: Real API Response Shapes (for reference) ### GET /api/v1/dashboard/coach?school_id=schl_001&sport=Football ```json { "sport": "Football", "stats": { "out": 3, "modified": 0, "cleared": 1 }, "teams": { "Varsity": [ { "id": "ath_001", "name": "Jake Larson", "grade": 12, "position": null, "current_status": "out", "status_label": "Out", "case_title": "Right ankle sprain", "avatar_color": "amber" } ], "JV": [ { "id": "ath_005", "name": "Cameron Davis", "grade": 10, "position": null, "current_status": "out", "status_label": "Out", "case_title": "Hamstring strain — right", "avatar_color": "blue" } ], "Freshman": [] }, "recent_activity": [] } ``` ### GET /api/v1/dashboard/parent?school_id=schl_001&athlete_id=ath_001 ```json { "athlete": { "id": "ath_001", "first_name": "Jake", "last_name": "Larson", "sport": "Football", "team": "Varsity", "grade": 12, "initials": "JL" }, "case": { "id": "case_001", "injury": "Right ankle sprain", "severity": "moderate", "attention": "stable", "status": "active", "phase_label": "Active Rehab", "total_phases": 4, "milestones": [ { "id": "mil_1", "title": "Initial Assessment", "status": "achieved", "target_date": "2026-04-17", "achieved_at": "2026-04-17" } ], "restrictions": [], "next_appointment": null, "care_team": [], "recent_activity": [] }, "restrictions": [], "next_appointment": null, "care_team": [], "recent_activity": [] } ``` ### GET /api/v1/dashboard/parent/children?school_id=schl_001 ```json { "children": [ { "id": "ath_001", "first_name": "Jake", "last_name": "Larson", "sport": "Football", "team": "Varsity", "grade": 12, "has_active_case": true, "current_status": "out", "initials": "JL", "avatar_color": "teal" } ] } ```