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>
This commit is contained in:
2026-01-11 22:21:52 +00:00
parent c708c2ed8b
commit 58f6c5605a
8 changed files with 442 additions and 45 deletions

View File

@@ -65,15 +65,25 @@ export const POST = withAuth(async (request: NextRequest, user) => {
const pb = createPocketBaseClient();
// Calculate predicted date based on previous cycle (if exists)
let predictedDateStr: string | null = null;
if (user.lastPeriodDate) {
const previousPeriod = new Date(user.lastPeriodDate);
const predictedDate = new Date(previousPeriod);
predictedDate.setDate(previousPeriod.getDate() + user.cycleLength);
predictedDateStr = predictedDate.toISOString().split("T")[0];
}
// Update user's lastPeriodDate
await pb.collection("users").update(user.id, {
lastPeriodDate: body.startDate,
});
// Create PeriodLog record
// Create PeriodLog record with prediction data
await pb.collection("period_logs").create({
user: user.id,
startDate: body.startDate,
predictedDate: predictedDateStr,
});
// Calculate updated cycle information
@@ -81,6 +91,22 @@ export const POST = withAuth(async (request: NextRequest, user) => {
const cycleDay = getCycleDay(lastPeriodDate, user.cycleLength, new Date());
const phase = getPhase(cycleDay);
// Calculate prediction accuracy
let daysEarly: number | undefined;
let daysLate: number | undefined;
if (predictedDateStr) {
const actual = new Date(body.startDate);
const predicted = new Date(predictedDateStr);
const diffDays = Math.floor(
(predicted.getTime() - actual.getTime()) / (1000 * 60 * 60 * 24),
);
if (diffDays > 0) {
daysEarly = diffDays;
} else if (diffDays < 0) {
daysLate = Math.abs(diffDays);
}
}
// Log successful period logging per observability spec
logger.info({ userId: user.id, date: body.startDate }, "Period logged");
@@ -89,6 +115,9 @@ export const POST = withAuth(async (request: NextRequest, user) => {
lastPeriodDate: body.startDate,
cycleDay,
phase,
...(predictedDateStr && { predictedDate: predictedDateStr }),
...(daysEarly !== undefined && { daysEarly }),
...(daysLate !== undefined && { daysLate }),
});
} catch (error) {
logger.error({ err: error, userId: user.id }, "Period logging error");