Commit Graph
42 Commits
Author SHA1 Message Date
Marc-André Bombeck 0f2b548138 style: apply prettier 3.9.1 formatting
Mechanical reflow from the prettier 3.9.1 bump; no behaviour change.
2026-06-28 19:26:16 +02:00
Marc-André Bombeck b02e3e4072 fix(mobile): close safe-area gaps and lift sub-44px tap targets
Subtract both the top and bottom safe-area insets from the full-page
Coach height so the composer no longer slides behind the bottom-nav on a
notched phone, and pad the Coach drawer composer and the ResponsiveSheet
bottom-sheet footer for the home-indicator band.

Raise the cycle and illness day-log severity controls to a 44px touch
floor, add hit-slop to the custom-symptom remove button, give the shared
day-log chips a min height, and let the mood-tag action cluster wrap so a
360px row no longer clips the tag name.
2026-06-28 02:53:06 +02:00
Marc-André Bombeck 75129c798d refine(cycle): consolidate disclaimer, align columns, unify tiles
Move the non-medical disclaimer to the cycle-tracking on-ramp and drop it
from the repeating in-feature surfaces (calendar tab, predictions panel);
delete the now-unused disclaimer component. Align the left ring column with
the tab content rather than the tab strip. Raise the settings subheading
sizes to the standard card-description scale. Reflow the reminder rows so a
wrapping label no longer collides with its button at narrow widths. Unify
the wheel and phase-education tile padding. Cite the symptothermal
double-check primary source under the second fertility sign.
2026-06-21 10:57:16 +02:00
Marc-André Bombeck 1f4ba5939d style: apply prettier across the tree
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
2026-06-19 22:20:31 +02:00
Marc-André Bombeck d2be72d57a feat(mobile): add scroll-touch momentum utility for horizontal strips
Add a `.scroll-touch` utility that restores iOS WebKit momentum
scrolling (`-webkit-overflow-scrolling: touch`) and contains the
overscroll chain so a flick at the end of a horizontal row does not
rubber-band the whole page. Apply it to the insights tab strip and the
cycle tab list — the two in-app horizontal scrollers that lost inertial
flick under the default `overflow-x-auto`.
2026-06-19 21:18:05 +02:00
Marc-André Bombeck 3ba542f797 fix(onboarding): make the guided tour first-time-only, drop every re-entry trigger
The guided tour now exists only as the first-time auto-start after onboarding.
Remove every in-app trigger that re-opened it: the per-module "show this tour"
button on the dashboard, measurements, mood, medications, labs, illness, cycle,
achievements, insights, and coach surfaces; and the Settings replay card.

Delete the now-orphaned ModuleTourTrigger component, the tour-restart worker,
the healthlog:module-tour and healthlog:tour-restart event listeners, the
single-stop overlay path, and the force-launch session marker. The first-time
auto-launch gate and the tour overlay itself are unchanged. Prune the orphaned
i18n keys across all locales and update the affected tests.
2026-06-18 23:58:27 +02:00
Marc-André Bombeck 33fec28b29 feat(onboarding): per-module tour re-entry + consolidated replay
Add a 'Diese Tour zeigen' affordance to every module header that opens the
tour narrowed to that module's card in place, plus additive data-tour
anchors (measurements/medications/labs/illness/vorsorge/cycle/mood/
insights/coach/integrations/export/achievements) so the cross-page walk
has a real target on each page. Consolidate the full-tour replay onto a
single Settings -> Advanced card, relabel it, and drop the duplicate from
About. The replay reset clears the stored progress so it restarts cleanly.
2026-06-18 19:49:37 +02:00
Marc-André Bombeck f3388ceb70 feat(cycle): redesign the log-day sheet — phase header, field info, wider dialog
Anchor the capture surface to the ring and lower its cognitive load:

- A hue-tinted phase-context strip (day-of-cycle + phase) at the top of the
  sheet, on the wellness-tile surface, honesty-gated the same way the phase-
  education card is — no phase claim until prediction is on, not raw-chart,
  and at least three cycles observed; otherwise a neutral day header.
- Inline field explainers (FieldInfo) on the clinical fields: BBT, cervical
  mucus, ovulation test, spotting, pregnancy/progesterone test, intercourse,
  protection — factual, descriptive-only, reachable by keyboard and screen
  reader.
- BBT is now the shadcn Input rather than a bare element, with a morning-
  timing hint, so it matches every other control.
- The symptothermal section (Temperature & ovulation) auto-expands for
  fertility goals and for an edited day that already holds a fertility sign,
  so a pre-filled value is never hidden behind a collapsed section.
- Desktop dialog widened to lg so the form breathes.

The payload builders and the form-hydration reconciliation are unchanged, so
the wire contract and their unit tests are untouched.
2026-06-16 21:32:58 +02:00
Marc-André Bombeck eb40f9b46f feat(cycle): field-explainer tooltip + log-sheet i18n scaffold
Add a cycle-scoped FieldInfo affordance (a focusable, SR-reachable info
button with a 44 px touch target, mirroring the mood-explainer precedent)
and the i18n keys the redesigned log-day sheet needs: a cycle.fieldInfo.*
namespace of descriptive-only explainers (BBT, cervical mucus, ovulation
test, spotting, pregnancy/progesterone test, intercourse, protection) plus
cycle.sheet.phaseDayHeader and cycle.sheet.bbtHint, across all six locales.
2026-06-16 21:32:48 +02:00
Marc-André Bombeck 3bbc0016d9 fix(cycle): restore the wellness-tile entrance so the ring renders
The wheel tile, phase-education card, and dashboard cycle tile each set
both data-revealed and .wellness-tile-rise on the same node. The keyframe
selector is [data-revealed="true"] .wellness-tile-rise (descendant
combinator), so it never matched a same-node pair — the tiles stayed at
the rise rule's opacity: 0, invisible but still boxed. Lift data-revealed
onto a contents wrapper (the documented pattern from the cycle insight
summary card) so the entrance plays and the signature ring paints.
2026-06-16 16:52:03 +02:00
Marc-André Bombeck 3f3b8cbf0c fix(ui): unify the add-entry pattern across feature pages and add the Vorsorge heading 2026-06-16 02:55:27 +02:00
Marc-André Bombeck 5b67dc636d feat(cycle): sectioned log-day sheet with summary-badge disclosure 2026-06-14 18:57:38 +02:00
Marc-André Bombeck a614ca78c6 feat(cycle): deepen phase-education copy with a second context line
Add a per-phase "context" line under cycle.phaseEducation.context.* that
expands the existing descriptive-only education with what someone might
notice — energy, sleep, basal temperature, mood — and a gentle framing of
normal variation. The original whatsHappening lines are kept verbatim; this
sits below them as a second, deeper paragraph in the same warm,
medically-grounded, non-prescriptive register.

Wire the new leaf through phase-copy.ts (PHASE_CONTEXT_KEY) and render it in
PhaseEducationCard, and add the copy across all six locales.
2026-06-14 15:42:58 +02:00
Marc-André Bombeck e832222402 feat(cycle): surface the cervix secondary-symptom choice in the UI
- Advanced cycle settings gains a "secondary fertility sign" selector
  (cervical mucus default | cervix observation). It sits below the priors
  in the advanced settings so it never forces itself on a casual user.
- The log-day sheet shows the cervix entry — position / firmness / opening
  chips — only when the user picked cervix observation; otherwise the
  cervical-mucus picker stays as before. Progressive disclosure: exactly
  one secondary sign is offered at a time so the sheet never overwhelms.
- Web client types, the cyclePrefs patch, and the day-log payload builders
  carry the new fields; copy added across all six locales.
2026-06-14 15:33:11 +02:00
Marc-André Bombeck 4bfe9f11d7 feat(cycle): exclude disturbed temperatures from the symptothermal engine
A fever, illness, alcohol or a late/short-sleep reading can fake or mask
a temperature rise. Add a per-day `temperatureExcluded` flag on
CycleDayLog (migration 0156): when set, the engine drops the reading from
the cover-line baseline AND the shift evaluation, so a spike neither
fabricates nor masks a rise (Sensiplan: the cover line is drawn over the
last six unbracketed values).

