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.
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
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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: ...".
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.
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.
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.
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
A quick answer whose request never reached the route left no trace: a
redirect in front of the app turns ntfy's POST into a GET, which Next
answered 405 before any wide event, and the worker-only and demo-mode
refusals in the proxy wrote nothing either. The proxy now answers a
non-POST on the action path itself with 405 and Allow: POST, and every
refusal it makes there writes one warn line with a reason and the path
without its token. The route's rate-limit refusal carries a reason too.
The regression test sends the action ntfy publishes, in the shape ntfy
sends it, through the real proxy and route against Postgres.
Refs #1203
A chronic ongoing condition has no end date, so counting it as rest mode
kept the account in it for good: Day N counted from an onset years back,
cadence nudges never resumed and every streak gap since the onset was
bridged. Only active episodes that are not CHRONIC_ONGOING (acute,
recurring, flare) now trigger rest mode, and Day N counts from the
earliest of those. The achievements streak freeze applies the same
filter to its own episode query.
Chronic conditions stay context: a coincident vital deviation is still
framed as explained by any active episode, and the Coach block and the
briefing still name them, with restMode false.
Refs #1216
Withings split its public API into plans on 2026-10-12. A request naming
one meastype outside the free plan is refused whole with status 277, so
every measure sync stopped importing anything, and the 200..299 reauth
range parked the connection at error_reauth.
The measure, sleep-summary and ECG legs now drop the documented advanced
types on a 277, split the rest until the refused keys are isolated
(bounded), remember the refusal per connection and re-probe weekly. 277
is classified persistent and never enters the ledger; connections parked
over it are lifted by the fallback tick, and the held watermark lets the
next run read the whole gap.
Slice 3 of phase 3 of #1095. The scan review no longer treats every reading in
another unit as unwritable. The server tells it which analyte the matched marker
is (markerAnalyteKey on each extracted row); a row whose unit differs but can be
converted starts selected and carries a badge saying what Save will store and
what the report stated ("Saved as 100.9 mg/dL, converted from 5.6 mmol/L"). A
row that cannot be converted keeps the existing blocked badge and error. Save
still sends what the report printed; the server converts, as in slice 2.
The review's duplicate flag also recognises a re-scan of a reading that was
stored converted, by what the report printed, the same rule the commit applies.
A converted reading carries a quiet "converted from 5.6 mmol/L" on the lab list
(row and tile) and the history list (phone and table), so a number nobody typed
can always be traced back to what the report printed.
Strings in all seven locales.
Refs #1095
Slice 2 of phase 3 of #1095. A reading that stated a unit other than its
marker's was refused. When the marker's analyte is known and the registry has the
pair, the three write paths now convert it instead: POST /api/labs, the OCR
commit and the document commit.
The converted value is stored at full precision in the marker's unit, with what
the report printed in the existing source_value and source_unit columns. The
report's own reference window is in the printed unit, so it is converted with the
value; otherwise the verdict would compare a converted value with an unconverted
window. A pair the registry does not have is still refused as before (the 422
now says why), as is a marker whose analyte is unknown.
The marker's key is filled the first time a reading needs it. The same scan
committed again is still a duplicate: a converted row holds the number in the
marker's unit, so the duplicate check also matches on what the report printed.
Editing the value of a converted reading clears the conversion record, because
the number is then the person's own.
The lab result DTO and the OpenAPI schemas (labs and the OCR commit) carry
converted, sourceValue and sourceUnit.
Refs #1095
Slice 1 of phase 3 of #1095. A Biomarker row keeps only its name, so nothing
said which analyte it was. This adds the explicit alias list in the catalog the
issue settled on, including the registry-only keys (urea, blood urea nitrogen,
uric acid, total calcium, total bilirubin), and sets biomarkers.analyte_key
(from migration 0385) from it.
matchAnalyteKey equals a marker's folded name against the localised names of the
catalog seeds the registry knows and against the alias list. It matches the
whole name only, so "Calcium (ionisiert)" is not "Calcium"; a name two analytes
claim matches neither; and the marker's own unit must be one the analyte reaches,
so a marker called "Glucose" in a unit the registry cannot convert stays
unlinked.
The key is set when a marker is created (the biomarker route, the mint used by
lab writes, the legacy backfill) and, for an existing marker, by
ensureAnalyteKey the first time a reading needs it. It never overwrites a key
that is set. There is no boot job for the existing markers: the conversion that
uses the key fills it on first use, and a sweep can be added later if wanted.
The key is returned on the biomarker API and in the OpenAPI schema.
Refs #1095
The day-strip journey opens /timeline?zoom=all, so closing the day
returns there, not to bare /timeline. An unreadable range now opens on
the default window, which for this record is three months.
The shared eligibility filter hid TRACKED_INTAKE_WHERE from the
structural guard that checks every schedule and intake reader honours
the switch. Each reader now spreads it itself.