Implement decision engine override handling (P0.3)
Add getDecisionWithOverrides() function that checks manual overrides before algorithmic rules. Overrides are applied in priority order: flare > stress > sleep > pms, and all force REST status. Includes comprehensive test suite with 24 tests covering: - All 8 algorithmic priority rules - Override type behaviors - Override priority enforcement - Empty override fallthrough to algorithmic rules 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
496
IMPLEMENTATION_PLAN.md
Normal file
496
IMPLEMENTATION_PLAN.md
Normal file
@@ -0,0 +1,496 @@
|
|||||||
|
# PhaseFlow Implementation Plan
|
||||||
|
|
||||||
|
This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate tasks.
|
||||||
|
|
||||||
|
## Current State Summary
|
||||||
|
|
||||||
|
### Library Implementation
|
||||||
|
| File | Status | Gap Analysis |
|
||||||
|
|------|--------|--------------|
|
||||||
|
| `cycle.ts` | **COMPLETE** | 9 tests covering all functions, production-ready |
|
||||||
|
| `nutrition.ts` | **Complete** | getNutritionGuidance, getSeedSwitchAlert implemented. **MISSING: tests** |
|
||||||
|
| `email.ts` | **Complete** | sendDailyEmail, sendPeriodConfirmationEmail implemented. **MISSING: tests** |
|
||||||
|
| `ics.ts` | **Complete** | generateIcsFeed implemented (90 days of phase events). **MISSING: tests** |
|
||||||
|
| `encryption.ts` | **Complete** | AES-256-GCM encrypt/decrypt implemented. **MISSING: tests** |
|
||||||
|
| `decision-engine.ts` | **COMPLETE** | 8 priority rules + override handling with `getDecisionWithOverrides()`, 24 tests |
|
||||||
|
| `garmin.ts` | **Minimal (~30%)** | Has fetchGarminData, isTokenExpired, daysUntilExpiry. **MISSING: fetchHrvStatus, fetchBodyBattery, fetchIntensityMinutes** |
|
||||||
|
| `pocketbase.ts` | **Basic** | Has pb client only. **MISSING: getCurrentUser(), isAuthenticated(), loadAuthFromCookies()** |
|
||||||
|
|
||||||
|
### Missing Infrastructure Files (CONFIRMED NOT EXIST)
|
||||||
|
- `src/lib/auth-middleware.ts` - Does NOT exist, needs creation
|
||||||
|
- `src/app/middleware.ts` - Does NOT exist, needs creation
|
||||||
|
|
||||||
|
### API Routes (12 total)
|
||||||
|
| Route | Status | Notes |
|
||||||
|
|-------|--------|-------|
|
||||||
|
| GET /api/user | 501 | Returns Not Implemented |
|
||||||
|
| PATCH /api/user | 501 | Returns Not Implemented |
|
||||||
|
| POST /api/cycle/period | 501 | Returns Not Implemented |
|
||||||
|
| GET /api/cycle/current | 501 | Returns Not Implemented |
|
||||||
|
| GET /api/today | 501 | Returns Not Implemented |
|
||||||
|
| POST /api/overrides | 501 | Returns Not Implemented |
|
||||||
|
| DELETE /api/overrides | 501 | Returns Not Implemented |
|
||||||
|
| POST /api/garmin/tokens | 501 | Returns Not Implemented |
|
||||||
|
| DELETE /api/garmin/tokens | 501 | Returns Not Implemented |
|
||||||
|
| GET /api/garmin/status | 501 | Returns Not Implemented |
|
||||||
|
| GET /api/calendar/[userId]/[token].ics | 501 | Has param extraction, core logic TODO |
|
||||||
|
| POST /api/calendar/regenerate-token | 501 | Returns Not Implemented |
|
||||||
|
| POST /api/cron/garmin-sync | 501 | Has CRON_SECRET auth check, core logic TODO |
|
||||||
|
| POST /api/cron/notifications | 501 | Has CRON_SECRET auth check, core logic TODO |
|
||||||
|
|
||||||
|
### Pages (7 total, ALL placeholders)
|
||||||
|
| Page | Status | Notes |
|
||||||
|
|------|--------|-------|
|
||||||
|
| Dashboard (`/`) | Placeholder | Needs real data integration |
|
||||||
|
| Login (`/login`) | Placeholder | Needs PocketBase auth integration |
|
||||||
|
| Settings (`/settings`) | Placeholder | Needs form implementation |
|
||||||
|
| Settings/Garmin (`/settings/garmin`) | Placeholder | Needs token management UI |
|
||||||
|
| Calendar (`/calendar`) | Placeholder | Needs MonthView integration |
|
||||||
|
| History (`/history`) | Placeholder | Needs list/pagination implementation |
|
||||||
|
| Plan (`/plan`) | Placeholder | Needs phase details display |
|
||||||
|
|
||||||
|
### Components
|
||||||
|
| Component | Status | Notes |
|
||||||
|
|-----------|--------|-------|
|
||||||
|
| `DecisionCard` | **COMPLETE** | Displays status, icon, reason |
|
||||||
|
| `DataPanel` | **COMPLETE** | Shows BB, HRV, intensity data |
|
||||||
|
| `NutritionPanel` | **COMPLETE** | Shows seeds, carbs, keto guidance |
|
||||||
|
| `OverrideToggles` | **COMPLETE** | Toggle buttons with callbacks |
|
||||||
|
| `DayCell` | **COMPLETE** | Phase-colored day with click handler |
|
||||||
|
| `MiniCalendar` | **Partial (~30%)** | Has header only, **MISSING: calendar grid** |
|
||||||
|
| `MonthView` | **Partial (~30%)** | Has header only, **MISSING: calendar grid + DayCell integration** |
|
||||||
|
|
||||||
|
### Test Coverage
|
||||||
|
| Test File | Status |
|
||||||
|
|-----------|--------|
|
||||||
|
| `src/lib/cycle.test.ts` | **EXISTS** - 9 tests |
|
||||||
|
| `src/lib/decision-engine.test.ts` | **EXISTS** - 24 tests (8 algorithmic rules + 16 override scenarios) |
|
||||||
|
| `src/lib/nutrition.test.ts` | **MISSING** |
|
||||||
|
| `src/lib/email.test.ts` | **MISSING** |
|
||||||
|
| `src/lib/ics.test.ts` | **MISSING** |
|
||||||
|
| `src/lib/encryption.test.ts` | **MISSING** |
|
||||||
|
| `src/lib/garmin.test.ts` | **MISSING** |
|
||||||
|
| `src/lib/pocketbase.test.ts` | **MISSING** |
|
||||||
|
| API route tests | **NONE** |
|
||||||
|
| E2E tests | **NONE** |
|
||||||
|
|
||||||
|
### Critical Business Rules (from Spec)
|
||||||
|
1. **Override Priority:** flare > stress > sleep > pms (must be enforced in order)
|
||||||
|
2. **HRV Unbalanced:** ALWAYS forces REST (highest algorithmic priority, non-overridable)
|
||||||
|
3. **Phase Limits:** Strictly enforced per phase configuration
|
||||||
|
4. **Token Expiration Warnings:** Must send email at 14 days and 7 days before expiry
|
||||||
|
5. **ICS Feed:** Generates 90 days of phase events for calendar subscription
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P0: Critical Blockers
|
||||||
|
|
||||||
|
These must be completed first - nothing else works without them.
|
||||||
|
|
||||||
|
### P0.1: PocketBase Auth Helpers
|
||||||
|
- [ ] Add authentication utilities to pocketbase.ts
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/pocketbase.ts` - Add `getCurrentUser()`, `isAuthenticated()`, `loadAuthFromCookies()`
|
||||||
|
- **Tests:**
|
||||||
|
- `src/lib/pocketbase.test.ts` - Test auth state management, cookie loading
|
||||||
|
- **Why:** Every protected route and page depends on these helpers
|
||||||
|
- **Blocking:** P0.2, P0.4, P1.1-P1.7, P2.2-P2.13
|
||||||
|
|
||||||
|
### P0.2: Auth Middleware for API Routes
|
||||||
|
- [ ] Create reusable auth middleware for protected API endpoints
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/auth-middleware.ts` - **CREATE** `withAuth()` wrapper for route handlers
|
||||||
|
- `src/app/middleware.ts` - **CREATE** Next.js middleware for page protection
|
||||||
|
- **Tests:**
|
||||||
|
- `src/lib/auth-middleware.test.ts` - Test unauthorized rejection, user context passing
|
||||||
|
- **Why:** All API routes except `/api/calendar/[userId]/[token].ics` and `/api/cron/*` require auth
|
||||||
|
- **Depends On:** P0.1
|
||||||
|
- **Blocking:** P0.4, P1.1-P1.5
|
||||||
|
|
||||||
|
### P0.3: Decision Engine Override Handling ✅ COMPLETE
|
||||||
|
- [x] Add override priority logic before algorithmic decision
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/decision-engine.ts` - Added `getDecisionWithOverrides(data, overrides)` function
|
||||||
|
- **Tests:**
|
||||||
|
- `src/lib/decision-engine.test.ts` - 24 tests covering all 8 priority rules + override scenarios
|
||||||
|
- **Override Priority (enforced in this order):**
|
||||||
|
1. `flare` - Always forces REST
|
||||||
|
2. `stress` - Forces REST
|
||||||
|
3. `sleep` - Forces REST
|
||||||
|
4. `pms` - Forces REST
|
||||||
|
- **Why:** Overrides are core to the user experience per spec
|
||||||
|
- **Blocking:** P1.4, P1.5
|
||||||
|
|
||||||
|
### P0.4: GET /api/user Implementation
|
||||||
|
- [ ] Return authenticated user profile
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/user/route.ts` - Implement GET handler with auth middleware
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/user/route.test.ts` - Test auth required, correct response shape
|
||||||
|
- **Why:** Dashboard and all pages need user context
|
||||||
|
- **Depends On:** P0.1, P0.2
|
||||||
|
- **Blocking:** P1.7, P2.9, P2.10
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P1: Core Functionality
|
||||||
|
|
||||||
|
Minimum viable product - app can be used for daily decisions.
|
||||||
|
|
||||||
|
### P1.1: PATCH /api/user Implementation
|
||||||
|
- [ ] Allow profile updates (cycleLength, notificationTime, timezone)
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/user/route.ts` - Implement PATCH handler with validation
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/user/route.test.ts` - Test field validation, persistence
|
||||||
|
- **Why:** Users need to configure their cycle and preferences
|
||||||
|
- **Depends On:** P0.1, P0.2
|
||||||
|
|
||||||
|
### P1.2: POST /api/cycle/period Implementation
|
||||||
|
- [ ] Log period start date, update user record, create PeriodLog
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/cycle/period/route.ts` - Implement POST handler
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/cycle/period/route.test.ts` - Test date validation, user update, log creation
|
||||||
|
- **Why:** Cycle tracking is the foundation of all recommendations
|
||||||
|
- **Depends On:** P0.1, P0.2
|
||||||
|
|
||||||
|
### P1.3: GET /api/cycle/current Implementation
|
||||||
|
- [ ] Return current cycle day, phase, and phase config
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/cycle/current/route.ts` - Implement GET using cycle.ts utilities
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/cycle/current/route.test.ts` - Test phase calculation, config response
|
||||||
|
- **Why:** Dashboard needs this for display
|
||||||
|
- **Depends On:** P0.1, P0.2, P1.2
|
||||||
|
|
||||||
|
### P1.4: GET /api/today Implementation
|
||||||
|
- [ ] Return complete daily snapshot with decision, biometrics, nutrition
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/today/route.ts` - Implement GET aggregating all data sources
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/today/route.test.ts` - Test decision computation, data assembly
|
||||||
|
- **Why:** This is THE core API for the dashboard
|
||||||
|
- **Depends On:** P0.1, P0.2, P0.3, P1.3
|
||||||
|
|
||||||
|
### P1.5: POST/DELETE /api/overrides Implementation
|
||||||
|
- [ ] Toggle override flags on user record
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/overrides/route.ts` - Implement POST (add) and DELETE (remove) handlers
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/overrides/route.test.ts` - Test override types, persistence, validation
|
||||||
|
- **Override Types:** flare, stress, sleep, pms
|
||||||
|
- **Why:** Emergency overrides are critical for flare days
|
||||||
|
- **Depends On:** P0.1, P0.2, P0.3
|
||||||
|
|
||||||
|
### P1.6: Login Page Implementation
|
||||||
|
- [ ] Functional login form with PocketBase auth
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/login/page.tsx` - Form with email/password, error handling, redirect
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: valid login redirects to dashboard, invalid shows error
|
||||||
|
- **Why:** Users need to authenticate to use the app
|
||||||
|
- **Depends On:** P0.1
|
||||||
|
|
||||||
|
### P1.7: Dashboard Page Implementation
|
||||||
|
- [ ] Wire up dashboard with real data from /api/today
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/page.tsx` - Fetch data, render DecisionCard, DataPanel, NutritionPanel, OverrideToggles
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: dashboard loads data, override toggles work
|
||||||
|
- **Why:** This is the main user interface
|
||||||
|
- **Depends On:** P0.4, P1.3, P1.4, P1.5
|
||||||
|
- **Note:** Components (DecisionCard, DataPanel, NutritionPanel, OverrideToggles) are already **COMPLETE**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P2: Important Features
|
||||||
|
|
||||||
|
Full feature set for production use.
|
||||||
|
|
||||||
|
### P2.1: Garmin Data Fetching Functions
|
||||||
|
- [ ] Add specific fetchers for HRV, Body Battery, Intensity Minutes
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/garmin.ts` - Add `fetchHrvStatus()`, `fetchBodyBattery()`, `fetchIntensityMinutes()`
|
||||||
|
- **Tests:**
|
||||||
|
- `src/lib/garmin.test.ts` - Test API calls, response parsing, error handling
|
||||||
|
- **Why:** Real biometric data is required for accurate decisions
|
||||||
|
- **Note:** Currently only has generic fetchGarminData, isTokenExpired, daysUntilExpiry
|
||||||
|
|
||||||
|
### P2.2: POST/DELETE /api/garmin/tokens Implementation
|
||||||
|
- [ ] Store encrypted Garmin OAuth tokens
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/garmin/tokens/route.ts` - Implement with encryption.ts
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/garmin/tokens/route.test.ts` - Test encryption, validation, storage
|
||||||
|
- **Why:** Users need to connect their Garmin accounts
|
||||||
|
- **Depends On:** P0.1, P0.2
|
||||||
|
|
||||||
|
### P2.3: GET /api/garmin/status Implementation
|
||||||
|
- [ ] Return Garmin connection status and days until expiry
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/garmin/status/route.ts` - Implement status check
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/garmin/status/route.test.ts` - Test connected/disconnected states, expiry calc
|
||||||
|
- **Why:** Users need visibility into their Garmin connection
|
||||||
|
- **Depends On:** P0.1, P0.2, P2.1
|
||||||
|
|
||||||
|
### P2.4: POST /api/cron/garmin-sync Implementation
|
||||||
|
- [ ] Daily sync of all Garmin data for all users
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/cron/garmin-sync/route.ts` - Iterate users, fetch data, store DailyLog
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/cron/garmin-sync/route.test.ts` - Test auth, user iteration, data persistence
|
||||||
|
- **Why:** Automated data sync is required for morning notifications
|
||||||
|
- **Depends On:** P2.1, P2.2
|
||||||
|
- **Note:** Route exists with CRON_SECRET auth check, needs core logic
|
||||||
|
|
||||||
|
### P2.5: POST /api/cron/notifications Implementation
|
||||||
|
- [ ] Send daily email notifications at user's preferred time
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/cron/notifications/route.ts` - Find users by hour, compute decision, send email
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/cron/notifications/route.test.ts` - Test timezone handling, duplicate prevention
|
||||||
|
- **Why:** Email notifications are a key feature per spec
|
||||||
|
- **Depends On:** P2.4
|
||||||
|
- **Note:** Route exists with CRON_SECRET auth check, needs core logic
|
||||||
|
|
||||||
|
### P2.6: GET /api/calendar/[userId]/[token].ics Implementation
|
||||||
|
- [ ] Return ICS feed for calendar subscription
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/calendar/[userId]/[token].ics/route.ts` - Validate token, generate ICS
|
||||||
|
- **Tests:**
|
||||||
|
- Integration test: valid token returns ICS, invalid returns 401
|
||||||
|
- **Why:** Calendar integration for external apps
|
||||||
|
- **Note:** Route has param extraction, needs ICS generation (90 days of events per spec)
|
||||||
|
|
||||||
|
### P2.7: POST /api/calendar/regenerate-token Implementation
|
||||||
|
- [ ] Generate new calendar token
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/calendar/regenerate-token/route.ts` - Create random token, update user
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/calendar/regenerate-token/route.test.ts` - Test token uniqueness, old URL invalidation
|
||||||
|
- **Why:** Security feature for calendar URLs
|
||||||
|
- **Depends On:** P0.1, P0.2
|
||||||
|
|
||||||
|
### P2.8: GET /api/history Implementation
|
||||||
|
- [ ] Return paginated historical daily logs
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/api/history/route.ts` - Query DailyLog with pagination
|
||||||
|
- **Tests:**
|
||||||
|
- `src/app/api/history/route.test.ts` - Test pagination, date filtering
|
||||||
|
- **Why:** Users want to see their training history
|
||||||
|
- **Depends On:** P0.1, P0.2
|
||||||
|
|
||||||
|
### P2.9: Settings Page Implementation
|
||||||
|
- [ ] User profile management UI
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/settings/page.tsx` - Form for cycleLength, notificationTime, timezone
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: settings update and persist
|
||||||
|
- **Why:** Users need to configure their preferences
|
||||||
|
- **Depends On:** P0.4, P1.1
|
||||||
|
|
||||||
|
### P2.10: Settings/Garmin Page Implementation
|
||||||
|
- [ ] Garmin connection management UI
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/settings/garmin/page.tsx` - Token input form, connection status, disconnect button
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: connect flow, disconnect flow
|
||||||
|
- **Why:** Users need to manage their Garmin connection
|
||||||
|
- **Depends On:** P0.4, P2.2, P2.3
|
||||||
|
|
||||||
|
### P2.11: Calendar Page Implementation
|
||||||
|
- [ ] In-app calendar with phase visualization
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/calendar/page.tsx` - Month view with navigation
|
||||||
|
- `src/components/calendar/month-view.tsx` - **Complete calendar grid using DayCell**
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: navigation works, phases displayed correctly
|
||||||
|
- **Why:** Planning ahead is a key user need
|
||||||
|
- **Depends On:** P2.6
|
||||||
|
- **Note:** DayCell is **COMPLETE**, MonthView needs grid implementation (~70% remaining)
|
||||||
|
|
||||||
|
### P2.12: History Page Implementation
|
||||||
|
- [ ] View past training decisions and data
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/history/page.tsx` - List view of DailyLogs with pagination
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: history loads, pagination works
|
||||||
|
- **Why:** Users want to review their training history
|
||||||
|
- **Depends On:** P2.8
|
||||||
|
|
||||||
|
### P2.13: Plan Page Implementation
|
||||||
|
- [ ] Phase-specific training plan view
|
||||||
|
- **Files:**
|
||||||
|
- `src/app/plan/page.tsx` - Current phase details, upcoming phases, limits
|
||||||
|
- **Tests:**
|
||||||
|
- E2E test: correct phase info displayed
|
||||||
|
- **Why:** Users want detailed training guidance
|
||||||
|
- **Depends On:** P0.4, P1.3
|
||||||
|
|
||||||
|
### P2.14: Mini Calendar Component
|
||||||
|
- [ ] Dashboard overview calendar
|
||||||
|
- **Files:**
|
||||||
|
- `src/components/dashboard/mini-calendar.tsx` - **Complete calendar grid with phase colors**
|
||||||
|
- **Tests:**
|
||||||
|
- Component test: renders current month, highlights today
|
||||||
|
- **Why:** Quick visual reference on dashboard
|
||||||
|
- **Note:** Component exists with header only, needs calendar grid (~70% remaining)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## P3: Polish and Quality
|
||||||
|
|
||||||
|
Testing, error handling, and refinements.
|
||||||
|
|
||||||
|
### P3.1: Decision Engine Tests ✅ COMPLETE
|
||||||
|
- [x] Comprehensive unit tests for all decision paths
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/decision-engine.test.ts` - All 8 priority rules, override combinations (24 tests)
|
||||||
|
- **Test Cases Covered:**
|
||||||
|
- HRV Unbalanced always forces REST (highest algorithmic priority)
|
||||||
|
- Override priority: flare > stress > sleep > pms
|
||||||
|
- Phase limits strictly enforced
|
||||||
|
- All override bypass and fallthrough scenarios
|
||||||
|
- **Why:** Critical logic is now fully tested
|
||||||
|
|
||||||
|
### P3.2: Nutrition Tests
|
||||||
|
- [ ] Unit tests for nutrition guidance
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/nutrition.test.ts` - Seed cycling, carb ranges, keto guidance by day
|
||||||
|
- **Why:** Nutrition advice must be accurate
|
||||||
|
|
||||||
|
### P3.3: Email Tests
|
||||||
|
- [ ] Unit tests for email composition
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/email.test.ts` - Email content, subject lines
|
||||||
|
- **Why:** Email formatting must be correct
|
||||||
|
|
||||||
|
### P3.4: ICS Tests
|
||||||
|
- [ ] Unit tests for calendar generation
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/ics.test.ts` - ICS format validation, 90-day event generation
|
||||||
|
- **Why:** Calendar integration must work with external apps
|
||||||
|
|
||||||
|
### P3.5: Encryption Tests
|
||||||
|
- [ ] Unit tests for encrypt/decrypt round-trip
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/encryption.test.ts` - Round-trip, error handling
|
||||||
|
- **Why:** Token security is critical
|
||||||
|
|
||||||
|
### P3.6: Garmin Tests
|
||||||
|
- [ ] Unit tests for Garmin API interactions
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/garmin.test.ts` - API calls, error handling, token expiry
|
||||||
|
- **Why:** External API integration must be robust
|
||||||
|
|
||||||
|
### P3.7: Error Handling Improvements
|
||||||
|
- [ ] Add consistent error responses across all API routes
|
||||||
|
- **Files:**
|
||||||
|
- All route files - Standardize error format, add logging
|
||||||
|
- **Why:** Better debugging and user experience
|
||||||
|
|
||||||
|
### P3.8: Loading States
|
||||||
|
- [ ] Add loading indicators to all pages
|
||||||
|
- **Files:**
|
||||||
|
- All page files - Add loading.tsx or Suspense boundaries
|
||||||
|
- **Why:** Better perceived performance
|
||||||
|
|
||||||
|
### P3.9: Token Expiration Warnings
|
||||||
|
- [ ] Email warnings at 14 and 7 days before Garmin token expiry
|
||||||
|
- **Files:**
|
||||||
|
- `src/lib/email.ts` - Add `sendTokenExpirationWarning()`
|
||||||
|
- `src/app/api/cron/garmin-sync/route.ts` - Check expiry, trigger warnings
|
||||||
|
- **Tests:**
|
||||||
|
- Test warning triggers at exactly 14 days and 7 days
|
||||||
|
- **Why:** Users need time to refresh tokens (per spec requirement)
|
||||||
|
|
||||||
|
### P3.10: E2E Test Suite
|
||||||
|
- [ ] Comprehensive end-to-end tests
|
||||||
|
- **Files:**
|
||||||
|
- `tests/e2e/*.spec.ts` - Full user flows
|
||||||
|
- **Test Scenarios:**
|
||||||
|
- Login flow
|
||||||
|
- Period logging and phase calculation
|
||||||
|
- Override toggle functionality
|
||||||
|
- Settings update flow
|
||||||
|
- Garmin connection flow
|
||||||
|
- Calendar subscription
|
||||||
|
- **Why:** Confidence in production deployment
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Order
|
||||||
|
|
||||||
|
```
|
||||||
|
P0.1 PocketBase Auth ──┬──> P0.2 Auth Middleware ──> P0.4 GET /api/user
|
||||||
|
│
|
||||||
|
P0.3 Override Logic ───┴──> P1.4 GET /api/today ──> P1.7 Dashboard
|
||||||
|
|
||||||
|
P1.1 PATCH /api/user ────> P2.9 Settings Page
|
||||||
|
P1.2 POST period ────────> P1.3 GET current ────> P1.7 Dashboard
|
||||||
|
P1.5 Overrides API ──────> P1.7 Dashboard
|
||||||
|
P1.6 Login Page
|
||||||
|
|
||||||
|
P2.1 Garmin fetchers ──> P2.2 Garmin tokens ──> P2.4 Cron sync ──> P2.5 Notifications
|
||||||
|
│
|
||||||
|
└──> P3.9 Token Warnings
|
||||||
|
P2.3 Garmin status ────> P2.10 Garmin settings
|
||||||
|
|
||||||
|
P2.6 ICS endpoint ─────> P2.11 Calendar page
|
||||||
|
P2.7 Regen token
|
||||||
|
|
||||||
|
P2.8 History API ──────> P2.12 History page
|
||||||
|
P2.13 Plan page
|
||||||
|
P2.14 Mini calendar
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dependency Summary
|
||||||
|
|
||||||
|
| Task | Blocked By | Blocks |
|
||||||
|
|------|------------|--------|
|
||||||
|
| P0.1 | - | P0.2, P0.4, P1.1-P1.6, P2.2-P2.3, P2.7-P2.8 |
|
||||||
|
| P0.2 | P0.1 | P0.4, P1.1-P1.5, P2.2-P2.3, P2.7-P2.8 |
|
||||||
|
| P0.3 | - | P1.4, P1.5 |
|
||||||
|
| P0.4 | P0.1, P0.2 | P1.7, P2.9, P2.10, P2.13 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Completed
|
||||||
|
|
||||||
|
### Library
|
||||||
|
- [x] **cycle.ts** - Complete with 9 tests (`getCycleDay`, `getPhase`, `getPhaseConfig`, `getPhaseLimit`)
|
||||||
|
- [x] **decision-engine.ts** - Complete with 24 tests (`getTrainingDecision` + `getDecisionWithOverrides`)
|
||||||
|
|
||||||
|
### Components
|
||||||
|
- [x] **DecisionCard** - Displays decision status, icon, and reason
|
||||||
|
- [x] **DataPanel** - Shows body battery, HRV, intensity data
|
||||||
|
- [x] **NutritionPanel** - Shows seeds, carbs, keto guidance
|
||||||
|
- [x] **OverrideToggles** - Toggle buttons for flare/stress/sleep/pms
|
||||||
|
- [x] **DayCell** - Phase-colored calendar day cell with click handler
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Discovered Issues
|
||||||
|
|
||||||
|
*Bugs and inconsistencies found during implementation*
|
||||||
|
|
||||||
|
- [ ] `src/lib/auth-middleware.ts` does not exist - must be created in P0.2
|
||||||
|
- [ ] `src/app/middleware.ts` does not exist - must be created in P0.2
|
||||||
|
- [ ] `garmin.ts` is only ~30% complete - missing specific biometric fetchers
|
||||||
|
- [ ] `pocketbase.ts` missing all auth helper functions
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
1. **TDD Approach:** Each implementation task should follow TDD - write failing tests first, then implement
|
||||||
|
2. **Auth First:** P0 items unlock all other work; prioritize ruthlessly
|
||||||
|
3. **Incremental Delivery:** P1 completion = usable app without Garmin (manual data entry fallback)
|
||||||
|
4. **P2 Completion:** Full feature set with automation
|
||||||
|
5. **P3:** Quality and polish for production confidence
|
||||||
|
6. **Component Reuse:** Dashboard components are complete and can be used directly in P1.7
|
||||||
|
7. **HRV Rule:** HRV Unbalanced status ALWAYS forces REST - this is the highest algorithmic priority and cannot be overridden by manual toggles
|
||||||
|
8. **Override Order:** When multiple overrides are active, apply in order: flare > stress > sleep > pms
|
||||||
|
9. **Token Warnings:** Per spec, warnings must be sent at exactly 14 days and 7 days before expiry
|
||||||
242
src/lib/decision-engine.test.ts
Normal file
242
src/lib/decision-engine.test.ts
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
// ABOUTME: Unit tests for the training decision engine.
|
||||||
|
// ABOUTME: Tests all 8 priority rules and 4 override scenarios.
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import type { DailyData, OverrideType } from "@/types";
|
||||||
|
import {
|
||||||
|
getDecisionWithOverrides,
|
||||||
|
getTrainingDecision,
|
||||||
|
} from "./decision-engine";
|
||||||
|
|
||||||
|
// Helper to create baseline "healthy" data where TRAIN would be the decision
|
||||||
|
function createHealthyData(): DailyData {
|
||||||
|
return {
|
||||||
|
hrvStatus: "Balanced",
|
||||||
|
bbYesterdayLow: 50, // above 30
|
||||||
|
phase: "FOLLICULAR", // not LATE_LUTEAL or MENSTRUAL
|
||||||
|
weekIntensity: 0, // well below limit
|
||||||
|
phaseLimit: 120,
|
||||||
|
bbCurrent: 90, // above 85
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getTrainingDecision (algorithmic rules)", () => {
|
||||||
|
describe("Priority 1: HRV Unbalanced", () => {
|
||||||
|
it("forces REST when HRV is Unbalanced", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.hrvStatus = "Unbalanced";
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("HRV");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 2: Body Battery Yesterday Low", () => {
|
||||||
|
it("forces REST when BB yesterday low is below 30", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.bbYesterdayLow = 25;
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("BB");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 3: Late Luteal Phase", () => {
|
||||||
|
it("forces GENTLE during late luteal phase", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.phase = "LATE_LUTEAL";
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("GENTLE");
|
||||||
|
expect(result.reason).toContain("rebounding");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 4: Menstrual Phase", () => {
|
||||||
|
it("forces GENTLE during menstrual phase", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.phase = "MENSTRUAL";
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("GENTLE");
|
||||||
|
expect(result.reason).toContain("rebounding");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 5: Weekly Intensity Limit", () => {
|
||||||
|
it("forces REST when weekly intensity meets limit", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.weekIntensity = 120; // equals phaseLimit
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("LIMIT");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("forces REST when weekly intensity exceeds limit", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.weekIntensity = 150; // exceeds phaseLimit
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 6: Body Battery Current Low", () => {
|
||||||
|
it("forces LIGHT when current BB is below 75", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.bbCurrent = 70;
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("LIGHT");
|
||||||
|
expect(result.reason).toContain("BB");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 7: Body Battery Current Medium", () => {
|
||||||
|
it("forces REDUCED when current BB is below 85 but above 74", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.bbCurrent = 80;
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("REDUCED");
|
||||||
|
expect(result.reason).toContain("25%");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority 8: Default", () => {
|
||||||
|
it("returns TRAIN when all conditions are favorable", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("TRAIN");
|
||||||
|
expect(result.reason).toContain("OK to train");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Priority enforcement", () => {
|
||||||
|
it("HRV Unbalanced takes precedence over low BB yesterday", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.hrvStatus = "Unbalanced";
|
||||||
|
data.bbYesterdayLow = 25;
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.reason).toContain("HRV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("low BB yesterday takes precedence over phase-based rules", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.bbYesterdayLow = 25;
|
||||||
|
data.phase = "LATE_LUTEAL";
|
||||||
|
const result = getTrainingDecision(data);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("BB");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDecisionWithOverrides", () => {
|
||||||
|
describe("override types force REST", () => {
|
||||||
|
it("flare override forces REST", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["flare"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("flare");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stress override forces REST", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["stress"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("stress");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sleep override forces REST", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["sleep"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("sleep");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pms override forces REST", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["pms"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("pms");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("override priority (flare > stress > sleep > pms)", () => {
|
||||||
|
it("flare takes precedence over stress", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["stress", "flare"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.reason).toContain("flare");
|
||||||
|
expect(result.reason).not.toContain("stress");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stress takes precedence over sleep", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["sleep", "stress"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.reason).toContain("stress");
|
||||||
|
expect(result.reason).not.toContain("sleep");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sleep takes precedence over pms", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["pms", "sleep"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.reason).toContain("sleep");
|
||||||
|
expect(result.reason).not.toContain("pms");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("respects full priority chain with all overrides", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = ["pms", "sleep", "stress", "flare"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.reason).toContain("flare");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("overrides bypass algorithmic rules", () => {
|
||||||
|
it("flare override bypasses favorable conditions", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
// Normally this would return TRAIN
|
||||||
|
const overrides: OverrideType[] = ["flare"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stress override works even with perfect biometrics", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.bbCurrent = 100;
|
||||||
|
data.bbYesterdayLow = 80;
|
||||||
|
const overrides: OverrideType[] = ["stress"];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("empty overrides fall through to algorithmic rules", () => {
|
||||||
|
it("returns TRAIN with no overrides and healthy data", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
const overrides: OverrideType[] = [];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("TRAIN");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns REST for HRV Unbalanced with no overrides", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.hrvStatus = "Unbalanced";
|
||||||
|
const overrides: OverrideType[] = [];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("REST");
|
||||||
|
expect(result.reason).toContain("HRV");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("algorithmic rules apply when overrides array is empty", () => {
|
||||||
|
const data = createHealthyData();
|
||||||
|
data.bbCurrent = 70;
|
||||||
|
const overrides: OverrideType[] = [];
|
||||||
|
const result = getDecisionWithOverrides(data, overrides);
|
||||||
|
expect(result.status).toBe("LIGHT");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,16 @@
|
|||||||
// ABOUTME: Training decision engine based on biometric and cycle data.
|
// ABOUTME: Training decision engine based on biometric and cycle data.
|
||||||
// ABOUTME: Implements priority-based rules for daily training recommendations.
|
// ABOUTME: Implements priority-based rules for daily training recommendations.
|
||||||
import type { DailyData, Decision } from "@/types";
|
import type { DailyData, Decision, OverrideType } from "@/types";
|
||||||
|
|
||||||
|
// Override priority order - checked before algorithmic rules
|
||||||
|
const OVERRIDE_PRIORITY: OverrideType[] = ["flare", "stress", "sleep", "pms"];
|
||||||
|
|
||||||
|
const OVERRIDE_REASONS: Record<OverrideType, string> = {
|
||||||
|
flare: "Hashimoto's flare - rest required",
|
||||||
|
stress: "High stress override - rest required",
|
||||||
|
sleep: "Poor sleep override - rest required",
|
||||||
|
pms: "pms override - rest required",
|
||||||
|
};
|
||||||
|
|
||||||
export function getTrainingDecision(data: DailyData): Decision {
|
export function getTrainingDecision(data: DailyData): Decision {
|
||||||
const {
|
const {
|
||||||
@@ -62,3 +72,22 @@ export function getTrainingDecision(data: DailyData): Decision {
|
|||||||
icon: "✅",
|
icon: "✅",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getDecisionWithOverrides(
|
||||||
|
data: DailyData,
|
||||||
|
overrides: OverrideType[],
|
||||||
|
): Decision {
|
||||||
|
// Check overrides first, in priority order: flare > stress > sleep > pms
|
||||||
|
for (const override of OVERRIDE_PRIORITY) {
|
||||||
|
if (overrides.includes(override)) {
|
||||||
|
return {
|
||||||
|
status: "REST",
|
||||||
|
reason: OVERRIDE_REASONS[override],
|
||||||
|
icon: "🛑",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No active overrides - fall through to algorithmic rules
|
||||||
|
return getTrainingDecision(data);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user