Wired end to end — schema + Prisma, the engine's `detectTempShift`
filter, the Zod input/patch schemas, the day-log upsert (partial merge +
change detection), the read DTO, the calendar/insights/coach selects, the
backup round-trip, and the OpenAPI contract (a new boolean on the day-log
and calendar-day DTOs for the iOS client). The log-day sheet gains a
"mark reading as disturbed" toggle that appears once a temperature is
entered, translated across all six locales.

Citation: de.wikipedia "Symptothermale Methode"; myNFP "Temperaturkurve
auswerten" — Störfaktor bracketing.
2026-06-14 15:04:25 +02:00
Marc-André Bombeck f5798e20d8 fix(cycle): gate the calendar grid to a learning state during cold start
The predictions panel suppressed its fertile-window and ovulation rows while
fewer than three cycles were observed, but the month calendar did not — so a
brand-new user who logged a single period saw the grid paint a confident
fertile-window shading, a predicted-ovulation dot, and full phase bands off a
population 28/14 prior at ~0.20 confidence. Calendar-based fertility estimation
is unreliable below several recorded cycles, so the grid was asserting days it
could not know.

Thread the same still-learning gate into the calendar build: while fewer than
three cycles are observed, suppress the fertile window, the ovulation dot, and
the phase band (the predicted-period bar stays, mirroring the panel). Surface a
top-level stillLearning flag on the calendar response so the client renders a
calm "learning your cycle" banner over the grid instead of a bare, falsely
confident calendar. The flag is additive and back-compatible; older clients
ignore it and degrade to the previous render.

Also pin the observed-cycle count to a single source — the engine's
post-exclusion value — so the response never carries two divergent counts.
2026-06-14 14:43:50 +02:00
Marc-André Bombeck 3b480b9885 perf(i18n,shell): ship one locale and stop blocking the first paint
The client bundle carries only the active locale — resolved on the
server and handed to the provider with no hydration flash, halving the
dashboard route to under a megabyte parsed (288 KiB gzip). The auth
shell renders the app chrome immediately instead of a spinner while the
session resolves in parallel, the widgets request folds into the
snapshot it duplicated, and insight pills prefetch their sub-pages —
a first visit to a sub-page lands in about ninety milliseconds instead
of nearly a second.
2026-06-11 02:19:27 +02:00
Marc-André Bombeck 04765ea8ac style: one spacing, title and skeleton language across the app
Icons inside buttons drop their manual margins in favour of the
button's built-in gap (two hundred call sites), form field stacks settle
on one spacing step, card titles on exactly two sizes, inline field
errors on one text size, and a dozen hand-rolled loading pulses move
onto the shared skeleton primitive while keeping their silhouettes. The
intake dialog wording joins the rest of the app on 'übersprungen', and
the UI guidelines now document the two deliberate footer and
touch-target systems.
2026-06-10 16:54:15 +02:00
Marc-André Bombeck bf77f5243d Fix cycle ring width, tab strip and calendar tap targets on narrow phones
- Cycle ring: the fixed 120 px shrink-0 ring overflowed grid cells on
  360 px Android widths. It now caps at its target size but is free to
  shrink to fill its cell, so it never pushes the page wider than the
  viewport.
- Cycle tab strip: the four long German labels overflowed the equal-width
  flex-1 cells and forced a horizontal scroll at 375 px. Below sm the
  triggers size to their content (text-xs, no flex-1) and scroll cleanly;
  from sm up they reclaim the equal-width layout.
- Calendar day cells: bump the 40 px min-height to 44 px to clear the
  mobile tap-target floor.
- Add a Pixel-5 spec asserting no horizontal scroll on /insights and
  /cycle plus the calendar tap-target floor.

