feat(cycle): choose how the calendar feed draws a forecast (#1350)

A forecast period was one all-day event over every day it might fall on,
so a period of a few days showed up as a week-long estimate next to the
logged ones. Each feed now carries a forecast style:

- EXACT: the calculated period only, from the most likely start for the
  predicted number of bleeding days.
- MARGIN: that event, plus separate "possible" events for the days it may
  start earlier and the days it may still run on, each only where the
  band reaches that side. Discreet titles use an open dot for them.
- BAND: the single range, unchanged.

Migration 0495 adds the column with BAND as its default, so every live
feed keeps the events it serves. A feed turned on from now on starts on
MARGIN, set by the mint route. PATCH changes it, rotate copies it, the
status and the audit diff carry it, and the settings card offers it with
a one-line explanation of the chosen style. The horizon and its stop rule
are the same in every style, and the period event keeps its UID across
styles so a calendar updates it in place.
This commit is contained in:
Marc-André Bombeck committed 2026-11-09 10:59:18 +01:00
1 parent f86a7a61e1
commit 546b9b2569
30 files changed
+647 -36

No files matched your search

+22 -4
View File
@@ -874,7 +874,7 @@ paths:
summary: Turn the calendar subscription on (#1350)
description: "Mints an `hlf_` token (192-bit), stores only its HMAC, and returns the URL EXACTLY ONCE. One live
subscription per record: a second answers 409 `cycle.feed.exists` (rotate instead). Defaults: three cycles
ahead, no history, no fertile window, discreet titles. Rate-limited per acting account
ahead, no history, no fertile window, discreet titles, `MARGIN` forecasts. Rate-limited per acting account
(`cycle-feed:mint:<actorId>`, 10/h, shared with rotate)."
responses:
"201": &a13
@@ -957,9 +957,9 @@ paths:
tags:
- Cycle
summary: Replace the calendar subscription's link (#1350)
description: "One transaction: the live link is revoked and a new one minted with the same options. Returns the new URL
EXACTLY ONCE. Calendars using the old link stop updating. Of two rotations racing, the one that loses answers
409 `cycle.feed.exists` and the winner's link stands."
description: "One transaction: the live link is revoked and a new one minted with the same options, the forecast style
included. Returns the new URL EXACTLY ONCE. Calendars using the old link stop updating. Of two rotations racing,
the one that loses answers 409 `cycle.feed.exists` and the winner's link stands."
responses:
"201": *a13
"401": *a14
@@ -28732,6 +28732,12 @@ components:
enum:
- DISCREET
- DESCRIPTIVE
forecastStyle:
type: string
enum:
- EXACT
- MARGIN
- BAND
additionalProperties: false
description: "The writable options. At least one field. `includeFertile: true` is refused (422
`cycle.feed.fertile_goal`) unless the goal is TRYING_TO_CONCEIVE."
@@ -41370,6 +41376,17 @@ components:
enum:
- DISCREET
- DESCRIPTIVE
forecastStyle:
type: string
enum:
- EXACT
- MARGIN
- BAND
description: "How a forecast period is drawn. `EXACT`: one event over the predicted bleeding days from the most likely
start. `MARGIN`: that event, plus a separate event for the days before it the period may start on and one
for the days after it may still run on (each only when the band reaches that side). `BAND`: one event from
the earliest likely start through the last bleeding day of the latest likely start. A subscription turned on
before the option existed is `BAND`."
required:
- tokenPrefix
- createdAt
@@ -41380,6 +41397,7 @@ components:
- historyCycles
- includeFertile
- titleMode
- forecastStyle
additionalProperties: false
description: "The live calendar subscription, without its secret: never the token hash and never the URL."
CycleFeedMinted: