Commit Graph
8106 Commits
Author SHA1 Message Date
Marc-André Bombeck 8ebc6fe546 fix(integrations): give Oura and WHOOP a 48 h silence window
Their rings and straps upload only when the phone app is opened, so a day
without opening it is ordinary and must not page the operator. Wearables
whose cloud receives data on its own keep 24 h, Apple Health keeps 48 h,
and scales, cuffs and Strava never alert on silence.
2026-10-15 14:30:55 +02:00
Marc-André Bombeck a4942e631f Merge branch 'v1424/ghfallback' into release/v1.42.4 2026-10-15 12:45:23 +02:00
Marc-André Bombeck 1f9180fe6f test(google-health): drive a backfill to completion against a Google that refuses the edge filter
Also documents the fallback and its log fields.
2026-10-15 12:35:49 +02:00
Marc-André Bombeck ebc0168f53 test(google-health): pin the fallback when Google refuses the resume edge filter 2026-10-15 12:33:34 +02:00
Marc-André Bombeck e2d5bf121c fix(google-health): read past the resume edge when Google refuses its filter 2026-10-15 12:31:39 +02:00
Marc-André Bombeck de40db7ad0 Merge branch 'v1424/fixes' into release/v1.42.4 2026-10-15 12:23:55 +02:00
Marc-André Bombeck 7be05608f8 test: date fixtures that feed a clock-relative window relative to now
A fixture dated near the day it was written passes until the date slides
out of a window the route counts back from the clock. Five did: a snooze
until a fixed date that becomes the past in January, a prognosis read
against the wall clock over days seeded at a fixed date, sleep rows and a
weight streak read through windows that end now, and a sync token that
would expire on a fixed date. The e2e mood heatmap mock and a timeline
URL are dated from today as well.

A guard scans the integration and e2e trees for near-now absolute dates
within reach of a window parameter, with an allowlist for files whose
windows are fixed on both ends.
2026-10-15 12:23:09 +02:00
Marc-André Bombeck b96c114a45 Merge branch 'v1424/gh' into release/v1.42.4 2026-10-15 11:45:45 +02:00
Marc-André Bombeck c8194c4a86 test(google-health): drive a three-year heart-rate backfill through the drain to completion
1.58 million readings, paged by a generated fake of the list API that
honours the edge and lower-bound filters, with a server error, a timeout and
an hour-long rate limit injected. Pins that the backfill completes over
several slices, imports every reading exactly once without rewriting a row,
lets the hourly sync run between and beside slices, writes one log event per
slice with its requests and edges, and keeps the heap flat across the
history under the production heap ceiling.
2026-10-15 11:45:15 +02:00
Marc-André Bombeck 2d03eba587 fix(google-health): resume a backfill inside a collection and run it in short slices
A backfill noted progress only once a whole collection was read to the end.
A heart-rate history of years that one attempt could not finish (its budget,
a rate limit it could not wait out, a single failed request among thousands
of pages) started the next attempt from the newest page again, finished
nothing, and failed with "marker not stamped" on every attempt.

- Every spot-reading collection read page by page now records, after each
  page it writes, the oldest sample it has written (backfillProgress.cursors).
  The next attempt asks Google only for samples at or beyond that edge. The
  edge is held back once a page's write fails, so no rows are skipped.
- The backfill runs in slices of five minutes or 500 pages. A slice that
  finished a collection or moved an edge continues with the next one; only a
  slice that moved nothing fails, and its error names each unfinished
  collection and why the slice stopped. The slice ceiling is derived from the
  collections and the page ceilings of the resumable ones.
- A rate limit Google will not lift within a minute ends the slice instead of
  failing the walk, and the next slice starts five minutes later. Server
  errors, timeouts and dropped connections are retried three times.
- The heart-rate stream is read last, so the light collections finish in the
  first slice.
- The hourly sync waits up to eight minutes for a running slice instead of
  skipping the account for as long as the import runs.
- Each slice annotates googleHealth.backfill.slice with pages, elapsed time,
  stop reason, per-collection status and edges, and reports progress on the
  job.progress line.
2026-10-15 11:26:49 +02:00
Marc-André Bombeck 47deda9d76 Merge branch 'v1424/integrations' into release/v1.42.4 2026-10-15 11:26:37 +02:00
Marc-André Bombeck 73c4dda224 fix(visits): reach every lab result, condition, document and visit from the link pickers
The visit form's lab picker offered the newest 200 results, the condition
picker the newest 100 conditions, the document pickers the newest thousand
documents and the document sheet's visit picker the newest 100 visits.
On a long record everything older could not be linked.

