Implement GET /api/today endpoint (P1.4)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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<DailyLog>(`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,
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user