Carries the four fixes that landed after the v1.34.1 tag plus the two found while
reviewing them.
The workout collapse that shipped in that batch only worked on the workout list,
because it keyed a session on a field three of the five callers never fetched.
Those three kept averaging one session into a person's baseline more than once.
The service-worker fallback version had drifted a release behind with nothing
checking it; both halves now have a gate.
v1.33.1 claimed custom cycle symptoms were carried in backups. They were not,
and the same release turned the old silent drop into a hard failure, so the
accounts the fix was written for went from losing one symptom link to being
unable to restore at all.
Eight workstreams, two migrations, and a pass by an independent reader that
found four things this release was claiming about itself that were not true.
Two of the fixes change what people receive. Medication reminders went out
every fifteen minutes because the dedup key could not tell one dose slot from
another, and "handled by the app" was silencing every channel rather than the
one it names. Both are called out in the notes, because a user who set that
flag as a general mute will notice.
Two came from reports: an OpenAI-compatible gateway that the provider list
had no room for, after an earlier answer from us said otherwise, and four
places where a timestamp followed the browser instead of the profile.
The backup half is the one worth reading. Three record types were carried and
could not be restored, or could not be carried at all, and each failed in a
different direction: one dropped silently, one stopped the restore outright,
one was discarded while the restore reported success. What a backup holds is
now declared in one file with a written reason for everything left out, and
the roughly thirty types that are still carried by neither end are named there
instead of waiting to be discovered by someone who needed them.
Interface housekeeping. The overview sections drop their hand-rolled card
frames for a shared shell with one inner rhythm, failed reads on dense metric
pages get a named row primitive that puts the message in the content tier, and
the last off-scale spacing and text sizes go back onto the scale.
The tag chip and the sub-twelve-pixel text step are primitives now, so neither
can be hand-copied at a slightly different value again.
Stable per-segment sleep keys plus the replace-by-window sweep, a one-shot
repair over the past year, Retry-After handling, and the unchanged-value write
skip. Migration 0270 records which connections the repair has been through.
Surface a self-hoster's report that the unit preference was stored but read by
almost no display. Weight, body composition, waist, and temperature now follow
metric or imperial at entry and everywhere they render, storage stays canonical
SI, and the MCP logging tool converts a known unit hint or refuses an unknown
one. OpenAPI diff is version-only; the wire stays canonical.
Per-collection isolation for the Polar vitals sync: one unavailable data type
no longer blanks the rest, a failing collection is named on the integration
status, and an unexpected response shape is logged instead of reading as no
data. Bump version, regenerate the OpenAPI version string, add the changelog
entry.
A failed sync no longer reads as success across WHOOP, Withings, Oura, Polar,
Nightscout, and Strava: a dead WHOOP token stays parked instead of flipping back
to connected, an interrupted history backfill is retried rather than marked
complete, Withings ECG syncs on its own hourly schedule, activity and sleep
write failures surface on the status card, poll-cohort sync errors that
previously vanished are recorded, and Strava history import starts at connect
time.
Extract the dashboard-layout optimistic-concurrency mechanism (issue #581)
into a shared server helper (takeBaseToken plus guardedUserUpdate /
guardedRowUpdate) and a client helper (readUpdatedAtToken, withBaseToken,
isConflict). Retrofit the dashboard-widgets route onto the helper and apply the
same guard to the insights layout, medications layout, and coach about-me write
endpoints.
A request that omits the base token keeps the byte-identical unconditional
write, so older clients are unaffected. Document the contract in the OpenAPI
registry, including the dashboard-widgets path that was previously absent.
A dose logged from another screen marked the Today hero and the dashboard
stale without re-reading them, so returning to the home page kept showing the
old state until the 120 s poll or a hard reload. The daily reads run with
refetchOnMount off, so a bundle invalidation with the default active
refetch-type never touched them while they were unmounted.
Route every intake-affecting medication write through one blessed
invalidateMedicationReads (bundle invalidation plus a forced inactive refetch
of the digest and snapshot): Take all due, the intake edit dialog, the history
bulk/row delete, the dose-history ledger and its add dialog, the lifecycle and
danger-zone actions, and the intake import. Give the Vorsorge reminder
mutations the same daily-rail refresh, bust the biomarker assessment and the
labs-changes read after a lab write, and refetch the daily reads after a
briefing regenerate so the Today lead updates without a reload.
Restore the dashboard-layout concurrency belt: the settings layout query now
always loads its own tokened copy, so a home-page visit can no longer strand a
save without its base token and silently drop the v1.32.16 conflict guard.
Add a structural guard that fails CI when a new site invalidates a daily-reads
bundle without the paired inactive refetch, so this class cannot recur a
fourth time, plus behavioural coverage for the batch take and the Vorsorge
refresh.
Refs #581
Hold the Fitbit sync verdict when the token is dead or a write fails, and
verdict-gate the history backfill so a partial first-time import retries
instead of marking itself complete. No schema or contract change; the OpenAPI
version string tracks the bump.
Pull the athlete's exercises from the stateless GET /v3/exercises list (last
30 days of uploads, no transaction, no userId in the path) and upsert each into
the Workout table as source POLAR, keyed on (userId, source, externalId) so a
re-fetch or Polar-side re-analysis overwrites in place. A new table-driven
sport map normalises the raw Polar sport labels into the canonical
WorkoutSportType bucket at ingest, mirroring the Strava/WHOOP mappers; the raw
labels and training load ride metadata for provenance.
The exercise leg rides the existing hourly polar-sync cohort tick alongside the
vitals leg, with independent attribution on the shared polar ledger: each leg
is attempted and records its own failure, and neither can mask the other's
result. Polar takes its place in the workout source ladder between Withings and
Strava. No migration and no OpenAPI change: MeasurementSource.POLAR and the
full workouts source enum already exist.
Refs #568
Replace the internal "[unverified]" token the grounding guard left in a Coach
reply with the editorial elision mark […], and add a quiet muted caption
under the bubble explaining that some figures could not be checked against the
user's data. The mark holds the slot grammar-safely mid-sentence, before a
surviving unit, and inside a range; the notice carries the meaning. The withheld
count rides the existing provenance envelope (unverifiedFigures) through the
stream, persistence, and reload, and the fenced document-chat path carries a
minimal envelope so its notice survives too. New i18n key across all six locales.
Also fix an adjacent reload gap: provenanceFromJson restored only a subset of the
serialised envelope, so a persisted suggestion card, confirm action card, and the
tool trace were dropped on conversation reload despite the render layer expecting
them to survive. Restore them defensively alongside the new field.
The persistent mood-rollup writer bucketed on the UTC truncation of
`mood_logged_at` and ignored the canonical per-row `MoodEntry.date`
label. Every coverage-miss fallback keys on `entry.date`, so the rollup
tier disagreed with the fallback and with the day the user logged the
mood: a late-evening or after-midnight local mood landed on the wrong
calendar day, and the label shifted the moment the boot backfill flipped
a user from the live fallback onto the rollup tier.
Key every bucket off the stored `date` label instead — DAY, WEEK, MONTH
and YEAR — with `bucket_start` set to that label's UTC-midnight instant.
The rollup is now byte-identical to the live `entry.date` fallback and
timezone-independent by construction: DST correctness stays where it
already lives, in `moodDateKey` at write time. No consumer changes, no
schema change; every rollup reader already formats `bucket_start` as a
UTC day label.
Migration 0269 empties `mood_entry_rollups` so the boot-time
full-backfill worker re-mints every user's buckets under the corrected
keying; the worker now deletes-then-refolds so it is self-cleaning
against any row left by an older instance. While the table is empty,
reads fall back to the live `date`-keyed walk, which was already
correct, so there is no window where a wrong day is shown.
Self-hosters can sign in to the iOS app through their own domain, so the
password manager files credentials under the right site and passkeys run in
the correct origin. The app opens the instance's own web login inside an
ASWebAuthenticationSession; after an interactive login the server mints a
single-use, PKCE-locked handoff code on a compiled-in custom scheme, which the
app exchanges for the standard native token bundle. The token pair never rides
a URL — only the opaque code does.
Three endpoints mirror the OIDC native handoff and share its mint/consume core,
now extracted to native-handoff.ts (the OIDC leg keeps working unchanged, as
the default flow):
- GET /api/auth/native/login validates the PKCE challenge, sets a
short-lived encrypted state cookie, and
redirects to /auth/login?flow=native.
- GET /api/auth/native/complete validates the web session, enforces the
freshness binding, mints the code, and
redirects to healthlog://login-callback.
- POST /api/auth/native/token exchanges the code under the cookie-less
native transport gate for the native bundle.
The freshness binding refuses any session older than the flow start, so a
pre-existing browser session can never silently complete a handoff. The start
time is stamped from the database clock (SELECT now()) to stay comparable to
Session.createdAt with no app/DB skew, and createdAt is read through a
dedicated projection so the comparison cannot be silently dropped. A flow
discriminator on the handoff row (migration 0268) makes an OIDC code
non-redeemable at the web-login route and vice versa.
The callback scheme is a compile-time constant with no redirect_uri parameter,
requireAdmin and requireFreshMfa stay cookie-only, and the exchange mints no
wider scope than password login. The state cookie is AES-256-GCM with a fresh
IV. Adds the flow=native login-page banner in all six locales and the three
routes to the OpenAPI contract.
The Coach output guard now tracks the numbers it was shown across the whole
conversation, so a figure carried forward or grounded in a goal, a schedule,
or a reference band stops being marked unverified, while an invented one is
still caught and a fabricated risk number now blocks in six languages.
Bumps the version, regenerates the OpenAPI contract and the service-worker
version fallback, and records the release notes for the intake-source and
sync-trigger provenance surface plus the shared hhmm util and migration-number
guard.