Commit Graph

30 Commits

Author SHA1 Message Date
59d70ee414 Use connectapi.garmin.com directly instead of web proxy
All checks were successful
Deploy / deploy (push) Successful in 1m38s
The connect.garmin.com/modern/proxy URL returns HTML (website) instead
of JSON API responses. Garth library uses connectapi.garmin.com subdomain
directly, which is the actual API endpoint.

- Change base URL from connect.garmin.com/modern/proxy to connectapi.garmin.com
- Update User-Agent to match garth library: GCM-iOS-5.19.1.2
- Factor out headers into getGarminHeaders() to avoid duplication
- Remove NK header (not needed when using connectapi subdomain)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 13:38:55 +00:00
51f4c8eb80 Add User-Agent header to Garmin API requests
All checks were successful
Deploy / deploy (push) Successful in 2m29s
Garmin now requires a mobile app User-Agent header (GCM-iOS-5.7.2.1)
for API access. Without it, they serve the website HTML instead of
JSON API responses.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 13:29:10 +00:00
98293f5ab5 Log raw response when Garmin returns non-JSON
All checks were successful
Deploy / deploy (push) Successful in 1m40s
Garmin is returning HTML error pages instead of JSON data. This
change reads the response as text first, checks if it starts with
{ or [, and logs the first 1000 chars of the response body if not.

This will help diagnose what page Garmin is returning (login, captcha,
rate limit, etc).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 13:20:28 +00:00
85b535f04a Add build info metric and diagnostic logging for Garmin sync
All checks were successful
Deploy / deploy (push) Successful in 1m44s
- Add phaseflow_build_info metric with version and commit labels
- Inject GIT_COMMIT env var at build time via next.config.ts
- Add logging to all Garmin fetch functions (HRV, body battery, intensity)
- Log API response status codes, actual data values, and errors

This enables verifying which build is deployed and diagnosing
silent failures where Garmin API returns errors but sync reports success.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 13:09:29 +00:00
3a06bff4d4 Fix Garmin sync to handle PocketBase date strings
All checks were successful
Deploy / deploy (push) Successful in 2m38s
PocketBase returns date fields as ISO strings, not Date objects.
The sync was failing with "e.getTime is not a function" because
the code expected Date objects.

- Export mapRecordToUser from pocketbase.ts
- Use mapRecordToUser in cron route to properly parse dates
- Add test for handling date fields as ISO strings

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-15 07:38:37 +00:00
b221acee40 Implement automatic Garmin token refresh and fix expiry tracking
- Add OAuth1 to OAuth2 token exchange using Garmin's exchange endpoint
- Track refresh token expiry (~30 days) instead of access token expiry (~21 hours)
- Auto-refresh access tokens in cron sync before they expire
- Update Python script to output refresh_token_expires_at
- Add garminRefreshTokenExpiresAt field to User type and database schema
- Fix token input UX: show when warning active, not just when disconnected
- Add Cache-Control headers to /api/user and /api/garmin/status to prevent stale data
- Add oauth-1.0a package for OAuth1 signature generation

The system now automatically refreshes OAuth2 tokens using the stored OAuth1 token,
so users only need to re-run the Python auth script every ~30 days (when refresh
token expires) instead of every ~21 hours (when access token expires).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-14 20:33:10 +00:00
8c59b3bd67 Add self-contained e2e test harness with ephemeral PocketBase
Some checks failed
CI / quality (push) Failing after 29s
Deploy / deploy (push) Successful in 2m37s
Previously, 15 e2e tests were skipped because TEST_USER_EMAIL and
TEST_USER_PASSWORD env vars weren't set. Now the test harness:

- Starts a fresh PocketBase instance in /tmp on port 8091
- Creates admin user, collections, and API rules automatically
- Seeds test user with period data for authenticated tests
- Cleans up temp directory after tests complete

Also fixes:
- Override toggle tests now use checkbox role (not button)
- Adds proper wait for OVERRIDES section before testing toggles
- Suppresses document.cookie lint warning with explanation

Test results: 64 e2e tests pass, 1014 unit tests pass

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 09:38:24 +00:00
eeeece17bf Add spec compliance improvements: seed switch alert, calendar emojis, period indicator, IP logging
Some checks failed
CI / quality (push) Failing after 28s
Deploy / deploy (push) Successful in 2m38s
- NutritionPanel: Display seed switch alert on day 15 per dashboard spec
- MonthView: Add phase emojis to legend (🩸🌱🌸🌙🌑) per calendar spec
- DayCell: Show period indicator (🩸) for days 1-3 per calendar spec
- Auth middleware: Log client IP from x-forwarded-for/x-real-ip per observability spec
- Updated NutritionGuidance type to include seedSwitchAlert field
- /api/today now returns seedSwitchAlert in nutrition response

Test coverage: 1005 tests (15 new tests added)
- nutrition-panel.test.tsx: +4 tests
- month-view.test.tsx: +1 test
- day-cell.test.tsx: +5 tests
- auth-middleware.test.ts: +3 tests
- today/route.test.ts: +2 tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:33:14 +00:00
d613417e47 Fix spec compliance gaps in email and dashboard
Some checks failed
CI / quality (push) Failing after 28s
Deploy / deploy (push) Successful in 2m39s
- Email subject now follows spec format: PhaseFlow: [STATUS] - Day [cycleDay] ([phase])
- Daily email includes seed switch alert on day 15 (using getSeedSwitchAlert)
- Data panel HRV status now color-coded: green=Balanced, red=Unbalanced, gray=Unknown
- Data panel shows progress bar for week intensity vs phase limit with color thresholds

Adds 13 new tests (990 total).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:20:18 +00:00
0ea8e2f2b5 Fix decision-engine override behavior: sleep/pms return GENTLE per spec
Some checks failed
CI / quality (push) Failing after 28s
Deploy / deploy (push) Successful in 1m40s
The spec (decision-engine.md lines 93-94) clearly states:
- sleep override -> GENTLE
- pms override -> GENTLE

But the implementation was returning REST for all overrides. This fix:
- Updates decision-engine.ts to use OVERRIDE_DECISIONS with correct status/reason/icon per override type
- Updates tests to expect GENTLE for sleep and pms overrides
- Aligns implementation with specification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 23:11:47 +00:00
2408839b8b Fix 404 error when saving user preferences
Routes using withAuth were creating new unauthenticated PocketBase
clients, causing 404 errors when trying to update records. Modified
withAuth to pass the authenticated pb client to handlers so they can
use it for database operations.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 16:45:55 +00:00
72706bb91b Fix Invalid Date error in auth middleware
All checks were successful
Deploy / deploy (push) Successful in 2m28s
Add parseDate helper that safely returns null for empty/invalid date
strings from PocketBase. This prevents RangeError when pino logger
tries to serialize Invalid Date objects via toISOString().

- Make garminTokenExpiresAt and lastPeriodDate nullable in User type
- Filter garmin-sync cron to skip users without required dates
- Add test assertions for null date handling

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 14:13:02 +00:00
2ae6804cc4 Fix OAuth login by syncing auth state to cookie
All checks were successful
Deploy / deploy (push) Successful in 1m37s
Root cause: PocketBase SDK stores auth in localStorage, but Next.js
middleware checks for pb_auth cookie. The cookie was never being set
after successful OAuth login.

Fix: Add pb.authStore.onChange() listener that syncs auth state to
cookie on any change (login, logout, token refresh). This is the
idiomatic PocketBase pattern for Next.js SSR apps.

Also updates authentication spec to reflect that the cookie is
non-HttpOnly by design (client SDK needs read/write access).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-12 13:59:53 +00:00
31932a88bf Add email sent/failed structured logging
All checks were successful
Deploy / deploy (push) Successful in 1m38s
Implement email logging per observability spec:
- Add structured logging for email sent (info level) and failed (error level)
- Include userId, type, and recipient fields in log events
- Add userId parameter to email functions (sendDailyEmail, sendPeriodConfirmationEmail, sendTokenExpirationWarning)
- Update cron routes (notifications, garmin-sync) to pass userId

6 new tests added to email.test.ts (now 30 tests total)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 23:06:19 +00:00
a977934c23 Fix critical bug: cycle phase boundaries now scale with cycle length
CRITICAL BUG FIX:
- Phase boundaries were hardcoded for 31-day cycle, breaking correct
  phase calculations for users with different cycle lengths (28, 35, etc.)
- Added getPhaseBoundaries(cycleLength) function in cycle.ts
- Updated getPhase() to accept cycleLength parameter (default 31)
- Updated all callers (API routes, components) to pass cycleLength
- Added 13 new tests for phase boundaries with 28, 31, and 35-day cycles

ICS IMPROVEMENTS:
- Fixed emojis to match calendar.md spec: 🩸🌱🌸🌙🌑
- Added CATEGORIES field for calendar app colors per spec:
  MENSTRUAL=Red, FOLLICULAR=Green, OVULATION=Pink,
  EARLY_LUTEAL=Yellow, LATE_LUTEAL=Orange
- Added 5 new tests for CATEGORIES

Updated IMPLEMENTATION_PLAN.md with discovered issues and test counts.

825 tests passing (up from 807)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:39:09 +00:00
58f6c5605a Add period prediction accuracy feedback (P4.5 complete)
All checks were successful
Deploy / deploy (push) Successful in 1m36s
Implements visual feedback for cycle prediction accuracy in ICS calendar feeds:

- Add predictedDate field to PeriodLog type for tracking predicted vs actual dates
- POST /api/cycle/period now calculates and stores predictedDate based on
  previous lastPeriodDate + cycleLength, returns daysEarly/daysLate in response
- ICS feed generates "(Predicted)" events when actual period start differs
  from predicted, with descriptions like "period arrived 2 days early"
- Calendar route fetches period logs and passes them to ICS generator

This creates an accuracy feedback loop helping users understand their cycle
variability over time per calendar.md spec.

807 tests passing across 43 test files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 22:21:52 +00:00
649fa29df2 Add accessibility improvements (P4.2 partial)
All checks were successful
Deploy / deploy (push) Successful in 1m36s
- Add skip navigation link to root layout
- Add semantic HTML landmarks (main element) to login and settings pages
- Add aria-labels to calendar day buttons with date, cycle day, and phase info
- Add id="main-content" to dashboard main element for skip link target
- Fix pre-existing type error in auth-middleware.test.ts

Tests: 781 passing (11 new accessibility tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:49:26 +00:00
f69e1fd614 Separate server-side and client-side PocketBase URLs
All checks were successful
Deploy / deploy (push) Successful in 2m29s
Server-side code (health checks, API routes) needs to use internal
sidecar URL (POCKETBASE_URL at runtime), while client-side needs
public URL (NEXT_PUBLIC_POCKETBASE_URL baked at build time).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 21:17:58 +00:00
3567fbafd7 Expose PocketBase URL to client-side for OIDC auth
Some checks failed
Deploy / deploy (push) Failing after 6m37s
POCKETBASE_URL was only available server-side, causing the login page
to fall back to localhost:8090 in the browser. Renamed to
NEXT_PUBLIC_POCKETBASE_URL so Next.js bundles it into client code.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 17:33:52 +00:00
9c5b8466f6 Implement skeleton loading states for dashboard and routes (P3.8)
Add skeleton loading components per specs/dashboard.md requirements:
- DecisionCardSkeleton: Shimmer placeholder for status and reason
- DataPanelSkeleton: Skeleton rows for 5 metrics
- NutritionPanelSkeleton: Skeleton for nutrition guidance
- MiniCalendarSkeleton: Placeholder grid with navigation and legend
- OverrideTogglesSkeleton: 4 toggle placeholders
- CycleInfoSkeleton: Cycle day and phase placeholders
- DashboardSkeleton: Combined skeleton for route-level loading

Add Next.js loading.tsx files for instant loading states:
- src/app/loading.tsx (Dashboard)
- src/app/calendar/loading.tsx
- src/app/history/loading.tsx
- src/app/plan/loading.tsx
- src/app/settings/loading.tsx

Update dashboard page to use DashboardSkeleton instead of "Loading..." text.

Fix flaky garmin test with wider date tolerance for timezone variations.

29 new tests in skeletons.test.tsx (749 total tests passing).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 09:32:09 +00:00
714194f2d3 Implement structured logging for API routes (P3.7)
Replace console.error with pino structured logger across API routes
and add key event logging per observability spec:
- Auth failure (warn): reason
- Period logged (info): userId, date
- Override toggled (info): userId, override, enabled
- Decision calculated (info): userId, decision, reason
- Error events (error): err object with stack trace

Files updated:
- auth-middleware.ts: Added structured logging for auth failures
- cycle/period/route.ts: Added Period logged event + error logging
- calendar/[userId]/[token].ics/route.ts: Replaced console.error
- overrides/route.ts: Added Override toggled events
- today/route.ts: Added Decision calculated event

Tests: 720 passing (added 3 new structured logging tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 09:19:55 +00:00
5a0cdf7450 Implement Prometheus metrics endpoint (P2.16)
Add comprehensive metrics collection for production monitoring:
- src/lib/metrics.ts: prom-client based metrics library with custom counters,
  gauges, and histograms for Garmin sync, email, and decision engine
- GET /api/metrics: Prometheus-format endpoint for scraping
- Integration into garmin-sync cron: sync duration, success/failure counts,
  active users gauge
- Integration into email.ts: daily and warning email counters
- Integration into decision-engine.ts: decision type counters

Custom metrics implemented:
- phaseflow_garmin_sync_total (counter with status label)
- phaseflow_garmin_sync_duration_seconds (histogram)
- phaseflow_email_sent_total (counter with type label)
- phaseflow_decision_engine_calls_total (counter with decision label)
- phaseflow_active_users (gauge)

33 new tests (18 library + 15 route), bringing total to 586 tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 08:40:42 +00:00
5ec3aba8b3 Implement structured logging with pino (P2.17)
Add pino-based logger module for production observability:
- JSON output to stdout for log aggregators (Loki, ELK)
- Configurable via LOG_LEVEL environment variable (defaults to "info")
- Log levels: error, warn, info, debug
- Error objects serialized with type, message, and stack trace
- Child logger support for bound context
- ISO 8601 timestamps in all log entries

Test coverage: 16 tests covering JSON format, log levels, error
serialization, and child loggers.

Total tests now: 553 passing across 31 test files.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 08:29:31 +00:00
2ffee63a59 Implement token expiration warnings (P3.9)
Add email warnings for Garmin token expiration at 14-day and 7-day thresholds.
When the garmin-sync cron job runs, it now checks each user's token expiry and
sends a warning email at exactly 14 days and 7 days before expiration.

Changes:
- Add sendTokenExpirationWarning() to email.ts with differentiated subject
  lines and urgency levels for 14-day vs 7-day warnings
- Integrate warning logic into garmin-sync cron route using daysUntilExpiry()
- Track warnings sent in sync response with new warningsSent counter
- Add 20 new tests (10 for email function, 10 for sync integration)

Test count: 517 → 537

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 08:24:19 +00:00
c1679789b5 Implement Garmin biometric fetching functions (P2.1)
Add specific fetchers for HRV, Body Battery, and Intensity Minutes
to enable real biometric data collection from Garmin Connect API.

Functions added:
- fetchHrvStatus(): Returns "Balanced", "Unbalanced", or "Unknown"
- fetchBodyBattery(): Returns current BB and yesterday's low value
- fetchIntensityMinutes(): Returns 7-day rolling sum of activity

All functions gracefully handle API failures with safe defaults.
Test count expanded from 14 to 33 covering all scenarios.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 19:38:22 +00:00
0d88066d00 Add unit tests for lib utilities (P3.2-P3.6)
Implement comprehensive test coverage for five library modules:

- encryption.test.ts (14 tests): AES-256-GCM encrypt/decrypt round-trip,
  ciphertext format validation, error handling, key padding/truncation
- nutrition.test.ts (17 tests): seed cycling by cycle day, carb ranges
  by phase, keto guidance by phase, seed switch alert on day 15
- garmin.test.ts (14 tests): token expiry checks, days until expiry
  calculation, API fetch with auth headers, error handling
- email.test.ts (14 tests): daily email composition with biometrics,
  nutrition guidance, period confirmation emails, null value handling
- ics.test.ts (23 tests): ICS format validation, phase events with
  emojis, warning events on days 22/25, cycle length handling

Total: 82 new tests bringing project total to 254 passing tests.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 19:32:17 +00:00
76a46439b3 Implement auth middleware for API routes (P0.2)
Add authentication infrastructure for protected routes:
- withAuth() wrapper for API route handlers (src/lib/auth-middleware.ts)
- Next.js middleware for page protection (src/middleware.ts)

withAuth() loads auth from cookies, validates session, and passes
user context to handlers. Returns 401 for unauthenticated requests.

Page middleware redirects unauthenticated users to /login, while
allowing public routes (/login), API routes (handled separately),
and static assets through.

Tests: 18 new tests (6 for withAuth, 12 for page middleware)
Total test count: 60 tests passing

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 18:43:19 +00:00
caed11a445 Implement PocketBase auth helpers (P0.1)
Add authentication utilities to pocketbase.ts for server-side auth:
- createPocketBaseClient() - factory for fresh instances per request
- isAuthenticated(pb) - checks authStore validity
- getCurrentUser(pb) - returns typed User from authStore
- loadAuthFromCookies(pb, cookies) - loads auth from Next.js cookies

Includes 9 unit tests covering all auth state scenarios and cookie loading.
This unblocks P0.2 (auth middleware) and all downstream API/page work.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 18:37:15 +00:00
03f1bde24c Implement decision engine override handling (P0.3)
Add getDecisionWithOverrides() function that checks manual overrides
before algorithmic rules. Overrides are applied in priority order:
flare > stress > sleep > pms, and all force REST status.

Includes comprehensive test suite with 24 tests covering:
- All 8 algorithmic priority rules
- Override type behaviors
- Override priority enforcement
- Empty override fallthrough to algorithmic rules

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-10 18:30:57 +00:00
f15e093254 Initial project setup for PhaseFlow
Set up Next.js 16 project with TypeScript for a training decision app
that integrates menstrual cycle phases with Garmin biometrics for
Hashimoto's thyroiditis management.

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

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

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

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