Commit Graph
8658 Commits
Author SHA1 Message Date
Marc-André Bombeck cc8ea3dfbb feat(measurements): accept indoor CO2, temperature and humidity
Room climate from a home automation system gets three types of its own:
INDOOR_CO2 (ppm), INDOOR_TEMPERATURE (celsius, shown in Fahrenheit for
imperial readers) and INDOOR_HUMIDITY (percent). They arrive through the
existing ingest token (measurements:write, source EXTERNAL), sit in the
environment cluster beside daylight, and the daily rollup carries their
mean, low and high like any spot reading. They never mix with the body
temperatures or with the outdoor environment channels.

The Home Assistant page documents the three types, a rest_command that
sends a room as one request, a single-room automation, and an
importable blueprint (docs/integrations/blueprints) that skips sensors
reporting unknown or non-numeric states, converts Fahrenheit, and
stamps readings to the minute so a retried push is refused rather than
written twice.
2026-10-21 20:26:15 +02:00
Marc-André Bombeck b76826a5f8 test(workouts): call the route opt-in handlers with their real arity 2026-10-21 20:20:10 +02:00
Marc-André Bombeck b045703493 feat(apple-health): ingest the activity rings, mindfulness, State of Mind and AFib burden
Six Apple Health signals leave the deferred list and ride
POST /api/measurements/batch and the export.zip import end to end:
EXERCISE_MINUTES, STAND_MINUTES and STAND_HOURS (the exercise and stand
rings), MINDFUL_MINUTES, STATE_OF_MIND_VALENCE (iOS 18 State of Mind,
-1..1) and AFIB_BURDEN (AFib History, percent of the week). The ring
types and mindful minutes are cumulative, so the daily rollup is their
sum; AFib burden is scaled strictly from Apple's fraction and a value
above 1 is refused rather than guessed at. The archive import takes
mindful minutes from the session's dates and skips idle stand hours.
Every registry carries them: units, ranges, labels in all locales,
categories, chart tokens, report catalogue (mindful minutes and the
valence fenced as sensitive), signal registry with FHIR codings, and
MCP exposure for the three ring types only.

Workout GPS tracks are now an opt-in, off by default
(PATCH /api/auth/me/workout-routes, a switch on Data & Privacy). While
it is off, the workout batch route and the export import keep every
workout and drop its track with the same per-entry status. With it on,
the import reads the referenced GPX files from the archive one bounded
member at a time, thins each to the point cap and stores it sealed.
The climb a track carries is written to Workout.elevationM at write
time, so the workout insight no longer opens the track, and the AI
egress guard now freezes the set of files that read route geometry.
2026-10-21 20:14:20 +02:00
Marc-André Bombeck d44af06467 docs: publish the UI standards in docs/contributing
The binding UI rulebook (card anatomy, spacing scale, text colour,
tokens, primitive mapping, enforcement) lived outside the repository,
so a contributor could follow a code comment to a file they could not
open. It now lives at docs/contributing/ui-standards.md, every comment
that cited it points there, and the docs index and ui-guidelines.md
say which document wins when they disagree.
2026-10-21 19:37:22 +02:00
Marc-André Bombeck 07f9615dba feat(ops): name 1536 MB as the supported memory floor
The getting-started requirements now say a host needs 3 GB of RAM, 4 GB
recommended, and drop the advice to lower APP_MEMORY_LIMIT to 1g on a
2 GB host. The compose and .env example comments say the same.

At boot the app reads its cgroup memory limit (v2, then v1) and logs one
line when it is below MEMORY_FLOOR_MB. The support report adds the
container limit and a note below the floor, and Admin > System status
shows "below the supported floor" next to the existing low-heap flag.
The app keeps running either way.
2026-10-21 19:36:06 +02:00
Marc-André Bombeck 9941db1549 refactor(insights): drop the readable cached-text column
Every write has gone to insights_cached_text_encrypted since 0411, and
the boot-time free-text backfill sealed the rows written before it.
Migration 0430 drops insights_cached_text and the backfill index that
served it. A host that skipped the intervening releases loses only a
cache of model output; the next warm writes a fresh, sealed payload.

