Address 21 previously undefined behaviors across specs: - Authentication: Replace email/password with OIDC (Pocket-ID) - Cycle tracking: Add fixed-luteal phase scaling formula with examples - Calendar: Document period logging behavior (preserve predictions) - Garmin: Clarify connection is required (no phase-only mode) - Dashboard: Add UI states, dark mode, onboarding, accessibility - Notifications: Document timezone batching approach - New specs: observability.md (health, metrics, logging) - New specs: testing.md (unit + integration strategy) - Main spec: Add backup/recovery, known limitations, API updates Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
3.4 KiB
3.4 KiB
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 ResendEMAIL_FROM- Sender address (e.g.,PhaseFlow <noreply@phaseflow.app>)
Email Utilities (src/lib/email.ts)
Functions:
sendDailyNotification(user, decision, dailyData)- Send morning emailsendTokenExpirationWarning(user, daysUntilExpiry)- Send warning email
Cron Jobs
/api/cron/notifications
Protected by CRON_SECRET header.
Trigger: Daily at notification times
Process:
- Find all users with
notificationTimematching current hour - For each user:
- Fetch current decision from decision engine
- Send email via Resend
- Update
DailyLog.notificationSentAt
Timezone Handling
Users are batched into hourly timezone windows for efficient scheduling.
Implementation:
- Cron job runs 24 times daily (once per hour, on the hour)
- Each run queries users whose
notificationTimehour matches the current UTC hour adjusted for their timezone - Users are processed in batches within their timezone window
Example:
- User A: timezone
America/New_York, notificationTime07:00 - User B: timezone
America/Los_Angeles, notificationTime07:00 - User C: timezone
Europe/London, notificationTime07:00
When cron runs at 12:00 UTC:
- User A receives notification (12:00 UTC = 07:00 EST)
- User B skipped (12:00 UTC = 04:00 PST)
- User C skipped (12:00 UTC = 12:00 GMT)
Query Logic:
SELECT * FROM users
WHERE EXTRACT(HOUR FROM NOW() AT TIME ZONE timezone) = CAST(SUBSTRING(notificationTime, 1, 2) AS INT)
Rate Limiting
- Max 1 notification per user per day
- Check
DailyLog.notificationSentAtbefore sending - Only send if null or different date
Success Criteria
- Email arrives within 5 minutes of scheduled time
- Email contains all relevant metrics and guidance
- Token warnings sent at 14 and 7 day thresholds
- 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