From 949cb1671af1ad5126a2cf391517263affc34121 Mon Sep 17 00:00:00 2001 From: Petru Paler Date: Sat, 10 Jan 2026 19:03:13 +0000 Subject: [PATCH] Implement GET /api/today endpoint (P1.4) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add the core daily snapshot API that powers the dashboard. Returns: - Training decision (status, reason, icon) using decision engine - Cycle data (cycleDay, phase, phaseConfig, daysUntilNextPhase) - Biometrics (hrvStatus, bodyBattery, weekIntensity, phaseLimit) - Nutrition guidance (seeds, carbRange, ketoGuidance) When no DailyLog exists (Garmin not synced), returns sensible defaults: hrvStatus="Unknown", bodyBattery=100, weekIntensity=0. This allows the app to function without Garmin integration. 22 tests covering auth, validation, all decision paths, override handling, phase-specific logic, and nutrition guidance. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- IMPLEMENTATION_PLAN.md | 17 +- src/app/api/today/route.test.ts | 519 ++++++++++++++++++++++++++++++++ src/app/api/today/route.ts | 107 ++++++- 3 files changed, 634 insertions(+), 9 deletions(-) create mode 100644 src/app/api/today/route.test.ts diff --git a/IMPLEMENTATION_PLAN.md b/IMPLEMENTATION_PLAN.md index e559c5d..571276e 100644 --- a/IMPLEMENTATION_PLAN.md +++ b/IMPLEMENTATION_PLAN.md @@ -29,7 +29,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | PATCH /api/user | 501 | Returns Not Implemented | | POST /api/cycle/period | **COMPLETE** | Logs period start, updates user, creates PeriodLog (8 tests) | | GET /api/cycle/current | **COMPLETE** | Returns cycle day, phase, config, daysUntilNextPhase (10 tests) | -| GET /api/today | 501 | Returns Not Implemented | +| GET /api/today | **COMPLETE** | Returns decision, cycle, biometrics, nutrition (22 tests) | | POST /api/overrides | 501 | Returns Not Implemented | | DELETE /api/overrides | 501 | Returns Not Implemented | | POST /api/garmin/tokens | 501 | Returns Not Implemented | @@ -73,6 +73,7 @@ This file is maintained by Ralph. Run `./ralph-sandbox.sh plan 3` to generate ta | `src/app/api/user/route.test.ts` | **EXISTS** - 4 tests (GET profile, auth, sensitive field exclusion) | | `src/app/api/cycle/period/route.test.ts` | **EXISTS** - 8 tests (POST period, auth, validation, date checks) | | `src/app/api/cycle/current/route.test.ts` | **EXISTS** - 10 tests (GET current cycle, auth, all phases, rollover, custom lengths) | +| `src/app/api/today/route.test.ts` | **EXISTS** - 22 tests (daily snapshot, auth, decision, overrides, phases, nutrition, biometrics) | | `src/lib/nutrition.test.ts` | **MISSING** | | `src/lib/email.test.ts` | **MISSING** | | `src/lib/ics.test.ts` | **MISSING** | @@ -176,12 +177,17 @@ Minimum viable product - app can be used for daily decisions. - **Why:** Dashboard needs this for display - **Depends On:** P0.1, P0.2, P1.2 -### P1.4: GET /api/today Implementation -- [ ] Return complete daily snapshot with decision, biometrics, nutrition +### P1.4: GET /api/today Implementation ✅ COMPLETE +- [x] Return complete daily snapshot with decision, biometrics, nutrition - **Files:** - - `src/app/api/today/route.ts` - Implement GET aggregating all data sources + - `src/app/api/today/route.ts` - Implemented GET with `withAuth()` wrapper, aggregates cycle, biometrics, and nutrition - **Tests:** - - `src/app/api/today/route.test.ts` - Test decision computation, data assembly + - `src/app/api/today/route.test.ts` - 22 tests covering auth, validation, decision calculation, overrides, phases, nutrition +- **Response Shape:** + - `decision` (status, reason, icon), `cycleDay`, `phase`, `phaseConfig`, `daysUntilNextPhase`, `cycleLength` + - `biometrics` (hrvStatus, bodyBatteryCurrent, bodyBatteryYesterdayLow, weekIntensityMinutes, phaseLimit) + - `nutrition` (seeds, carbRange, ketoGuidance) +- **Fallback Behavior:** When no DailyLog exists (Garmin not synced), returns defaults: hrvStatus="Unknown", BB=100, weekIntensity=0 - **Why:** This is THE core API for the dashboard - **Depends On:** P0.1, P0.2, P0.3, P1.3 @@ -487,6 +493,7 @@ P2.14 Mini calendar - [x] **GET /api/user** - Returns authenticated user profile, 4 tests (P0.4) - [x] **POST /api/cycle/period** - Logs period start date, updates user, creates PeriodLog, 8 tests (P1.2) - [x] **GET /api/cycle/current** - Returns cycle day, phase, phaseConfig, daysUntilNextPhase, cycleLength, 10 tests (P1.3) +- [x] **GET /api/today** - Returns complete daily snapshot with decision, biometrics, nutrition, 22 tests (P1.4) --- diff --git a/src/app/api/today/route.test.ts b/src/app/api/today/route.test.ts new file mode 100644 index 0000000..86b780b --- /dev/null +++ b/src/app/api/today/route.test.ts @@ -0,0 +1,519 @@ +// ABOUTME: Unit tests for today's daily snapshot API route. +// ABOUTME: Tests GET /api/today for decision, biometrics, and nutrition data. +import type { NextRequest } from "next/server"; +import { NextResponse } from "next/server"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import type { DailyLog, User } from "@/types"; + +// Module-level variable to control mock user in tests +let currentMockUser: User | null = null; + +// Module-level variable to control mock daily log in tests +let currentMockDailyLog: DailyLog | null = null; + +// Mock PocketBase client for database operations +vi.mock("@/lib/pocketbase", () => ({ + createPocketBaseClient: vi.fn(() => ({ + collection: vi.fn(() => ({ + getFirstListItem: vi.fn(async () => { + if (!currentMockDailyLog) { + const error = new Error("No DailyLog found"); + (error as { status?: number }).status = 404; + throw error; + } + return currentMockDailyLog; + }), + })), + })), + loadAuthFromCookies: vi.fn(), + isAuthenticated: vi.fn(() => currentMockUser !== null), + getCurrentUser: vi.fn(() => currentMockUser), +})); + +// Mock the auth-middleware module +vi.mock("@/lib/auth-middleware", () => ({ + withAuth: vi.fn((handler) => { + return async (request: NextRequest) => { + if (!currentMockUser) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + return handler(request, currentMockUser); + }; + }), +})); + +import { GET } from "./route"; + +describe("GET /api/today", () => { + const createMockUser = (overrides: Partial = {}): User => ({ + id: "user123", + email: "test@example.com", + garminConnected: false, + garminOauth1Token: "", + garminOauth2Token: "", + garminTokenExpiresAt: new Date("2025-06-01"), + calendarToken: "cal-secret-token", + lastPeriodDate: new Date("2025-01-01"), + cycleLength: 31, + notificationTime: "07:00", + timezone: "America/New_York", + activeOverrides: [], + created: new Date("2024-01-01"), + updated: new Date("2025-01-10"), + ...overrides, + }); + + const createMockDailyLog = (overrides: Partial = {}): DailyLog => ({ + id: "log123", + user: "user123", + date: new Date("2025-01-10"), + cycleDay: 10, + phase: "FOLLICULAR", + bodyBatteryCurrent: 85, + bodyBatteryYesterdayLow: 40, + hrvStatus: "Balanced", + weekIntensityMinutes: 45, + phaseLimit: 120, + remainingMinutes: 75, + trainingDecision: "TRAIN", + decisionReason: "All systems go", + notificationSentAt: null, + created: new Date("2025-01-10"), + ...overrides, + }); + + const mockRequest = {} as NextRequest; + + beforeEach(() => { + vi.clearAllMocks(); + currentMockUser = null; + currentMockDailyLog = null; + // Mock current date to 2025-01-10 for predictable testing + vi.useFakeTimers(); + vi.setSystemTime(new Date("2025-01-10T12:00:00Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + describe("authentication", () => { + it("returns 401 when not authenticated", async () => { + currentMockUser = null; + + const response = await GET(mockRequest); + + expect(response.status).toBe(401); + const body = await response.json(); + expect(body.error).toBe("Unauthorized"); + }); + }); + + describe("validation", () => { + it("returns 400 when user has no lastPeriodDate", async () => { + currentMockUser = createMockUser({ + lastPeriodDate: null as unknown as Date, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(400); + const body = await response.json(); + expect(body.error).toContain("lastPeriodDate"); + }); + }); + + describe("response structure", () => { + it("returns complete daily snapshot with all required fields", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + // Decision fields + expect(body).toHaveProperty("decision"); + expect(body.decision).toHaveProperty("status"); + expect(body.decision).toHaveProperty("reason"); + expect(body.decision).toHaveProperty("icon"); + + // Cycle fields + expect(body).toHaveProperty("cycleDay"); + expect(body).toHaveProperty("phase"); + expect(body).toHaveProperty("phaseConfig"); + expect(body).toHaveProperty("daysUntilNextPhase"); + expect(body).toHaveProperty("cycleLength"); + + // Biometric fields + expect(body).toHaveProperty("biometrics"); + expect(body.biometrics).toHaveProperty("hrvStatus"); + expect(body.biometrics).toHaveProperty("bodyBatteryCurrent"); + expect(body.biometrics).toHaveProperty("bodyBatteryYesterdayLow"); + expect(body.biometrics).toHaveProperty("weekIntensityMinutes"); + expect(body.biometrics).toHaveProperty("phaseLimit"); + + // Nutrition fields + expect(body).toHaveProperty("nutrition"); + expect(body.nutrition).toHaveProperty("seeds"); + expect(body.nutrition).toHaveProperty("carbRange"); + expect(body.nutrition).toHaveProperty("ketoGuidance"); + }); + }); + + describe("decision calculation", () => { + it("returns TRAIN when all biometrics are good", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 50, + weekIntensityMinutes: 30, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("TRAIN"); + expect(body.decision.icon).toBe("✅"); + }); + + it("returns REST when HRV is unbalanced", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Unbalanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 50, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REST"); + expect(body.decision.reason).toContain("HRV"); + }); + + it("returns REST when body battery was depleted yesterday", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 25, // Below 30 threshold + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REST"); + expect(body.decision.reason).toContain("depleted"); + }); + + it("returns LIGHT when current body battery is low", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 70, // Below 75 threshold + bodyBatteryYesterdayLow: 50, + weekIntensityMinutes: 30, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("LIGHT"); + }); + + it("returns REDUCED when current body battery is medium", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 80, // Between 75 and 85 + bodyBatteryYesterdayLow: 50, + weekIntensityMinutes: 30, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REDUCED"); + }); + + it("returns REST when weekly limit is reached", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 50, + weekIntensityMinutes: 125, // Above 120 limit for FOLLICULAR + phaseLimit: 120, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REST"); + expect(body.decision.reason).toContain("WEEKLY LIMIT"); + }); + }); + + describe("override handling", () => { + it("returns REST with flare override active", async () => { + currentMockUser = createMockUser({ + activeOverrides: ["flare"], + }); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 50, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REST"); + expect(body.decision.reason).toContain("flare"); + }); + + it("returns REST with stress override active", async () => { + currentMockUser = createMockUser({ + activeOverrides: ["stress"], + }); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REST"); + expect(body.decision.reason).toContain("stress"); + }); + + it("prioritizes flare over stress when both active", async () => { + currentMockUser = createMockUser({ + activeOverrides: ["stress", "flare"], + }); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.decision.status).toBe("REST"); + expect(body.decision.reason).toContain("flare"); + }); + }); + + describe("cycle data", () => { + it("returns correct cycle day and phase", async () => { + // lastPeriodDate: 2025-01-01, current: 2025-01-10 = cycle day 10 + currentMockUser = createMockUser({ + lastPeriodDate: new Date("2025-01-01"), + cycleLength: 31, + }); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.cycleDay).toBe(10); + expect(body.phase).toBe("FOLLICULAR"); + expect(body.cycleLength).toBe(31); + }); + + it("returns phase configuration", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.phaseConfig.name).toBe("FOLLICULAR"); + expect(body.phaseConfig.weeklyLimit).toBe(120); + }); + + it("returns days until next phase", async () => { + // Cycle day 10 in FOLLICULAR (days 4-14) + // Next phase (OVULATION) starts day 15, so 5 days away + currentMockUser = createMockUser({ + lastPeriodDate: new Date("2025-01-01"), + }); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.daysUntilNextPhase).toBe(5); + }); + }); + + describe("phase-specific decisions", () => { + it("returns GENTLE during MENSTRUAL phase", async () => { + // Set lastPeriodDate so cycle day = 2 (MENSTRUAL) + currentMockUser = createMockUser({ + lastPeriodDate: new Date("2025-01-09"), // 1 day ago = day 2 + }); + currentMockDailyLog = createMockDailyLog({ + phase: "MENSTRUAL", + cycleDay: 2, + hrvStatus: "Balanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 50, + weekIntensityMinutes: 0, + phaseLimit: 30, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.phase).toBe("MENSTRUAL"); + expect(body.decision.status).toBe("GENTLE"); + }); + + it("returns GENTLE during LATE_LUTEAL phase", async () => { + // Set lastPeriodDate so cycle day = 28 (LATE_LUTEAL) + currentMockUser = createMockUser({ + lastPeriodDate: new Date("2024-12-14"), // 27 days ago = day 28 + }); + currentMockDailyLog = createMockDailyLog({ + phase: "LATE_LUTEAL", + cycleDay: 28, + hrvStatus: "Balanced", + bodyBatteryCurrent: 90, + bodyBatteryYesterdayLow: 50, + weekIntensityMinutes: 0, + phaseLimit: 50, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.phase).toBe("LATE_LUTEAL"); + expect(body.decision.status).toBe("GENTLE"); + }); + }); + + describe("nutrition guidance", () => { + it("returns correct nutrition for cycle day 10 (FOLLICULAR)", async () => { + currentMockUser = createMockUser({ + lastPeriodDate: new Date("2025-01-01"), // cycle day 10 + }); + currentMockDailyLog = createMockDailyLog(); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + // Day 10 is in days 7-14 range per nutrition.ts + expect(body.nutrition.seeds).toBe("Flax (1-2 tbsp) + Pumpkin (1-2 tbsp)"); + expect(body.nutrition.carbRange).toBe("20-100g"); + expect(body.nutrition.ketoGuidance).toBe( + "OPTIONAL - optimal keto window", + ); + }); + + it("returns correct nutrition during luteal phase", async () => { + // Set to cycle day 20 (EARLY_LUTEAL) + currentMockUser = createMockUser({ + lastPeriodDate: new Date("2024-12-22"), // 19 days ago = day 20 + }); + currentMockDailyLog = createMockDailyLog({ + cycleDay: 20, + phase: "EARLY_LUTEAL", + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + // Day 20 is in days 17-24 range (sesame+sunflower) + expect(body.nutrition.seeds).toBe( + "Sesame (1-2 tbsp) + Sunflower (1-2 tbsp)", + ); + expect(body.nutrition.carbRange).toBe("75-125g"); + }); + }); + + describe("biometrics data", () => { + it("returns biometrics from daily log when available", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = createMockDailyLog({ + hrvStatus: "Balanced", + bodyBatteryCurrent: 85, + bodyBatteryYesterdayLow: 40, + weekIntensityMinutes: 45, + phaseLimit: 120, + }); + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + expect(body.biometrics.hrvStatus).toBe("Balanced"); + expect(body.biometrics.bodyBatteryCurrent).toBe(85); + expect(body.biometrics.bodyBatteryYesterdayLow).toBe(40); + expect(body.biometrics.weekIntensityMinutes).toBe(45); + expect(body.biometrics.phaseLimit).toBe(120); + }); + + it("returns default biometrics when no daily log exists", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = null; // No daily log + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + // Defaults when no Garmin data + expect(body.biometrics.hrvStatus).toBe("Unknown"); + expect(body.biometrics.bodyBatteryCurrent).toBe(100); + expect(body.biometrics.bodyBatteryYesterdayLow).toBe(100); + expect(body.biometrics.weekIntensityMinutes).toBe(0); + }); + + it("uses TRAIN decision with default biometrics (no Garmin data)", async () => { + currentMockUser = createMockUser(); + currentMockDailyLog = null; + + const response = await GET(mockRequest); + + expect(response.status).toBe(200); + const body = await response.json(); + + // With defaults (BB=100, HRV=Unknown), should allow training + // unless in restrictive phase + expect(body.decision.status).toBe("TRAIN"); + }); + }); +}); diff --git a/src/app/api/today/route.ts b/src/app/api/today/route.ts index 54d5b1c..51454e7 100644 --- a/src/app/api/today/route.ts +++ b/src/app/api/today/route.ts @@ -2,7 +2,106 @@ // ABOUTME: Returns complete daily snapshot with decision, biometrics, and nutrition. import { NextResponse } from "next/server"; -export async function GET() { - // TODO: Implement today's data retrieval - return NextResponse.json({ message: "Not implemented" }, { status: 501 }); -} +import { withAuth } from "@/lib/auth-middleware"; +import { + getCycleDay, + getPhase, + getPhaseConfig, + getPhaseLimit, + PHASE_CONFIGS, +} from "@/lib/cycle"; +import { getDecisionWithOverrides } from "@/lib/decision-engine"; +import { getNutritionGuidance } from "@/lib/nutrition"; +import { createPocketBaseClient } from "@/lib/pocketbase"; +import type { DailyData, DailyLog } from "@/types"; + +// Default biometrics when no Garmin data is available +const DEFAULT_BIOMETRICS = { + hrvStatus: "Unknown" as const, + bodyBatteryCurrent: 100, + bodyBatteryYesterdayLow: 100, + weekIntensityMinutes: 0, +}; + +export const GET = withAuth(async (_request, user) => { + // Validate required user data + if (!user.lastPeriodDate) { + return NextResponse.json( + { + error: + "User has no lastPeriodDate set. Please log your period start date first.", + }, + { status: 400 }, + ); + } + + // Calculate cycle information + const cycleDay = getCycleDay( + new Date(user.lastPeriodDate), + user.cycleLength, + new Date(), + ); + const phase = getPhase(cycleDay); + const phaseConfig = getPhaseConfig(phase); + const phaseLimit = getPhaseLimit(phase); + + // Calculate days until next phase + const currentPhaseIndex = PHASE_CONFIGS.findIndex((c) => c.name === phase); + const nextPhaseIndex = (currentPhaseIndex + 1) % PHASE_CONFIGS.length; + const nextPhaseStartDay = PHASE_CONFIGS[nextPhaseIndex].days[0]; + + let daysUntilNextPhase: number; + if (nextPhaseIndex === 0) { + // Currently in LATE_LUTEAL, next phase is MENSTRUAL (start of new cycle) + daysUntilNextPhase = user.cycleLength - cycleDay + 1; + } else { + daysUntilNextPhase = nextPhaseStartDay - cycleDay; + } + + // Try to fetch today's DailyLog for biometrics + let biometrics = { ...DEFAULT_BIOMETRICS, phaseLimit }; + try { + const pb = createPocketBaseClient(); + const today = new Date().toISOString().split("T")[0]; + const dailyLog = await pb + .collection("dailyLogs") + .getFirstListItem(`user="${user.id}" && date~"${today}"`); + + biometrics = { + hrvStatus: dailyLog.hrvStatus, + bodyBatteryCurrent: dailyLog.bodyBatteryCurrent, + bodyBatteryYesterdayLow: dailyLog.bodyBatteryYesterdayLow, + weekIntensityMinutes: dailyLog.weekIntensityMinutes, + phaseLimit: dailyLog.phaseLimit, + }; + } catch { + // No daily log found - use defaults + } + + // Build DailyData for decision engine + const dailyData: DailyData = { + hrvStatus: biometrics.hrvStatus, + bbYesterdayLow: biometrics.bodyBatteryYesterdayLow, + phase, + weekIntensity: biometrics.weekIntensityMinutes, + phaseLimit: biometrics.phaseLimit, + bbCurrent: biometrics.bodyBatteryCurrent, + }; + + // Get training decision with override handling + const decision = getDecisionWithOverrides(dailyData, user.activeOverrides); + + // Get nutrition guidance + const nutrition = getNutritionGuidance(cycleDay); + + return NextResponse.json({ + decision, + cycleDay, + phase, + phaseConfig, + daysUntilNextPhase, + cycleLength: user.cycleLength, + biometrics, + nutrition, + }); +});