The cipher module now reads and writes the sealed column only, the
backfill job loses its insights step, and consent withdrawal, the wipe
plan, the backup plan and the session projection follow the schema.
Backups never carried the column, and restore ignores unknown account
settings; a round-trip test now proves an old backup naming the field
still restores cleanly. The access guard pins exact counts for where
the sealed name may appear. Fixtures, the demo seed and the demo
migration script seed the sealed column instead of plaintext.
2026-10-21 19:24:21 +02:00
Marc-André Bombeck 45da8ae971 Merge pull request #1260 from MBombeck/release/v1.43.11
Release v1.43.11
v1.43.11
2026-10-21 17:54:10 +02:00
Marc-André Bombeck aaf826290f test(e2e): follow the checklist's visit link to the visits tab 2026-10-21 17:27:53 +02:00
Marc-André Bombeck 2ec62e9d66 chore(notifications): drop the unused re-export of the defaults input type 2026-10-21 17:01:30 +02:00
Marc-André Bombeck ce2a909ed5 fix(ai): write no key-removal audit for a slot that held no key 2026-10-21 16:50:14 +02:00
Marc-André Bombeck a3f5f998cf fix(report): end the visit offer on the record's day from the server
The preparation window now carries to, the booked visit's own day on
the record's calendar, and the report offer uses it. Cutting the end in
the reader's zone was wrong for a guardian viewing a managed profile in
another zone.
2026-10-21 16:50:14 +02:00
Marc-André Bombeck fdecefd1d3 fix(hrv): match an RMSSD twin on both unique identities in 0420 and the restore
Migration 0420 looked for an RMSSD twin by externalId only. A Fitbit or
Google Health SDNN row with no externalId, or another one, on the same
night as an RMSSD row would have broken the measured-at unique key and
aborted the block at boot. The twin check now matches either identity,
a row left behind by a live twin is tombstoned, and an RMSSD tombstone
that would make a live row look twinned is removed so the row moves.
The backup restore applies the same rule and no longer counts a
tombstone as a twin.
2026-10-21 16:50:13 +02:00
Marc-André Bombeck 4dd70ee60c fix(withings): give up a sleep night that keeps failing, and fail the leg only on recent nights
A night Withings answered with an error on every read kept the
failed-first slot and failed the sleep leg on every run, holding the
connection in an error state and paging for as long as the night stayed
in the window. Failed reads now count per night and a night is given up
after three, like an unstaged one (a full sync still reads it). A
failed read of a night older than 48 hours, while the rest synced, is a
warning instead of a leg failure.
2026-10-21 16:50:13 +02:00
Marc-André Bombeck d24b24473a chore(release): v1.43.11 2026-10-21 16:26:43 +02:00
Marc-André Bombeck bc2d237530 fix(visits): anchor the notable window at the next visit's preparation
Without from, GET /api/day/notable started at the last completed visit
of any kind, while the web passes the next planned visit's
preparation.from, so a client that omits from read a different list for
the same appointment. Without from the window now starts at that
preparation day (anchor nextVisit, documented in OpenAPI); with nothing
booked it keeps the last completed visit or the 90-day fallback.
2026-10-21 16:25:26 +02:00
Marc-André Bombeck 6bdd6f1c58 fix(withings): keep the sleep nights read around a failing one
One failing per-night stage read threw out of the loop, so the nights
already read and the summary vitals were never written. A failed read
now leaves the others to be written, books the leg failed afterwards
and marks the night to be read first next run. A night that answers
without a stage three times is no longer read on every run; a full sync
still reads it. The per-night record lives on the connection (migration
0421) and is reset on reconnect.
2026-10-21 16:25:26 +02:00
Marc-André Bombeck a04a8352f5 fix(withings): keep a brand-18 activity day no other source delivered
Skipping every day Health Mate holds from another app (brand 18) lost
the steps of an account whose phone steps only Health Mate reads. Such a
day is now skipped only when another connected source (not Withings,
not a hand entry) has steps on that local day; otherwise it is kept.
2026-10-21 16:25:26 +02:00
Marc-André Bombeck bfb8711f1d test(apple-health): order the own-sample rows deterministically
Rows of one type came back in insertion order only by chance, so the
three-weight case failed intermittently. Sort by type, time and value.
2026-10-21 16:25:25 +02:00
Marc-André Bombeck 8e2e4f658b fix(backup): restore pre-0420 Fitbit and Google Health HRV as RMSSD
A backup written before migration 0420 files Fitbit and Google Health
HRV as HEART_RATE_VARIABILITY, and the restore wrote it back verbatim,
undoing the migration for the account (and able to duplicate an
HRV_RMSSD row of the same night, since type is part of the unique key).
The restore now applies the same move: those rows come back as
HRV_RMSSD, a legacy row whose RMSSD twin the file also holds is skipped,
and SDNN personal records set by them are left out as the migration
deleted them.
2026-10-21 16:25:25 +02:00
Marc-André Bombeck 65c296d430 fix(ai): audit setting and removing a stored provider key
Removing a provider key, like setting one, left no audit row, unlike
every other credential change. Each key slot a PATCH touches now writes
user.ai-provider.key.set or .remove with the slot and never the value.
2026-10-21 15:07:06 +02:00
Marc-André Bombeck e94257928a fix(notifications): fall back to the shared defaults on an older payload
A preferences response cached by a build before eventDefaults existed
drew every default-on event as off in the matrix until the next refetch.
The no-row rule moves to a client-safe module the server read still
wraps, and the matrix applies it when the map is absent.
2026-10-21 15:07:05 +02:00
Marc-André Bombeck 88200a9273 fix(report): cut the visit offer's end day on the record's calendar
The offer took its start from the server's preparation window, cut on
the record's calendar, and its end from the appointment's day in the
browser's zone. In a browser in another zone a late-evening or
early-morning visit moved the end a day against the start. Both ends now
use the record's zone.
2026-10-21 15:07:05 +02:00
Marc-André Bombeck 03cacc996c fix(profile): one display-name bound, checked after trimming
The setup flow capped a display name at 50 characters while the profile
took 80, and the profile checked its minimum before trimming, so a name
of only spaces was stored as an empty string. Every surface now reads
one bound (80), the profile trims before validating, and only spaces
means no name on both paths: the profile clears it to null, the setup
flow leaves it unset.
2026-10-21 15:06:57 +02:00
Marc-André Bombeck a264dee030 fix(settings): keep the end-access button while the grant list loads
The button for a grant that sign-out-everywhere left standing rendered
only once the grant list (which carries the audit window its
confirmation names) had loaded, so while it loaded, and for good when
the read failed, the row explained the access but offered no way to end
it. The button now shows disabled until the list arrives, and a failed
read draws the standard error row with a retry.
2026-10-21 15:06:47 +02:00
Marc-André Bombeck b53434daa0 fix(fitbit): show the retired card only to an account that had Fitbit
After the Web API turndown every account still saw a Fitbit card that
can no longer connect. The status entry now publishes
retirement.offered: false once the API is gone for an account with no
Fitbit connection, no Fitbit app of its own and no Fitbit data, and the
connections panel leaves the card out. Accounts that had Fitbit keep it
with the retirement notice.
2026-10-21 15:06:40 +02:00
Marc-André Bombeck bf3830203f fix(hrv): keep RMSSD on the display surfaces for an account with no SDNN
Fitbit and Google Health HRV is filed as RMSSD since migration 0420, so
an account whose only HRV comes from them lost it wherever a display
surface read SDNN alone. The dashboard HRV tile keeps its RMSSD fallback
with Recovery off while the account has no SDNN, and its toggle stays
offered for either estimator. A briefing HRV signal counts as measured
today when either series has a reading. The doctor report already files
RMSSD as its own ungated leaf; a guard now holds that. Readiness, HRV
balance and stress stay on SDNN; a test pins that an RMSSD-only account
renormalises over the other components.
2026-10-21 14:32:56 +02:00
Marc-André Bombeck 8d16799479 Merge branch 'fix/v14311-visits' into release/v1.43.11 2026-10-21 14:13:52 +02:00
Marc-André Bombeck eabfeaef08 Merge branch 'fix/v14311-settings' into release/v1.43.11
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/nl.json
#	messages/pl.json
2026-10-21 14:13:51 +02:00
Marc-André Bombeck eaaff23858 Merge branch 'fix/v14311-integ' into release/v1.43.11 2026-10-21 14:13:37 +02:00
Marc-André Bombeck 1213a12c47 Merge branch 'fix/ocr-limits' into release/v1.43.11 2026-10-21 14:13:36 +02:00
Marc-André Bombeck c456a236a7 fix(analytics): rank Health Connect and the hand-entered sources on the ladders
A source a ladder does not name ranks 90, below every source it does.
Five had no place anywhere: a weight typed as a reply to a Telegram
reminder lost its day to a WHOOP strap's estimate, one an assistant
logged over MCP lost to a Fitbit estimate, and a Health Connect day
lost to a hand-typed count.