The cycle view also now feeds the profile's typical lengths into the
wheel so a low-data tracker gets the canonical four-phase ring.
2026-06-07 14:15:47 +02:00
Marc-André Bombeck cd9a0b6152 Draw the canonical four-phase cycle ring for low-data trackers
When only a few cycle days are logged, the labelled forward run was short
enough that normalising it filled the whole ring with a single phase. For
a tracker below seven labelled days, derive the four-phase proportions
from the profile's typical lengths instead (period / luteal / cycle
length, textbook 28/5/14 defaults), so the dial shows the familiar
MENSTRUAL/FOLLICULAR/OVULATORY/LUTEAL arcs. The current phase and the
day-of-cycle marker stay driven by the observed labels.

The ring tile reads the shared, 5-min-cached cycle profile to source the
typical lengths (no extra request). Well-populated cycles keep the
observed-share spans unchanged.
2026-06-07 14:15:39 +02:00
Marc-André Bombeck 6abc178c88 Even out the Insights overview inter-section spacing
The overview is a vertical stack on a single space-y-6 rhythm, but two
seams read wrong: Trends to period-in-review felt too wide while the
cycle summary to signals-of-the-day seam had no gap at all. The missing
gap traced to the cycle-summary card wrapping itself in a display:contents
box, which collapsed its margin so space-y skipped it.

- Give the cycle summary a real section wrapper (heading above the card,
  matching every other section) so the inter-section margin applies.
- Document space-y-6 as the single gap, top to bottom; every section is
  a real section or renders null, never a contents wrapper that strands
  a phantom margin.
2026-06-07 14:15:22 +02:00
Marc-André Bombeck fcf1366fea feat(insights): cycle ring replaces strain tile, tidy overview spacing, nav order, password-reset CLI
When cycle tracking is on, the cycle ring takes the Strain slot in the
health-scores strip rather than adding a sixth tile: the wellness strip
gains a hideStrain flag the insights page sets from the cycle gate, so
Strain is hidden for cycle accounts and shown unchanged for everyone else.

Fix the oversized vertical gap on the insights overview. The cycle summary
card set data-revealed on the same node that carried wellness-tile-rise,
but the entrance keyframe selector is a descendant combinator, so the
animation never matched and the card stayed at the rise rule's opacity: 0 —
invisible yet still occupying its box. Wrap the card so data-revealed sits
on an ancestor, restoring the entrance and removing the phantom gap.

Move the gated cycle entry directly after Medications (before Insights) in
the sidebar nav instead of appending it at the end, spliced by the
Medications index so the position survives a list reorder.

Add an operator-only password-reset CLI. It runs under plain node in the
production standalone image (which strips tsx) via the in-image pg and
@node-rs/argon2, reusing the app's Argon2id parameters through a shared
.mjs module so a reset hash matches one the app would mint. It matches a
single user by username or email, reads the new password without echoing,
never prints it back, and exits non-zero on zero or multiple matches.
Documented in docs/ops/password-reset.md.
2026-06-06 21:31:12 +02:00
Marc-André Bombeck a9f57af602 feat(insights): cycle ring in the wellness strip, bound mood/medication calendar height
Surface the cycle phase ring in the main Insights health-scores strip as a
gated sibling tile. It reuses the cycle wheel SVG, sized to a score-ring
tile, and sources day-of-cycle + phase from the same calendar read +
deriveWheelState the cycle page wheel uses, so the two surfaces never
disagree. Mounted only for a cycle-tracking account (the same /api/auth/me
gate the sidebar nav and the cycle summary card use), so its calendar read
never fires otherwise; it renders nothing while resolving, on error, and
when there is no active cycle — no gap, no placeholder.

Cap the adaptive cell size in the mood and medication compliance heatmaps.
A short window (a single medication, a 30-day mood calendar) left only a
few columns, so the container-width cell sizing ballooned and the square
grid blew the tile to roughly three times its neighbours' height. The cell
ceiling keeps the grid dense and left-aligned within the tile-height rhythm
at mobile and desktop.
2026-06-06 20:47:47 +02:00
Marc-André Bombeck 2b9d2370e9 fix(cycle): drop card log button, fill predicted-period days, assessment on batch
- Phase education card no longer carries its own log-today button (the
  top-level log entry covers it).
- Predicted-period calendar days render as a soft dashed fill instead of an
  underline, so they read as period rather than an unfinished dash.
- The derived batch route now attaches the deterministic per-score assessment
  for the five scores so the dashboard grid (and iOS) paints the explanation
  without a second per-id round-trip; guarded so it never fails the grid.
2026-06-06 20:36:29 +02:00
Marc-André Bombeck 546835a937 fix(cycle): trim duplicate phase card, drop redundant disclaimers, tidy settings
Remove the duplicate phase-education card from the calendar tab, keeping the
single instance under the wheel and surfacing its "you often notice this here"
chip row there. Drop the generic prediction-estimate caption on the predictions
tab for every goal except AVOID_PREGNANCY, where the contraceptive-safety caveat
stays as a deliberate medical note. Stop rendering the associations-not-causes
footer on the insights crosstab. In the cycle settings, align card-header
descriptions with their titles, and remove the standalone cycle export + purge
affordances now that cycle data rides the main export and account-level delete.
Move the web-push enable action into the card header row.
2026-06-06 20:10:15 +02:00
Marc-André Bombeck c1d8ff9db9 feat(cycle): surface a gated cycle summary card on the insights page
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.
2026-06-06 19:44:01 +02:00
Marc-André Bombeck 73d0b0a3a4 fix(cycle): render mood and glucose rows in the phase crosstab without crashing
The phase-crosstab server emits display "mood" and "glucose", but the
client unit/label maps only covered the vital channels, so a surviving
mood or glucose phase correlation passed an undefined key to t() and
crashed the insights tab. Add the missing maps and harden both unit
lookups to fall back to an empty unit on any unmapped display.
2026-06-06 19:14:28 +02:00
Marc-André Bombeck a635c1de05 fix(cycle): v1.15.1 QA reconcile — purge, rate limits, marker contrast, token discipline
- Add CYCLE_FERTILE_SOON to the cycle-purge push_attempts eventType list so a
  data reset leaves no dated reproductive-intent delivery trace.
- Rate-limit the custom-symptom mutations (POST create, PATCH, DELETE) under a
  shared cycle:symptom:custom:<userId> bucket (30/min), mirroring the insights
  route, so a session can't flood the encrypted-label catalogue.
- Re-run the active-count cap inside PATCH when a symptom reactivates
  (hidden → active); a hidden row doesn't count toward the cap, so without the
  recheck a user could exceed it by hiding then re-enabling rows.
- Gate the log-day "limit reached" copy on the cap errorCode rather than any
  create failure, so a transient/validation error no longer masquerades as the
  cap being hit; thread the returned key so a new custom symptom auto-selects.
- Move the history-chart bar fill alphas into named phase-tokens
  (HISTORY_REST_OPACITY / HISTORY_PERIOD_OPACITY) so bar intensity lives in one
  token source alongside FLOW_OPACITY.
- Strengthen the confirmed-ovulation calendar marker (ring 1.5px → 2px, fill
  alpha 0.16 → 0.22) so it stays distinct from the predicted dot on small light
  screens; keeps its data-attr + aria naming.
- Raise the phase-education chip border from /10 to /15 for legible contrast on
  the tinted tile.
2026-06-06 17:19:59 +02:00
Marc-André Bombeck 2f44654dbe fix(cycle): keep custom-symptom client constants out of the server crypto module
The log sheet's chip grid imported the icon allow-list from custom-symptoms.ts,
which pulls node:crypto + the AES helpers into the client bundle and broke the
Turbopack build for /cycle. Split the client-safe constants and Zod schemas into
custom-symptoms-shared.ts; the server module re-exports them so the API routes
are unchanged, and the client imports from the shared module.
2026-06-06 17:06:51 +02:00
Marc-André Bombeck 32552f02ec feat(cycle): flow-shaded calendar, confirmed-ovulation marker, and cycle-length history chart 2026-06-06 17:01:33 +02:00
Marc-André Bombeck 985e197db4 feat(cycle): user-defined custom symptoms
Mirror the v1.13 custom-mood-tag CRUD for cycle symptoms. A user can mint
their own symptoms beyond the 15 seeded ones, picked from a label + icon
popover via a dashed ghost-chip in the log-day sheet's symptom grid.

