From d7ecc2944d17475dcb171803d1cc2dd69a625319 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sat, 10 Jan 2026 17:13:18 +0000 Subject: [PATCH] Setup Ralph. --- AGENTS.md | 51 +++++++++++++ PROMPT_build.md | 84 +++++++++++++++++++++ PROMPT_plan.md | 64 ++++++++++++++++ flake.nix | 49 +++++++++--- loop.sh | 100 ++++++++++++++++++++++++ ralph-sandbox.sh | 20 +++++ specs/authentication.md | 147 ++++++++++++++++++++++++++++++++++++ specs/calendar.md | 114 ++++++++++++++++++++++++++++ specs/cycle-tracking.md | 120 +++++++++++++++++++++++++++++ specs/dashboard.md | 94 +++++++++++++++++++++++ specs/decision-engine.md | 143 +++++++++++++++++++++++++++++++++++ specs/garmin-integration.md | 108 ++++++++++++++++++++++++++ specs/notifications.md | 110 +++++++++++++++++++++++++++ specs/nutrition.md | 94 +++++++++++++++++++++++ 14 files changed, 1287 insertions(+), 11 deletions(-) create mode 100644 AGENTS.md create mode 100644 PROMPT_build.md create mode 100644 PROMPT_plan.md create mode 100755 loop.sh create mode 100755 ralph-sandbox.sh create mode 100644 specs/authentication.md create mode 100644 specs/calendar.md create mode 100644 specs/cycle-tracking.md create mode 100644 specs/dashboard.md create mode 100644 specs/decision-engine.md create mode 100644 specs/garmin-integration.md create mode 100644 specs/notifications.md create mode 100644 specs/nutrition.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8fe1e51 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,51 @@ +# ABOUTME: Operational notes for Ralph autonomous development loops. +# ABOUTME: Contains build/test commands and codebase patterns specific to PhaseFlow. + +## Build & Run + +- Dev server: `pnpm dev` +- Build: `pnpm build` +- Start production: `pnpm start` + +## Validation + +Run these after implementing to get immediate feedback: + +- Tests: `pnpm test:run` +- Lint: `pnpm lint` +- Lint fix: `pnpm lint:fix` +- Typecheck: `pnpm tsc --noEmit` + +## Operational Notes + +- Database: PocketBase at `POCKETBASE_URL` env var +- Garmin tokens encrypted with AES-256 using `ENCRYPTION_KEY` (32 chars) +- Path aliases: `@/*` maps to `./src/*` +- Pre-commit hooks: Biome lint + Vitest tests via Lefthook + +## Codebase Patterns + +- TDD required: Write tests before implementation +- All files start with 2-line ABOUTME comments +- No mock mode: Use real data and APIs only +- Never use `--no-verify` for git commits +- Commit format: Descriptive message + Claude footer + +## Tech Stack + +| Layer | Choice | +|-------|--------| +| Framework | Next.js 16 (App Router) | +| Runtime | Node.js 24 | +| Database | PocketBase | +| Validation | Zod | +| Testing | Vitest + jsdom | +| Linting | Biome | + +## File Structure + +- `src/app/` - Next.js pages and API routes +- `src/components/` - React UI components +- `src/lib/` - Business logic utilities +- `src/types/` - TypeScript type definitions +- `specs/` - Feature specifications for Ralph diff --git a/PROMPT_build.md b/PROMPT_build.md new file mode 100644 index 0000000..4890ffe --- /dev/null +++ b/PROMPT_build.md @@ -0,0 +1,84 @@ +# PhaseFlow Build Mode + +Implement functionality from the plan, validate with tests, and commit. + +## 0. Orient + +0a. Study `specs/*` with parallel Sonnet subagents to learn the application specifications. +0b. Study @IMPLEMENTATION_PLAN.md to understand current priorities. +0c. Study @AGENTS.md to understand build/test commands and codebase patterns. +0d. For reference, the application source code is in `src/*`. + +## 1. Select and Implement + +Your task is to implement functionality per the specifications using parallel subagents. + +Follow @IMPLEMENTATION_PLAN.md and choose the most important item to address. + +Before making changes, search the codebase (don't assume not implemented) using Sonnet subagents. You may use up to 8 parallel Sonnet subagents for searches/reads and only 1 Sonnet subagent for build/tests. Use Opus subagents when complex reasoning is needed (debugging, architectural decisions). + +## 2. TDD Workflow + +Follow TDD per CLAUDE.md: + +1. **Write a failing test** that defines the desired behavior +2. Run the test to confirm it fails: `pnpm test:run` +3. **Write minimal code** to make the test pass +4. Run tests to confirm success +5. **Refactor** while keeping tests green +6. Repeat for each piece of functionality + +Ultrathink. + +## 3. Update Plan + +When you discover issues, immediately update @IMPLEMENTATION_PLAN.md with your findings using a subagent. + +When resolved, update and remove the item. + +## 4. Commit + +When the tests pass: +1. Update @IMPLEMENTATION_PLAN.md +2. Run `pnpm lint` and fix any issues +3. `git add -A` +4. `git commit` with a descriptive message (NEVER use --no-verify) +5. `git push` + +## 5. Documentation + +When authoring documentation, capture the why — tests and implementation importance. + +## 6. Quality Rules + +- Single sources of truth, no migrations/adapters +- If tests unrelated to your work fail, resolve them as part of the increment +- Implement functionality completely. Placeholders and stubs waste efforts and time redoing the same work +- All files must start with 2-line ABOUTME comments +- No mock mode - use real data and APIs only + +## 7. Maintenance + +- Keep @IMPLEMENTATION_PLAN.md current with learnings using a subagent — future work depends on this to avoid duplicating efforts +- Update especially after finishing your turn +- When @IMPLEMENTATION_PLAN.md becomes large, periodically clean out completed items + +## 8. Operational Learning + +When you learn something new about how to run the application, update @AGENTS.md using a subagent but keep it brief. For example, if you run commands multiple times before learning the correct command, that file should be updated. + +## 9. Bug Handling + +For any bugs you notice, resolve them or document them in @IMPLEMENTATION_PLAN.md using a subagent, even if unrelated to the current piece of work. + +## 10. Spec Consistency + +If you find inconsistencies in the `specs/*` then use an Opus subagent with 'ultrathink' requested to update the specs. + +## 11. Keep AGENTS.md Lean + +IMPORTANT: Keep @AGENTS.md operational only — status updates and progress notes belong in @IMPLEMENTATION_PLAN.md. A bloated AGENTS.md pollutes every future loop's context. + +## 12. Exit + +After committing, exit cleanly. The loop will restart with fresh context. diff --git a/PROMPT_plan.md b/PROMPT_plan.md new file mode 100644 index 0000000..72d2f5f --- /dev/null +++ b/PROMPT_plan.md @@ -0,0 +1,64 @@ +# PhaseFlow Planning Mode + +Study the project to understand current state and generate an implementation plan. + +## 0. Orient + +0a. Study `specs/*` with up to 8 parallel Sonnet subagents to learn the application specifications. +0b. Study @IMPLEMENTATION_PLAN.md (if present) to understand the plan so far. +0c. Study `src/lib/*` with parallel Sonnet subagents to understand shared utilities & components. +0d. Study @AGENTS.md to understand build/test commands and codebase patterns. +0e. For reference, the application source code is in `src/*`. + +## 1. Gap Analysis + +Study @IMPLEMENTATION_PLAN.md (if present; it may be incorrect) and use parallel Sonnet subagents to study existing source code in `src/*` and compare it against `specs/*`. + +Use an Opus subagent to analyze findings, prioritize tasks, and create/update @IMPLEMENTATION_PLAN.md as a bullet point list sorted in priority of items yet to be implemented. + +Ultrathink. Consider searching for: +- TODO comments +- Minimal implementations +- Placeholder components +- Skipped or flaky tests +- Inconsistent patterns +- Missing acceptance tests from specs + +Study @IMPLEMENTATION_PLAN.md to determine starting point for research and keep it up to date with items considered complete/incomplete using subagents. + +## 2. Important Constraints + +**IMPORTANT: Plan only. Do NOT implement anything.** + +Do NOT assume functionality is missing; confirm with code search first. + +Treat `src/lib` as the project's standard library for shared utilities and components. Prefer consolidated, idiomatic implementations there over ad-hoc copies. + +## 3. Ultimate Goal + +We want to achieve: **A training decision app that integrates menstrual cycle phases with Garmin biometrics for Hashimoto's thyroiditis management.** + +The app should: +- Display daily training decisions (REST/GENTLE/LIGHT/REDUCED/TRAIN) +- Sync biometric data from Garmin (HRV, Body Battery, Intensity Minutes) +- Track menstrual cycle phases and adjust training limits +- Provide nutrition guidance (seed cycling, carb ranges) +- Send daily email notifications +- Offer ICS calendar subscription + +Consider missing elements and plan accordingly. If an element is missing: +1. Search first to confirm it doesn't exist +2. If needed, author the specification at `specs/FILENAME.md` +3. Document the plan to implement it in @IMPLEMENTATION_PLAN.md using a subagent + +## 4. TDD Requirement + +When planning implementation tasks, ensure each task includes: +- Writing tests BEFORE implementation (per CLAUDE.md) +- Running tests to confirm they fail +- Implementing minimal code to pass +- Refactoring while keeping tests green + +## 5. Exit + +After updating @IMPLEMENTATION_PLAN.md, exit cleanly. diff --git a/flake.nix b/flake.nix index 21ccc3c..107af50 100644 --- a/flake.nix +++ b/flake.nix @@ -1,5 +1,5 @@ # ABOUTME: Nix flake for PhaseFlow development environment. -# ABOUTME: Provides Node.js 24, pnpm, turbo, and lefthook. +# ABOUTME: Provides Node.js 24, pnpm, turbo, lefthook, and Ralph sandbox shell. { inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; @@ -7,17 +7,44 @@ let system = "x86_64-linux"; pkgs = nixpkgs.legacyPackages.${system}; - in { - devShells.${system}.default = pkgs.mkShell { - packages = with pkgs; [ - nodejs_24 - pnpm - turbo - lefthook - ]; - # For native modules (sharp, better-sqlite3, etc.) - LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ]; + # Common packages for development + commonPackages = with pkgs; [ + nodejs_24 + pnpm + git + ]; + in { + devShells.${system} = { + # Default development shell with all tools + default = pkgs.mkShell { + packages = commonPackages ++ (with pkgs; [ + turbo + lefthook + ]); + + # For native modules (sharp, better-sqlite3, etc.) + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ]; + }; + + # Ralph sandbox shell with minimal permissions + # Used for autonomous Ralph loop execution + ralph = pkgs.mkShell { + packages = commonPackages ++ (with pkgs; [ + # Claude CLI (assumes installed globally or via npm) + # Add any other tools Ralph needs here + ]); + + # Restrictive environment for sandboxed execution + shellHook = '' + echo "🔒 Ralph Sandbox Environment" + echo " Limited to: nodejs, pnpm, git" + echo "" + ''; + + # For native modules + LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ]; + }; }; }; } diff --git a/loop.sh b/loop.sh new file mode 100755 index 0000000..46a6ff0 --- /dev/null +++ b/loop.sh @@ -0,0 +1,100 @@ +#!/usr/bin/env bash +# ABOUTME: Ralph outer loop script for autonomous development. +# ABOUTME: Runs Claude in headless mode, iterating on IMPLEMENTATION_PLAN.md. + +# Usage: ./loop.sh [plan] [max_iterations] +# Examples: +# ./loop.sh # Build mode, unlimited iterations +# ./loop.sh 20 # Build mode, max 20 iterations +# ./loop.sh plan # Plan mode, unlimited iterations +# ./loop.sh plan 5 # Plan mode, max 5 iterations + +set -e + +# Parse arguments +if [ "$1" = "plan" ]; then + MODE="plan" + PROMPT_FILE="PROMPT_plan.md" + MAX_ITERATIONS=${2:-0} +elif [[ "$1" =~ ^[0-9]+$ ]]; then + MODE="build" + PROMPT_FILE="PROMPT_build.md" + MAX_ITERATIONS=$1 +else + MODE="build" + PROMPT_FILE="PROMPT_build.md" + MAX_ITERATIONS=0 +fi + +ITERATION=0 +CURRENT_BRANCH=$(git branch --show-current) + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🔄 Ralph Loop Starting" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "Mode: $MODE" +echo "Prompt: $PROMPT_FILE" +echo "Branch: $CURRENT_BRANCH" +[ $MAX_ITERATIONS -gt 0 ] && echo "Max iterations: $MAX_ITERATIONS" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# Verify prompt file exists +if [ ! -f "$PROMPT_FILE" ]; then + echo "Error: $PROMPT_FILE not found" + exit 1 +fi + +# Verify we're in a git repo +if ! git rev-parse --git-dir > /dev/null 2>&1; then + echo "Error: Not in a git repository" + exit 1 +fi + +while true; do + if [ $MAX_ITERATIONS -gt 0 ] && [ $ITERATION -ge $MAX_ITERATIONS ]; then + echo "" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "✅ Reached max iterations: $MAX_ITERATIONS" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + break + fi + + ITERATION=$((ITERATION + 1)) + echo "" + echo "═══════════════════════════════════════════" + echo "🔄 ITERATION $ITERATION" + echo "═══════════════════════════════════════════" + echo "" + + # Run Ralph iteration with selected prompt + # -p: Headless mode (non-interactive, reads from stdin) + # --dangerously-skip-permissions: Auto-approve all tool calls + # --output-format=stream-json: Structured output for logging + # --model opus: Use Opus for complex reasoning + cat "$PROMPT_FILE" | claude -p \ + --dangerously-skip-permissions \ + --output-format=stream-json \ + --model opus \ + --verbose + + # Push changes after each iteration + if git diff --quiet && git diff --cached --quiet; then + echo "No changes to push" + else + git push origin "$CURRENT_BRANCH" 2>/dev/null || { + echo "Creating remote branch..." + git push -u origin "$CURRENT_BRANCH" + } + fi + + echo "" + echo "───────────────────────────────────────────" + echo "Iteration $ITERATION complete" + echo "───────────────────────────────────────────" +done + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🏁 Ralph Loop Complete" +echo "Total iterations: $ITERATION" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" diff --git a/ralph-sandbox.sh b/ralph-sandbox.sh new file mode 100755 index 0000000..5426940 --- /dev/null +++ b/ralph-sandbox.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# ABOUTME: Wrapper script to run Ralph in Nix-isolated environment. +# ABOUTME: Uses the 'ralph' devShell for restricted execution. + +# Usage: ./ralph-sandbox.sh [args...] +# Examples: +# ./ralph-sandbox.sh plan 3 # Plan mode, 3 iterations +# ./ralph-sandbox.sh 10 # Build mode, 10 iterations +# ./ralph-sandbox.sh # Build mode, unlimited + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "🔒 Launching Ralph in Nix Sandbox" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# Run loop.sh inside the ralph devShell +nix develop "${SCRIPT_DIR}#ralph" --command "${SCRIPT_DIR}/loop.sh" "$@" diff --git a/specs/authentication.md b/specs/authentication.md new file mode 100644 index 0000000..7a86e0e --- /dev/null +++ b/specs/authentication.md @@ -0,0 +1,147 @@ +# Authentication Specification + +## Job to Be Done + +When I access PhaseFlow, I want to securely log in with my email, so that my personal health data remains private. + +## Auth Provider + +Using PocketBase for authentication and data storage. + +**Connection:** +- `POCKETBASE_URL` environment variable +- `src/lib/pocketbase.ts` initializes client + +## Login Flow + +### Email/Password Authentication + +1. User enters email and password on `/login` +2. App calls PocketBase `authWithPassword` +3. On success, PocketBase sets auth cookie +4. User redirected to dashboard + +### Session Management + +- PocketBase manages session tokens automatically +- Auth state persisted in browser (cookie/localStorage) +- Session expires after 14 days of inactivity + +## Pages + +### `/login` + +**Elements:** +- Email input +- Password input +- "Sign In" button +- Error message display +- Link to password reset (future) + +**Behavior:** +- Redirect to `/` on successful login +- Show error message on failed attempt +- Rate limit: 5 attempts per minute + +### Protected Routes + +All routes except `/login` require authentication. + +**Middleware Check:** +1. Check for valid PocketBase auth token +2. If invalid/missing, redirect to `/login` +3. If valid, proceed to requested page + +## API Authentication + +### User Context + +API routes access current user via: +```typescript +const pb = new PocketBase(process.env.POCKETBASE_URL); +// Auth token from request cookies +const user = pb.authStore.model; +``` + +### Protected Endpoints + +All `/api/*` routes except: +- `/api/calendar/[userId]/[token].ics` (token-based auth) +- `/api/cron/*` (CRON_SECRET auth) + +## User API + +### GET `/api/user` + +Returns current authenticated user profile. + +**Response:** +```json +{ + "id": "user123", + "email": "user@example.com", + "garminConnected": true, + "cycleLength": 31, + "lastPeriodDate": "2024-01-01", + "notificationTime": "07:00", + "timezone": "America/New_York" +} +``` + +### PATCH `/api/user` + +Updates user profile fields. + +**Request Body (partial update):** +```json +{ + "cycleLength": 28, + "notificationTime": "06:30" +} +``` + +## User Schema + +See `src/types/index.ts` for full `User` interface. + +**Auth-related fields:** +- `id` - PocketBase record ID +- `email` - Login email + +**Profile fields:** +- `cycleLength` - Personal cycle length (days) +- `notificationTime` - Preferred notification hour +- `timezone` - User's timezone + +## PocketBase Client (`src/lib/pocketbase.ts`) + +**Exports:** +- `pb` - Initialized PocketBase client +- `getCurrentUser()` - Get authenticated user +- `isAuthenticated()` - Check auth status + +## Settings Page (`/settings`) + +User profile management: +- View/edit cycle length +- View/edit notification time +- View/edit timezone +- Link to Garmin settings + +## Success Criteria + +1. Login completes in under 2 seconds +2. Session persists across browser refreshes +3. Unauthorized access redirects to login +4. User data isolated by authentication + +## Acceptance Tests + +- [ ] Valid credentials authenticate successfully +- [ ] Invalid credentials show error message +- [ ] Session persists after page refresh +- [ ] Protected routes redirect when not authenticated +- [ ] GET `/api/user` returns current user data +- [ ] PATCH `/api/user` updates user record +- [ ] Logout clears session completely +- [ ] Auth cookie is HttpOnly and Secure diff --git a/specs/calendar.md b/specs/calendar.md new file mode 100644 index 0000000..e38bddd --- /dev/null +++ b/specs/calendar.md @@ -0,0 +1,114 @@ +# Calendar Specification + +## Job to Be Done + +When I want to plan ahead, I want to view my cycle phases on a calendar, so that I can schedule training and rest days appropriately. + +## ICS Feed + +### Endpoint: GET `/api/calendar/[userId]/[token].ics` + +Returns ICS calendar feed for subscription in external apps. + +**URL Format:** +``` +https://phaseflow.app/api/calendar/user123/abc123token.ics +``` + +**Security:** +- `token` is a random 32-character secret per user +- Token stored in `user.calendarToken` +- No authentication required (token IS authentication) + +### ICS Events + +Generate events for the next 90 days: + +**Phase Events (all-day):** +```ics +BEGIN:VEVENT +SUMMARY:🩸 Menstrual (Days 1-3) +DTSTART;VALUE=DATE:20240110 +DTEND;VALUE=DATE:20240113 +DESCRIPTION:Gentle rebounding only. Weekly limit: 30 min. +END:VEVENT +``` + +**Color Coding (via categories):** +- MENSTRUAL: Red +- FOLLICULAR: Green +- OVULATION: Pink +- EARLY_LUTEAL: Yellow +- LATE_LUTEAL: Orange + +### Token Regeneration + +POST `/api/calendar/regenerate-token` + +Generates new `calendarToken`, invalidating previous subscriptions. + +**Response:** +```json +{ + "token": "newRandomToken123...", + "url": "https://phaseflow.app/api/calendar/user123/newRandomToken123.ics" +} +``` + +## In-App Calendar + +### Month View (`/calendar`) + +Full-page calendar showing current month with phase visualization. + +**Components:** +- `month-view.tsx` - Main calendar grid +- `day-cell.tsx` - Individual day cell + +**Day Cell Contents:** +- Date number +- Phase color background +- Period indicator (if day 1-3) +- Today highlight +- Training decision icon (for today only) + +### Navigation + +- Previous/Next month buttons +- "Today" button to jump to current date +- Month/Year header + +### Phase Legend + +Below calendar: +``` +🩸 Menstrual | 🌱 Follicular | 🌸 Ovulation | 🌙 Early Luteal | 🌑 Late Luteal +``` + +## ICS Utilities (`src/lib/ics.ts`) + +**Functions:** +- `generateCalendarFeed(user, startDate, endDate)` - Create ICS string +- `generatePhaseEvents(lastPeriodDate, cycleLength, range)` - Create phase events + +**Dependencies:** +- `ics` npm package for ICS generation + +## Success Criteria + +1. ICS feed subscribable in Google Calendar, Apple Calendar, Outlook +2. Phase colors render correctly in calendar apps +3. In-app calendar responsive on mobile +4. Token regeneration immediately invalidates old URLs + +## Acceptance Tests + +- [ ] ICS endpoint returns valid ICS format +- [ ] ICS contains events for next 90 days +- [ ] Invalid token returns 401 +- [ ] Regenerate token creates new unique token +- [ ] Month view renders all days in month +- [ ] Day cells show correct phase colors +- [ ] Today is visually highlighted +- [ ] Navigation between months works +- [ ] Phase legend displays correctly diff --git a/specs/cycle-tracking.md b/specs/cycle-tracking.md new file mode 100644 index 0000000..ac3ab2e --- /dev/null +++ b/specs/cycle-tracking.md @@ -0,0 +1,120 @@ +# Cycle Tracking Specification + +## Job to Be Done + +When I log my period start date, I want the app to calculate my current cycle day and phase, so that training and nutrition guidance is accurate. + +## Core Concepts + +### Cycle Day + +Day 1 = first day of menstruation. + +**Calculation:** +``` +cycleDay = ((currentDate - lastPeriodDate) mod cycleLength) + 1 +``` + +Default cycle length: 31 days (configurable per user). + +### Cycle Phases + +Based on a 31-day cycle: + +| Phase | Days | Weekly Limit | Training Type | +|-------|------|--------------|---------------| +| MENSTRUAL | 1-3 | 30 min | Gentle rebounding only | +| FOLLICULAR | 4-14 | 120 min | Strength + rebounding | +| OVULATION | 15-16 | 80 min | Peak performance | +| EARLY_LUTEAL | 17-24 | 100 min | Moderate training | +| LATE_LUTEAL | 25-31 | 50 min | Gentle rebounding ONLY | + +## API Endpoints + +### POST `/api/cycle/period` + +Log a new period start date. + +**Request Body:** +```json +{ + "startDate": "2024-01-10" +} +``` + +**Behavior:** +1. Update `user.lastPeriodDate` +2. Create `PeriodLog` record for history +3. Recalculate today's cycle day and phase + +### GET `/api/cycle/current` + +Returns current cycle information. + +**Response:** +```json +{ + "cycleDay": 12, + "phase": "FOLLICULAR", + "daysUntilNextPhase": 3, + "phaseConfig": { + "weeklyLimit": 120, + "dailyAvg": 17, + "trainingType": "Strength + rebounding" + } +} +``` + +## Cycle Utilities (`src/lib/cycle.ts`) + +**Functions:** +- `getCycleDay(lastPeriodDate, cycleLength, currentDate)` - Calculate day in cycle +- `getPhase(cycleDay)` - Determine current phase +- `getPhaseConfig(phase)` - Get phase configuration +- `getPhaseLimit(phase)` - Get weekly intensity limit + +**Constants:** +- `PHASE_CONFIGS` - Array of phase definitions + +## Period History + +### PeriodLog Schema + +```typescript +interface PeriodLog { + id: string; + user: string; + startDate: Date; + created: Date; +} +``` + +### History Display (`/history`) + +- List of all logged period dates +- Calculated cycle lengths between periods +- Average cycle length over time +- Ability to edit/delete entries + +## Configurable Cycle Length + +Users with irregular cycles can adjust: +- Default: 31 days +- Range: 21-45 days +- Affects phase day boundaries proportionally + +## Success Criteria + +1. Cycle day resets to 1 on period log +2. Phase transitions at correct day boundaries +3. Weekly limits adjust per phase automatically +4. History shows all logged periods + +## Acceptance Tests + +- [ ] `getCycleDay` returns 1 on period start date +- [ ] `getCycleDay` handles cycle rollover correctly +- [ ] `getPhase` returns correct phase for each day range +- [ ] POST `/api/cycle/period` updates user record +- [ ] GET `/api/cycle/current` returns accurate phase info +- [ ] Days beyond cycle length default to LATE_LUTEAL diff --git a/specs/dashboard.md b/specs/dashboard.md new file mode 100644 index 0000000..ced0e88 --- /dev/null +++ b/specs/dashboard.md @@ -0,0 +1,94 @@ +# Dashboard Specification + +## Job to Be Done + +When I open the app each morning, I want to immediately see whether I should train today, so that I can make informed decisions without manual data analysis. + +## Components + +### Decision Card (`decision-card.tsx`) + +Displays the daily training decision prominently. + +**Required Elements:** +- Large status indicator (REST / GENTLE / LIGHT / REDUCED / TRAIN) +- Color-coded background (red for REST, yellow for GENTLE/LIGHT/REDUCED, green for TRAIN) +- Reason text explaining why this decision was made +- Icon matching the decision status + +**Data Source:** +- `/api/today` endpoint returns current decision + +### Data Panel (`data-panel.tsx`) + +Shows the biometric data used to make the decision. + +**Required Elements:** +- HRV Status (Balanced/Unbalanced/Unknown) +- Body Battery Current (0-100) +- Body Battery Yesterday Low (0-100) +- Cycle Day (e.g., "Day 12") +- Current Phase (MENSTRUAL, FOLLICULAR, OVULATION, EARLY_LUTEAL, LATE_LUTEAL) +- Week Intensity Minutes (accumulated) +- Phase Limit (weekly cap based on phase) +- Remaining Minutes until limit + +**Visual Indicators:** +- Color-code HRV status (green=Balanced, red=Unbalanced, gray=Unknown) +- Progress bar for week intensity vs phase limit + +### Nutrition Panel (`nutrition-panel.tsx`) + +Displays seed cycling and macro guidance. + +**Required Elements:** +- Current seeds to consume (Flax+Pumpkin OR Sesame+Sunflower) +- Carb range for the day (e.g., "20-100g") +- Keto guidance (OPTIONAL / No / NEVER with reason) +- Seed switch alert on day 15 + +**Data Source:** +- `getNutritionGuidance(cycleDay)` from `src/lib/nutrition.ts` + +### Override Toggles (`override-toggles.tsx`) + +Emergency modifications to the standard decision. + +**Override Types:** +- `flare` - Hashimoto's flare (force REST) +- `stress` - High stress day (force REST) +- `sleep` - Poor sleep (<6 hours) (force GENTLE) +- `pms` - PMS symptoms (force GENTLE) + +**Behavior:** +- Toggle buttons for each override +- Active overrides persist until manually cleared +- Active overrides override the algorithmic decision +- Store in `user.activeOverrides[]` + +### Mini Calendar (`mini-calendar.tsx`) + +Visual cycle overview for the current month. + +**Required Elements:** +- Monthly grid view +- Today highlighted +- Phase colors for each day +- Period days marked distinctly +- Quick navigation to previous/next month + +## Success Criteria + +1. User can determine training status within 3 seconds of opening app +2. All biometric data visible without scrolling on mobile +3. Override toggles accessible but not accidentally activated +4. Phase colors consistent across all components + +## Acceptance Tests + +- [ ] Dashboard loads decision from `/api/today` +- [ ] Decision card shows correct status and color +- [ ] Data panel displays all 8 required metrics +- [ ] Nutrition panel shows correct seeds for cycle day +- [ ] Override toggles update user record immediately +- [ ] Mini calendar renders current month with phase colors diff --git a/specs/decision-engine.md b/specs/decision-engine.md new file mode 100644 index 0000000..0fc0b83 --- /dev/null +++ b/specs/decision-engine.md @@ -0,0 +1,143 @@ +# Decision Engine Specification + +## Job to Be Done + +When I check the app, I want a clear training decision based on my current biometrics and cycle phase, so that I avoid overtraining while managing Hashimoto's. + +## Decision Status Hierarchy + +Decisions are evaluated in priority order. First matching rule wins. + +### Priority 1: HRV Unbalanced → REST +``` +IF hrvStatus == "Unbalanced" THEN REST +Reason: "HRV Unbalanced" +``` + +HRV imbalance indicates autonomic stress. No training. + +### Priority 2: Body Battery Depleted → REST +``` +IF bbYesterdayLow < 30 THEN REST +Reason: "BB too depleted" +``` + +Yesterday's low BB < 30 indicates insufficient recovery. + +### Priority 3: Late Luteal Phase → GENTLE +``` +IF phase == "LATE_LUTEAL" THEN GENTLE +Reason: "Gentle rebounding only (10-15min)" +``` + +PMS window - only gentle movement allowed. + +### Priority 4: Menstrual Phase → GENTLE +``` +IF phase == "MENSTRUAL" THEN GENTLE +Reason: "Gentle rebounding only (10min)" +``` + +Menstruation - even gentler than late luteal. + +### Priority 5: Weekly Limit Reached → REST +``` +IF weekIntensity >= phaseLimit THEN REST +Reason: "WEEKLY LIMIT REACHED - Rest" +``` + +Prevents overtraining within phase constraints. + +### Priority 6: Current BB Low → LIGHT +``` +IF bbCurrent < 75 THEN LIGHT +Reason: "Light activity only - BB not recovered" +``` + +Current energy insufficient for full training. + +### Priority 7: Current BB Medium → REDUCED +``` +IF bbCurrent < 85 THEN REDUCED +Reason: "Reduce intensity 25%" +``` + +Slight energy deficit - reduce but don't skip. + +### Priority 8: Default → TRAIN +``` +ELSE TRAIN +Reason: "OK to train - follow phase plan" +``` + +All systems go - follow normal phase programming. + +## Decision Statuses + +| Status | Icon | Meaning | +|--------|------|---------| +| REST | 🛑 | No training today | +| GENTLE | 🟡 | Light rebounding only (10-15 min) | +| LIGHT | 🟡 | Light activity, no intensity | +| REDUCED | 🟡 | Normal activity, reduce intensity 25% | +| TRAIN | ✅ | Full training per phase plan | + +## Override Integration + +Active overrides in `user.activeOverrides[]` bypass algorithmic decision: + +| Override | Forced Decision | +|----------|-----------------| +| `flare` | REST | +| `stress` | REST | +| `sleep` | GENTLE | +| `pms` | GENTLE | + +Override priority: flare > stress > sleep > pms + +## Input Data Structure + +```typescript +interface DailyData { + hrvStatus: HrvStatus; // "Balanced" | "Unbalanced" | "Unknown" + bbYesterdayLow: number; // 0-100 + phase: CyclePhase; // Current menstrual phase + weekIntensity: number; // Minutes this week + phaseLimit: number; // Max minutes for phase + bbCurrent: number; // 0-100 +} +``` + +## Output Decision Structure + +```typescript +interface Decision { + status: DecisionStatus; // REST | GENTLE | LIGHT | REDUCED | TRAIN + reason: string; // Human-readable explanation + icon: string; // Emoji indicator +} +``` + +## Decision Engine (`src/lib/decision-engine.ts`) + +**Function:** +- `getTrainingDecision(data: DailyData): Decision` + +## Success Criteria + +1. HRV Unbalanced always forces REST regardless of other metrics +2. Phase limits are respected (no training when limit reached) +3. Overrides take precedence when active +4. Decision reason clearly explains the limiting factor + +## Acceptance Tests + +- [ ] HRV Unbalanced returns REST +- [ ] BB yesterday low < 30 returns REST +- [ ] Late luteal phase returns GENTLE +- [ ] Menstrual phase returns GENTLE +- [ ] Week intensity >= limit returns REST +- [ ] BB current < 75 returns LIGHT +- [ ] BB current 75-84 returns REDUCED +- [ ] BB current >= 85 with good metrics returns TRAIN +- [ ] Priority order is maintained (HRV beats BB beats phase) diff --git a/specs/garmin-integration.md b/specs/garmin-integration.md new file mode 100644 index 0000000..4e89475 --- /dev/null +++ b/specs/garmin-integration.md @@ -0,0 +1,108 @@ +# Garmin Integration Specification + +## Job to Be Done + +When I connect my Garmin account, I want the app to automatically sync my biometrics daily, so that training decisions are based on real physiological data. + +## OAuth Flow + +### Initial Token Bootstrap (Manual) + +Garmin's Connect API uses OAuth 1.0a → OAuth 2.0 exchange. + +**Process:** +1. User runs `scripts/garmin_auth.py` locally +2. Script obtains OAuth 1.0a request token +3. User authorizes in browser +4. Script exchanges for OAuth 2.0 access token +5. User pastes tokens into Settings > Garmin page + +**Tokens Stored:** +- `oauth1` - OAuth 1.0a credentials (JSON, encrypted) +- `oauth2` - OAuth 2.0 access token (JSON, encrypted) +- `expires_at` - Token expiration timestamp + +### Token Storage + +Tokens encrypted with AES-256-GCM using `ENCRYPTION_KEY` env var. + +**Functions (`src/lib/encryption.ts`):** +- `encrypt(plaintext: string): string` - Returns base64 ciphertext +- `decrypt(ciphertext: string): string` - Returns plaintext + +## API Endpoints + +### GET `/api/garmin/status` + +Returns current Garmin connection status. + +**Response:** +```json +{ + "connected": true, + "daysUntilExpiry": 85, + "lastSync": "2024-01-10T07:00:00Z" +} +``` + +### POST `/api/garmin/tokens` + +Updates stored Garmin tokens. + +**Request Body:** +```json +{ + "oauth1": { ... }, + "oauth2": { ... }, + "expires_at": "2024-04-10T00:00:00Z" +} +``` + +## Garmin Data Fetching + +### Daily Sync (`/api/cron/garmin-sync`) + +Runs via cron job at 6:00 AM user's timezone. + +**Endpoints Called:** +1. `/usersummary-service/stats/bodyBattery/dates/{date}` - Body Battery +2. `/hrv-service/hrv/{date}` - HRV Status +3. `/fitnessstats-service/activity` - Intensity Minutes + +**Data Extracted:** +- `bodyBatteryCurrent` - Latest BB value +- `bodyBatteryYesterdayLow` - Yesterday's minimum BB +- `hrvStatus` - "Balanced" or "Unbalanced" +- `weekIntensityMinutes` - 7-day rolling sum + +### Garmin Client (`src/lib/garmin.ts`) + +**Functions:** +- `fetchGarminData(endpoint, { oauth2Token })` - Generic API caller +- `isTokenExpired(tokens)` - Check if refresh needed +- `daysUntilExpiry(tokens)` - Days until token expires + +## Token Expiration Handling + +Garmin OAuth 2.0 tokens expire after ~90 days. + +**Warning Triggers:** +- 14 days before: Yellow warning in Settings +- 7 days before: Email notification +- 0 days: Red alert, manual refresh required + +## Success Criteria + +1. Tokens stored encrypted at rest +2. Daily sync completes before 7 AM notification +3. Token expiration warnings sent 14 and 7 days before +4. Failed syncs logged with actionable error messages + +## Acceptance Tests + +- [ ] Encrypt/decrypt round-trips correctly +- [ ] `/api/garmin/status` returns accurate days until expiry +- [ ] `/api/garmin/tokens` validates token structure +- [ ] Cron sync fetches all three Garmin endpoints +- [ ] Expired token triggers appropriate error handling +- [ ] Token expiration warning at 14-day threshold diff --git a/specs/notifications.md b/specs/notifications.md new file mode 100644 index 0000000..003749f --- /dev/null +++ b/specs/notifications.md @@ -0,0 +1,110 @@ +# Notifications Specification + +## Job to Be Done + +When I wake up each morning, I want to receive an email with my training decision, so that I don't need to open the app to know if I should train. + +## Email Notifications + +### Daily Training Email + +Sent at user's preferred `notificationTime` (default: 07:00). + +**Subject:** +``` +PhaseFlow: [STATUS] - Day [cycleDay] ([phase]) +``` + +Example: `PhaseFlow: ✅ TRAIN - Day 12 (FOLLICULAR)` + +**Body:** +``` +Good morning! + +Today's Decision: [STATUS] +Reason: [reason] + +Current Metrics: +- Cycle Day: [cycleDay] ([phase]) +- Body Battery: [bbCurrent] +- HRV: [hrvStatus] +- Week Intensity: [weekIntensity]/[phaseLimit] min + +Nutrition Today: +- Seeds: [seeds] +- Carbs: [carbRange] +- Keto: [ketoGuidance] + +[Optional: Seed switch alert if day 15] +[Optional: Token expiration warning] + +--- +PhaseFlow - Training with your cycle, not against it +``` + +### Token Expiration Warnings + +**14 Days Before:** +Subject: `⚠️ PhaseFlow: Garmin tokens expire in 14 days` + +**7 Days Before:** +Subject: `🚨 PhaseFlow: Garmin tokens expire in 7 days - action required` + +## Email Provider + +Using Resend via `resend` npm package. + +**Configuration:** +- `RESEND_API_KEY` - API key for Resend +- `EMAIL_FROM` - Sender address (e.g., `PhaseFlow `) + +## Email Utilities (`src/lib/email.ts`) + +**Functions:** +- `sendDailyNotification(user, decision, dailyData)` - Send morning email +- `sendTokenExpirationWarning(user, daysUntilExpiry)` - Send warning email + +## Cron Jobs + +### `/api/cron/notifications` + +Protected by `CRON_SECRET` header. + +**Trigger:** Daily at notification times + +**Process:** +1. Find all users with `notificationTime` matching current hour +2. For each user: + - Fetch current decision from decision engine + - Send email via Resend + - Update `DailyLog.notificationSentAt` + +### Timezone Handling + +Users store preferred timezone (e.g., `America/New_York`). + +Cron runs every hour. Check if current hour in user's timezone matches their `notificationTime`. + +## Rate Limiting + +- Max 1 notification per user per day +- Check `DailyLog.notificationSentAt` before sending +- Only send if null or different date + +## Success Criteria + +1. Email arrives within 5 minutes of scheduled time +2. Email contains all relevant metrics and guidance +3. Token warnings sent at 14 and 7 day thresholds +4. No duplicate notifications on same day + +## Acceptance Tests + +- [ ] Daily email contains decision status and reason +- [ ] Daily email includes nutrition guidance +- [ ] Seed switch alert included on day 15 +- [ ] Token warning email sent at 14-day threshold +- [ ] Token warning email sent at 7-day threshold +- [ ] Duplicate notifications prevented +- [ ] Timezone conversion correct for notification time +- [ ] CRON_SECRET required for endpoint access diff --git a/specs/nutrition.md b/specs/nutrition.md new file mode 100644 index 0000000..c7086d7 --- /dev/null +++ b/specs/nutrition.md @@ -0,0 +1,94 @@ +# Nutrition Specification + +## Job to Be Done + +When I view my daily guidance, I want to see seed cycling and macro recommendations for my current cycle day, so that I can support my hormones through nutrition. + +## Seed Cycling Protocol + +Seed cycling provides lignans and essential fatty acids aligned with hormonal phases. + +### Phase 1: Days 1-14 (Follicular) +- **Flax seeds** (1-2 tbsp) - Lignans support estrogen metabolism +- **Pumpkin seeds** (1-2 tbsp) - Zinc supports hormone production + +### Phase 2: Days 15-31 (Luteal) +- **Sesame seeds** (1-2 tbsp) - Lignans support progesterone +- **Sunflower seeds** (1-2 tbsp) - Vitamin E supports luteal phase + +### Seed Switch Alert + +On day 15, display prominent alert: +> "🌱 SWITCH TODAY! Start Sesame + Sunflower" + +## Macro Guidance + +### Carbohydrate Ranges by Day + +| Days | Carb Range | Keto Guidance | +|------|------------|---------------| +| 1-3 (Menstrual) | 100-150g | No - body needs carbs during menstruation | +| 4-6 (Early Follicular) | 75-100g | No - transition phase | +| 7-14 (Late Follicular) | 20-100g | OPTIONAL - optimal keto window | +| 15-16 (Ovulation) | 100-150g | No - exit keto, need carbs for ovulation | +| 17-24 (Early Luteal) | 75-125g | No - progesterone needs carbs | +| 25-31 (Late Luteal) | 100-150g+ | NEVER - mood/hormones need carbs for PMS | + +### Hashimoto's Considerations + +- Never go full keto during late luteal (thyroid stress) +- Extra carbs during flare days +- Avoid goitrogenic foods in raw form + +## Nutrition Panel UI + +### Required Elements + +1. **Current Seeds** + - Display seed combination for today + - Quantity guidance (1-2 tbsp each) + +2. **Carb Range** + - Target carbohydrate intake + - Visual indicator (e.g., low/medium/high) + +3. **Keto Guidance** + - Clear yes/no/optional indicator + - Reason for guidance + - Color coding (green=OPTIONAL, yellow=No, red=NEVER) + +4. **Seed Switch Alert** + - Only visible on day 15 + - Prominent, dismissible + +## Nutrition Utilities (`src/lib/nutrition.ts`) + +**Functions:** +- `getNutritionGuidance(cycleDay)` - Returns seeds, carbRange, ketoGuidance +- `getSeedSwitchAlert(cycleDay)` - Returns alert text or null + +**Return Type:** +```typescript +interface NutritionGuidance { + seeds: string; + carbRange: string; + ketoGuidance: string; +} +``` + +## Success Criteria + +1. Seed recommendations match phase (1-14 vs 15-31) +2. Keto guidance is never "optional" during late luteal +3. Seed switch alert appears only on day 15 +4. Carb ranges are specific, not vague + +## Acceptance Tests + +- [ ] Day 1 returns Flax + Pumpkin seeds +- [ ] Day 15 returns Sesame + Sunflower seeds +- [ ] Day 15 triggers seed switch alert +- [ ] Day 16 has no seed switch alert +- [ ] Days 7-14 show keto OPTIONAL +- [ ] Days 25-31 show keto NEVER +- [ ] Carb ranges are non-overlapping and specific