Files
phaseflow/spec.md
Petru Paler 6bd5eb663b Add Playwright E2E testing infrastructure
- Add playwright-web-flake to flake.nix for NixOS browser support
- Pin @playwright/test@1.56.1 to match nixpkgs version
- Create playwright.config.ts with Chromium-only, auto-start dev server
- Add e2e/smoke.spec.ts with initial smoke tests
- Add .mcp.json for Claude browser control via MCP
- Update .gitignore for playwright artifacts
- Remove E2E test skip from spec.md Known Limitations
- Update specs/testing.md to require three-tier testing approach

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 21:43:24 +00:00

910 lines
31 KiB
Markdown

# PhaseFlow - MVP Specification
## Overview
A self-hosted webapp that automates training decisions for individuals with Hashimoto's thyroiditis by integrating menstrual cycle phases with biometric data from Garmin wearables. Provides daily actionable guidance via email notifications and calendar integration.
**Core Value Proposition**: Zero daily effort. Data-driven, body-respectful training decisions delivered automatically each morning.
---
## Tech Stack
| Layer | Choice |
|-------|--------|
| Language | TypeScript (strict) |
| Runtime | Node.js 24 LTS |
| Framework | Next.js 16 (App Router) |
| Database | PocketBase (SQLite, auth, real-time) |
| ORM | Drizzle (for any direct queries) |
| Validation | Zod |
| UI | Tailwind + shadcn/ui |
| Scheduling | Node-cron or PocketBase hooks |
| Email | Resend or Nodemailer |
| Calendar | ICS feed (user subscribes via URL) |
| Garmin | Python `garth` library for token bootstrap |
| Deployment | Nomad + Traefik (homelab) |
---
## Core Features
### 1. Garmin Integration
**Data Points Fetched Daily**:
| Metric | Source | Purpose |
|--------|--------|---------|
| Body Battery (current) | Garmin Connect | Assess readiness to train |
| Body Battery (yesterday's low) | Garmin Connect | Detect recovery adequacy |
| HRV Status | Garmin Connect | Detect autonomic stress |
| Weekly Intensity Minutes | Garmin Connect | Track cumulative load |
#### Authentication Strategy
The npm `garmin-connect` library does **not** support MFA. The Python `garth` library does. Since MFA is required, we use a hybrid approach:
**Token Bootstrap Flow**:
1. User runs a provided Python script locally (or via container exec):
```bash
# One-time setup script included with PhaseFlow
python3 garmin_auth.py
```
2. Script uses `garth` to authenticate (prompts for MFA code)
3. Script outputs OAuth1 + OAuth2 tokens as JSON
4. User pastes tokens into PhaseFlow settings page
5. App stores tokens encrypted in PocketBase
6. App uses tokens directly for Garmin API calls (no library needed for data fetch, just HTTP with Bearer auth)
**Token Lifecycle**:
- Tokens valid for ~90 days
- App tracks expiry, warns user 7 days before
- User re-runs bootstrap script when needed
- Future: auto-refresh if garth adds programmatic refresh support
**Garmin API Calls** (with stored tokens):
```typescript
// Direct HTTP calls using stored OAuth2 token
const response = await fetch('https://connect.garmin.com/modern/proxy/usersummary-service/stats/heartRate/daily/2025-01-09', {
headers: {
'Authorization': `Bearer ${oauth2Token}`,
'NK': 'NT'
}
});
```
**Provided Bootstrap Script** (`scripts/garmin_auth.py`):
```python
#!/usr/bin/env python3
"""
PhaseFlow Garmin Token Generator
Run this locally to authenticate with Garmin (supports MFA)
"""
import garth
import json
from getpass import getpass
email = input("Garmin email: ")
password = getpass("Garmin password: ")
# MFA handled automatically - prompts if needed
garth.login(email, password)
tokens = {
"oauth1": garth.client.oauth1_token.serialize(),
"oauth2": garth.client.oauth2_token.serialize(),
"expires_at": garth.client.oauth2_token.expires_at.isoformat()
}
print("\n--- Copy everything below this line ---")
print(json.dumps(tokens, indent=2))
print("--- Copy everything above this line ---")
print(f"\nTokens expire: {tokens['expires_at']}")
```
### 2. Cycle Phase Engine
**Phase Definitions** (based on cycle day from last period):
| Phase | Days | Weekly Limit | Daily Avg | Training Type |
|-------|------|--------------|-----------|---------------|
| MENSTRUAL | 1-3 | 30 min | 10 min | Gentle rebounding only |
| FOLLICULAR | 4-14 | 120 min | 17 min | Strength + rebounding |
| OVULATION | 15-16 | 80 min | 40 min | Peak performance |
| EARLY LUTEAL | 17-24 | 100 min | 14 min | Moderate training |
| **LATE LUTEAL** ⚠️ | 25-31 | 50 min | 8 min | **Gentle rebounding ONLY** |
**Configuration**:
- Last period date (user updates when period arrives)
- Average cycle length (default: 31 days, adjustable)
- Cycle day formula: `((daysSinceLastPeriod) % cycleLength) + 1`
### 3. Training Decision Engine
**Decision Priority (evaluated in order)**:
```typescript
type DecisionStatus = 'REST' | 'GENTLE' | 'LIGHT' | 'REDUCED' | 'TRAIN';
interface Decision {
status: DecisionStatus;
reason: string;
icon: string;
}
function getTrainingDecision(data: DailyData): Decision {
const { hrvStatus, bbYesterdayLow, phase, weekIntensity, phaseLimit, bbCurrent } = data;
if (hrvStatus === 'Unbalanced')
return { status: 'REST', reason: 'HRV Unbalanced', icon: '🛑' };
if (bbYesterdayLow < 30)
return { status: 'REST', reason: 'BB too depleted', icon: '🛑' };
if (phase === 'LATE_LUTEAL')
return { status: 'GENTLE', reason: 'Gentle rebounding only (10-15min)', icon: '🟡' };
if (phase === 'MENSTRUAL')
return { status: 'GENTLE', reason: 'Gentle rebounding only (10min)', icon: '🟡' };
if (weekIntensity >= phaseLimit)
return { status: 'REST', reason: 'WEEKLY LIMIT REACHED - Rest', icon: '🛑' };
if (bbCurrent < 75)
return { status: 'LIGHT', reason: 'Light activity only - BB not recovered', icon: '🟡' };
if (bbCurrent < 85)
return { status: 'REDUCED', reason: 'Reduce intensity 25%', icon: '🟡' };
return { status: 'TRAIN', reason: 'OK to train - follow phase plan', icon: '✅' };
}
```
### 4. Daily Email Notification
**Delivery**: Email at user-configured time (default 7:00 AM local)
**Content Structure**:
```
Subject: Today's Training: ✅ TRAIN (or 🛑 REST or 🟡 GENTLE)
Good morning!
📅 CYCLE DAY: 12 (FOLLICULAR)
💪 TODAY'S PLAN:
✅ OK to train - follow phase plan
📊 YOUR DATA:
• Body Battery Now: 95
• Yesterday's Low: 42
• HRV Status: Balanced
• Week Intensity: 85 / 120 minutes
• Remaining: 35 minutes
🏋️ EXERCISE:
Great day for strength training! Do your full workout.
🌱 SEEDS: Flax (1-2 tbsp) + Pumpkin (1-2 tbsp)
🍽️ MACROS: Variable (can go low 20-100g).
🥑 KETO: OPTIONAL - this is your optimal keto window if you want to try!
Days 7-10: Transition in | Days 11-14: Full keto | Day 14 evening: Transition out
---
Auto-generated by PhaseFlow
```
### 5. Nutrition Guidance
**Seed Cycling** (included in daily email):
| Days | Seeds |
|------|-------|
| 1-14 | Flax (1-2 tbsp) + Pumpkin (1-2 tbsp) |
| 15+ | Sesame (1-2 tbsp) + Sunflower (1-2 tbsp) |
Day 15 alert: "🌱 SWITCH TODAY! Start Sesame + Sunflower"
**Macro Guidance by Phase**:
| Days | Carb Range | Keto Guidance |
|------|------------|---------------|
| 1-3 | 100-150g | No - body needs carbs during menstruation |
| 4-6 | 75-100g | No - transition phase |
| 7-14 | 20-100g | OPTIONAL - optimal keto window |
| 15-16 | 100-150g | No - exit keto, need carbs for ovulation |
| 17-24 | 75-125g | No - progesterone needs carbs |
| 25-31 | 100-150g+ | NEVER - mood/hormones need carbs for PMS |
### 6. Calendar Integration (ICS Feed)
**Approach**: Serve a dynamic ICS/iCal file that users subscribe to in any calendar app.
**Why ICS Feed vs Google Calendar API**:
- No Google OAuth required
- Works with Google Calendar, Apple Calendar, Outlook, etc.
- No Google Cloud project setup
- User just adds URL: "Add calendar from URL"
- Calendar apps poll automatically (Google: ~12-24 hours)
- When period date changes, feed regenerates - picked up on next poll
**Feed URL**: `https://phaseflow.yourdomain.com/api/calendar/{userId}/{token}.ics`
- `userId`: identifies the user
- `token`: random secret token for security (regeneratable)
**ICS Content**:
```ics
BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//PhaseFlow//Cycle Calendar//EN
NAME:PhaseFlow Cycle
X-WR-CALNAME:PhaseFlow Cycle
BEGIN:VEVENT
DTSTART;VALUE=DATE:20250105
DTEND;VALUE=DATE:20250108
SUMMARY:🔵 MENSTRUAL
DESCRIPTION:Gentle rebounding only (10 min max)
COLOR:blue
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20250108
DTEND;VALUE=DATE:20250119
SUMMARY:🟢 FOLLICULAR
DESCRIPTION:Strength training + rebounding (15-20 min/day avg)
COLOR:green
END:VEVENT
...
BEGIN:VEVENT
DTSTART;VALUE=DATE:20250126
DTEND;VALUE=DATE:20250126
SUMMARY:⚠️ Late Luteal Starts in 3 Days
DESCRIPTION:Begin reducing training intensity
END:VEVENT
BEGIN:VEVENT
DTSTART;VALUE=DATE:20250129
DTEND;VALUE=DATE:20250129
SUMMARY:🔴 CRITICAL PHASE - Gentle Rebounding Only!
DESCRIPTION:Late luteal phase - protect your cycle
END:VEVENT
END:VCALENDAR
```
**Phase Colors** (as emoji prefix since ICS color support varies):
| Phase | Emoji | Description |
|-------|-------|-------------|
| MENSTRUAL | 🔵 | Blue |
| FOLLICULAR | 🟢 | Green |
| OVULATION | 🟣 | Purple |
| EARLY LUTEAL | 🟡 | Yellow |
| LATE LUTEAL | 🔴 | Red |
**Warning Events**:
- Day 22: "⚠️ Late Luteal Phase Starts in 3 Days - Reduce Training 50%"
- Day 25: "🔴 CRITICAL PHASE - Gentle Rebounding Only!"
### 7. In-App Calendar View
**Display**:
- Monthly calendar showing phase colors
- Current day highlighted
- Click day to see that day's data/decision
- Visual indicator when approaching late luteal
### 8. Period Logging
**User Flow**:
1. Click "Period Started" button
2. Confirm date (defaults to today, can select past date)
3. System recalculates all phase dates
4. ICS feed automatically reflects new dates
5. Confirmation email sent
**Confirmation Email**:
```
Subject: 🔵 Period Tracking Updated
Your cycle has been reset. Last period: [date]
Phase calendar updated for next [cycle_length] days.
Your calendar will update automatically within 24 hours.
```
### 9. Emergency Modifications
User-triggered overrides accessible from dashboard:
| Override | Effect | Duration |
|----------|--------|----------|
| Hashimoto's flare | All training → gentle rebounding only | Until deactivated |
| High stress week | Reduce all intensity limits by 50% | Until deactivated |
| Poor sleep | Replace strength with gentle rebounding | Until deactivated |
| PMS symptoms | Extra rebounding, no strength training | Until deactivated |
These overrides apply on top of the normal decision engine.
### 10. Exercise Plan Reference
**Accessible in-app** - the full monthly exercise plan:
#### Week 1: Menstrual Phase (Days 1-7)
- Morning: 10-15 min gentle rebounding
- Evening: 15-20 min restorative movement
- No strength training
#### Week 2: Follicular Phase (Days 8-14)
- Mon/Wed/Fri: Strength training (20-25 min)
- Squats: 3x8-12
- Push-ups: 3x5-10
- Single-leg deadlifts: 3x6-8 each
- Plank: 3x20-45s
- Kettlebell swings: 2x10-15
- Tue/Thu: Active recovery rebounding (20 min)
- Weekend: Choose your adventure
#### Week 3: Ovulation + Early Luteal (Days 15-24)
- Days 15-16: Peak performance (25-30 min strength)
- Days 17-21: Modified strength (reduce intensity 10-20%)
#### Week 4: Late Luteal (Days 22-28)
- Daily: Gentle rebounding only (15-20 min)
- Optional light bodyweight Mon/Wed if feeling good
- Rest days: Tue/Thu/Sat/Sun
**Rebounding Techniques by Phase**:
- Menstrual: Health bounce, lymphatic drainage
- Follicular: Strength bounce, intervals
- Ovulation: Maximum intensity, plyometric
- Luteal: Therapeutic, stress relief
---
## Data Model
### Users (PocketBase collection)
```typescript
interface User {
id: string;
email: string;
// Garmin
garminConnected: boolean;
garminOauth1Token: string; // encrypted JSON
garminOauth2Token: string; // encrypted JSON
garminTokenExpiresAt: Date;
// Calendar
calendarToken: string; // random secret for ICS URL
// Cycle
lastPeriodDate: Date;
cycleLength: number; // default: 31
// Preferences
notificationTime: string; // "07:00"
timezone: string;
// Overrides
activeOverrides: string[]; // ['flare', 'stress', 'sleep', 'pms']
created: Date;
updated: Date;
}
```
### Daily Logs (PocketBase collection)
```typescript
interface DailyLog {
id: string;
user: string; // relation
date: Date;
cycleDay: number;
phase: 'MENSTRUAL' | 'FOLLICULAR' | 'OVULATION' | 'EARLY_LUTEAL' | 'LATE_LUTEAL';
bodyBatteryCurrent: number | null;
bodyBatteryYesterdayLow: number | null;
hrvStatus: 'Balanced' | 'Unbalanced' | 'Unknown';
weekIntensityMinutes: number;
phaseLimit: number;
remainingMinutes: number;
trainingDecision: string;
decisionReason: string;
notificationSentAt: Date | null;
created: Date;
}
```
### Period Logs (PocketBase collection)
```typescript
interface PeriodLog {
id: string;
user: string; // relation
startDate: Date;
created: Date;
}
```
### Data Retention
All logs (DailyLog, PeriodLog) are retained indefinitely. No automatic deletion or archival. SQLite handles large datasets efficiently for single-user scenarios.
---
## API Routes
```
# Auth (PocketBase handles via OIDC)
GET /api/auth/login - Initiate OIDC flow
GET /api/auth/callback - OIDC callback handler
POST /api/auth/logout
# Garmin Token Management
POST /api/garmin/tokens - Store tokens from bootstrap script
DELETE /api/garmin/tokens - Clear stored tokens
GET /api/garmin/status - Check connection & token expiry
# User Settings
GET /api/user - Get profile + settings
PATCH /api/user - Update settings
# Cycle
POST /api/cycle/period - Log period start
GET /api/cycle/current - Current phase info
# Daily
GET /api/today - Today's decision + all data
GET /api/history - Historical logs (cursor-based pagination)
Query params: ?cursor=<lastId>&limit=20
# Calendar
GET /api/calendar/:userId/:token.ics - ICS feed (public, token-protected)
POST /api/calendar/regenerate-token - Generate new calendar token
# Overrides
POST /api/overrides - Set active overrides
DELETE /api/overrides/:type - Remove override
# Observability
GET /api/health - Health check for monitoring
GET /metrics - Prometheus metrics endpoint
# Cron (internal, protected)
POST /api/cron/garmin-sync - Fetch Garmin data (6 AM)
POST /api/cron/notifications - Send emails (7 AM)
```
---
## Scheduled Jobs
| Job | Schedule | Function |
|-----|----------|----------|
| Garmin Sync | Daily at user's configured time (default 6:00) | Fetch BB, HRV, intensity |
| Morning Email | Daily at user's configured time + 1hr (default 7:00) | Send training decision |
| Token Expiry Check | Daily | Warn user if tokens expire within 7 days |
**Implementation**:
- Nomad periodic jobs hitting internal API endpoints
- Or: node-cron within the Next.js app
- Or: PocketBase hooks + external cron
---
## Page Structure
```
src/app/
page.tsx # Dashboard (today's decision, quick actions)
login/page.tsx # Auth
settings/page.tsx # Profile, Garmin tokens, notification time
settings/garmin/page.tsx # Garmin token paste UI + instructions
calendar/page.tsx # In-app calendar view + ICS subscription instructions
history/page.tsx # Historical data table
plan/page.tsx # Exercise plan reference
api/
auth/...
garmin/tokens/route.ts
garmin/status/route.ts
user/route.ts
cycle/period/route.ts
cycle/current/route.ts
today/route.ts
history/route.ts
calendar/[userId]/[token].ics/route.ts
calendar/regenerate-token/route.ts
overrides/route.ts
cron/garmin-sync/route.ts
cron/notifications/route.ts
```
---
## Dashboard Layout
```
┌─────────────────────────────────────────────────────────────┐
│ PhaseFlow [Settings]│
├─────────────────────────────────────────────────────────────┤
│ │
│ 📅 Day 12 of 31 • FOLLICULAR │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ (progress bar) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ ✅ OK TO TRAIN │ │
│ │ Follow your phase plan - strength training day! │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ YOUR DATA NUTRITION TODAY │
│ ┌──────────────────────┐ ┌──────────────────────┐│
│ │ Body Battery: 95 │ │ 🌱 Flax + Pumpkin ││
│ │ Yesterday Low: 42 │ │ 🍽️ Carbs: 20-100g ││
│ │ HRV: Balanced │ │ 🥑 Keto: Optional ││
│ │ Week: 85/120 min │ └──────────────────────┘│
│ │ Remaining: 35 min │ │
│ └──────────────────────┘ QUICK ACTIONS │
│ ┌──────────────────────┐ │
│ OVERRIDES │ [Period Started] │ │
│ ┌──────────────────────┐ │ [View Plan] │ │
│ │ ○ Flare Mode │ │ [Calendar] │ │
│ │ ○ High Stress │ └──────────────────────┘ │
│ │ ○ Poor Sleep │ │
│ │ ○ PMS Symptoms │ │
│ └──────────────────────┘ │
│ │
│ [Mini Calendar - current month with phase colors] │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Garmin Settings Page
```
┌─────────────────────────────────────────────────────────────┐
│ Settings > Garmin Connection │
├─────────────────────────────────────────────────────────────┤
│ │
│ STATUS: 🟢 Connected │
│ Tokens expire: March 15, 2025 (65 days) │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ To connect or refresh your Garmin tokens: │
│ │
│ 1. Run this command on your computer: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ curl -O https://phaseflow.../garmin_auth.py │ │
│ │ python3 garmin_auth.py │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ 2. Enter your Garmin credentials when prompted │
│ (MFA code will be requested if enabled) │
│ │
│ 3. Paste the JSON output here: │
│ ┌─────────────────────────────────────────────────┐ │
│ │ │ │
│ │ │ │
│ │ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ [Save Tokens] │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ [Disconnect Garmin] │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Calendar Settings Page
```
┌─────────────────────────────────────────────────────────────┐
│ Settings > Calendar │
├─────────────────────────────────────────────────────────────┤
│ │
│ Subscribe to your cycle calendar in any calendar app: │
│ │
│ Calendar URL: │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ https://phaseflow.example.com/api/calendar/abc123/ │ │
│ │ x7f9k2m4n8.ics [📋] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ Instructions: │
│ │
│ Google Calendar: │
│ 1. Open Google Calendar settings │
│ 2. Click "Add calendar" → "From URL" │
│ 3. Paste the URL above │
│ │
│ Apple Calendar: │
│ 1. File → New Calendar Subscription │
│ 2. Paste the URL above │
│ │
│ Note: Calendar updates within 12-24 hours of changes. │
│ │
│ ───────────────────────────────────────────────────────── │
│ │
│ [Regenerate URL] (invalidates old URL) │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Environment Variables
```env
# App
APP_URL=https://phaseflow.yourdomain.com
NODE_ENV=production
# PocketBase
NEXT_PUBLIC_POCKETBASE_URL=http://localhost:8090
# Email (Resend)
RESEND_API_KEY=xxx
EMAIL_FROM=phaseflow@yourdomain.com
# Encryption (for Garmin tokens)
ENCRYPTION_KEY=xxx # 32-byte key for AES-256
# Cron secret (for protected endpoints)
CRON_SECRET=xxx
```
---
## Deployment (Nomad)
```hcl
job "phaseflow" {
datacenters = ["dc1"]
type = "service"
group "web" {
count = 1
network {
port "http" { to = 3000 }
}
service {
name = "phaseflow"
port = "http"
tags = [
"traefik.enable=true",
"traefik.http.routers.phaseflow.rule=Host(`phaseflow.yourdomain.com`)",
"traefik.http.routers.phaseflow.tls.certresolver=letsencrypt"
]
}
task "nextjs" {
driver = "docker"
config {
image = "phaseflow:latest"
ports = ["http"]
}
env {
NODE_ENV = "production"
}
template {
data = <<EOF
{{ with nomadVar "nomad/jobs/phaseflow" }}
APP_URL={{ .app_url }}
NEXT_PUBLIC_POCKETBASE_URL={{ .pocketbase_url }}
RESEND_API_KEY={{ .resend_key }}
ENCRYPTION_KEY={{ .encryption_key }}
CRON_SECRET={{ .cron_secret }}
{{ end }}
EOF
destination = "secrets/.env"
env = true
}
resources {
cpu = 256
memory = 512
}
}
}
group "pocketbase" {
count = 1
network {
port "pb" { to = 8090 }
}
volume "pb_data" {
type = "host"
source = "pocketbase-data"
}
task "pocketbase" {
driver = "docker"
config {
image = "ghcr.io/muchobien/pocketbase:latest"
ports = ["pb"]
}
volume_mount {
volume = "pb_data"
destination = "/pb_data"
}
resources {
cpu = 128
memory = 256
}
}
}
# Periodic job for daily sync
group "cron" {
type = "batch"
periodic {
cron = "0 6 * * *" # 6 AM daily
prohibit_overlap = true
}
task "garmin-sync" {
driver = "exec"
config {
command = "curl"
args = [
"-X", "POST",
"-H", "Authorization: Bearer ${CRON_SECRET}",
"${APP_URL}/api/cron/garmin-sync"
]
}
}
}
}
```
---
## Error Handling
| Scenario | Handling |
|----------|----------|
| Garmin API unavailable | Use last known values, note in email |
| Garmin tokens expired | Email user with re-auth instructions |
| Garmin not connected | Block app usage, show onboarding prompt |
| User hasn't set period date | Prompt in dashboard, block email until set |
| Email delivery failure | Log, retry once |
| Invalid ICS request | Return 404 |
---
## Security Considerations
- Garmin tokens encrypted at rest (AES-256)
- ICS feed URL contains random token (not guessable)
- Cron endpoints protected by secret header
- PocketBase handles user auth via OIDC
- HTTPS enforced via Traefik
---
## Backup & Recovery
PocketBase stores all data in a single SQLite database file.
**Backup Procedure:**
```bash
# Stop PocketBase or use SQLite backup API
cp /path/to/pb_data/data.db /path/to/backups/data-$(date +%Y%m%d).db
```
**What to backup:**
- `pb_data/data.db` - Main database (users, logs, settings)
- `pb_data/storage/` - Any uploaded files (if applicable)
**Recovery:**
```bash
# Stop PocketBase
cp /path/to/backups/data-YYYYMMDD.db /path/to/pb_data/data.db
# Restart PocketBase
```
Backups are manual. Set up your own cron job or backup solution as needed.
---
## Known Limitations
The following are **out of scope** for MVP:
| Limitation | Notes |
|------------|-------|
| Phase-only mode | Garmin connection required; no fallback without biometrics |
| Pregnancy/menopause | Cycle tracking assumes regular menstrual cycles |
| Hormonal birth control | May disrupt natural cycle phases |
| API versioning | Single version; breaking changes via deprecation |
| Formal API documentation | Endpoints documented in spec only |
| Multi-user support | Single-user design only |
---
## File Structure
```
phaseflow/
├── scripts/
│ └── garmin_auth.py # Token bootstrap script
├── src/
│ ├── app/
│ │ ├── page.tsx
│ │ ├── login/page.tsx
│ │ ├── settings/
│ │ │ ├── page.tsx
│ │ │ └── garmin/page.tsx
│ │ ├── calendar/page.tsx
│ │ ├── history/page.tsx
│ │ ├── plan/page.tsx
│ │ ├── layout.tsx
│ │ └── api/
│ │ ├── garmin/
│ │ │ ├── tokens/route.ts
│ │ │ └── status/route.ts
│ │ ├── user/route.ts
│ │ ├── cycle/
│ │ │ ├── period/route.ts
│ │ │ └── current/route.ts
│ │ ├── today/route.ts
│ │ ├── history/route.ts
│ │ ├── calendar/
│ │ │ ├── [userId]/[token].ics/route.ts
│ │ │ └── regenerate-token/route.ts
│ │ ├── overrides/route.ts
│ │ ├── health/route.ts
│ │ └── cron/
│ │ ├── garmin-sync/route.ts
│ │ └── notifications/route.ts
│ │ └── metrics/
│ │ └── route.ts # Prometheus metrics
│ ├── components/
│ │ ├── dashboard/
│ │ │ ├── decision-card.tsx
│ │ │ ├── data-panel.tsx
│ │ │ ├── nutrition-panel.tsx
│ │ │ ├── override-toggles.tsx
│ │ │ └── mini-calendar.tsx
│ │ ├── calendar/
│ │ │ ├── month-view.tsx
│ │ │ └── day-cell.tsx
│ │ └── ui/ # shadcn components
│ ├── lib/
│ │ ├── pocketbase.ts
│ │ ├── garmin.ts # API calls with stored tokens
│ │ ├── decision-engine.ts
│ │ ├── cycle.ts
│ │ ├── nutrition.ts
│ │ ├── email.ts
│ │ ├── ics.ts # ICS feed generation
│ │ ├── encryption.ts
│ │ ├── logger.ts # Structured JSON logging
│ │ └── metrics.ts # Prometheus metrics
│ └── types/
│ └── index.ts
├── flake.nix
├── .envrc
├── biome.json
├── vitest.config.ts
├── lefthook.yml
└── package.json
```