The one-time disclaimer acknowledgment route shipped without an OpenAPI
entry, leaving a gap in the contract the iOS client consumes. Add the POST
path and regenerate the spec.
Add the GET /api/insights/coach/nudge-status unread signal and the POST
/api/insights/coach/seen open-stamp to the OpenAPI contract and regenerate
the spec.
Add POST /api/measurement-reminders/{id}/complete so a client can mark a
measurement reminder done on the server, rather than only dismissing the
notification locally. The route is the explicit user-action equivalent of
the cron auto-resolve: it routes through the one shared satisfaction
primitive, stamps lastSatisfiedAt, re-anchors the server-authoritative
nextDueAt, and fires no notification of its own.
Idempotent via the primitive's forward-only guard — completing an
already-completed or auto-satisfied reminder returns 200 with
completed=false so a repeat tap need not be special-cased. Owner-scoped;
a cross-user or tombstoned id 404s and never satisfies. No schema change:
lastSatisfiedAt already records the completion instant.
Extend the OpenAPI contract with the new path and the completion result
schema, and cover the route with unit tests (marks done, idempotent
no-op, owner-scoped 404, no double-notify).
Introduce an explicit, user-declared "I have diabetes / clinician glucose
targets" preference (`User.hasDiabetes`, migration 0174, defaulted false) plus
its GET/PATCH endpoint at `/api/auth/me/diabetes`. The flag selects which
glucose target band a reading is judged against; it is never inferred from a
value and never asserts a diagnosis. Mirrors the existing per-user-scalar
preference pattern: 60/min rate limit, Zod validation, audit-logged,
field-by-field write. OpenAPI regenerated for the new route.
Add nullable onboarding_tour_progress_json to users (migration 0173),
a Zod-validated progress shape, and extend POST /api/onboarding/tour to
accept a mid-tour resume checkpoint and clear it on replay. Surface the
resume point on GET /api/auth/me and register the contract in OpenAPI so
the native client mirrors it.
Add GET /api/measurements/series-batch returning every requested
MeasurementType's daily series in one response, so the dashboard chart
row stops issuing one self-fetch per chart. Extract the rollup-daily read
(per-source collapse, coverage probe + inline fold, live-SQL fallback)
into a shared readDailySeries reader so the single-type route and the
batch route read through the same path with no SQL duplication; the
single route now delegates to it. Cap the batch's per-type reads with
p-limit to protect the shared pool. Sleep stays on its per-night /series
adapter and is not batched.
HealthChart gains an optional preloadedSeries prop: when the dashboard
hands it a covered slice for the daily window it folds those rows instead
of self-fetching, keyed so a range-tab change re-fetches cleanly. Non-
dashboard mounts are unchanged (empty discriminator keeps the cache tuple
byte-identical). Register the endpoint in the OpenAPI table and regen the
spec; add unit + parity coverage for the batch route.
An installed PWA opened offline rendered empty skeletons forever: the
service worker hard-excluded every /api/* path and there was no client
query persistence. Close both gaps for read value.
Service worker: add a stale-while-revalidate branch for an allowlist of
safe GET /api/* reads (dashboard snapshot + widgets, measurements,
medications, insights, analytics, version) in a release-versioned
healthlog-data cache. Two-layer refusal — a path deny guard for any
auth/token/session/login segment plus a body-level secret refusal for
hlk_/hlr_/sk- shapes and no-store responses — so a secret-shaped body or
an auth surface can never be persisted. Mutations stay network-only.
Client: add a dependency-free IndexedDB query persister that hydrates the
last successful reads before the first authenticated paint, build-version
and age gated, debounced on cache change, with an auth/session/admin/token
denylist. Cleared on logout alongside the SW page and data caches so a
shared device never leaks one account's data.
Offline banner now states it is showing last-synced data when a cached
snapshot is present. Bump release to v1.18.6.
Add an optional numeric value-range filter to the measurement list:
readings whose `value` falls within `[valueMin, valueMax]`, either bound
omittable for an open-ended range. Server-authoritative — both bounds are
parsed on `listMeasurementsSchema` (coerced from the query string), an
inverted `valueMin > valueMax` returns 422, and the bounds thread into the
Prisma `where` as a `value: { gte, lte }` range on the raw per-sample list.
The filter rides a new `FilterBarNumberRange` pill that mirrors the existing
date-range control, hidden for the day-grouped and sleep-night types where
the painted value is a synthesised total rather than a stored reading.
Add POST /api/illness/episodes/{id}/restore: clears deletedAt so the
episode and its preserved day-logs and encrypted note re-surface in
every read. Soft-delete keeps the row and its children intact, so
restore is a pure flag flip rather than a re-create. apiHandler +
idempotency + module gate + owner-scoped 404; restoring a live episode
is a no-op.
Wire the episode-menu delete to a restore Undo toast, mirroring the
labs delete-Undo affordance. Register the route in the OpenAPI source
and regenerate the contract.
The illness module shipped opt-in/default-off via a bornGated flag, the
inverse of the standard disabled-allowlist. Make it a normal optional
module that defaults on (opt-out), matching labs and its siblings.
No module used bornGated other than illness, so drop the flag, its
isBornGatedModule helper, and the gate resolver's born-gated branch
entirely. The module stays a normal toggle under What you track and the
operator-level availability override is unchanged; an account that wants
it off sets an explicit false. Refresh the gate, doctor-report, labs and
biomarkers comments plus the OpenAPI route descriptions accordingly.
An inventory item's unit count comes from a non-null Decimal column, but a
corrupt or legacy row could hold a missing or non-finite value. The
serialiser ran `Number(item.unitsTotal)`, and `Number(null) === 0` (and
`Number("corrupt") === NaN`), so an unknown count went out on the wire as a
fabricated 0 that a client could decrement into negatives.
Serialise a genuinely-unknown count (null / non-finite) as `null` so clients
render "unknown" instead. A real tracked 0 stays 0. No model change — the
distinction is serialisation-only.
The supply tab now renders an unknown-units row as "—", pre-fills the
correction editor empty, drops the capacity ceiling when total is unknown,
and keeps such a row out of the pooled available-supply headline. The
low-stock alert already excludes non-usable rows, so it does not fire on
unknown stock. OpenAPI marks both unit fields nullable.
The episode day-logs endpoint only accepted a single `date`, so the
detail timeline could anchor on today alone. Add a date-less LIST variant
to GET /api/illness/episodes/{id}/day-logs: when no `date` is given,
return the episode's day-logs newest-first with limit/offset/sortDir
paging and meta.total (mirrors the Labs list envelope). The single-date
read stays intact, selected by the param's presence.
Wire the detail timeline onto the list for a full historical scroll
instead of today only, keeping the neutral palette and retrospective
copy. Register the list response in the OpenAPI contract for the iOS
client.
Add a calm, value-free Rest Mode indicator to the dashboard score card and
the recovery insight so the web matches what iOS already shows: when an
illness/condition episode is active the score and recovery are framed, never
penalised, and the surface now says so. The new banner is neutral and
untinted, self-gates on an active episode, and reveals only the onset date —
never a decrypted note or episode label. The dashboard reads the annotation
from the snapshot (resolved server-side alongside the score); the recovery
section reads it from the analytics payload.
Give the sleep page one assessment per distinct chart group: the
duration/architecture assessment now sits with its charts, and the
sleep-quality block carries its own SLEEP_SCORE assessment, matching the
"multiple charts, multiple texts" structure the recovery page meets.
Add the steps explainer (insights.subPage.explainer.steps*) and pass
explainerMetric="steps", and demote the recovery per-block title to an h3 so
the heading outline nests cleanly under the page heading and group heading.
Register the per-episode correlation and cross-episode insights GET routes in
the OpenAPI source and regenerate the spec so the iOS contract carries the
Derived findings shape. Cap and strip the free-text episode label before it
enters the Coach prompt — it is the one user-controlled string in the illness
context block, so collapse control and newline sequences and bound the length.
Twice-daily BP protocol (RRULE BYHOUR=7,19) now fires at every clock
hour: byHourTimesOfDay expands BYHOUR into the engine's timesOfDay so
the label, RRULE, and engine output agree instead of collapsing to one
nudge at notifyHour.
Vorsorge surface:
- move create/edit into a ResponsiveSheet with a pinned Cancel/Save
footer, matching Labs/Illness/Medications
- wire the unused PATCH support: Edit affordance opens the form
pre-filled; an enable/disable Switch drives the enabled flag
- translate COACH-origin labels (the row stores an i18n key, not prose)
and render a neutral Coach provenance badge + an until-<date> line
- surface origin + endsOn on the hook type
- pull-to-refresh + always-on rationale documented on the nav entry
- NativeSelect-vs-Radix choice documented as deliberate
Coach:
- expired COACH course-window reminders self-clean: the cron tick
soft-deletes finished finite cadences instead of leaving dead rows
- accept-dedup gains a partial unique index (user, metric) where
origin=COACH and not deleted, with a P2002 catch in the route
- per-user rate-limit bucket on the suggestion-action endpoint
- SUGGEST-REMINDER parser keeps prose written after the block
- document POST /api/coach/reminder-suggestions in OpenAPI
Coach chrome:
- HistoryRail hideHeading prop drops the duplicate Conversations
heading on the page surface
- align the rail-tray strip inset to px-3 sm:px-4 so the dividers line
up; reword the FAB inset comment
satisfyReminder closes the forward-only TOCTOU via a conditional
updateMany (count===0 no-op). Migration headers corrected to v1.18.1.
Register /api/illness/episodes (list + create), the {id} resource
(read + edit + soft-delete), the {id}/resolve verb, and the
{id}/day-logs upsert in the OpenAPI route table, and add the Illness
tag. Request bodies + queries reuse the runtime Zod schemas from
validations/illness so the wire contract stays single-source; the
response shapes mirror the server-authoritative episode + day-log DTOs.
Regenerated docs/api/openapi.yaml; openapi:check is in sync.
The recovery page rendered a grid of compact device-score tiles plus a
redundant cross-link to the recovery-score anatomy at the top. It now
follows the same structure as every other metric page (blood pressure,
pulse, the HealthKit scaffold): each present device-native signal renders
a block carrying a short explainer, the max / median / mean stat strip,
the shared chart with the identical 7 / 30 / 90 / All toggle, and a
per-chart assessment. Multiple charts now carry multiple matching texts.
Register the seven device-native recovery / strain signals (ANS charge,
day / workout strain, cardio load, whole-day average / max heart rate,
energy expenditure) in the generic metric-status registry so each chart
gets a generated assessment, gate them on the recovery module, and give
each its own chart-overlay slot. Drop the duplicate top score-link block
(this surface is reached from the overview already).
Regenerate the OpenAPI contract for the widened metric-status enum.
Medications graduated from the always-on core set (weight / blood
pressure / pulse stay core) to an opt-out module. The surface gates
off the resolved module map — the left-nav entry, the dashboard
medication widget, and the dedicated Medikamente settings entry hide
when the account turns the module off. The medication data routes stay
exempt from the gate (same data-layer reasoning as mood / labs): an
importer, a sync, and the user's ability to clean up entries keep
working while the surface is hidden, and re-enabling finds the
schedule and intake history intact.
Wire the module-route gate inventory, the dashboard widget/summary
maps, the PATCH allowlist, and the nav model; regenerate the OpenAPI
contract for the new module key.
The Coach can append a ---SUGGEST-REMINDER--- sentinel to propose a
measurement cadence (weight daily, the ESH/AHA 7-2-2 home-BP protocol,
structured glucose). The server resolves the cadence from a closed catalog
and emits an additive 'suggestion' SSE frame the chat renders as a one-tap
action card; accepting it creates a MeasurementReminder with origin COACH
through the same engine the Vorsorge surface uses (server-authoritative
schedule, finite course window self-expires via endsOn).
Non-naggy by construction: a frequency-cap cooldown, dismissal memory, a
master opt-out under Coach settings, and an explicit 'I measure enough'
stop path, all in the coach-prefs blob. Triple dedup at suggest, render,
and create so a cadence is never double-suggested or double-created, and
suggestions for a disabled module never surface. RHR/HRV stay passive --
the Coach surfaces the baseline, it never nudges to 'go measure' them.
The reminder DTO now echoes origin and endsOn for the iOS client.
Introduce a retrospective illness/condition journal: episodes with a
lifecycle (acute / chronic-ongoing / recurring / flare) and a daily
symptom timeline forked structurally from the cycle log-day surface. The
journal is a record, not a medical device — no prediction or diagnosis UI.
- Register `illness` as a born-gated (opt-in / default-off) module. The
gate resolver flips the disabled-allowlist default to off for born-gated
keys, so the surface stays dark until the account opts in from the
Modules hub. `isIllnessEnabled` + `requireIllnessEnabled` delegate to the
module foundation (one source of truth) and re-stamp an illness errorCode.
- Episode CRUD under /api/illness: list / create / get / edit / resolve /
delete, plus a per-episode-day day-log upsert on (episodeId, date). Every
route wraps apiHandler, Zod-validates with returnAllZodIssues, narrows
userId from auth, builds data field-by-field, and gates the module.
Free-text notes encrypt at rest through the shared Bytes codec.
- Log-day sheet (cycle fork): ~8 Jackson/WURSS symptom chips 0-3 + a
functional-impact group + fever + an encrypted note, one row per day.
- Episode history page + a born-gated nav entry, neutral palette, status
via a neutral Badge, retrospective-only copy. i18n across all six locales.
- Cover the new routes in the module-route-gate inventory; unit tests for
the day-log upsert, the gate enforcement, and the born-gated resolver.
The /api/auth/me module map now projects the `illness` flag (openapi.yaml
regenerated).
Add the Biomarker + restore routes to the OpenAPI registry and carry
biomarkerId on the LabResult schema (additive; iOS resolves the unit +
range from the linked marker, never recomputes). Regenerate the YAML.
Add the labs.* / labs.catalog.* keys across en/de/es/fr/it/pl. List the
biomarker catalog tree + restore route on the module-gate inventory's
data-layer exemptions (the module gates the surfaces, not the row store).
The reminder type allow-list exposed 7 of the full MeasurementType enum.
Widen it to the ~15 active-measurement set: vitals (weight, BP, pulse,
glucose, SpO2, temperature) plus the body-composition family a scale
produces (fat mass, fat-free mass, muscle, lean body mass, bone mass,
body water, visceral fat, BMI). One step on the scale satisfies every
linked reminder at once. Passive scores, cumulative activity, and device
events stay excluded — nudging someone to 'go measure' a passive sample
makes no sense.
App-layer only: the column is the full enum, so no migration. Group the
dropdown (Vitals / Body composition) and add the labels across all six
locales.
Maintenance patch clearing two transitive security advisories. Bump the
package version, record the changelog entry, and regenerate the OpenAPI
version line. No functional changes.
Add the resolved `modules` map to ProfileResponse (shared ModuleMap
schema) so the spec matches the alias payload, and add a reusable
module-disabled 403 response documenting `meta.errorCode = module.disabled`
plus `meta.module` (the disabled key). Wire that 403 onto the module-gated
sleep night/rhythm routes, which emit it via requireModuleEnabled.
Regenerate docs/api/openapi.yaml; openapi:check is in sync.
Register the four /api/insights/chat surfaces in the OpenAPI contract:
list (cursor-paginated), streaming send (SSE), detail (messages decrypted
server-side), and delete. Adds CoachConversation / CoachConversationsPage /
CoachConversationDetail / CoachMessage / CoachProvenance / CoachChatRequest
schemas and regenerates docs/api/openapi.yaml. Additive; the endpoints
themselves are unchanged.
GET /api/auth/me now returns a resolved modules map for every toggleable
module (cycle/coach reflect their delegated state). New PATCH/GET
/api/auth/me/modules read and update the preferences: the body is strict
over the toggleable key set so a core-domain or unknown key is a 422, the
merge is field-by-field (no mass assignment), and userId is narrowed from
the session. Documented in the OpenAPI registry and yaml.
The disable-coach gate-inventory allowlist records the module gate's
intentional coach delegation read.
Two additive measurement_type values for the Oura coverage completion:
- SLEEP_SCORE — the headline 0-100 sleep-quality score (unit score),
kept apart from the WHOOP sleep sub-scores so two vendors' headline
numbers never share a bucket.
- BODY_TEMPERATURE_DEVIATION — a signed nightly body-temperature offset
from the personal baseline (celsius). It is a deviation, not an
absolute reading, so it cannot share the body / skin / wrist
temperature buckets.
Migration 0165 extends the enum (additive, no backfill). Register both in
the validation enum, units, plausibility ranges, category overlay, list-UI
label/icon/colour maps, chart-token allowlist, and the PR-direction switch
(both resolve to no record direction). Both are descriptive signals, not
clinical vitals, so they stay out of the doctor-report PDF. Localise the
two list labels across all six locales and regenerate the OpenAPI contract.
Polar Nightly Recharge and Training Load Pro carry two native signals with
no existing home: the HRV-based autonomic-nervous-system charge (distinct
from the 1-6 recovery band that maps to RECOVERY_SCORE) and the device-native
cardio-load strain figure (distinct from WHOOP DAY_STRAIN and the COMPUTED
STRAIN_SCORE). Add both to the canonical enum (migration 0166) and wire them
through the validation enum, canonical units, plausibility ranges, category
cluster, chart-token allowlist, PR-direction resolver, and the measurement-
list label/icon/colour maps, with labels across all six locales. Both are
device-derived composites, so they read no PR direction and stay out of the
clinical vitals PDF, matching the WHOOP-native score classes.
Regenerate the OpenAPI contract for the two new enum members.
Add an optional hex liveActivityPushToken to device registration. The iOS
client registers the current per-Activity ActivityKit push token so the
server can address a Live Activity update or end push on a
medication-intake mutation, and sends null to clear it when no Activity is
running. Following the medicationDelivery override convention, the field
is only touched when present, so an ordinary re-register keeps the prior
value. Regenerate the OpenAPI contract.