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:
Marc-André Bombeck committed 2026-08-01 10:37:18 +02:00
1 parent 6c87c96fa6
commit dbecbbcbdb
21 files changed
+702 -554

No files matched your search

+97
View File
@@ -15585,6 +15585,8 @@ components:
anyOf:
- $ref: "#/components/schemas/CyclePredictionDTO"
- type: "null"
verdict:
$ref: "#/components/schemas/CycleVerdictDTO"
stillLearning:
type: boolean
days:
@@ -15697,6 +15699,7 @@ components:
required:
- profile
- prediction
- verdict
- stillLearning
- days
- meta
@@ -15771,6 +15774,100 @@ components:
- SYMPTOTHERMAL
- TEMPERATURE_TREND
- BLENDED
CycleVerdictDTO:
type: object
properties:
state:
type: string
enum:
- IN_CYCLE
- OVERDUE
- INSUFFICIENT_DATA
dayOfCycle:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
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:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: "Days the ring represents: the observed labelled run, or the profile-derived idealized cycle for a low-data
tracker."
phase:
anyOf:
- $ref: "#/components/schemas/CyclePhase"
- type: "null"
spans:
type: array
items:
type: object
properties:
phase:
$ref: "#/components/schemas/CyclePhase"
fraction:
type: number
required:
- phase
- fraction
additionalProperties: false
cycleStartDate:
anyOf:
- type: string
- type: "null"
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:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
description: How many days past the profile's typical cycle length the open cycle has run. Set only in OVERDUE.
daysUntilNext:
anyOf:
- type: integer
minimum: -9007199254740991
maximum: 9007199254740991
- type: "null"
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:
type: object
properties:
start:
anyOf:
- type: string
- type: "null"
end:
anyOf:
- type: string
- type: "null"
active:
type: boolean
required:
- start
- end
- active
additionalProperties: false
required:
- state
- dayOfCycle
- cycleLength
- phase
- spans
- cycleStartDate
- overdueDays
- daysUntilNext
- fertileWindow
additionalProperties: false
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`.
CyclePhase:
type: string
enum: