A round coach button sits fixed bottom-right on every authenticated
page, above the bottom nav and safe-area inset, with a small dot when a
nudge is unread — replacing the bubble that only existed while a nudge
was pending. The per-chart target-adjust link is gone (the page-level
control remains), the duplicated customize entry on insights sub-pages
collapses onto the tab strip, and the full-width show-all-values foot
button becomes an icon beside the target control. The tab strip's
scroll container keeps four pixels of paint room so pill borders and
focus rings stop clipping at fractional zoom, and the mood tag
breakdown bars adopt the muted opacity the other mood charts use.
Sleep nights collapse per writer instead of per source: several
HealthKit apps land behind the same source value, so a phone's coarse
awake and in-bed blocks could blend into or fully constitute a night
whose stages another writer had recorded. The winning writer per
session is the one with the most distinct granular stages, then the
user's source ladder, and its rows are kept whole. The device type is
threaded through every sleep read including the stage stacked bar, and
the sync overlap for late-arriving nights widens from one day to seven
so a night scored after the cursor passed is fetched on the next tick —
assembly is read-time, so affected nights heal without manual repair.
Mood notes expand to their full text in the list on desktop and touch,
replacing the hover-only tooltip, and the mood CSV export carries a
note column.
The update poller and the chunk-error reload guard key on the target
version instead of disarming for the whole session, so a second deploy
re-arms the self-heal and a stale shell can no longer strand with
404ing chunks. Lazy chart imports retry once and mount inside a
per-card error boundary with a reload affordance — a failed chunk
degrades to one card instead of replacing the page. The JSON API
client attaches a fifteen-second timeout unless the caller brings its
own signal (the raw streaming path is exempt), chart queries surface
errors with a retry instead of rendering as an empty period, per-type
fetch failures reject instead of caching an empty series as fresh, and
the session and snapshot queries retry once on transient network or
server errors.
The dashboard snapshot fires the moment login succeeds instead of
waiting for the page chunk (start ~670 to ~200 ms throttled). Guided
follow-ups are personalised from the user's snapshot and an answer gets
the coach's streamed reaction before the next question. Archived
schedule eras can be corrected through a superseding revision that
keeps the original as an audit trail, era writes serialise per
medication, and duplicate dose times collapse in validation. A rolling
medication whose next dose is tomorrow can no longer read as heavily
overdue — the card status derives strictly from the served next-due —
and the adherence bars keep a fixed geometry in every state. The
server-wide assistant key gains its admin card, the one duplicated
coach preference collapses onto a single owner, twenty-seven admin and
settings card headers move onto the shared pattern with sixty
containers normalised, and the remaining hydration warnings are gone.
Open follow-up questions now run as a guided in-chat sequence — a quiet
card offers to start, the coach asks one question at a time with
visible progress, answers can be adopted into the matching self-context
field, skipping keeps a question pending and a closing summary says
what was taken over. Proactive nudges learn three new deterministic
signals — weight drifting from its target band, a run of short nights,
an active user falling silent — address the focus the user asked to
watch, and gain per-group toggles and a frequency preference.
Server and first client render now agree everywhere query state used to
leak into the chrome, checkboxes no longer nest inside buttons, delete
confirmations admit the undo they offer, raw step counts drop their
decimals, the truncated unit label wraps instead, streaks know their
singular, the medication header speaks the user's date format, a failed
coach turn keeps the user's message and says what went wrong, settings
status notes follow a language switch, the locale only flips once its
bundle actually loaded, and the last raw api fetches in hooks move onto
the shared client.
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.
An intake can carry the dose that was actually taken — editable in the
quick-add and log dialogs, shown as a quiet deviation line in the
history. The measurements and mood lists refresh by pull on touch, the
insight sub-pages gain the same customize entry as the overview, and an
answered coach follow-up offers to adopt the answer into the matching
self-context field.
A dose past its slot time but still inside its catch-up window now stays
on the medication card as overdue and takeable instead of yielding to
the next occurrence — the next-due field can therefore carry a past open
slot, flagged with nextDueOverdue. Y-axis padding no longer dips below
zero for series that cannot be negative, so a steps chart stops
suggesting minus nine hundred steps.
A night with coarse apple-health samples next to granular watch data
showed only awake and asleep — the per-session source pick now prefers
the source that knows its stages. Tour popovers clamp into the
viewport, wrap their footer and fall back to a bottom sheet on narrow
screens. Settings and admin sections share one alignment rule — icon
column left, everything else flush on the content column — with neutral
header icons, no duplicated section titles under the shell heading, and
every select on the account-style idiom with a visible chevron. The
dashboard's customize icon matches insights, the target-settings link
leaves the home charts, and the mood heatmap adopts the damped fills
the other mood charts already use.
The pregenerate pass now warms the status cards even when the
comprehensive step is budget-blocked, failed or timed out — the gap
that left cards generating on first visit despite the nightly runs —
and the loop gains a per-user timeout so one hanging provider cannot
stall the tick. Schedule edits recompute their rollup days immediately,
the compliance cache keys on the timezone, open doses no longer count
against the nudge trigger, an invite sent against an open registration
still lands in the ledger, the self-report prompt fence uses explicit
data markers, the maintenance crons invalidate the caches they touch,
and a flaky audit assertion polls instead of racing. Six care-focused
achievements join the engine: miss-free streaks, measurement routine,
a complete self-context and a sleep-log run.
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.
Invites move into their own admin section with a real ledger: status
badges, expiry, uses, a redemption history of who joined when (new
redemption table, revocation is now soft so history survives), and an
honest one-time link/QR moment. The assistant self-context grows
structured fields for conditions, allergies and focus — encrypted like
the free text — merges age and gender from the profile into one fenced
prompt block, and asks up to three targeted follow-up questions that
surface as tappable chips above the coach composer. A fourth nudge
trigger gently suggests a check-up when the self-context is stale and
the coach is actually in use.
Users can now tell the assistant about themselves in an encrypted
free-text profile that feeds the coach and briefing prompts as clearly
fenced context, with the coach allowed one targeted follow-up question
when the picture is incomplete. Operators on closed registrations can
mint expiring invite links with a QR code from the admin area; a valid
invite opens registration exactly as many times as it allows and records
who redeemed it. A nightly pass watches three deterministic signals —
low adherence, blood pressure above its target band, a falling recovery
trend — and sends at most one coach nudge per week through the existing
notification channels, gated behind the same opt-outs as every other
assistant surface.
The clock format followed the UI language with no way out — an English
interface forced AM/PM everywhere. A new profile preference (automatic,
24-hour, 12-hour) feeds every formatter through the shared locale
context, persists on the user row and rides the profile endpoints. The
insight pill list in settings gains the same eye toggle the overview
tiles have, so ordering and visibility live in one place; the page-side
'manage detail pages' block folds into a link there. Includes the
locale bundles for both features and the regenerated API contract.
The score ring's bloom was clipped by the SVG's own viewport — the
drop-shadow sits on a child of the viewBox-filling arc — so the glow
never extended past the circle; the SVG now overflows visibly on every
host. The insight pill strip gains chevron buttons and wheel scrolling
so an overflowing row is reachable with a mouse. Safari favourites
rendered the transparent icon on a white plate; a flattened
apple-touch-icon and a maskable manifest icon fix both. The
time-of-day mood chart adopts the damped fill the weekday chart already
uses, eighteen delete confirmations move onto the destructive button
variant (one of them had the wrong text colour), five confirmation
dialogs gain pending states, the sources dialog stacks its primary
action on top on mobile, and the token form aligns input and button
heights.
An intake edit accepted any takenAt value, so a date typo could park a
dose a month away from its slot and poison the today feed, the rollup
and the rolling next-due anchor at once. The update schema now rejects
future timestamps (with clock-skew tolerance) and values older than five
years, the route rejects edits before the medication's start date, and
the edit dialog caps the picker at now, shows the scheduled slot and
warns when the new time sits more than 48 hours from it. The dialog's
note field is gone — it was never persisted and silently discarded
input.
Introduce <DoseWindowEditor>, the configurable on-time window control
shared by the schedule tab and the wizard. Per dose time the user keeps
the precise time (the symmetric +/-1h default band) or sets an explicit
range such as 07:00-09:00; the row shows the resulting on-time band plus
the cadence-aware late tail. The component is controlled over the
persisted doseWindows contract ([{timeOfDay,start,end}]) and validates
HH:mm, start<=end, and timeOfDay membership to mirror the server schema.
Pure window math lives in dose-window.ts (band derivation, late tail,
default-vs-explicit detection, serialise) so it is unit-tested without a
render. Adds the medications.detail.zeitplan namespace across six
locales.
Replace the Verlauf tab's flat intake table with the per-slot dose-history
ledger the shared band engine mints. The tab now renders EVERY expected slot
with its status — on-time, late, skipped, missed, upcoming — even when the
dose was never taken, and tags every off-schedule intake as a standalone
ad-hoc row at its real time. Rows group by day, most-recent first.
Genommen / Übersprungen sit on the dose row and recompute instantly: the
cached ledger and the headline adherence are mutated optimistically (the
engines are pure, so the client mirrors their accounting) before the write
fires, then the dependent-key invalidation reconciles; a failed write rolls
the optimistic snapshot back. A header add dialog records a dose — including a
backdated or off-schedule one — and an off-window take surfaces the "diesem
Slot zuordnen?" nudge, pinning it onto a chosen slot via forceSlotInstant
rather than orphaning it. A compact took/skipped bar sits at the top of the
tab; the card body still never tints by status.
Adherence is derived from the same ledger the user is reading (taken over
taken + missed; skip, ad-hoc, upcoming excluded), so the history view and the
percentage cannot contradict. New status + dialog copy in all six locales.
Make /medications/[id] the canonical full-page home for one medication:
a Radix tab strip (Übersicht · Zeitplan · Erinnerung · Bestand · Verlauf ·
Injektion · Erweitert) with ?tab= URL state, a read-only hero, and a
"Vollständig bearbeiten" jump into the wizard for structural edits.
Everyday levers move inline into the tabs.
Übersicht surfaces the adherence bars, Erinnerung owns notifications +
reminder grace, Bestand reads the pen/vial inventory (all medications,
calm empty state), Verlauf renders the editable intake history, and
Injektion (injectable routes only) carries the existing GLP-1 charts +
side-effect log. The ledger, the per-dose window editor and the
injection body-map land in the following waves.
The legacy /medications/[id]/history route now redirects to ?tab=verlauf
for bookmarks and deep links. Tab labels are i18n across six locales.
The Insights overview customise surface now has a home in Settings,
analogous to the Dashboard section. It hosts two blocks:
- "Arrange the overview" embeds the inline section show/hide + order
editor, persisting to insightsLayoutJson.sections.
- "Sort the navigation pills" is a dedicated drag-reorder control for
the tab-strip pills, persisting to insightsLayoutJson.tiles[].order —
a field already separate from the section order in layout v2, so the
two concerns stay independent. Both write through the existing
/api/insights/layout contract the tab strip and overview share.
Wire the new slug into the settings registry, the section router, the
mobile/desktop nav, and the placeholder icon map. Adds the section copy
and the pill-sort + overview-arrange strings across all six locales.
The default insights layout shipped a curated visible subset (v1.15.11),
authored for the overview Vitals/Mobility grid. Once the tab strip began
gating its nav pills on the same layout, that subset dropped ~20 nav pills
for a fresh account with data in Sleep, Steps, Active-Energy, body
composition, Walking/mobility, Audio, Daylight, etc. — pills that show
today — and caused a flash-then-vanish on first paint.
The grid renders a fixed mapped subset regardless of tile visibility, so
defaulting every tile visible does not bloat it; what it restores is the
nav's everything-with-data default. The user hides what they don't want.
Also:
- mood distribution + weekday charts: switch axis tick fill from the raw
--dracula-fg (white-on-white on the light-mode card) to the shared
--muted-foreground axis-text token; dark mode unchanged.
- drop the orphaned insights.editMode.manageTiles key from all six locales
(replaced by manageSubPages) and add a manageSubPagesHint noting that
hiding a detail page also removes its top-nav pill.
- pin the all-visible default with a layout test and a tab-strip
default-nav regression test.
The Anpassen tile manager only listed the ~14 vitals overview-grid
tiles, so the rest of the routed sub-pages had no toggle anywhere. Now
that the tab strip reads the same per-tile visibility, the manager
becomes the one place to show, hide, and reorder every sub-page.
Broaden the Kacheln-verwalten disclosure to the full sub-page universe,
grouped by category (vitals, body, activity, sleep, cardiovascular,
hearing, environment, metabolic, mood, events) the same way the strip
groups them. Each group is its own drag context so a reorder never
crosses a group boundary; rows reuse the strip's labels so a manager row
reads like the nav pill it governs. The single tiles layout this writes
now drives the overview vitals grid, the tab-strip pills, and this
manager. Relabel the disclosure to Detailseiten verwalten / Manage
detail pages across the six locales.
The page-scoped selection bar sat sticky bottom-2, behind the fixed
64px mobile bottom-nav, so the destructive Delete (N) button was
unreachable on phones and ignored the iOS home-indicator inset. Lift
it to clear the nav band + safe area on mobile and drop back to
bottom-4 at sm+ where the nav is hidden.
The per-row select checkbox wrapped a Radix checkbox (a 16px button)
in a 44px label; a label does not forward taps to a button, so the
effective target failed WCAG 2.5.5. Make the 44px area a single
button that owns the toggle, with the checkbox a pointer-events-none
controlled visual, avoiding any double-toggle.
Bring the mood pagination buttons to the 44px target used by the
measurements list. Add (sort: Desc) to the two source-filter indexes
so the schema matches the DESC migration SQL; schema-fidelity only,
no new migration.
Extract the duplicated list chrome — SortableHead and the single-row
DeleteButton — into a shared src/components/data-list/ module and add a
page-scoped multi-select with a selection action bar, used by both the
measurements and mood management lists.
Filter bar: each list keeps its primary Select (type / mood level) and
gains a source Select (measurements sends sourceEq, mood sends source)
plus an optional date range wired to the existing from/to params. Every
filter resets pagination to page 1 and is baked into the query key via
the centralised factory (queryKeys.measurementsList / moodEntriesList)
so the cache stays correct per filter combination.
Multi-select is scoped to the current page: a leading checkbox column on
desktop, a checkbox on the mobile cards, and a select-all-on-page header
checkbox. When at least one row is selected an action bar shows the count,
a clear affordance, and a destructive Delete (N) behind an AlertDialog
confirm that POSTs to the bulk-delete route, then invalidates the same
dependent-key bundle the single delete busts and clears the selection.
Selection clears on any page / filter / sort change; the selection math
lives in a pure, unit-tested helper. The locked-type insights subpage
keeps its focused single-metric view (source filter + date range
suppressed).
New strings land in a dataList namespace plus per-list source-filter and
bulk-delete keys, translated across all six locales.
The score-anatomy detail page now renders the per-score AI assessment that
already rides GET /api/insights/derived (shipped to iOS in v1.13.2) — the web
type dropped the field, so the card was silently discarded. The assessment
mounts below the composition card via the same InsightStatusCard the metric
sub-pages use, so it reads identically for readiness, sleep, recovery, strain
and stress.
Each score detail page also gains an educational intro caption beneath the
heading, mirroring the metric sub-pages: new explainer bodies for the five
scores across all six locales (sleepScore kept distinct from the sleep
metric's own body), wired through SubPageShell's explainerMetric prop.
The explainer-key guard resolves the score page's closed lookup record
statically so the single dynamic prop stays verifiable.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a dedicated version tile to the admin overview showing the running
version, short build SHA, and build timestamp, alongside a best-effort
'update available' hint computed against the latest GitHub release tag. The
hint reuses the existing /api/version/check-updates proxy (server-side
safeFetch past the CSP) so no new route or OpenAPI schema is introduced; the
check is day-stale and never blocks the render. Cookie-admin only by virtue of
the admin layout.
Shorten the period-narrative section title to 'Rückblick' (and the idiomatic
short equivalent per locale) — the same i18n key the customizable-overview
edit-mode labels read, so the rename flows through automatically.
Drop the redundant 300px min-height floor on the Trends cards. The chart slot
is a fixed 180px and the caption is clamped to three lines, so the floor
padded dead space below every caption that read as an oversized Trends to
Rückblick seam. The grid's auto-rows-fr plus the fixed chart slot already pin
the cards to a single baseline, so the seam now matches the uniform rhythm.
Shorten the notification-area label and inbox title from the longer
"Notification Center" form to plain "Notifications" across all six
locales (nav entry, page title, breadcrumb). Refresh the stale inbox
references in the settings-shell and sections tests.
The GLP-1 card overrode the shared next/last slot with appointment-phrased
labels (Next / Last, "Nächster Termin" / "Letzter Termin") while the generic
card used Next intake / Last intake. Drop the override so both cards name the
concept identically; the GLP-1 relative-day value phrasing (in N days / today)
is unchanged. Remove the now-dead label override props from the shared slot
and the orphaned glp1NextLabel / glp1LastLabel strings from all locales.
Relabel the Export section to Export & Import (heading + description,
all six locales; slug unchanged so existing links keep working) and
mount the import area in it. Add a cycle export tile that reuses the
health-record FHIR export with the reproductive section opted in, gated
on cycle tracking being enabled so a non-cycle account never sees it.
Compress the health-record export hero — format and date range share a
row, the practice-name field tightens, and the FHIR note rides the
footer with the generate action — so the card opens denser without
losing any controls. Add the import + cycle i18n keys across all
locales.
Make "Period in review" the period-narrative card heading (a proper
title, not a faint uppercase label) and drop the possessive across all
locales.
Render the today-signal card only when there is an actual signal: an
all-clear day (every vital inside its personal range) or an account still
building its baselines now produces no card, rather than an empty header.
Remove the moodLog card, its render, and the moodLog-only UI plumbing
from the integrations settings — the integration is no longer offered.
The backing columns, routes, and status aggregator entry stay in place
for a later dedicated removal. Also shorten the integration save button
from "Save credentials" to "Save" across all locales, and add the
nav account-settings label.
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.
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.
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.
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.
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.
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>
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>
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>
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>
- When the sensitive-category encryption preference is on (the default),
encrypt the intent-revealing day-log fields (sexual activity, protection,
pregnancy/progesterone tests, contraception) into an encrypted envelope and
leave the queryable columns null, decrypting on read. The toggle now does
what it says rather than storing those fields in plaintext.
- Make the day-log write a partial merge so a re-log only overwrites the
fields it carries, never nulling untouched ones (no more data loss when the
same day is logged again).
- Make the upsert collision-safe: a manual entry and an imported entry on the
same date adopt one canonical row instead of throwing, and the single POST
returns a clean conflict rather than a 500; one bad date no longer aborts a
whole import.
- Compare the decrypted note, not the ciphertext, so an unchanged re-log is a
true no-op and stops needless re-sync.
- Hoist cycle attribution out of the per-entry loop; map bulk failures to
stable reason codes; audit the import goal change; mask the event name in
discreet reminder pushes; add a one-tap hard-delete purge for all cycle data.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add "OTHER" to the accepted gender values (the column is a free String, so no
migration) and surface it in the account selector. This lets a non-binary or
trans user record their gender and opt into the cycle-tracking surface through
the per-user enable flag, rather than being gated out by a binary field.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Promote the reproductive HealthKit category types out of the
Apple-Health-import privacy-hold list and route them into cycle day-logs
instead of measurements, and surface cycle data on the three export
surfaces.
Import promotion:
- A new HealthKit -> cycle mapping module is the single source of truth for
how MenstrualFlow, IntermenstrualBleeding, CervicalMucusQuality,
OvulationTestResult, SexualActivity, PregnancyTestResult,
ProgesteroneTestResult, Contraceptive and the symptom category types fold
into CycleDayLog fields / seeded symptom keys. The XML importer
accumulates same-day reproductive samples and writes one canonical
day-log per day with source APPLE_HEALTH and a synthetic per-day
externalId for idempotent re-import. The fold is gated on cycle tracking
being enabled, so a non-cycle export never provisions cycle rows. A
contraceptive method nudges the profile goal toward avoiding pregnancy
only when the goal is still on its default. Basal body temperature stays
a measurement and mirrors onto the day-log. Pregnancy/Lactation status
and the pregnancy-bleeding types stay deferred; the cycle awareness types
remain server-derived.
Export:
- FHIR emits LMP (LOINC 8665-2), average cycle length (64700-8), period
length (64698-4) and the current phase as opt-in reproductive-health
observations grouped into their own document section.
- The doctor PDF gains an opt-in cycle section (recent cycles, average
length and variability, current phase), privacy-default off like mood.
- The full-backup export, the weekly backup worker and the off-host DR
snapshot include the cycle tables, and the admin restore re-creates them;
the day-log note round-trips as ciphertext and is never decrypted onto a
report surface.
Mapping table documented in docs/api/cycle-healthkit-mapping.md. Adds unit
+ integration coverage for the mapping, the import routing, the FHIR
observations and the backup round-trip. OpenAPI regenerated. Six-locale
strings added.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
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>