- POST/GET /api/cycle/symptoms/custom and PATCH/DELETE on /[key]: mint a
  custom:<uuid> key, encrypt the free-text label at rest (AES-256-GCM),
  per-user cap, owner-scoped, gated behind requireCycleEnabled. Soft-hide by
  default, hard-purge on ?purge=true.
- The day-log write already resolves a symptom key against the catalogue plus
  the caller's own rows, so a custom: key links by construction.
- The log-day sheet fetches the user's customs and merges them under a new
  custom category in the chip grid; the ghost-chip + popover reuse the chip
  styling and the Lucide icon set.
- Seed the global custom symptom category (migration 0134).
- The purge route (DELETE /api/cycle/all) now drops the user's custom symptom
  rows too, so no intent-revealing label survives a purge.
- The label is never logged: only the icon + counts reach annotate/auditLog.
- i18n: cycle.symptom.custom.* across all six locales.
2026-06-06 16:54:43 +02:00
Marc-André Bombeck 81ec98fc97 feat(cycle): per-phase what's-happening education card 2026-06-06 16:45:15 +02:00
Marc-André Bombeck 93d47ddd05 feat(cycle): fertile-window reminder for the conception goal
Add a CYCLE_FERTILE_SOON reminder that fires two days before the
predicted fertile-window start. Twice-gated and default-OFF: it only
surfaces when CycleProfile.goal is TRYING_TO_CONCEIVE and fertileWindowStart
is populated, so fertile language never reaches the avoid / general / peri
goals. Reuses the existing cycle-reminder cron, discreet mode, clientManaged
suppression, idempotency ledger, and push-attempts plumbing verbatim; the
body carries the non-medical, not-a-contraceptive framing.
2026-06-06 16:39:45 +02:00
Marc-André BombeckandClaude Opus 4.8 eec3557137 feat(cycle): symptom-by-phase patterns on the insights tab
Add the cycle-native counterpart to the phase×vitals crosstab: for each
logged symptom, where in the cycle it clusters. A pure aggregator tallies
each symptom's occurrences across the phase-labelled days (the same phase
map the calendar uses), surfaces only symptoms logged on at least three
phased days, and reports per-phase counts + the dominant phase + its
share. The insights tab renders it as a proportional phase-hued bar with
the dominant phase named in words (never colour-only), observational and
never causal.

Also correct the phase-crosstab OpenAPI row enum, which had drifted
behind the runtime (mood + glucose channels were missing).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:47:04 +02:00
Marc-André BombeckandClaude Opus 4.8 c21131e26e feat(cycle): second-audit feature, QoL, and wiring close
Close the second-round audit UI cluster:

- Cycle reminders: add the missing notification-event labels so the
  period-soon / period-confirm toggles read correctly on the
  notifications page, and point to them from cycle settings.
- Per-cycle history: render a recent-cycles list (range, length, period
  length, confirmed-ovulation chip) — the cycles were fetched but only
  the aggregate stats showed.
- Confirmed ovulation: carry it on the prediction DTO and surface it as a
  solid BBT-chart marker + a predictions row + a history chip.
- Log-day sheet: deselecting an enum now clears it (edits route through
  the nullable PATCH); period start/end work on any selected date for
  retroactive correction; "end period" shows only while one is open.
  Extract the payload builders as pure, unit-tested functions.
- Cycle-tracking enable toggle in account settings so any account can opt
  in before the gated page is reachable; wire the cycle data-purge button
  to the hard-delete route and document the route in the OpenAPI spec.
- Calendar legend: add the symptom dot, render fertile as a ring to match
  the grid; tighten the luteal prior bound to the engine clamp; mute the
  BBT ovulation-label colour for contrast.
