mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 13:52:20 +00:00
Publish the resolved cycle verdict instead of letting each client work it out
The cycle ring's day count, its phase arcs, and the judgement that a period is late all used to be worked out in web client code, including the fourteen-day grace window that decides when the count stops being an observed fact. Anything else that wanted the same answer had to reproduce that rule, and a second copy of it is how a ring ends up showing someone a cycle day their record does not hold. GET /api/cycle/calendar now carries a verdict: the state, the cycle day, the ring's arcs, the cycle start, how many days until the next period, and when the period is late, how late. It is resolved against the user's own timezone day, which a browser in another zone cannot do. The web wheel, the wellness-strip tile, the insights teaser and the temperature chart all render it, the grace constant is gone from the components directory, and the spec says the field is required. The overdue caption now names the number of days instead of only saying later than usual.
This commit is contained in:
1 parent
6c87c96fa6
commit
dbecbbcbdb
21 files changed
+702
-554
No files matched your search
@@ -191,6 +191,45 @@ const cycleCalendarDayDto = z.object({
|
||||
cervixOpening: cervixOpeningEnum.nullable(),
|
||||
});
|
||||
|
||||
const cycleVerdictDto = z
|
||||
.object({
|
||||
state: z.enum(["IN_CYCLE", "OVERDUE", "INSUFFICIENT_DATA"]),
|
||||
dayOfCycle: z.number().int().nullable().meta({
|
||||
description:
|
||||
"1-based day of the current cycle. Null in OVERDUE and INSUFFICIENT_DATA — beyond the typical length plus the server's grace window the count is no longer an observed fact.",
|
||||
}),
|
||||
cycleLength: z.number().int().nullable().meta({
|
||||
description:
|
||||
"Days the ring represents: the observed labelled run, or the profile-derived idealized cycle for a low-data tracker.",
|
||||
}),
|
||||
phase: cyclePhaseEnumOpenapi.nullable(),
|
||||
spans: z.array(
|
||||
z.object({ phase: cyclePhaseEnumOpenapi, fraction: z.number() }),
|
||||
),
|
||||
cycleStartDate: z.string().nullable().meta({
|
||||
description:
|
||||
"First day of the current cycle (YYYY-MM-DD). Still set while OVERDUE — the last logged period start remains a fact once the count stops.",
|
||||
}),
|
||||
overdueDays: z.number().int().nullable().meta({
|
||||
description:
|
||||
"How many days past the profile's typical cycle length the open cycle has run. Set only in OVERDUE.",
|
||||
}),
|
||||
daysUntilNext: z.number().int().nullable().meta({
|
||||
description:
|
||||
"Days from today to the predicted next period start. Null when no prediction ran and null once that start is in the past.",
|
||||
}),
|
||||
fertileWindow: z.object({
|
||||
start: z.string().nullable(),
|
||||
end: z.string().nullable(),
|
||||
active: z.boolean(),
|
||||
}),
|
||||
})
|
||||
.meta({
|
||||
id: "CycleVerdictDTO",
|
||||
description:
|
||||
"The resolved cycle verdict for the user's own timezone day. Render these values; do not recompute them. The grace window that decides OVERDUE is deliberately not published — read `state` and `overdueDays`.",
|
||||
});
|
||||
|
||||
const cycleProfileDto = z
|
||||
.object({
|
||||
goal: cycleTrackingGoalEnum,
|
||||
@@ -218,6 +257,10 @@ const cycleCalendarResponse = z.object({
|
||||
cyclesObserved: z.number().int(),
|
||||
}),
|
||||
prediction: cyclePredictionDto.nullable(),
|
||||
// The resolved verdict — required, never optional. An optional verdict is a
|
||||
// plausible default that invites a client to fall back to deriving its own,
|
||||
// which is the defect this field exists to remove.
|
||||
verdict: cycleVerdictDto,
|
||||
// Cold-start gate (mirrors `prediction.stillLearning`): true while < 3 cycles
|
||||
// are observed. When set, the `days` grid carries no fertile window, no
|
||||
// predicted-ovulation dot, and no phase band (those would rest on a
|
||||
|
||||
Reference in new issue
Block a user