Settings > Export & Import gains a card that reads the head of the file,
suggests the column for each field from its header in eight languages,
suggests the separator, decimal mark and date order, and lets every
choice be corrected. The check lists each refused row by line and the
markers the names land on; the import button names how many rows it
will write. Templates for both kinds download from the sheet.
Two streamed endpoints read a spreadsheet record by record with a
column mapping from the query: separator, decimal mark, date order and
zone are stated, never guessed per row. Lab rows resolve to markers the
way scanned reports do, convert units where the conversion is known and
keep what the file said; medication rows map name, dose, daily times,
as-needed and a dated window, checked against the create schema, with
reminders off unless asked for. A dry run writes nothing, a re-upload
only finds duplicates, and every refused row comes back with its line
and reason.
/api/health gives the database two seconds and answers 503 past that.
With three retries, a pool saturated for a minute after the start period
could mark the container unhealthy and take it out of the proxy's
routing. The compose healthcheck and the image's HEALTHCHECK now allow
five misses (two and a half minutes) and end wget after four seconds,
inside Docker's five, so a stalled request is one clean miss. A test
holds both to these bounds and to each other.
The UI standards drop working shorthand, state the spacing scale once in
section 2 (section 13 had listed an older scale without 2.5 and 3.5) and
use no em dashes. The v1.3 to v1.4 migration note no longer claims that
/settings#anchor URLs redirect to their section: the fragment never
reaches the server. A test holds both documents to this.
The Topics preview describes what it will add, the workout-route detail
says off for new accounts, and German reads 'Zurück zu den Modulen',
'Unter Module ändern', 'Unter Anordnung öffnen' and 'Erfassung' for
the Tracking group. Strings that name the Insights area call it
Insights; generic uses of Auswertung stay.
The room-climate guide said both to create one automation per room and
to track only one; it now recommends the bedroom and says why a second
room cannot work (no room name, same-minute pushes collide on 409). The
blueprint offers 5, 10, 15, 20, 30 or 60 minutes, so the push lands on
the stated minutes, and it and the guide's examples use the triggers,
conditions and actions keys of Home Assistant 2024.10 and later. A test
holds all three.
The Coach overlay had become a read-only line with a link to Settings >
AI, which took away switching the model mid-conversation. The provider
and model selects are back; they write through the same routes and the
same stored preference as Settings > AI, so there is still one setting,
and the link to Settings > AI stays below them.
The Telegram, ntfy, webhook and email cards had folded the enable
switch into the form, so turning a channel off took a Save as well.
The switch saves on tap again, optimistically with a rollback, and sits
outside the form; the form's Save covers the channel's fields and sends
the stored enable state unchanged.
The save-model guard now splits a card into save units, each form and
the rest of the card, and refuses a unit that mixes both models: an
instant switch inside the form its Save submits, or a Save outside any
form next to one, still fails. A form shows its refusal inline only,
no longer as a toast on top.
A read-only view per metric and UTC day: every source with readings, its
day value, its place in the person's source priority, and which source
the charts use with the reason. A second card lines two sources up over
30, 90 or 365 days. The pick is explained from the rank the canonical
SQL returns, so the page cannot disagree with the charts. Linked from
the metric pages and from Settings > Source priority.
The week label and each day's accessible name went through the
zone-aware formatters, which show a noon-UTC day key as the next day
east of UTC+12. Read them in UTC like every other stated date.
GET /api/fhir/Observation keeps its snapshot answer when no date is
sent. A date range (FHIR prefixes eq/gt/ge/lt/le, UTC when no offset)
switches it to the time series: every reading in the range plus
workouts, newest first, paged in the database through one UNION id
query and a count, so a page costs what it holds. Series ids are the
row's own (m-<id>, w-<id>) and read back through /Observation/{id}.
The series replays the same saved report selection as the snapshot,
pairs systolic and diastolic recorded at one instant into the
blood-pressure panel, leaves sleep stage segments out, and reads an
hourly heart-rate bucket as a mean over its hour. A code parameter
narrows either answer; a malformed date is a 400 OperationOutcome.
A workout is an activity Observation after the Physical Activity IG:
LOINC 73985-4 with the sport as its value and duration (55411-3) and
energy (41981-2) as components. RMSSD gets its own registry coding,
text-only because LOINC has no RMSSD term, so it never borrows the SDNN
code. The capability statement lists the two new parameters.
The standard report template now carries sleep duration, heart-rate
variability as two labelled rows (SDNN and RMSSD), daily steps, a
per-sport workout summary and the change around each medication start.
Each still rides its module, and the template is still only applied by
the button that names it; saved selections and share links are
untouched.
Two structured leaves join the catalogue: WORKOUTS (activity group,
workouts module) and MEDICATION_EFFECT (medications group). The workout
block reads narrow rows through the canonical-workout picker and sums
them per sport. The effect block reuses the efficacy loader with the
report's end as its horizon, carries a target only when that target's
own measurement leaf is admitted, and is capped at eight medications.
In the PDF the wearable series chart at half width, two to a row, in
the units the table prints; the workout and effect blocks are compact
tables with short notes. The clinician view renders both blocks from
the same payload. RMSSD joins the dense day-bucket path so long windows
stay aggregated.
A same-page link to a moved anchor changes only the hash, so the
arrival check never forwarded it; the shell now also listens for
hashchange. The legacy-routes guard walks the component tree of every
forward target's page and fails when the anchor it forwards to is not
rendered there.
The efficacy tab keeps its before/after comparison and gains, per metric
target, the level change and the change in weekly trend at the start,
each with a 95 % interval. The fit covers up to 90 days before and 180
after the start and ends before the first dose change. After the start,
days are left out when fewer than 80 % of the doses of the preceding 14
days were taken (cadence-aware, from the dose ledger) or when the
medication was paused or between courses. Other medications that
started, stopped, changed dose or paused inside the window are listed.
Below 14 days with readings across 21 days on either side the view says
what is missing instead of showing numbers.
The efficacy response gains a nullable segmented block per target.
The checkups page links to the checkup reminders under Notifications
and the screening hints under the health profile, the two cards that
left its settings in this release. Data & privacy gets a row pointing
at sessions and devices under Security.
Both routes resolved through bare requireAuth, which refuses a session
acting for another record, so the list view controls and the preview
switch failed in exactly the context they are used in. They are the
person's own presentation and now resolve through requireActorAuth,
admitted on the actor-surface list with their reasons.
The layout PUT and the preview PATCH read and write the stored blob
under a row lock in one transaction, so two saves at once no longer
start from the same value and drop each other's change.
A four-parameter interrupted time series fit (pre-trend, level change,
slope change) by OLS, with a Bartlett-kernel HAC covariance, the n/(n-k)
small-sample factor and Student t intervals. The tests pin the
coefficients, standard errors and intervals to independently computed
reference values on contiguous and gapped series.
Posts one batch for two accounts with the same stored rows, once as the
route runs and once with the set-based insert forced onto its fallback,
and asserts identical per-entry outcomes and stored rows across fresh
samples, a stored duplicate, a tombstone, a stats: overwrite, an
external id twice in the batch and a natural-key collision.
0440 adds its columns with ADD COLUMN IF NOT EXISTS like 0424 and
0432; a structural test holds every migration from 0422 on to that
form. 0430 clears insights_cached_at where no sealed payload is left,
so the nightly pregenerate refills an account whose only copy was the
readable column instead of keeping an empty card fresh.
The deploy runbook notes that 0430 drops a users column: an old process
fails on its next full user read, so stop before starting rather than
running old and new side by side.
A week or month of steps, active minutes, workouts by sport and training
load, each day placed against the person's own spread over the 84 days
before the period and each period against the median of the twelve weeks
or six months before it. Optional weekly and monthly goals live on the
account (activity_goals_json), travel with the backup and are cleared by
a data wipe. The calendar sits on the workouts page; the steps page links
to it. Computed server-side without AI.
The server answers before pg-boss is bound, and a producer rebinds in
the background after losing its connection. A request in either window
read the boss as null and skipped its enqueue: OAuth callbacks lost the
first sync and the history backfill, measurement and mood writes lost
their rollup recompute, WHOOP webhook deliveries were dropped.
whenBossReady() waits for the boss while a start is under way (boot or
reconnect), bounded at 60 s and logged when it gives up, and answers
null at once when nothing is coming. enqueueWhenBossReady() sends at
once through a bound boss and otherwise defers the send to the
background, so the request is not held for the boot.
A guardian finishing a connect for a managed profile could attach their
own provider account to the profile. Every OAuth callback (Withings,
WHOOP, Fitbit, Google Health, Strava, Polar, Oura) now refuses a
provider account another record already holds with reason
already_linked, shown as its own message on the integrations page.
WHOOP's owner_conflict tag becomes already_linked as well.
Oura keeps the account id of a grant (personal_info, migration 0441) so
the check has something to compare; existing connections fill it at
their next connect.
A disconnect leaves Withings webhook subscriptions and the Strava app
authorisation in place while another record holds the same account,
and the Withings webhook routes an account held twice to the record
that linked it first instead of whichever row a lookup returned.
The provider app copied from a guardian onto a managed profile is now
marked (migration 0442) and removed again on disconnect or when that
guardian no longer holds a live MANAGE grant: revoke, renounce,
removal, handover, account deletion and data wipe. Each removal is
audited as integration.client_app.removed.
Migration 0434 turns the route opt-in on for every account that already
holds a stored workout track, so its tracks keep arriving after the
upgrade; only accounts without one start off.
Workouts stored before this release with a track but no elevation get
their metres climbed written into Workout.elevationM by a one-time
background pass. It runs detached at worker boot, reads one track at a
time with a pause between batches of 100, and keeps its position in
app_settings, so a restart resumes and a finished pass ends at once.
The "starting" health reason is bounded: after five minutes the probe
says plain degraded and the process logs one line, and the time the
background role took to come up is logged either way. The compose
healthcheck's ten-minute start period already keeps those probes from
counting; its comment now says so.
POST /api/measurements/batch ran one interactive reconciler transaction
per fresh sample: a savepoint, three advisory locks, two candidate reads
and the insert, about ten round trips. A 500-entry upload of new samples
was 500 of those, the 2 to 15 second outliers. Fresh samples now land
through INSERT ... ON CONFLICT DO NOTHING under the same per-account
advisory lock. Both measurement identities are full unique indexes, so
the statement inserts exactly what the reconciler would have inserted,
and every skipped row still goes through the reconciler for its own
verdict. Only rows whose external and natural keys each occur once in
the batch are buffered, and the buffer is flushed before any other row
is reconciled, so the order of effects stays the sequential one. The
contract and every per-entry status are unchanged; a golden test runs
each batch through both paths against the same table.
/api/health took seconds after a start because register() awaited the
whole pg-boss worker start (queues, schedules, boot discovery) before
the server took a request. The worker, or a web process's producer, now
starts in the background after the blocking boot checks; the probe
answers at once with reason "starting" until it is up, and gives the
database two seconds before calling it not answering.
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.
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.
Every Oura collection, workouts and heart rate included, is now read
back to the start of the account's history by a backfill that runs
through the shared integration lane in five-minute slices. Each
collection walks from the day the backfill began toward the past one
window at a time (ninety days, thirty for sleep, seven for heart rate)
and the progress note on the user row records after every window where
it got to, so a slice cut short by its budget, the lane or a rate limit
resumes there. A 429 ends the slice and the next one waits for the
Retry-After Oura named. Activity and sleep are walked first to learn
where history starts; the rest stop a week before it.
Heart rate streams a page at a time and keeps only the days it touched,
so memory stays flat however long the history is. A collection the
grant does not cover is a scope note, not a failure; the first slice of
the next chain asks again, and a connect clears the completion stamp so
a newly granted scope is read without walking anything twice.
The two columns arrive empty, so every existing connection is offered
the backfill at the next boot without reconnecting. Before anything
else the backfill retires the WALKING_RUNNING_DISTANCE rows earlier
syncs wrote from the equivalent walking distance and refolds their days;
an account that kept such rows after disconnecting is offered too.
A guardian acting on a managed profile can now connect and disconnect its
Withings, WHOOP, Oura, Polar, Strava and Google Health from the profile's
integration cards, and each card says whose record it connects for. The
connect starts with a fenced fetch that mints a state naming the profile;
the provider's callback lands tokens and data there only after checking
that the browser belongs to a guardian with a live MANAGE grant on it.
Without a provider app of its own, the profile borrows the guardian's,
with an audit row. Withings and Google Health callbacks read the session
directly, since a provider redirect cannot carry the record-session header.
Minting the state, building the consent URL and dropping a grant each had a
copy per provider route. The routes keep their auth, rate limits and
redirects and hand the rest to one implementation, which the managed-profile
routes reuse.
Anbieter instead of Provider, Standard instead of Default, Gesundheitsscore
for every spelling of the score, and Insights where the area was called
Auswertungen. The Coach's rate-limit notice no longer shows English in the
German interface. A guard reads the Settings namespaces of the German
catalog and fails on the retired spellings.
Point the header, overscroll and timezone specs at the pages that hold
those cards now, add the module pages and Preview features to the
one-piece sweep, and add a 390 px journey through the grouped section
sheet. Unit coverage for the Preview features card.
Settings now reads as nine groups: Account, Tracking, Arrangement,
Notifications, Health profile, AI, Connections, Data and System. On a
phone one button names the current section and opens the grouped list,
replacing the strip of every section.
What moved, every old address still working:
- Sessions, trusted devices and security activity sit under Security;
the privacy page anchors forward there.
- Notifications holds every reminder kind: the Coach nudges and the
checkup reminder switches joined, and a card names the reminders set
where their subject lives (dose times, cycle reminders).
- Arrangement holds order and view only. Mood tags, the labs biomarker
catalog and on-device text recognition, and the injection sites live on
module pages under /settings/modules/<id>, linked from each Modules row
and from the module's own wrench. /settings/layout/mood redirects.
- The timezone saves on change in the Display card, with a one-tap offer
of the device zone.
- Screening hints sit beside the life circumstances they come from.
- System gains Preview features with the Topics switch.
Duplicates: the Coach and MCP pages show their state and link to the one
switch in Modules (turning MCP off keeps tokens), the Coach overlay shows
the provider and model with a link to Settings > AI instead of a second
set of selects, and the privacy page drops its copy of the export links.
A guard walks every historical /settings address through the redirects
and every forwarded anchor to a page that renders.
The Telegram, ntfy, webhook and email cards saved their enable switch on
tap and the rest of the form on Save; the switch is now part of the form
and the one Save sends it. The display selects confirm a change with a
toast. Destructive confirmations in Settings go through ConfirmButton or
ConfirmDialog with the destructive style. Two structural guards hold both
rules: a card may not mix instant and explicit saves, and no settings file
reaches for a raw AlertDialog without a written reason.
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.
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.
Workouts from /v2/usercollection/workout land as Workout rows keyed on
the Oura id, with the activity mapped onto the canonical sport set and
the raw label, intensity and recording source kept in metadata. Heart
rate from /v2/usercollection/heartrate lands as PULSE, streamed page by
page and written in bounded batches, first write wins per instant. The
hourly window for heart rate is hours, not days: six by default,
reaching back to the newest reading after a gap, at most 72.
Both collections sit behind their own scopes (workout, heartrate). A
grant from before they were requested answers 401 naming the scope; that
is recorded as a scope note on the card, never as a failure or a
reconnect. Collection refusals carry Retry-After on a 429.
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.
Oura's equivalent_walking_distance is the distance a walk of the day's
effort would have covered, not distance travelled. Written as
WALKING_RUNNING_DISTANCE it counted a bike ride or a yoga hour as
kilometres walked. The daily activity mapper now writes steps and active
energy only.