Files
phaseflow/spec.md
Petru Paler f15e093254 Initial project setup for PhaseFlow
Set up Next.js 16 project with TypeScript for a training decision app
that integrates menstrual cycle phases with Garmin biometrics for
Hashimoto's thyroiditis management.

Stack: Next.js 16, React 19, Tailwind/shadcn, PocketBase, Drizzle,
Zod, Resend, Vitest, Biome, Lefthook, Nix dev environment.

Includes:
- 7 page routes (dashboard, login, settings, calendar, history, plan)
- 12 API endpoints (garmin, user, cycle, calendar, overrides, cron)
- Core lib utilities (decision engine, cycle phases, nutrition, ICS)
- Type definitions and component scaffolding
- Python script for Garmin token bootstrapping
- Initial unit tests for cycle utilities

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-09 16:50:39 +00:00

29 KiB

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):
    # 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):

// 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):

#!/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):

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:

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)

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)

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)

interface PeriodLog {
  id: string;
  user: string; // relation
  startDate: Date;
  created: Date;
}

API Routes

# Auth (PocketBase handles user auth)
POST   /api/auth/login
POST   /api/auth/register
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

# 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

# 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

# App
APP_URL=https://phaseflow.yourdomain.com
NODE_ENV=production

# PocketBase
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)

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 }}
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
No Garmin data yet Use phase-only decision
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
  • HTTPS enforced via Traefik

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
│   │       └── cron/
│   │           ├── garmin-sync/route.ts
│   │           └── notifications/route.ts
│   ├── 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
│   └── types/
│       └── index.ts
├── flake.nix
├── .envrc
├── biome.json
├── vitest.config.ts
├── lefthook.yml
└── package.json