- Share the cycle-start backward-walk between the wheel and the chart.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:32:25 +02:00
Marc-André BombeckandClaude Opus 4.8 3375bbabac fix(cycle): second-audit safety, privacy, and write-path hardening
Address the second-round audit (server cluster):

- AVOID_PREGNANCY surfaces the fertile window, so it must carry the
  stronger "not a contraceptive method" disclaimer — wire the existing
  strong copy via a shared cycleDisclaimerKey(goal), server and client.
- Re-anchor the FOLLOWING neighbour cycle on a back-filled period start so
  an inserted-between-two-cycles start no longer leaves a stale open cycle.
- Resolve the sensitive-category encryption flag once per bulk/import
  batch instead of re-reading the profile per day; hoist the bulk cycle
  attribution in-memory (no per-entry query).
- Drop the raw driver message from the bulk wide-event meta (annotate meta
  is not run through the central redactor); keep the stable reason + code.
- Hard-purge now clears the cycle audit trail and the cycle reminder rows
  in the push-attempts ledger, so no dated reproductive trace survives.
- Discreet mode collapses the Web-Push coalescing tag and drops cycle
  metadata (phase) from APNs, matching the existing APNs event collapse.
- Add the reproductive intent field names to the central redactor denylist.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:15:22 +02:00
Marc-André BombeckandClaude Opus 4.8 31a14ce7bf feat(cycle): premium cycle ring, BBT chart, and full read-state coverage
Finish the cycle web vertical's signature surfaces:

- Rebuild the cycle wheel to the wellness-ring premium signature — a
  clockwise clock-wipe that draws the phase arcs on from the 12-o'clock
  day-0 origin, a phase-tinted bloom on the active arc, the shared
  specular sheen, a day-number count-up, and a soft current-day marker
  halo. All on the wellness-tile surface, gated once per session and
  zeroed under prefers-reduced-motion.
- Add the basal-body-temperature chart to the predictions tab: the
  current-cycle BBT series with phase-tinted points, fertility signs in
  the tooltip, and an estimated-ovulation reference line (suppressed in
  raw-chart mode). Surface basalBodyTempC + ovulation-test + mucus on the
  calendar day DTO so the chart reads from the existing calendar load.
- Cover the read states the prediction/disclaimer surfaces were missing:
  pass the disclaimer to the predictions panel, render it on the calendar
  tab, add per-tab error + retry, and gate the settings spinner.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 13:09:00 +02:00
Marc-André BombeckandClaude Opus 4.8 4b10bdc5da feat(cycle): single-day read endpoint, symptom severity, and a richer log-day sheet
Add `GET /api/cycle/day-logs?date=YYYY-MM-DD` returning the full day-log DTO
(or null) for one tz-anchored day so the web log-day sheet pre-fills instead
of opening blank — a blank re-log no longer nulls untouched or HealthKit-sourced
fields. Delete resolves the row id from the same read.

Persist an optional 1-4 Likert severity per symptom link (migration
0133_v1150_cycle_symptom_severity), carried through the shared symptom-link
writer and surfaced in the day-log DTO.

Sheet now captures the previously missing fields — spotting between periods,
pregnancy/progesterone home-test results, contraceptive kind — plus per-symptom
severity chips, a one-tap end-period boundary, and a Delete action. The wheel
ring marker sits at the 12-o'clock origin again (drop the double rotation), the
cycle view goes two-column on desktop, and the empty ring carries a first-period
call to action. New copy is localized across all six locales; the prediction and
fertility surfaces keep their disclaimers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 12:39:28 +02:00
Marc-André BombeckandClaude Opus 4.8 55f40df0ac refactor(cycle): wire the phase-correlation card into the insights tab
The cycle insights tab rendered an empty placeholder while the phase-contrast
card + its `/api/cycle/insights` feed sat unused. Wire them together: a
`useCycleInsights` read drives the headline finding + the luteal-vs-follicular
crosstab on the insights tab, matching the mood-factor crosstab styling. Drops
the placeholder card + a duplicate disclaimer component in favour of the ones
already in use, and removes the unused recency/phase-result/label exports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:26:13 +02:00
Marc-André BombeckandClaude Opus 4.8 7333d8e40f feat(cycle): add the cycle-tracking web vertical
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>
2026-06-06 10:06:20 +02:00
Marc-André BombeckandClaude Opus 4.8 6fdbb8d889 feat(cycle): predicted-period notifications + reproductive-data privacy docs
Add two cycle reminders through the existing notification cascade and the
honest self-hosting privacy framing for reproductive-health data.

