// ABOUTME: API route for today's training decision and data. // ABOUTME: Returns complete daily snapshot with decision, biometrics, and nutrition. import { NextResponse } from "next/server"; import { withAuth } from "@/lib/auth-middleware"; import { getCycleDay, getPhase, getPhaseConfig, getPhaseLimit, } from "@/lib/cycle"; import { getDecisionWithOverrides } from "@/lib/decision-engine"; import { logger } from "@/lib/logger"; import { getNutritionGuidance, getSeedSwitchAlert } from "@/lib/nutrition"; import type { DailyData, DailyLog, HrvStatus } from "@/types"; // Default biometrics when no Garmin data is available const DEFAULT_BIOMETRICS: { hrvStatus: HrvStatus; bodyBatteryCurrent: number; bodyBatteryYesterdayLow: number; weekIntensityMinutes: number; } = { hrvStatus: "Unknown", bodyBatteryCurrent: 100, bodyBatteryYesterdayLow: 100, weekIntensityMinutes: 0, }; export const GET = withAuth(async (_request, user, pb) => { // Fetch fresh user data from database to get latest values // The user param from withAuth is from auth store cache which may be stale // (e.g., after logging a period, the cookie still has old data) const freshUser = await pb.collection("users").getOne(user.id); // Validate required user data if (!freshUser.lastPeriodDate) { return NextResponse.json( { error: "User has no lastPeriodDate set. Please log your period start date first.", }, { status: 400 }, ); } const lastPeriodDate = new Date(freshUser.lastPeriodDate as string); const cycleLength = freshUser.cycleLength as number; const activeOverrides = (freshUser.activeOverrides as string[]) || []; // Calculate cycle information const cycleDay = getCycleDay(lastPeriodDate, cycleLength, new Date()); const phase = getPhase(cycleDay, cycleLength); const phaseConfig = getPhaseConfig(phase); const phaseLimit = getPhaseLimit(phase); // Calculate days until next phase using dynamic boundaries // Phase boundaries: MENSTRUAL 1-3, FOLLICULAR 4-(cl-16), OVULATION (cl-15)-(cl-14), // EARLY_LUTEAL (cl-13)-(cl-7), LATE_LUTEAL (cl-6)-cl let daysUntilNextPhase: number; if (phase === "LATE_LUTEAL") { daysUntilNextPhase = cycleLength - cycleDay + 1; } else if (phase === "MENSTRUAL") { daysUntilNextPhase = 4 - cycleDay; } else if (phase === "FOLLICULAR") { daysUntilNextPhase = cycleLength - 15 - cycleDay; } else if (phase === "OVULATION") { daysUntilNextPhase = cycleLength - 13 - cycleDay; } else { // EARLY_LUTEAL daysUntilNextPhase = cycleLength - 6 - cycleDay; } // Try to fetch today's DailyLog for biometrics // Sort by date DESC to get the most recent record if multiple exist let biometrics = { ...DEFAULT_BIOMETRICS, phaseLimit }; try { // Use YYYY-MM-DD format with >= and < operators for PocketBase date field // PocketBase accepts simple date strings in comparison operators const today = new Date().toISOString().split("T")[0]; const tomorrow = new Date(Date.now() + 86400000) .toISOString() .split("T")[0]; logger.info({ userId: user.id, today, tomorrow }, "Fetching dailyLog"); const dailyLog = await pb .collection("dailyLogs") .getFirstListItem( `user="${user.id}" && date>="${today}" && date<"${tomorrow}"`, { sort: "-date" }, ); logger.info( { userId: user.id, dailyLogId: dailyLog.id, hrvStatus: dailyLog.hrvStatus, bodyBatteryCurrent: dailyLog.bodyBatteryCurrent, }, "Found dailyLog", ); biometrics = { hrvStatus: dailyLog.hrvStatus, bodyBatteryCurrent: dailyLog.bodyBatteryCurrent ?? DEFAULT_BIOMETRICS.bodyBatteryCurrent, bodyBatteryYesterdayLow: dailyLog.bodyBatteryYesterdayLow ?? DEFAULT_BIOMETRICS.bodyBatteryYesterdayLow, weekIntensityMinutes: dailyLog.weekIntensityMinutes, phaseLimit: dailyLog.phaseLimit, }; } catch (err) { logger.warn({ userId: user.id, err }, "No dailyLog found, using 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, activeOverrides as import("@/types").OverrideType[], ); // Log decision calculation per observability spec logger.info( { userId: user.id, decision: decision.status, reason: decision.reason }, "Decision calculated", ); // Get nutrition guidance with seed switch alert const baseNutrition = getNutritionGuidance(cycleDay); const nutrition = { ...baseNutrition, seedSwitchAlert: getSeedSwitchAlert(cycleDay), }; return NextResponse.json({ decision, cycleDay, phase, phaseConfig, daysUntilNextPhase, cycleLength, biometrics, nutrition, }); });