Implement health check endpoint (P2.15)
Add GET /api/health endpoint for deployment monitoring and load balancer health probes. Returns 200 with status "ok" when PocketBase is reachable, 503 with status "unhealthy" when PocketBase connection fails. Response includes timestamp (ISO 8601), version, and error message (on failure). Uses PocketBase SDK's built-in health.check() method for connectivity testing. 14 tests covering healthy/unhealthy states and edge cases. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,8 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
|
||||
|
||||
## Current State Summary
|
||||
|
||||
### Overall Status: 517 tests passing across 30 test files
|
||||
|
||||
### Library Implementation
|
||||
| File | Status | Gap Analysis |
|
||||
|------|--------|--------------|
|
||||
@@ -17,12 +19,19 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
|
||||
| `pocketbase.ts` | **COMPLETE** | 9 tests covering `createPocketBaseClient()`, `isAuthenticated()`, `getCurrentUser()`, `loadAuthFromCookies()` |
|
||||
| `auth-middleware.ts` | **COMPLETE** | 6 tests covering `withAuth()` wrapper for API route protection |
|
||||
| `middleware.ts` (Next.js) | **COMPLETE** | 12 tests covering page protection, redirects to login |
|
||||
| `logger.ts` | **NOT IMPLEMENTED** | P2.17 - Structured logging with pino |
|
||||
| `metrics.ts` | **NOT IMPLEMENTED** | P2.16 - Prometheus metrics collection |
|
||||
|
||||
### Missing Infrastructure Files (CONFIRMED NOT EXIST)
|
||||
- ~~`src/lib/auth-middleware.ts`~~ - **CREATED** in P0.2
|
||||
- ~~`src/middleware.ts`~~ - **CREATED** in P0.2
|
||||
### Infrastructure Gaps (from specs/ - pending implementation)
|
||||
| Gap | Spec Reference | Task | Priority |
|
||||
|-----|----------------|------|----------|
|
||||
| Health Check Endpoint | specs/observability.md | P2.15 | **COMPLETE** |
|
||||
| Prometheus Metrics | specs/observability.md | P2.16 | Medium |
|
||||
| Structured Logging (pino) | specs/observability.md | P2.17 | Medium |
|
||||
| OIDC Authentication | specs/authentication.md | P2.18 | Medium |
|
||||
| Token Expiration Warnings | specs/email.md | P3.9 | Medium |
|
||||
|
||||
### API Routes (15 total)
|
||||
### API Routes (17 total)
|
||||
| Route | Status | Notes |
|
||||
|-------|--------|-------|
|
||||
| GET /api/user | **COMPLETE** | Returns user profile with `withAuth()` |
|
||||
@@ -40,6 +49,8 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
|
||||
| POST /api/cron/garmin-sync | **COMPLETE** | Syncs Garmin data for all users, creates DailyLogs (22 tests) |
|
||||
| POST /api/cron/notifications | **COMPLETE** | Sends daily emails with timezone matching, DailyLog handling (20 tests) |
|
||||
| GET /api/history | **COMPLETE** | Paginated historical daily logs with date filtering (19 tests) |
|
||||
| GET /api/health | **COMPLETE** | Health check for deployment monitoring (14 tests) |
|
||||
| GET /metrics | **NOT IMPLEMENTED** | Prometheus metrics endpoint (P2.16) |
|
||||
|
||||
### Pages (7 total)
|
||||
| Page | Status | Notes |
|
||||
@@ -50,7 +61,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
|
||||
| Settings/Garmin (`/settings/garmin`) | **COMPLETE** | Token management UI, connection status, disconnect functionality, 27 tests |
|
||||
| Calendar (`/calendar`) | **COMPLETE** | MonthView with navigation, ICS subscription section, token regeneration, 23 tests |
|
||||
| History (`/history`) | **COMPLETE** | Table view with date filtering, pagination, decision styling, 26 tests |
|
||||
| Plan (`/plan`) | Placeholder | Needs phase details display |
|
||||
| Plan (`/plan`) | **NOT IMPLEMENTED** | Placeholder only - shows heading and placeholder text, needs phase details display |
|
||||
|
||||
### Components
|
||||
| Component | Status | Notes |
|
||||
@@ -60,7 +71,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
|
||||
| `NutritionPanel` | **COMPLETE** | Shows seeds, carbs, keto guidance |
|
||||
| `OverrideToggles` | **COMPLETE** | Toggle buttons with callbacks |
|
||||
| `DayCell` | **COMPLETE** | Phase-colored day with click handler |
|
||||
| `MiniCalendar` | **Partial (~30%)** | Has header only, **MISSING: calendar grid** |
|
||||
| `MiniCalendar` | **PARTIAL (~30%)** | Has header with cycle info only, **MISSING: calendar grid with DayCell integration** |
|
||||
| `MonthView` | **COMPLETE** | Calendar grid with DayCell integration, navigation controls, phase legend |
|
||||
|
||||
### Test Coverage
|
||||
@@ -90,21 +101,30 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta
|
||||
| `src/app/api/calendar/[userId]/[token].ics/route.test.ts` | **EXISTS** - 10 tests (token validation, ICS generation, caching, error handling) |
|
||||
| `src/app/api/calendar/regenerate-token/route.test.ts` | **EXISTS** - 9 tests (token generation, URL formatting, auth) |
|
||||
| `src/app/api/history/route.test.ts` | **EXISTS** - 19 tests (pagination, date filtering, auth, validation) |
|
||||
| `src/app/api/health/route.test.ts` | **EXISTS** - 14 tests (healthy/unhealthy states, PocketBase connectivity, error handling) |
|
||||
| `src/app/history/page.test.tsx` | **EXISTS** - 26 tests (rendering, data loading, pagination, date filtering, styling) |
|
||||
| `src/components/calendar/month-view.test.tsx` | **EXISTS** - 21 tests (calendar grid, phase colors, navigation, legend) |
|
||||
| `src/app/calendar/page.test.tsx` | **EXISTS** - 23 tests (rendering, navigation, ICS subscription, token regeneration) |
|
||||
| E2E tests | **NONE** |
|
||||
| `src/app/settings/page.test.tsx` | **EXISTS** - 24+ tests (form rendering, validation, submission) |
|
||||
| `src/app/settings/garmin/page.test.tsx` | **EXISTS** - 27 tests (connection status, token management) |
|
||||
| `src/components/dashboard/decision-card.test.tsx` | **MISSING** - Needs tests for rendering, status icons |
|
||||
| `src/components/dashboard/data-panel.test.tsx` | **MISSING** - Needs tests for biometrics display |
|
||||
| `src/components/dashboard/nutrition-panel.test.tsx` | **MISSING** - Needs tests for nutrition guidance display |
|
||||
| `src/components/dashboard/override-toggles.test.tsx` | **MISSING** - Needs tests for toggle state and callbacks |
|
||||
| `src/components/dashboard/mini-calendar.test.tsx` | **MISSING** - Needs tests for header/partial implementation |
|
||||
| `src/components/calendar/day-cell.test.tsx` | **MISSING** - Needs tests for phase coloring, click handler |
|
||||
| E2E tests | **AUTHORIZED SKIP** - Per specs/testing.md |
|
||||
|
||||
### Critical Business Rules (from Spec)
|
||||
1. **Override Priority:** flare > stress > sleep > pms (must be enforced in order)
|
||||
2. **HRV Unbalanced:** ALWAYS forces REST (highest algorithmic priority, non-overridable)
|
||||
3. **Phase Limits:** Strictly enforced per phase configuration
|
||||
4. **Token Expiration Warnings:** Must send email at 14 days and 7 days before expiry
|
||||
4. **Token Expiration Warnings:** Must send email at 14 days and 7 days before expiry (NOT IMPLEMENTED - P3.9)
|
||||
5. **ICS Feed:** Generates 90 days of phase events for calendar subscription
|
||||
|
||||
---
|
||||
|
||||
## P0: Critical Blockers
|
||||
## P0: Critical Blockers ✅ ALL COMPLETE
|
||||
|
||||
These must be completed first - nothing else works without them.
|
||||
|
||||
@@ -158,7 +178,7 @@ These must be completed first - nothing else works without them.
|
||||
|
||||
---
|
||||
|
||||
## P1: Core Functionality
|
||||
## P1: Core Functionality ✅ ALL COMPLETE
|
||||
|
||||
Minimum viable product - app can be used for daily decisions.
|
||||
|
||||
@@ -295,7 +315,7 @@ Full feature set for production use.
|
||||
- `connected` - Boolean indicating if tokens exist
|
||||
- `daysUntilExpiry` - Days until token expires (null if not connected)
|
||||
- `expired` - Boolean indicating if tokens have expired
|
||||
- `warningLevel` - "critical" (≤7 days), "warning" (8-14 days), or null
|
||||
- `warningLevel` - "critical" (<=7 days), "warning" (8-14 days), or null
|
||||
- **Why:** Users need visibility into their Garmin connection
|
||||
- **Depends On:** P0.1, P0.2, P2.1
|
||||
|
||||
@@ -425,21 +445,98 @@ Full feature set for production use.
|
||||
|
||||
### P2.13: Plan Page Implementation
|
||||
- [ ] Phase-specific training plan view
|
||||
- **Current State:** Placeholder only - shows "Exercise Plan" heading and placeholder text, no actual content
|
||||
- **Files:**
|
||||
- `src/app/plan/page.tsx` - Current phase details, upcoming phases, limits
|
||||
- **Tests:**
|
||||
- E2E test: correct phase info displayed
|
||||
- Component tests: phase info display, limits shown correctly
|
||||
- **Features Needed:**
|
||||
- Current phase details (name, day range, characteristics)
|
||||
- Upcoming phases preview
|
||||
- Phase-specific training limits and recommendations
|
||||
- Integration with cycle.ts utilities
|
||||
- **Why:** Users want detailed training guidance
|
||||
- **Depends On:** P0.4, P1.3
|
||||
|
||||
### P2.14: Mini Calendar Component
|
||||
- [ ] Dashboard overview calendar
|
||||
- **Current State:** Component exists with header/cycle info only (~30% complete), NO calendar grid
|
||||
- **Files:**
|
||||
- `src/components/dashboard/mini-calendar.tsx` - **Complete calendar grid with phase colors**
|
||||
- `src/components/dashboard/mini-calendar.tsx` - **Needs: complete calendar grid with phase colors using DayCell**
|
||||
- **Tests:**
|
||||
- Component test: renders current month, highlights today
|
||||
- `src/components/dashboard/mini-calendar.test.tsx` - Component test: renders current month, highlights today
|
||||
- **Features Needed:**
|
||||
- Calendar grid (reuse DayCell component from MonthView)
|
||||
- Current week/month view
|
||||
- Phase color coding
|
||||
- Today highlight
|
||||
- **Why:** Quick visual reference on dashboard
|
||||
- **Note:** Component exists with header only, needs calendar grid (~70% remaining)
|
||||
- **Note:** Can leverage existing MonthView/DayCell components for implementation
|
||||
|
||||
### P2.15: Health Check Endpoint ✅ COMPLETE
|
||||
- [x] GET /api/health for deployment monitoring
|
||||
- **Current State:** Fully implemented with PocketBase connectivity checks
|
||||
- **Files:**
|
||||
- `src/app/api/health/route.ts` - Returns health status with PocketBase connectivity check
|
||||
- **Tests:**
|
||||
- `src/app/api/health/route.test.ts` - 14 tests for healthy (200) and unhealthy (503) states
|
||||
- **Response Shape:**
|
||||
- `status` - "ok" or "unhealthy"
|
||||
- `timestamp` - ISO 8601 timestamp
|
||||
- `version` - Application version
|
||||
- **Checks Performed:**
|
||||
- PocketBase connectivity
|
||||
- Basic app startup complete
|
||||
- **Why:** Required for Nomad health checks, load balancer probes, and uptime monitoring (per specs/observability.md)
|
||||
|
||||
### P2.16: Prometheus Metrics Endpoint
|
||||
- [ ] GET /metrics for monitoring
|
||||
- **Current State:** Endpoint and metrics library do not exist
|
||||
- **Files:**
|
||||
- `src/app/api/metrics/route.ts` - Returns Prometheus-format metrics
|
||||
- `src/lib/metrics.ts` - Metrics collection with prom-client
|
||||
- **Tests:**
|
||||
- `src/app/api/metrics/route.test.ts` - Tests for valid Prometheus format output
|
||||
- **Metrics:**
|
||||
- Standard Node.js metrics (heap, eventloop lag, http requests)
|
||||
- Custom: `phaseflow_garmin_sync_total`, `phaseflow_email_sent_total`, `phaseflow_decision_engine_calls_total`, `phaseflow_active_users`
|
||||
- **Why:** Required for Prometheus scraping and production monitoring (per specs/observability.md)
|
||||
- **Depends On:** None
|
||||
|
||||
### P2.17: Structured Logging with Pino
|
||||
- [ ] Replace console.error with structured JSON logging
|
||||
- **Current State:** logger.ts does not exist, using console.log/error
|
||||
- **Files:**
|
||||
- `src/lib/logger.ts` - Pino logger configuration
|
||||
- All route files - Replace console.error/log with logger calls
|
||||
- **Tests:**
|
||||
- `src/lib/logger.test.ts` - Tests for log format, levels, and JSON output
|
||||
- **Log Levels:** error, warn, info
|
||||
- **Key Events:** Auth success/failure, Garmin sync, email sent/failed, decision calculated, period logged, override toggled
|
||||
- **Why:** Required for log aggregators (Loki, ELK) and production debugging (per specs/observability.md)
|
||||
- **Depends On:** None
|
||||
|
||||
### P2.18: OIDC Authentication
|
||||
- [ ] Replace email/password login with OIDC (Pocket-ID)
|
||||
- **Current State:** Using email/password form, no OIDC code exists
|
||||
- **Files:**
|
||||
- `src/app/login/page.tsx` - Replace form with "Sign In with Pocket-ID" button
|
||||
- `src/lib/pocketbase.ts` - Add OIDC redirect and callback handling
|
||||
- **Tests:**
|
||||
- Update `src/app/login/page.test.tsx` - Tests for OIDC redirect flow
|
||||
- **Flow:**
|
||||
1. User clicks "Sign In with Pocket-ID"
|
||||
2. Redirect to Pocket-ID authorization endpoint
|
||||
3. User authenticates (MFA if configured)
|
||||
4. Callback with authorization code
|
||||
5. PocketBase exchanges code for tokens
|
||||
6. Redirect to dashboard
|
||||
- **Environment Variables:**
|
||||
- `POCKETBASE_OIDC_CLIENT_ID`
|
||||
- `POCKETBASE_OIDC_CLIENT_SECRET`
|
||||
- `POCKETBASE_OIDC_ISSUER_URL`
|
||||
- **Why:** Required per specs/authentication.md for secure identity management
|
||||
- **Note:** Current email/password implementation works but OIDC is the production requirement
|
||||
|
||||
---
|
||||
|
||||
@@ -528,14 +625,15 @@ Testing, error handling, and refinements.
|
||||
|
||||
### P3.9: Token Expiration Warnings
|
||||
- [ ] Email warnings at 14 and 7 days before Garmin token expiry
|
||||
- **Current State:** `sendTokenExpirationWarning()` function does not exist in email.ts
|
||||
- **Files:**
|
||||
- `src/lib/email.ts` - Add `sendTokenExpirationWarning()`
|
||||
- `src/app/api/cron/garmin-sync/route.ts` - Check expiry, trigger warnings
|
||||
- **Tests:**
|
||||
- Test warning triggers at exactly 14 days and 7 days
|
||||
- **Why:** Users need time to refresh tokens (per spec requirement)
|
||||
- **Why:** Users need time to refresh tokens (per spec requirement in specs/email.md)
|
||||
|
||||
### P3.10: E2E Test Suite
|
||||
### P3.10: E2E Test Suite (AUTHORIZED SKIP)
|
||||
- [ ] Comprehensive end-to-end tests
|
||||
- **Files:**
|
||||
- `tests/e2e/*.spec.ts` - Full user flows
|
||||
@@ -547,6 +645,102 @@ Testing, error handling, and refinements.
|
||||
- Garmin connection flow
|
||||
- Calendar subscription
|
||||
- **Why:** Confidence in production deployment
|
||||
- **Status:** Per specs/testing.md: "End-to-end tests are not required for MVP (authorized skip)"
|
||||
|
||||
### P3.11: Missing Component Tests
|
||||
- [ ] Add unit tests for untested components
|
||||
- **Components Needing Tests (6 total):**
|
||||
- `src/components/dashboard/decision-card.tsx` - Tests for rendering decision status, icon, and reason
|
||||
- `src/components/dashboard/data-panel.tsx` - Tests for biometrics display (BB, HRV, intensity)
|
||||
- `src/components/dashboard/nutrition-panel.tsx` - Tests for seeds, carbs, keto guidance display
|
||||
- `src/components/dashboard/override-toggles.tsx` - Tests for toggle states and callbacks (has interactive state)
|
||||
- `src/components/dashboard/mini-calendar.tsx` - Tests for header rendering (partial implementation)
|
||||
- `src/components/calendar/day-cell.tsx` - Tests for phase coloring and click handler
|
||||
- **Test Files to Create:**
|
||||
- `src/components/dashboard/decision-card.test.tsx`
|
||||
- `src/components/dashboard/data-panel.test.tsx`
|
||||
- `src/components/dashboard/nutrition-panel.test.tsx`
|
||||
- `src/components/dashboard/override-toggles.test.tsx`
|
||||
- `src/components/dashboard/mini-calendar.test.tsx`
|
||||
- `src/components/calendar/day-cell.test.tsx`
|
||||
- **Why:** Component isolation ensures UI correctness and prevents regressions
|
||||
|
||||
---
|
||||
|
||||
## P4: UX Polish and Accessibility
|
||||
|
||||
Enhancements from spec requirements that improve user experience.
|
||||
|
||||
### P4.1: Dashboard Onboarding Banners
|
||||
- [ ] Show setup prompts for missing configuration
|
||||
- **Spec Reference:** specs/dashboard.md mentions onboarding banners
|
||||
- **Features Needed:**
|
||||
- Banner when Garmin not connected
|
||||
- Banner when period date not set
|
||||
- Banner when notification time not configured
|
||||
- Dismissible after user completes setup
|
||||
- **Files:**
|
||||
- `src/app/page.tsx` - Add conditional banner rendering
|
||||
- `src/components/dashboard/onboarding-banner.tsx` - New component
|
||||
- **Why:** Helps new users complete setup for full functionality
|
||||
|
||||
### P4.2: Accessibility Improvements
|
||||
- [ ] Keyboard navigation and focus indicators
|
||||
- **Spec Reference:** specs/dashboard.md accessibility requirements
|
||||
- **Requirements:**
|
||||
- Keyboard navigation for all interactive elements
|
||||
- Visible focus indicators (focus:ring styles)
|
||||
- 4.5:1 minimum contrast ratio
|
||||
- Screen reader labels where needed
|
||||
- **Files:**
|
||||
- All component files - Add focus:ring classes, aria-labels
|
||||
- **Why:** Required for accessibility compliance
|
||||
|
||||
### P4.3: Dark Mode Configuration
|
||||
- [ ] Complete dark mode support
|
||||
- **Current State:** Partial Tailwind support via dark: classes exists in some components
|
||||
- **Needs:**
|
||||
- Configure prefers-color-scheme detection in tailwind.config.js
|
||||
- Theme toggle in settings (optional)
|
||||
- Ensure all components have dark: variants
|
||||
- Test contrast ratios in dark mode
|
||||
- **Files:**
|
||||
- `tailwind.config.js` - Add darkMode configuration
|
||||
- Component files - Verify dark: class coverage
|
||||
- **Why:** User preference for dark mode
|
||||
|
||||
### P4.4: Loading Performance
|
||||
- [ ] Loading states within 100ms target
|
||||
- **Spec Reference:** specs/dashboard.md performance requirements
|
||||
- **Features:**
|
||||
- Skeleton loading states
|
||||
- Optimistic UI updates (partially done with overrides)
|
||||
- Suspense boundaries for code splitting
|
||||
- **Files:**
|
||||
- Page files - Add loading.tsx skeletons
|
||||
- **Why:** Perceived performance improvement
|
||||
|
||||
### P4.5: Period Prediction Accuracy Feedback
|
||||
- [ ] Mark predicted vs confirmed period dates
|
||||
- **Spec Reference:** specs/calendar.md mentions predictions marked with "Predicted" suffix
|
||||
- **Features:**
|
||||
- Visual distinction between logged and predicted periods
|
||||
- Calendar events show "Predicted" label for future periods
|
||||
- **Files:**
|
||||
- `src/lib/ics.ts` - Add "Predicted" suffix to future phase events
|
||||
- `src/components/calendar/day-cell.tsx` - Visual indicator for predictions
|
||||
- **Why:** Helps users understand prediction accuracy
|
||||
|
||||
### P4.6: Rate Limiting
|
||||
- [ ] Login attempt rate limiting
|
||||
- **Spec Reference:** specs/email.md mentions 5 login attempts per minute
|
||||
- **Features:**
|
||||
- Rate limit login attempts by IP/email
|
||||
- Show remaining attempts on error
|
||||
- Temporary lockout after exceeding limit
|
||||
- **Files:**
|
||||
- `src/app/api/auth/route.ts` or PocketBase config - Rate limiting logic
|
||||
- **Why:** Security requirement from spec
|
||||
|
||||
---
|
||||
|
||||
@@ -560,7 +754,7 @@ P0.3 Override Logic ───┴──> P1.4 GET /api/today ──> P1.7 Dashboa
|
||||
P1.1 PATCH /api/user ────> P2.9 Settings Page
|
||||
P1.2 POST period ────────> P1.3 GET current ────> P1.7 Dashboard
|
||||
P1.5 Overrides API ──────> P1.7 Dashboard
|
||||
P1.6 Login Page
|
||||
P1.6 Login Page ─────────> P2.18 OIDC Auth (upgrade)
|
||||
|
||||
P2.1 Garmin fetchers ──> P2.2 Garmin tokens ──> P2.4 Cron sync ──> P2.5 Notifications
|
||||
│
|
||||
@@ -573,8 +767,30 @@ P2.7 Regen token
|
||||
P2.8 History API ──────> P2.12 History page
|
||||
P2.13 Plan page
|
||||
P2.14 Mini calendar
|
||||
|
||||
P2.15 Health endpoint (independent - HIGH PRIORITY for deployment)
|
||||
P2.16 Metrics endpoint (independent)
|
||||
P2.17 Structured logging (independent, but should be done before other items for proper logging)
|
||||
|
||||
P3.11 Component tests ─> Can be done in parallel with other work
|
||||
P4.* UX Polish ────────> After core functionality complete
|
||||
```
|
||||
|
||||
### Remaining Work Priority
|
||||
|
||||
| Priority | Task | Effort | Notes |
|
||||
|----------|------|--------|-------|
|
||||
| HIGH | P3.9 Token Warnings | Small | Spec requirement, security-related |
|
||||
| Medium | P2.13 Plan Page | Medium | Placeholder exists, needs content |
|
||||
| Medium | P2.14 MiniCalendar | Small | Can reuse DayCell, ~70% remaining |
|
||||
| Medium | P2.16 Metrics | Medium | Production monitoring |
|
||||
| Medium | P2.17 Logging | Medium | Should be done early for coverage |
|
||||
| Medium | P2.18 OIDC Auth | Large | Production auth requirement |
|
||||
| Medium | P3.11 Component Tests | Medium | 6 components need tests |
|
||||
| Low | P3.7 Error Handling | Small | Polish |
|
||||
| Low | P3.8 Loading States | Small | Polish |
|
||||
| Low | P4.* UX Polish | Various | After core complete |
|
||||
|
||||
### Dependency Summary
|
||||
|
||||
| Task | Blocked By | Blocks |
|
||||
@@ -583,6 +799,11 @@ P2.14 Mini calendar
|
||||
| P0.2 | P0.1 | P0.4, P1.1-P1.5, P2.2-P2.3, P2.7-P2.8 |
|
||||
| P0.3 | - | P1.4, P1.5 |
|
||||
| P0.4 | P0.1, P0.2 | P1.7, P2.9, P2.10, P2.13 |
|
||||
| P2.16 | - | - |
|
||||
| P2.17 | - | - (recommended early for logging coverage) |
|
||||
| P2.18 | P1.6 | - |
|
||||
| P3.9 | P2.4 | - |
|
||||
| P3.11 | - | - |
|
||||
|
||||
---
|
||||
|
||||
@@ -597,6 +818,8 @@ P2.14 Mini calendar
|
||||
- [x] **ics.ts** - Complete with 23 tests (`generateIcsFeed`, ICS format validation, 90-day event generation) (P3.4)
|
||||
- [x] **encryption.ts** - Complete with 14 tests (AES-256-GCM encrypt/decrypt, round-trip validation, error handling) (P3.5)
|
||||
- [x] **garmin.ts** - Complete with 33 tests (`fetchGarminData`, `fetchHrvStatus`, `fetchBodyBattery`, `fetchIntensityMinutes`, `isTokenExpired`, `daysUntilExpiry`, error handling) (P2.1, P3.6)
|
||||
- [x] **auth-middleware.ts** - Complete with 6 tests (`withAuth()` wrapper)
|
||||
- [x] **middleware.ts** - Complete with 12 tests (Next.js page protection)
|
||||
|
||||
### Components
|
||||
- [x] **DecisionCard** - Displays decision status, icon, and reason
|
||||
@@ -606,7 +829,7 @@ P2.14 Mini calendar
|
||||
- [x] **DayCell** - Phase-colored calendar day cell with click handler
|
||||
- [x] **MonthView** - Calendar grid with DayCell integration, navigation controls (prev/next month, Today button), phase legend, 21 tests
|
||||
|
||||
### API Routes
|
||||
### API Routes (16 complete, 1 not implemented)
|
||||
- [x] **GET /api/user** - Returns authenticated user profile, 4 tests (P0.4)
|
||||
- [x] **PATCH /api/user** - Updates user profile (cycleLength, notificationTime, timezone), 17 tests (P1.1)
|
||||
- [x] **POST /api/cycle/period** - Logs period start date, updates user, creates PeriodLog, 8 tests (P1.2)
|
||||
@@ -622,8 +845,9 @@ P2.14 Mini calendar
|
||||
- [x] **GET /api/calendar/[userId]/[token].ics** - Returns ICS feed with 90-day phase events, token validation, caching headers, 10 tests (P2.6)
|
||||
- [x] **POST /api/calendar/regenerate-token** - Generates new 32-char calendar token, returns URL, 9 tests (P2.7)
|
||||
- [x] **GET /api/history** - Paginated historical daily logs with date filtering, validation, 19 tests (P2.8)
|
||||
- [x] **GET /api/health** - Health check endpoint with PocketBase connectivity check, 14 tests (P2.15)
|
||||
|
||||
### Pages
|
||||
### Pages (6 complete, 1 placeholder)
|
||||
- [x] **Login Page** - Email/password form with PocketBase auth, error handling, loading states, redirect, 14 tests (P1.6)
|
||||
- [x] **Dashboard Page** - Complete daily interface with /api/today integration, DecisionCard, DataPanel, NutritionPanel, OverrideToggles, 23 tests (P1.7)
|
||||
- [x] **Settings Page** - Form for cycleLength, notificationTime, timezone with validation, loading states, error handling, 28 tests (P2.9)
|
||||
@@ -655,7 +879,14 @@ P2.14 Mini calendar
|
||||
3. **Incremental Delivery:** P1 completion = usable app without Garmin (manual data entry fallback)
|
||||
4. **P2 Completion:** Full feature set with automation
|
||||
5. **P3:** Quality and polish for production confidence
|
||||
6. **Component Reuse:** Dashboard components are complete and can be used directly in P1.7
|
||||
7. **HRV Rule:** HRV Unbalanced status ALWAYS forces REST - this is the highest algorithmic priority and cannot be overridden by manual toggles
|
||||
8. **Override Order:** When multiple overrides are active, apply in order: flare > stress > sleep > pms
|
||||
9. **Token Warnings:** Per spec, warnings must be sent at exactly 14 days and 7 days before expiry
|
||||
6. **P4:** UX polish and accessibility improvements from spec requirements
|
||||
7. **Component Reuse:** Dashboard components are complete and can be used directly in P1.7
|
||||
8. **HRV Rule:** HRV Unbalanced status ALWAYS forces REST - this is the highest algorithmic priority and cannot be overridden by manual toggles
|
||||
9. **Override Order:** When multiple overrides are active, apply in order: flare > stress > sleep > pms
|
||||
10. **Token Warnings:** Per spec, warnings must be sent at exactly 14 days and 7 days before expiry (P3.9 NOT IMPLEMENTED)
|
||||
11. **Health Check Priority:** P2.15 (GET /api/health) should be implemented early - it's required for deployment monitoring and load balancer health probes
|
||||
12. **Structured Logging:** P2.17 (pino logger) should be implemented before other P2 items if possible, so new code can use proper logging from the start
|
||||
13. **OIDC vs Email/Password:** Current email/password login (P1.6) works for development. P2.18 upgrades to OIDC for production security per specs/authentication.md
|
||||
14. **E2E Tests:** Authorized skip per specs/testing.md - unit and integration tests are sufficient for MVP
|
||||
15. **Dark Mode:** Partial Tailwind support exists via dark: classes but may need prefers-color-scheme configuration in tailwind.config.js (see P4.3)
|
||||
16. **Component Tests:** 6 components lack unit tests (P3.11) - DecisionCard, DataPanel, NutritionPanel, OverrideToggles, MiniCalendar, DayCell
|
||||
|
||||
197
src/app/api/health/route.test.ts
Normal file
197
src/app/api/health/route.test.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
// ABOUTME: Tests for health check endpoint used by deployment monitoring and load balancers.
|
||||
// ABOUTME: Covers healthy (200) and unhealthy (503) states based on PocketBase connectivity.
|
||||
|
||||
import type Client from "pocketbase";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
// Mock PocketBase before importing the route
|
||||
vi.mock("@/lib/pocketbase", () => ({
|
||||
createPocketBaseClient: vi.fn(),
|
||||
}));
|
||||
|
||||
import { createPocketBaseClient } from "@/lib/pocketbase";
|
||||
import { GET } from "./route";
|
||||
|
||||
const mockCreatePocketBaseClient = vi.mocked(createPocketBaseClient);
|
||||
|
||||
function mockPocketBaseWithHealth(checkFn: ReturnType<typeof vi.fn>): void {
|
||||
const mockPb = {
|
||||
health: { check: checkFn },
|
||||
} as unknown as Client;
|
||||
mockCreatePocketBaseClient.mockReturnValue(mockPb);
|
||||
}
|
||||
|
||||
describe("GET /api/health", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("healthy state", () => {
|
||||
it("returns 200 when PocketBase is reachable", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockResolvedValue({ code: 200, message: "OK" }),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it("returns status ok when healthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockResolvedValue({ code: 200, message: "OK" }),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.status).toBe("ok");
|
||||
});
|
||||
|
||||
it("includes ISO 8601 timestamp when healthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockResolvedValue({ code: 200, message: "OK" }),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.timestamp).toBeDefined();
|
||||
// Verify it's a valid ISO 8601 date
|
||||
const date = new Date(body.timestamp);
|
||||
expect(date.toISOString()).toBe(body.timestamp);
|
||||
});
|
||||
|
||||
it("includes version when healthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockResolvedValue({ code: 200, message: "OK" }),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.version).toBeDefined();
|
||||
expect(typeof body.version).toBe("string");
|
||||
});
|
||||
|
||||
it("does not include error field when healthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockResolvedValue({ code: 200, message: "OK" }),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.error).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("unhealthy state", () => {
|
||||
it("returns 503 when PocketBase is unreachable", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("Connection refused")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
});
|
||||
|
||||
it("returns status unhealthy when PocketBase fails", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("Connection refused")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.status).toBe("unhealthy");
|
||||
});
|
||||
|
||||
it("includes ISO 8601 timestamp when unhealthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("Connection refused")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.timestamp).toBeDefined();
|
||||
const date = new Date(body.timestamp);
|
||||
expect(date.toISOString()).toBe(body.timestamp);
|
||||
});
|
||||
|
||||
it("includes error message when unhealthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("Connection refused")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.error).toBeDefined();
|
||||
expect(typeof body.error).toBe("string");
|
||||
expect(body.error.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("describes PocketBase failure in error message", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("ECONNREFUSED")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
expect(body.error).toContain("PocketBase");
|
||||
});
|
||||
|
||||
it("does not include version field when unhealthy", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("Connection refused")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
const body = await response.json();
|
||||
|
||||
// Per spec, version is only in healthy response
|
||||
expect(body.version).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("handles PocketBase timeout", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi.fn().mockRejectedValue(new Error("timeout of 5000ms exceeded")),
|
||||
);
|
||||
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(503);
|
||||
const body = await response.json();
|
||||
expect(body.status).toBe("unhealthy");
|
||||
});
|
||||
|
||||
it("handles PocketBase returning error status code", async () => {
|
||||
mockPocketBaseWithHealth(
|
||||
vi
|
||||
.fn()
|
||||
.mockResolvedValue({ code: 500, message: "Internal Server Error" }),
|
||||
);
|
||||
|
||||
// PocketBase returning 500 should still be considered healthy from connectivity perspective
|
||||
// as long as the check() call succeeds
|
||||
const response = await GET();
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
it("calls PocketBase health.check exactly once", async () => {
|
||||
const mockCheck = vi.fn().mockResolvedValue({ code: 200, message: "OK" });
|
||||
mockPocketBaseWithHealth(mockCheck);
|
||||
|
||||
await GET();
|
||||
|
||||
expect(mockCheck).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
31
src/app/api/health/route.ts
Normal file
31
src/app/api/health/route.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// ABOUTME: Health check endpoint for deployment monitoring and load balancer probes.
|
||||
// ABOUTME: Returns application health status based on PocketBase connectivity.
|
||||
|
||||
import { NextResponse } from "next/server";
|
||||
import { createPocketBaseClient } from "@/lib/pocketbase";
|
||||
|
||||
const APP_VERSION = "1.0.0";
|
||||
|
||||
export async function GET(): Promise<NextResponse> {
|
||||
const timestamp = new Date().toISOString();
|
||||
const pb = createPocketBaseClient();
|
||||
|
||||
try {
|
||||
await pb.health.check();
|
||||
|
||||
return NextResponse.json({
|
||||
status: "ok",
|
||||
timestamp,
|
||||
version: APP_VERSION,
|
||||
});
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{
|
||||
status: "unhealthy",
|
||||
timestamp,
|
||||
error: "PocketBase connection failed",
|
||||
},
|
||||
{ status: 503 },
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user