Notifications:
- New event types CYCLE_PERIOD_SOON (fires ~2 days before the predicted
  next-period start, from the cached prediction) and CYCLE_PERIOD_CONFIRM
  (a gentle "did your period start?" on/after the predicted start while no
  period is logged, within a short grace window). Both default OFF and are
  gated again on cycleTrackingEnabled — opt in twice for a sensitive surface.
- A 15-minute pg-boss cron mirrors the mood-reminder worker: it scans users
  with cycle tracking enabled + predictionEnabled, computes who is in-window
  from the cached forecast, and dispatches one push per event per local day.
  Idempotency anchors on the push_attempts ledger (no new table). The queue
  is declared, registered in allQueues, scheduled, and wired to a boss.work
  handler, with a source-grep guard so it cannot ship unregistered.
- Honours notificationPrefs.cycle.clientManaged (skip the server push when
  the device owns local reminders, with a suppressed annotation, mirroring
  the medication path) and CycleProfile.discreetNotifications (generic
  "HealthLog reminder" body so no cycle event is named on the lock screen).
- Every attempt is recorded in push_attempts via the unchanged dispatcher.

Privacy / disclaimer:
- docs/self-hosting/reproductive-health-data.md — the "no cloud vendor to
  subpoena" framing with the honest residual-threat caveat (your own host,
  provider, backups, and devices remain part of the surface), citing the
  2021 FTC settlement and 2025 verdict as public record only.
- docs/security/cycle-data-threat-model.md — a compact "protects against /
  does not protect against" note, plus README pointers.
- CyclePredictionDisclaimer component (reusing the GLP-1 estimate-disclaimer
  pattern) for every fertility/prediction surface: descriptive, not a
  contraceptive method, not medical advice, never a safe-to-have-unprotected
  -sex claim. 6-locale copy under cycle.disclaimer.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 10:00:42 +02:00
Marc-André BombeckandClaude Opus 4.8 77a652a47c feat(cycle): cycle-phase correlation insight + Coach phase dimension
Add the menstrual-cycle phase as a channel in the existing FDR-guarded
correlation machinery, the same way rated mood factors were folded in —
no new statistics, the same Welch + Benjamini-Hochberg pipeline, the same
day floors and q = 0.10 gate.

Two complementary mechanisms, both confined to a gender-gated surface:
- a categorical luteal-vs-follicular contrast per outcome metric (resting
  heart rate, HRV, sleep, steps, weight, basal/wrist/skin temperature),
  surfacing one headline finding (resting-heart-rate-by-phase, falling
  back to HRV); and
- the continuous phase ordinal folded into the lagged-Pearson matrix.

Phase never reaches the general correlations route — the temperature
channels held out of the general matrix are un-blocked only here, behind
the cycle gate, so a reproductive inference can never leak onto a
non-gated surface. Every row shows n, effect size, p and q; nothing is
fabricated on thin data and nothing is framed as causal.

Surface it through a new gated GET /api/cycle/insights and a drop-in
card that mirrors the mood-factor crosstab.

Add a cycle dimension to the Coach snapshot: current phase + day-of-cycle,
the next predicted event (period range + confidence + method, fertile
window goal-gated), and the headline phase finding — present only for a
cycle-enabled account, so a non-cycle snapshot is unchanged. A new ground
rule keeps the Coach descriptive: never contraception-grade, never a
"safe day" claim.

OpenAPI regenerated; phase-correlation + snapshot-inclusion tests added;
copy localised across all six locales.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 09:59:02 +02:00