Six page gates read `inSharedRecord || user?.modules?.X`, with a comment saying
the delegate's own module map does not govern the owner's record. That was true
when it was written and stopped being true in v1.38.14: `GET /api/auth/me` now
publishes the RECORD's map, masked to what the grant opens, so the prefix that
protected against reading the wrong map now widens the gate instead. The
navigation drops the entry correctly while a typed URL still renders the page
for a record whose owner switched the module off, and `/api/vaccinations`
carries no module gate of its own, so it renders and works.
Deleting the prefix answers both cases from the one map, which is what it is
for. A structural guard holds the six expressions: it locates each gate, proves
it is a module gate, and then asserts it does not widen on the sharing
question. It finds nothing to assert on if the expression moves, and says so,
rather than passing on an empty match. All six legs fail against the previous
gates.
Introduce a shared PageAuthGate (header + tile skeleton) and adopt it on the
top-level pages that flashed a lone centered spinner while auth resolved:
mood, measurements, achievements, illness, mental-wellbeing, checkups, cycle,
and the labs / medications / custom-metrics / illness detail routes. The
auth-resolving frame now reserves the page geometry instead of snapping in
after a spinner.
The insights cycle ring tile and the /cycle page redirect gated on
user.cycleTrackingEnabled, which /api/auth/me resolves without the
operator availability layer — so with the Cycle module disabled
server-wide the ring still rendered (and fired its calendar read) and a
direct /cycle URL still loaded.
Read user.modules.cycle instead, which the /me map already AND-s with the
operator switch, so an operator-off instance suppresses both surfaces.
Add a compact cycle teaser to the main Insights overview for accounts with
cycle tracking enabled. The card shows the current phase, cycle day, and the
single headline phase finding, and deep-links into the full cycle insights tab
so the cycle tab stays the single source of truth.
The card mounts only when cycleTrackingEnabled resolves true from
/api/auth/me — the same gate the sidebar cycle entry uses — and renders nothing
for every other account. Phase and cycle day derive from the same calendar read
plus deriveWheelState the cycle wheel uses, and the headline reuses the shared
CyclePhaseHeadline, including its honest empty line when nothing has cleared the
significance check.
The cycle view now opens the insights tab when the URL carries ?tab=insights,
validated against the known tab values; CycleView is wrapped in a Suspense
boundary for the search-params read.
Build the web UI for the cycle-tracking category against the existing
`/api/cycle/*` routes: a month calendar + circular phase wheel, a fast
log-day sheet, a predictions panel, phase-insight cards, and a single-page
settings form, all under a gated `/cycle` tab.
- Calendar + wheel: a hand-rolled SVG phase ring (0 KB runtime, matching the
wellness-ring aesthetic with calm per-phase hues) showing the current
phase + day-of-cycle, alongside a month grid where logged period days show
a filled pip, the predicted next period renders as a ranged band (never a
single date), the fertile window shows only when the goal allows it
(the API suppresses it server-side otherwise), and ovulation + per-day
symptoms carry markers. Reads `GET /api/cycle/calendar`.
- Log-day sheet: one-tap period start, flow / symptom / ovulation-test /
mucus chips, basal-temperature, intercourse + protection, and a free-text
note. Optimistic write to the day-log + period routes; invalidates the
cycle query keys.
- Predictions panel: next-period range with a confidence pill, goal-gated
fertile window, a still-learning state for fewer than three cycles or
raw-chart mode, cycle-history stats, and the fixed non-medical disclaimer
(reusing the drug-level-estimate disclaimer pattern).
- Insights cards render whatever phase-correlation cards the API surfaces,
with a graceful empty-state until the feed lands.
- Settings: a single dynamic form where the inclusive goal selector drives
the copy (conception framing only for trying-to-conceive), plus typical-
length priors, raw-chart mode, sensitive-category encryption, discreet
notifications, and export / delete. PATCHes `/api/auth/me/cycle-prefs`.
- Nav: a Cycle entry in the sidebar and the mobile More hub, gated on
`cycleTrackingEnabled` from `/api/auth/me` and hidden otherwise.
Add the cycle query keys + dependent-key bundle to the factory, full copy
across all six locales, and dedicated per-theme phase hues. Plain
second-person voice, WCAG 2.1 AA, no new dependencies.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>