Each picker now opens on the records around the date of the thing being
linked, pages older and newer from there, and searches through the list
route itself. Records already linked always show, from the labels the
record's own read carries and from what was picked in the sheet.

The lab list takes a q filter over analyte and panel, the condition list
from/to/q/offset/sortDir, and the visit list an offset.
2026-10-15 11:24:40 +02:00
Marc-André Bombeck f2646bc186 test(integrations): cover the health field on the status routes 2026-10-15 11:15:06 +02:00
Marc-André Bombeck 8162afc0e1 docs(api): publish the connection health state 2026-10-15 11:09:50 +02:00
Marc-André Bombeck c094f217fc feat(integrations): watch connections for silence and lasting trouble
An hourly and a daily pass flag a continuous source that delivered on
most of the previous 14 days and has gone quiet, and open one operator
incident per connection when a connection stays degraded or needs action
for more than a day. The incident is announced once through the existing
admin alert path, adopted rather than repeated when the sync ledger
already paged, and closed with one recovery note. Scales, cuffs and
workout logs are watched for errors only, never for silence.
2026-10-15 11:07:57 +02:00
Marc-André Bombeck 82d8960044 fix(jobs): run every admitted import inside its own background event
The admission drain ran each full-history import with no wide event in
scope, so every external call, warning and annotation the provider walk
recorded was dropped and a failing backfill left only its error message.
Each admitted run now opens a job.integration_backfill event with its kind,
identity, job id and slice, emitted when the run settles. The Health Connect
import worker gets the same treatment.

The external call list on one event is bounded so a long walk cannot hold
thousands of entries: failed and annotated calls keep the larger share,
the rest is counted in external_calls_omitted.
2026-10-15 11:07:28 +02:00
Marc-André Bombeck dad9a7778e fix(medications): keep an extra Apple Health dose off a slot that already holds one
A HealthKit dose event binds to a slot by window band, and a weekly
schedule's band reaches a day either side of its slot. An extra dose
logged the day after a scheduled one landed on that slot and overwrote it
last-write-wins: the slot's time changed and the extra dose was stored
nowhere. An external dose record whose band slot already carries a
different take is now recorded as its own unscheduled dose.

The dose history now lets the take stored on a slot claim it before an
extra dose stored on its own instant, instead of following the order the
rows were read in, so the extra dose reads as off-schedule.
2026-10-15 11:02:38 +02:00
Marc-André Bombeck d10179f9a3 feat(integrations): resolve one health state per connection
Fold the ledger state, liveness verdict, plan and scope gaps and the
provider reauth flag into ok / limited / degraded / action_required with a
reason code, the one action, the unavailable types and redacted
diagnostics. Keep the last ten sync attempts per connection for those
diagnostics, publish the state on the integrations envelope, every
per-provider status route and the Apple Health config read, and page the
operator once per failure streak instead of once a day.
2026-10-15 11:01:16 +02:00
Marc-André Bombeck cdd1e9c87f Merge pull request #1219 from MBombeck/fix/lab-dialog-overflow
fix(labs): keep the lab form's single column at the dialog width
v1.42.3
2026-10-15 07:06:49 +02:00
Marc-André Bombeck 07d991adf7 fix(labs): keep the value and draw-date column inside the dialog body
The single-column grid used the implicit auto track, which grows to the
widest cell's min-content. The draw date's date and time inputs size
from the font, so with wider metrics that min-content passed the dialog
body (about 422 px against 398 px) and the value input ran 24 px into
the dialog's padding. An explicit grid-cols-1 track is minmax(0, 1fr)
and stays at the body's width; the date row already shrinks inside it.

