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: 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";
|
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
|
||||||
@@ -7,17 +7,44 @@
|
|||||||
let
|
let
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
pkgs = nixpkgs.legacyPackages.${system};
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
in {
|
|
||||||
devShells.${system}.default = pkgs.mkShell {
|
# Common packages for development
|
||||||
packages = with pkgs; [
|
commonPackages = with pkgs; [
|
||||||
nodejs_24
|
nodejs_24
|
||||||
pnpm
|
pnpm
|
||||||
|
git
|
||||||
|
];
|
||||||
|
in {
|
||||||
|
devShells.${system} = {
|
||||||
|
# Default development shell with all tools
|
||||||
|
default = pkgs.mkShell {
|
||||||
|
packages = commonPackages ++ (with pkgs; [
|
||||||
turbo
|
turbo
|
||||||
lefthook
|
lefthook
|
||||||
];
|
]);
|
||||||
|
|
||||||
# For native modules (sharp, better-sqlite3, etc.)
|
# For native modules (sharp, better-sqlite3, etc.)
|
||||||
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc ];
|
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