Commit Graph
4 Commits
Author SHA1 Message Date
Marc-André Bombeck b77dbea6b1 fix(cycle): reopen a re-tapped start that has nothing after it
The earlier rule could leave a one-day cycle in front of a real start
when day two of a period was logged before day one. Tapping "My period
started this day" on the real first day folds the later start in, but
the re-tapped row kept the end date and length its vanished successor
had given it, so the repaired cycle stayed closed after one day.

A start with no cycle after it is open, so opening one now clears a
stale end and length. The repair of such a history is covered end to
end: the broken state is seeded as the old rule left it, the one tap
is made through the period route, and the result is a single open
cycle that owns every day of the period.
2026-09-24 08:11:14 +02:00
Marc-André Bombeck 7d25bb7352 fix(cycle): log past dates as themselves, in any order (#1004)
The log sheet took its "Day N" header and its "period ended" button
from the verdict, which is the answer about today. A January date opened
in September read "Day 2", and a back-dated period could only be closed
on dates where today happened to be a bleeding day, so the button came
and went as unrelated months were filled in.

The calendar read now carries, per date, the cycle day of the logged
cycle that date belongs to (null before the first start, after today, or
past the verdict's ceiling for an open cycle) and whether a one-tap
period end can land there. The sheet reads both off the grid day for the
date it opened on.

Saving a new entry with nothing picked used to write an empty row: the
request succeeded, the sheet closed and the calendar had nothing to
draw, which read as a save that did nothing. Save now needs something to
store and says what, and an emptied existing entry still saves. The
sheet also stops writing until the opened date's own entry has loaded,
because until then it held the previous date's row id and a quick Save
patched the wrong day.

On the server, a start recorded a few days after an earlier one is the
same period entered in the other order: opening the earlier start now
folds the later one into it instead of leaving a one-day cycle in front
of it, and a bleed too far before the next start to be its first day and
too close to be a cycle of its own no longer infers a start. Opening or
removing a start re-attributes the day-logs whose cycle changed.
2026-09-24 08:02:00 +02:00
Marc-André Bombeck 0849d59ecf test(cycle): pin the fixture clock to the suite's clock
Two cycle suites compute their relative day keys with local Date arithmetic
under TZ=UTC while the routes resolve today in the user's zone, falling back
to Europe/Berlin when the fixture sets none. For two hours every evening the
two clocks disagree about the date and every day-of-cycle assertion is off
by one — the suite was red from 22:00 UTC to midnight and green the rest of
the day. The fixtures now carry an explicit UTC timezone so the test's clock
and the route's clock are the same clock at any hour. Reproduced red at
23:07 UTC, green with the pin at 23:19 UTC.
2026-08-08 01:02:08 +02:00
Marc-André Bombeck 3596720905 fix(cycle): let a period logged as flow count as a period
MenstrualCycle rows were only ever written by the one-tap period
boundary. Every other way of recording bleeding wrote a day-log and
stopped there: the log sheet's flow chips, the bulk drain, an Apple
Health export full of menstrual-flow samples. The engine reads cycle
rows, so a person could log every period they had and the module still
held that none had happened. No cycle length, no forecast, an empty
chart, and an offer to log a first period. An Apple Health user who
never found the one-tap button had the whole module dead.

A first bleeding day now opens the cycle it belongs to, through one
shared seam in the day-log write helper, so the single capture, the
bulk drain and the Apple Health fold all infer a start the same way and
the one-tap boundary's own write lands on a date that already has a
cycle, which the rule declines.

The rule is conservative, because a false start corrupts every length
derived after it. Spotting does not open a cycle: it is as often the
tail or the herald of a period as its first day. Bleeding flagged as
between periods does not, since that flag exists to say so. Neither does
a bleeding day within the hard physiological minimum of an existing
start, which is what keeps day three of a period, or a forgotten day
two, from starting a cycle of its own without needing a contiguity
check. A genuinely short cycle is not inferred from flow alone; the
one-tap start still opens one on any date.

An edit is symmetric. Setting a flow opens the cycle; clearing it, or
flagging it as between periods, removes the cycle that day opened and
re-opens the one before it. A cycle that an edit can create and not undo
leaves the record holding a period nothing on the day says happened.

- the period route's start arm and the flow inference now share one
  boundary helper, so a start means the same thing however it is entered
- back-dating re-anchors both neighbours, as the one-tap start does
- integration: eleven cases through the real routes, of which five are
  about what must NOT open a cycle
2026-08-07 15:50:23 +02:00