Refs #1214
2026-10-14 21:01:13 +02:00
Marc-André Bombeck 056af17f62 Merge pull request #1218 from MBombeck/release/v1.42.3
Release v1.42.3
2026-10-14 20:33:50 +02:00
Marc-André Bombeck e0573069c5 test(sharing): seed the series weight relative to now so it stays in the 90-day window 2026-10-14 20:09:14 +02:00
Marc-André Bombeck 38ef4c7fa7 fix(labs): keep three significant digits for a lab value below one 2026-10-14 19:57:34 +02:00
Marc-André Bombeck 784ea4ca9d Merge branch 'v1423/review' into release/v1.42.3 2026-10-14 19:57:04 +02:00
Marc-André Bombeck fcced06fa7 fix(labs): never open a small lab value as zero in the edit sheet
The edit seed rounded every value to two decimals, so a TSH of 0.004 opened
as 0. Values at or above 1 keep the two-decimal display precision; below 1
the seed keeps three significant figures, written without exponent
notation.
2026-10-14 19:50:26 +02:00
Marc-André Bombeck 03e80c1a94 fix(labs): drop the printed range text when an edit retires the conversion
Changing the value of a converted reading cleared sourceValue and
sourceUnit but kept the printed range text, which is written in the
report's unit. With the unit gone every reader presented it as the marker
unit's window, beside numbers in another unit. The text now goes with the
conversion record (a whole-form echo of it does not bring it back); the
bounds, already converted into the marker's unit, stay.
2026-10-14 19:49:33 +02:00
Marc-André Bombeck 530d1a6be2 fix(oura): keep the scope note when a run never proved the token
The refresh-abort paths (no client credentials, refresh token lost to a
peer) and a run where no collection succeeded wrote an empty scope list,
clearing the reconnect hint until the next good run. Such a run learned
nothing about the grant, so the stored note now stands.
2026-10-14 19:47:37 +02:00
Marc-André Bombeck 55ddad3d6a fix(oura): read a 401 as a scope gap only on the scope-gated collections
A 401 on any collection next to a success was recorded as a scope gap, so a
real auth failure on sleep, readiness or activity was hidden from the
failure ledger and shown as a reconnect hint. Only spo2, vo2max, cardio_age
and resilience sit behind a scope beyond daily; a 401 anywhere else keeps
its normal failure handling, and a stored note naming another collection
no longer skips the token refresh.
2026-10-14 19:47:05 +02:00
Marc-André Bombeck c7164be0d3 fix(withings): keep the plan re-probe date on a run that only narrowed
A run that skipped remembered types and met one more refusal restamped the
leg's checkedAt, pushing the weekly re-probe of the remembered types out on
every such run. Only a re-probe, or a leg with nothing remembered yet, now
starts the week again.
2026-10-14 19:46:03 +02:00
Marc-André Bombeck 6feaf6849a fix(withings): keep weight and blood pressure when every request is refused
A run where Withings refused every requested type stored all of them, core
types included, as outside the plan for a week; later runs then requested
nothing and moved the watermark on with no readings, losing the gap. A
refusal is now remembered only from a run in which another type was
accepted. A run that read nothing keeps the watermark and reports that it
did not land, and the watermark also holds while plan memory skips a core
type (weight, blood pressure, pulse, free body composition).
2026-10-14 19:45:38 +02:00
Marc-André Bombeck 8ce6a83da5 fix(withings): never remember a type the plan probe could not resolve
When the request budget ran out mid-split, every subset not yet tried was
stored as refused, so free types (temperature, SpO2, body composition) could
be skipped for a week and named as withheld by the plan. Those types are now
unverified: not remembered, asked for again on the next run, and the measure
watermark holds until every type was read or proven refused.
2026-10-14 19:44:05 +02:00
Marc-André Bombeck e9be8e0a5f chore(bundle): raise the aggregate cap to 4790 on a measured delta 2026-10-14 19:42:40 +02:00
Marc-André Bombeck 787aafaee1 docs(changelog): list only the analytes the registry converts 2026-10-14 19:41:19 +02:00
Marc-André Bombeck 9d167069db chore(release): v1.42.3 2026-10-14 19:35:25 +02:00
Marc-André Bombeck f564c44753 chore(labs): drop the pending-consumer markers now that the readers have landed 2026-10-14 19:35:00 +02:00
Marc-André Bombeck 42b66b2228 Merge branch 'v1423/labs' into release/v1.42.3 2026-10-14 19:34:44 +02:00
Marc-André Bombeck 035ae37ee4 Merge branch 'v1423/integrations' into release/v1.42.3 2026-10-14 19:20:16 +02:00
Marc-André Bombeck ec57ae10b1 docs(labs): describe the registry listing by what uses it
Rewords a code comment that referred to the work by its planning step, and a
loop counter's comment, to say what the code does.
2026-10-14 19:20:01 +02:00
Marc-André Bombeck eabbe3e032 fix(labs): let the conversion badge in the scan review wrap
At 390 px the badge truncated, and the part it cut off was the value the
report printed. It now wraps inside the row like a sentence.
2026-10-14 19:19:44 +02:00
Marc-André Bombeck 65669e52d2 fix(withings): bind the plan-state timestamp instead of reading the database clock 2026-10-14 19:19:27 +02:00
Marc-André Bombeck 350fd1b767 fix(labs): name calcifediol as the source of the vitamin D factor
The note said the factor was taken from cholecalciferol (C27H44O2). Vitamin
D3 is C27H44O; C27H44O2 is 25-hydroxyvitamin D3 (calcifediol), the form the
assay reports and the one whose molar mass gives 1 ng/mL = 2.496 nmol/L. The
factor was right; the note now says what it rests on.
2026-10-14 19:19:15 +02:00
Marc-André Bombeck 07f09e1b61 fix(labs): store the catalog slug as a marker's analyte key
The matcher stored the registry entry's key, so LDL and HDL markers were
recorded as total-cholesterol. The key now names what the marker is (the
catalog slug, or a registry-only key for an analyte with no seed), and the
registry resolves ldl and hdl to the cholesterol entry for the conversion as
before. A key already written in the old form is rewritten by the same fill
step that brings a stale key in line with its name, the first time a reading
needs it. The OpenAPI descriptions say what the key is and that it follows
the marker's name and unit.
2026-10-14 19:18:49 +02:00
Marc-André Bombeck cd1449e63e fix(labs): keep a reading's conversion record unless its value actually changes
The single-reading edit cleared sourceValue and sourceUnit whenever the body
carried a value, and the web edit sheet always sends one, so a date or note
edit erased the "converted from" hint. It is now cleared only when the value
differs from the stored one. The sheet also opened a converted reading at its
stored precision (100.90191999999999); it now starts from the value as the lab
surfaces show it and leaves an untouched value out of the request, so the
stored full-precision number is not replaced by its rounded display.
2026-10-14 19:15:33 +02:00
Marc-André Bombeck 47a91d6922 fix(labs): never present a converted reading's printed range in the wrong unit
A converted reading's bounds move with its value, but the printed range text
stays in the unit the report used, and every reader preferred that text: the
list showed "Report range 3.9 to 5.5" beside 100.9 mg/dL, and the doctor
report and the FHIR referenceRange.text did the same. The text is still
stored verbatim. The range resolver now takes the reading's sourceUnit (as a
required field, so no reader can forget to select it) and, for a converted
reading, leaves the window's text empty so every surface states the converted
bounds in the marker's unit, and hands the printed text over separately with
its unit. The lab DTO carries it as printedReferenceText, and the note beside
a reading shows it as "as printed in mmol/L: ...".
2026-10-14 19:10:49 +02:00
Marc-André Bombeck 4793edb961 test(integrations): pin the Withings plan handling and the Oura scope gap; document the plan change 2026-10-14 19:08:22 +02:00
Marc-André Bombeck 9c5fa6d14a Merge branch 'v1423/fixes' into release/v1.42.3 2026-10-14 19:05:56 +02:00
Marc-André Bombeck 044f9008ba fix(oura): ask for the spo2 and heart_health scopes, explain a missing scope calmly
Oura's current authorization server enforces scopes per collection.
daily_spo2 needs spo2; vO2_max, daily_cardiovascular_age and
daily_resilience need heart_health. The authorize request now asks for
both.