Health Connect now sits right after Apple Health on the ladders for
what it imports; EXTERNAL, MCP and TELEGRAM sit right after MANUAL, in
the alphabetical order they already broke ties in, so MANUAL still
outranks the person's own bridge. Relative to each other the sources
the ladders already named keep their order, and saved ladders gain
the new entries at their end as before.
2026-10-21 13:07:34 +02:00
Marc-André Bombeck d02fba6509 fix(withings): skip activity days Health Mate holds from another app
Getactivity marks a day that came from outside Withings with brand 18
("external"): an Apple Health or Google Fit day written into Health
Mate. Imported under WITHINGS it was a second copy of data HealthLog
reads from that app's own integration, and on a day it was the only
Withings row it could outrank a native source lower on the ladder.
Such days are now skipped at ingest and counted on the wide event.
2026-10-21 12:59:44 +02:00
Marc-André Bombeck c1921d1dd1 fix(withings): read the sleep stages night by night
Sleep v2 Get answers only the first 24 hours after its startdate. The
sync asked for the whole 30-day window, so it got the night a month
back and nothing since: every night's stages arrived when the sliding
window reached it, weeks late.

The summary now runs first and lists the nights; each night's stages
are read over its own span (at most 24 hours). Every run re-reads the
nights that ended, or that Withings changed, in the last 48 hours, and
fills older nights that hold no stage row yet, eight per run with a
rotation so a night without stages cannot hold up the rest; a full
sync reads them all. Without the night list the last 24 hours are read
as one slice and nothing is swept, since a slice can cut a night.

