Setup Ralph.

This commit is contained in:
2026-01-10 17:13:18 +00:00
parent f15e093254
commit d7ecc2944d
14 changed files with 1287 additions and 11 deletions

51
AGENTS.md Normal file
View File

@@ -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

84
PROMPT_build.md Normal file
View File

@@ -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.

64
PROMPT_plan.md Normal file
View File

@@ -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.

View File

@@ -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; [
# 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 ];
};
};
};
}

100
loop.sh Executable file
View File

@@ -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 "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"

20
ralph-sandbox.sh Executable file
View File

@@ -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" "$@"

147
specs/authentication.md Normal file
View File

@@ -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

114
specs/calendar.md Normal file
View File

@@ -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

120
specs/cycle-tracking.md Normal file
View File

@@ -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

94
specs/dashboard.md Normal file
View File

@@ -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

143
specs/decision-engine.md Normal file
View File

@@ -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)

108
specs/garmin-integration.md Normal file
View File

@@ -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

110
specs/notifications.md Normal file
View File

@@ -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 <noreply@phaseflow.app>`)
## 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

94
specs/nutrition.md Normal file
View File

@@ -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