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

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