A summary failure used to be a warning beside a green leg. It is now
the sleep leg's failure, recorded after the stages land.
2026-10-21 12:59:43 +02:00
Marc-André Bombeck 3b1cdd7a1f fix(strava): keep a rate-limited run's progress and a summary-only row's calories
A 429 in the middle of the activity walk threw out of it before the
upsert, so everything the run had read was discarded and the next run
started over. The walk now ends where the limit hit: what was read
lands, the cursor moves only as far as an oldest-first walk proves,
the failure is booked transient, and the run reports the wait Strava's
X-RateLimit / X-ReadRateLimit headers name (next quarter hour, or
midnight UTC once the daily count is spent). The backfill waits that
long before its next run. `walkPages` learns an `interrupted` page for
this, reported truncated like a walk cut off at its ceiling.

An update mapped from the summary alone (detail budget spent, or the
detail read failed) wrote a null over the calories an earlier detail
read had stored, and over its heart rate and description too. Those
detail-only fields are now kept unless this run read the detail.
2026-10-21 12:51:19 +02:00
Marc-André Bombeck ba1271275f feat(fitbit): stop at the Web API turndown and point to Google Health
Google turns the legacy Fitbit Web API off on 30 October 2026 (support
ended on 30 September). From that day the hourly poll, the backfill,
the sleep repair and the token refresh make no call, the connect, sync
and test routes refuse with `provider_retired`, and the connection
reads as not connected to the health read and the watchdog, so the
shutdown pages nobody and asks nobody to reconnect. The connection row
and everything it imported stay.

The status envelope publishes `retirement` on the Fitbit entry (the
day, whether it has come, the successor), and the Fitbit card shows
when the connection stops, how to move to Google Health (same devices,
one more consent, tokens cannot be carried over) and that the data
stays; sync and connect grey out after the day. Both integrations can
run side by side meanwhile: every type Fitbit writes is laddered with
Fitbit just above Google Health, so a day both deliver counts once.