A grant issued before keeps answering 401 on those four collections
while every other one succeeds. Such a 401 next to a success is now
recorded as a scope gap rather than a failure: it no longer counts
toward the reconnect threshold, it no longer forces a token refresh once
known, and the Oura card asks for one reconnect instead. The Withings
card names the readings the account's API plan withholds.
2026-10-14 19:01:02 +02:00
Marc-André Bombeck 8b18b1c9fe test(e2e): fail clearly on a medications POST body that is not JSON
The route stub parsed the body inside the un-awaited route callback, so
a malformed body threw there and the request hung. The parse error is
now recorded and the route is fulfilled; reading capture.postBody in
the spec throws with the parse error.
2026-10-14 18:59:33 +02:00
Marc-André Bombeck cc0313c955 fix(labs): re-derive a marker's analyte key when its name or unit changes
The key is a function of the marker's name and unit, but it was only ever
filled, never replaced, so a marker renamed from Glucose to Cholesterol kept
converting readings with the glucose factor. The marker edit now re-derives
it with the same match the creation paths use (clearing it when the new name
matches nothing), and the fill step a reading runs brings a stored key back in
line with the name, so a key written before this fix is corrected the first
time it matters. The scan review derives the key the same way as the commit.
2026-10-14 18:59:32 +02:00
Marc-André Bombeck 63882bf3ec fix(labs): give the draw date a full-width row
The lab form placed the draw date in the second column of a two-column
grid. DateTimeField sets its date and time halves side by side from sm
up, so the date half was squeezed until a date could only be set
through the calendar. The value and the date now stack in one column,
as in the measurement form, and the date can be typed again.

Refs #1214
2026-10-14 18:58:55 +02:00