Setup Ralph.
This commit is contained in:
51
AGENTS.md
Normal file
51
AGENTS.md
Normal 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
84
PROMPT_build.md
Normal 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
64
PROMPT_plan.md
Normal 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.
|
||||
37
flake.nix
37
flake.nix
@@ -1,5 +1,5 @@
|
||||
# ABOUTME: Nix flake for PhaseFlow development environment.
|
||||
# ABOUTME: Provides Node.js 24, pnpm, turbo, and lefthook.
|
||||
# ABOUTME: Provides Node.js 24, pnpm, turbo, lefthook, and Ralph sandbox shell.
|
||||
{
|
||||
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
|
||||
@@ -7,17 +7,44 @@
|
||||
let
|
||||
system = "x86_64-linux";
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in {
|
||||
devShells.${system}.default = pkgs.mkShell {
|
||||
packages = with pkgs; [
|
||||
|
||||
# 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
100
loop.sh
Executable 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
20
ralph-sandbox.sh
Executable 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
147
specs/authentication.md
Normal 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
114
specs/calendar.md
Normal 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
120
specs/cycle-tracking.md
Normal 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
94
specs/dashboard.md
Normal 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
143
specs/decision-engine.md
Normal 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
108
specs/garmin-integration.md
Normal 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
110
specs/notifications.md
Normal 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
94
specs/nutrition.md
Normal 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
|
||||
Reference in New Issue
Block a user