The test setups pin the turndown off, so the Fitbit suites do not turn
red on the day; the turndown's own tests flip it.
2026-10-21 12:46:14 +02:00
Marc-André Bombeck 2914832ed8 fix(visits): keep the appointment rail action's wire tokens for the shipped app
The shipped iOS app routes the rail by intent and resolves the label
from its own catalogue; a new `visit.view` intent would leave its tap on
an appointment inert. The upcoming-visit action keeps `checkup.view`
and `daily.action.viewCheckups` on the wire and only its `href` moves
to the visit. The web card names what that link opens: "View
appointment", or "View appointments" for the visits list.
2026-10-21 12:38:08 +02:00
Marc-André Bombeck 27a59c29fd fix(records): let an emergency select go back to not recorded
The blood type, organ donor and advance directive selects had no empty
option, and the save left out an empty select, so a value once chosen
could be changed but never removed. Each select now offers "Not
recorded", and a select the person changed rides the save, as null when
it was cleared; an untouched one stays out of the request.
2026-10-21 12:36:15 +02:00
Marc-André Bombeck 1e606834d8 fix(settings): let the display name change after the setup flow
The setup flow asks for the name the greeting and the reports use, and
the profile route has always accepted it, but no settings surface offered
the field and /api/auth/me did not publish it. A name chosen on the
first day stayed for good.

The account payload now carries `displayName`, the profile card shows the
field beside the username and saves it with the rest of the profile, and
an emptied field clears the name.
2026-10-21 12:36:15 +02:00
Marc-André Bombeck 6037ebd3d6 fix(settings): let a saved AI API key be removed
Every personal provider form showed a stored key and offered no way to
take it back, since saving with an empty key field keeps the stored one.
The OpenAI, Anthropic, local model and gateway forms now carry a
confirmed "Remove API key" while a key is stored. It sends the existing
PATCH /api/user/ai-provider with that one key as null, which clears the
column and leaves the provider choice, model and the other keys alone.
2026-10-21 12:36:14 +02:00
Marc-André Bombeck 41cf584a65 fix(settings): say what the AI provider picker does
The select on the AI provider card read "Primary provider" under a card
that said it held the provider HealthLog asks first. It only picks which
provider's key and model the form below edits, and saves nothing; the
order of the fallback chain decides who answers first. A person who
picked a provider there expected it to answer first and it did not.

The picker and the card description now say what they do and point at
the chain for the order.
2026-10-21 12:36:14 +02:00
Marc-André Bombeck 2bb3783b14 fix(settings): report the outcome of quiet settings actions
Adding or removing a passkey or a security key only redrew the list.
Disconnecting an integration and revoking a share link ran from a dialog
that had already closed, so a refused request left everything as it was
without a word. The injection-site exclusions, the local OCR switch and
the preventive-care reminder switches saved, or did not, in silence.

Each now reports through the outcome toast and, where the card has no
inline error line, the shared error toast with its error id.
2026-10-21 12:36:14 +02:00
Marc-André Bombeck b6e3b47bec fix(settings): never leave a card saving after a failed request
Six settings cards raised their own saving flag, awaited a raw fetch and
lowered the flag on the next line. A request that never reached the
server rejected instead, the line never ran, and the card kept its
control disabled until a reload: the Telegram form, the mood reminder,
the low-stock threshold, the lock-screen detail switch, the Coach nudges
and the profile form.

A network failure now falls through to the card's own error line, and the
flag is lowered in a finally. A guard holds every hand-rolled pending
flag under settings to that shape.
2026-10-21 12:36:14 +02:00
Marc-André Bombeck 6a9543b4f8 fix(settings): confirm before ending a grant from the sessions card
After sign-out-everywhere the sessions card lists the access grants it
left standing, each with an end button that revoked on the tap. It is the
same act the access card confirms, permanent for the other account, so it
now asks the same question with the same disclosure, audit window
included, and the row names the account the way the access card does.
2026-10-21 12:36:14 +02:00
Marc-André Bombeck 99fec79177 fix(visits): start the visit preparation and its report at the matching visit
Both surfaces read the start of their window from `preparation` and say
why it starts there: "Since your last visit with <practice> on <date>",
"Since your last <specialty> visit on <date>", "Since your last visit
of the same kind (<kind>) on <date>", or "The last 90 days" with the
reason that no earlier visit matched. The visits tab no longer picks
the last visit of any kind from the past list.

The report panel's appointment offer now sets both dates, the start
from the server and the end on the appointment's day, instead of the
end alone beside whatever start the panel held. Both stay editable.
2026-10-21 12:32:26 +02:00
Marc-André Bombeck 402cf1a35a feat(visits): resolve where the window before a booked visit starts
The visit preparation and the doctor report offered for an appointment
started at the last visit of any kind. An internist one week and a
dermatologist the next gave the dermatologist a one-week window of
nothing it is about.

The list read now publishes `preparation` on every booked upcoming
visit: the first day of the window, the rule that chose it and the
earlier visit it is the day of. First match wins over the visits that
took place before it: the same practitioner, else a practitioner of
the same specialty (free text, compared without case or spacing), else
the same kind for routine, preventive and therapy visits, else the last
90 days. One query of past visits per page, independent of the list's
own filters. Null on every other row; absent on the detail and on
write responses.
2026-10-21 12:32:12 +02:00
Marc-André Bombeck 4c50f48b26 fix(labs): name a failed re-check as an error and close the last budget gaps
A re-check or reservation that failed itself stopped the scan as
"unavailable", and the review screen said AI had been turned off. It now
stops as stoppedBy error with its own message in every locale, and the
failure is reported as one: an error signal and the worker error
reporter. A scan that failed that way before any row answers 500
aiRuns.failed.

The overspend booked before a further read is now net of the cached
share, as the settlement books it. A reservation that names no known
owner stops the scan instead of taking the user plan's larger cap.

A page's text layer is cut at the last whole line; only a page whose
first line alone runs past the cap keeps part of it, cut at a space or
hard, rather than collapsing to the bare cut mark, and an endless line is
no longer read past the cap. The unused per-call text limit is gone. The
run result's description names every outcome a stopped scan can have.
2026-10-21 12:28:32 +02:00
Marc-André Bombeck 1fc0268b3d fix(polar): clear each leg on its own success, read a lone 403 as refused
The workouts leg recorded its failures under its own leg but left the
success to the vitals leg. A leg-scoped success clears only its own
leg, so one workouts failure stayed on the row for good and kept the
connection in error however healthy the exercises read became. Each
leg now records its own success.

AccessLink documents its 403 as missing consent for that data. A 403
on one collection while a sibling succeeded with the same token was
classified reauth_required and put a working connection behind a
reconnect prompt no reconnect clears. Like Oura's scope refusals, such
a collection (or the exercises read, once the vitals leg proved the
token) is now noted on the wide event and not counted as a failure. A
403 on every collection, or any 401, stays one.
2026-10-21 12:23:57 +02:00
Marc-André Bombeck 0dcafeeb70 fix(visits): open an upcoming appointment where it lives
Visits are the second tab of `/checkups`, and the bare path opens the
first one, the preventive-care list, which never shows an appointment.
Every surface that listed an upcoming appointment linked it there: the
dashboard's next-appointment line, the Today rail item, the Today
overview fact and the appointment's push notification. A tap on the
appointment landed on the list it is not on.

Each now opens the visit itself (`/checkups?visit=<id>`), or the visits
tab (`/checkups?view=visits`) for a line naming several appointments.
The checkups page reads `view` to pick a tab. The rail item stops
borrowing the check-up item's action: it carries its own label
("View appointment") and intent `visit.view`. The appointment push names
its visit through the producer's `metadata.url` override; a check-up's
push keeps the checkups list.

The same mis-route sat on three links that lead to visits without
being upcoming: a condition's visit rows, the address book's back link
and the onboarding and timeline prompts to add a visit.

`visit-link-guard.test.ts` keeps the visit paths in the builder and
counts every bare `/checkups` per file, each with the reason it names
the checkups list.
2026-10-21 12:22:36 +02:00
Marc-André Bombeck 78bacb0d86 refactor(visits): build every link to a visit in one place
Seven surfaces spelled `/checkups?visit=<id>` by hand, some encoding
the id and some not. They now share `visitPath()` from
`src/lib/encounters/visit-links.ts`, the place the next fix extends.
No behaviour change: a cuid encodes to itself.
2026-10-21 12:22:22 +02:00
Marc-André Bombeck a66c54a1f1 feat(insights): scan the nightly RMSSD series for correlations and illness
HRV_RMSSD was read nowhere HRV is analysed: the correlation scan and the
illness engine only knew the SDNN series, so a strap or ring wearer's
HRV never took part in either. RMSSD is now its own outcome channel
(owned by the recovery module, like the summary tile) and its own
illness scan type with a drop read as adverse. It is not merged into
the SDNN channel: the estimators differ in scale, and one series of
both would correlate the change of device rather than the body.
2026-10-21 12:13:39 +02:00