Directions such as "30 to 60 minutes before food" had nowhere to go: a
medication carried no free text of its own, only the notes on a dose
change or a course. The medication now has one note, edited on the last
step of the wizard and shown under the name on the detail page, above
the dose actions on both card variants, under the picked medication in
the log-dose sheet, and per row in the take-all-due list. One
MedicationNote component renders it everywhere, in foreground text.
The note is sealed at rest in Medication.noteEncrypted (migration
0498_medication_note, lock-bounded), with no plaintext column. Create
and update take an optional `note`: trimmed, at most 500 characters, an
empty string or null clears it, absent keeps it. The wizard sends it on
an edit only when it changed, so an unrelated save never clears it. The
list and detail reads strip the ciphertext and answer `note`.
Both ends of the data path carry it: the backup (decrypted in a portable
file, ciphertext in a disaster-recovery file), the restore, the data
export, the key-rotation registry and script, and the MCP medication
schedule, resources and fetch, fenced as user text. The doctor report
and the FHIR export leave it out: it is the person's own reminder, and
the report has no selection for personal prose.
The size baselines rise by the one-line wiring of the field in the
medication page, detail tabs, cards, log-dose sheet, wizard step and
list read; the wizard payload grows by the field's own encode and
hydrate.
Enabling Browser Push showed only "Activation failed" whenever
pushManager.subscribe() rejected. Brave keeps push messaging off by
default and rejects with "AbortError: Registration failed - push
service error", which left people with nothing to act on.
The browser's own refusal is now classified apart from the server
answers around it. An AbortError from subscribe reads as the push
service refusing and links the Browser Push docs; in Brave the message
names the setting under brave://settings/privacy. A blocked permission
stays blocked, and a dismissed prompt or a rejected key keeps the
generic message. The subscribe flow moves out of the card component so
it stays under the function size baseline.
Refs Discussion #1378
The record switcher opened as a side sub-menu next to the account menu.
On a phone the account menu sits against the right edge and takes most
of the width, so the 240 px sub-menu had room on neither side and ran
off the screen with the names cut off. Below the sm breakpoint the
trigger now unfolds the records in place inside the account menu;
wider screens keep the sub-menu. The trigger, list and entry slots are
unchanged, so the journeys read the same at either width.
Refs #1372
A script or a home-automation rule holding a measurement token can now
post a value to POST /api/custom-metrics/{id}/entries for a custom metric
its owner already created. The route names MEASUREMENTS_WRITE_SCOPE on
the POST only; the value list, the metric list and every create, edit,
delete and restore leg still name no scope and refuse the token 403.
The token stays on its owner's record: requireAuth refuses any account
selector, and an id that is unknown, deleted or another account's answers
the same 404. The entry has no source column, so the audit row names the
token that wrote it.
The bearer-scope guard now freezes the scope's reach at three files, with
the custom-metric file naming it once and only on the POST. Integration
cases cover the admitted write, idempotent retry, the foreign and deleted
ids, five other narrow scopes, every other custom-metric leg, and a shared
record. The OpenAPI descriptions and the token card text in every locale
say what the token can do and where a metric's id is found.
Refs #1377
- The translation issue template called eight locales "AI-initial",
left Russian out of its list and its dropdown; it now says they
started as machine-translated drafts reviewed by the community.
- The pull request template asked for two locales where the checks
require all ten, and pointed at an ignored docs folder.
- Code comments named the maintainer notes file, a release by its
working name and an internal host; old changelog entries explained
the notes files by their tool-specific names. A fixture token and
the deploy webhook test's application id (a real one) are replaced.
The first due slot of a Coach course honoured endsOn, but the tick's
roll-on, satisfy and skip all built their schedule input without it, so
a seven-day twice-daily course kept rolling on indefinitely and never
reached the null nextDueAt the expired-course cleanup waits for.
Carry endsOn on RollableReminder and SatisfiableReminder and select it
wherever a caller narrows the row.
The override in pnpm-workspace.yaml now floors node-forge at 1.4.3, which
clears the five advisories against 1.4.2 and leaves pnpm audit --prod
clean. It arrives only under @parse/node-apn, which pins 1.4.2 exactly;
the APNs sender uses token auth, so the certificate path never runs.
A rolling reminder ("every N days") is anchored on its last reading, but
the reminder tick rolled any cycle of a week or less on after a delivered
reminder, re-anchoring it on the send. A missed every-two-days blood
pressure reading therefore moved two days ahead each evening, read "in 2
days" on the card, and never showed as today or overdue.
A rolling reminder now stays due after its reminder, whatever its
interval, until a reading lands or it is done, skipped or snoozed. While
it stays open the repeat follows its interval, capped at a week, still in
its notify hour and under the per-day claim. Calendar rules that recur
within a week (a twice-daily course) keep rolling on; appointments are
unchanged.
holdsOpenAfterReminder stays the one definition for the tick and the
edit route. The daily digest asks the narrower question underneath it,
hasLongCycle, so a short rhythm still waits for its time of day on the
start page and is not pinned under the rail cap.
Refs #1369
A document unit that held only "(calc)" read as no unit and went straight to the record; it now goes to review. Saving a category order keeps the saved keys not on screen after the shown ones. The changelog says what each fix does and no more.
A domain with no new rows kept its last row's stamp, so once that was older than the tombstone horizon every pull answered cursorExpired and the client resynced from scratch on every pull. A domain whose page runs dry now takes serverNow minus the overlap on every page where its watermark has not passed that line, and on the last page every row domain does, raising a quiet domain and holding back one that delivered rows in the stretch. A domain drained early in a long walk is covered as well. The overlap grows from 60 to 120 seconds, above the 60-second timeout of the longest transactions that write served rows.
Ketones had to live in a custom metric, with the glucose-ketone index typed
in by hand beside it, where it drifted from the readings it came from.
- Type: BLOOD_KETONES (beta-hydroxybutyrate, mmol/L, migration 0496),
plausible from 0 to 10: the blood ketone meters read 0.0 to 8.0 and print
HI above it, and a venous value off a lab sheet fits under 10. Zero is a
real reading. No band, no direction, no personal record, off the Coach
snapshot and the correlations; readable over MCP and owned by the glucose
module like glucose.
- Entry: choosing blood ketones in the measurement form also takes the
glucose from the same fingerstick, in the account's glucose unit. Both
rows go through the array arm of POST /api/measurements with one
timestamp, so they land in one transaction or not at all.
- Index: glucose (mg/dL / 18.0182, the tree's one factor) over ketones,
computed at read time and never stored. Each ketone reading above zero
pairs with the glucose reading at the same instant, else the nearest one
within 15 minutes either side; a tie goes to the same source, then to the
earlier reading. Nothing pairs across a wider gap.
- Surfaces: GET /api/measurements/series takes kind=ketones and
kind=glucoseKetoneIndex (each point names the paired readings), and the
day carries glucoseKetoneIndex when both reading types are visible. The
new Ketones page charts the readings with the index beneath; the day view
shows the index beside the value tiles; the doctor report, its PDF and the
clinician share print it in the glucose group when both leaves are chosen,
since beside the ketones it gives the glucose back.
- FHIR: ketone readings export under LOINC 104816-4 (Beta hydroxybutyrate
[Moles/volume] in Blood). The index is left out: it has no LOINC term,
and both readings it is built from are in the bundle.
- The measurement form's action row moved into its own component to keep
the form inside its size budget.
OpenAPI regenerated; all ten locales; the import unit table lists the type.
Rotation revoked the feed it had read without checking it was still live, so a turn-off in between came back under the new link, and it copied options read before a concurrent change. The revoke is guarded and the options are read after it; PATCH writes only to the live feed and follows a rotation to its successor.
The labs list layout gains categoryOrder, a bounded allow-list of
category keys (200 entries, 64 characters each, deduplicated, malformed
values dropped on read). GET /api/labs/overview reads the caller's own
layout and answers categories already in that order, so every client
renders them as given. Listed categories come first; keys the data does
not have are ignored; a category the order does not name follows in
the default order (catalog panels, then the person's own by label, then
other). Other left at the end of the order stays last, behind any
category added later; moved anywhere else, it stays there.
Settings > Arrangement > Labs shows a category order card while
categories are on. It lists the overview's categories under their
localised labels, sends the whole order after every move, moves the
cached overview at once and refetches it. The card says how the default
order works. The OpenAPI document carries the new field.
A read that copied "(calc)" into the unit minted a marker in "g/dL (calc)" that refused the next report's "g/dL". The note is dropped at the read and ignored in the unit comparison; any other bracket still counts.
A forecast period was one all-day event over every day it might fall on,
so a period of a few days showed up as a week-long estimate next to the
logged ones. Each feed now carries a forecast style:
- EXACT: the calculated period only, from the most likely start for the
predicted number of bleeding days.
- MARGIN: that event, plus separate "possible" events for the days it may
start earlier and the days it may still run on, each only where the
band reaches that side. Discreet titles use an open dot for them.
- BAND: the single range, unchanged.
Migration 0495 adds the column with BAND as its default, so every live
feed keeps the events it serves. A feed turned on from now on starts on
MARGIN, set by the mint route. PATCH changes it, rotate copies it, the
status and the audit diff carry it, and the settings card offers it with
a one-line explanation of the chosen style. The horizon and its stop rule
are the same in every style, and the period event keeps its UID across
styles so a calendar updates it in place.
16.4.5 is a security release with seven moderate advisories. Most are about 'use cache' and Cache Components, which the app does not use; one covers self-hosted apps that forward fetched responses.
The inbound route admits two uploads per person (four per process) and
answers the next with 429 `uploadBusy` and `Retry-After: 1`. The vault
queue sent three at once, so a multi-file drop provoked that answer
itself, and the card read it as the hourly limit: "Try again in a few
minutes".
The queue now runs at `DOCUMENT_UPLOAD_PER_USER_CONCURRENCY`, moved to
`inbound-constants.ts` so client and route share one number. A busy
answer is classified on its own, carries its Retry-After, and is retried
for the same file (Retry-After clamped to 1-10 s plus jitter, at most six
attempts within 30 s) while the row stays in its uploading state. Only a
spent budget shows an error, with copy that says the server was busy.
Unmounting aborts any pending wait. The Coach's new-file attach uses the
same retry. The hourly-limit copy now names the hour.
The workout batch kept `route` only for entries it inserted. An entry
matching a stored workout reported `duplicate` and its track was dropped,
so turning on `storeWorkoutRoutes` only helped workouts recorded from then
on.
A matching entry that carries a route now attaches it when the stored
workout has no track and the account keeps tracks, the same way the
heart-rate series enrichment works: the planner probes for a stored
track, the write seals it like an insert does, and the entry reports
`enriched` (once, also when it attaches series and track together). The
climb derived from the track fills an empty `elevationM`, as it would on
insert. A workout that already has a track keeps it, and with routes off
nothing is written.
Track rows now go through `createManyAndReturn` on the unique
`workoutId`, so a track that lands between the probe and the write wins
and this entry settles as a duplicate instead of failing or adding a
second row.
The window before a booked visit started at the last matching visit
however far back it lay, while the health-report export covers 730 days
and fell back to its 90-day default for anything longer. The card then
offered a report the export did not build. The window now stops at the
export's limit, read from one shared constant, and preparation.capped says
when it did; the web says "The last two years" for it.
The export clamps an over-long explicit range to the last 730 days before
its end instead of falling back to 90 days, so a custom range or a linked
window cannot shrink silently either, and its day count forgives the hour
a clock change adds, so two years of local days count as 730.
A read whose readings all lie past the lookback limit was reported on its
step as outside_window, and its fetch entry carried no reason, so a client
could not tell an empty window from data the limit keeps out. The step now
carries reason outside_reach and the fetch entry mirrors it. The method
line and the web trail keep the window sentence; the answer names the
limit as before.
A report that prints a text result such as "SEE NOTE" with a numeric
range beside it minted the marker without bounds: the document commit,
the scan commit, the free-text lab write and the CSV import each passed
null bounds for a qualitative reading, and the CSV row reader and the
scan review payload dropped the range before that. The marker's first
number then had nothing to be judged against.
Every mint path now passes the printed window. A range that is text
itself yields no bounds, an existing marker keeps its own, a transposed
window is not minted, and the reading itself still records no window.
The scan review shows the range on a text row that mints a new marker.
Refs #1313
A blob: document inherits the policy of the page that minted it, and
WebKit applies the inherited frame-ancestors to the blob frame's own
parent. With 'none', Safari on macOS and iOS refused the PDF preview
iframe and showed an empty box; Chromium does not apply it there and
rendered the preview. 'self' keeps every foreign origin out.
Refs #1311
Fitbit shut its classic Web API down on 2026-10-30 and Fitbit data now
arrives through Google Health, yet answering "Fitbit" still ended the setup
on the classic card, read the classic connection's status and named it.
Fitbit stays an answer, because people still own the devices. Its task now
opens the Google Health card, reads that connection, is named after it, and
says on every tile that Fitbit data comes through Google Health. The
question carries the same hint under the Fitbit choice, and the checklist
names the connection the same way.
The onboarding-source guard keeps Google Health out of the questions, now
with a reason a test holds: the Fitbit answer is the way there.
HealthKit's basalEnergyBurned sat in the deferred set, so the batch route
refused it per entry and acceptedTypes left it out. It now maps to a new
BASAL_ENERGY_BURNED type in kcal (migration 0493), beside active energy
and never added to it.
It is cumulative like active energy: summed per day on the read path and
in the rollups, drained into one daily row, imported from export.zip,
listed and charted with its own label in every locale, part of the report
selection and the backup, and readable over MCP. It carries no personal
record direction and feeds no score, insight or Coach snapshot.
Health Connect is left unmapped on purpose: its basal metabolic rate is a
rate at one instant, not a day's energy. No other integration reports a
resting figure of its own.
Refs #1340
The details of the Garmin and Oura cards listed measurement types and
workouts only, so cycle days a wearable imported appeared nowhere, and a
bug report could not say whether any had arrived. The status envelope now
carries a `CYCLE_DAYS` entry per cycle-importing source with the newest
day and the count, and the freshness list renders it. It counts as a
sparse type, so a month between periods never reads as a dead pipe.
Refs #1333
The setup question about where readings come from listed every other
connection on Settings and not Garmin, so the flow could neither record it
nor end on connecting it. Garmin is now a Q4 source and a browser
connectable one: the key is pasted on its card, so nothing can be missing
up front.
A guard now pins that every connection the health read covers is either an
onboarding source or named with the reason it is not.
Refs #1333
Both wearables wrote their period days with a bare create and so skipped
the seam every other cycle writer goes through: a first bleeding day never
opened its cycle and no day was attributed to one. An account whose
periods came from a watch or a ring held day-logs only, with no cycle
start, no lengths and no forecast.
The create now goes through one shared helper that keeps the
create-only-if-absent rule and then runs the same attribution and
`ensureCycleForBleedingDay` inference as the shared upsert. The hourly
legs and the newest-first Garmin backfill both use it; a finished Garmin
period also stamps its last day on a cycle that has no period end yet.
Rows imported before this change are repaired by a one-shot job sent from
boot discovery while candidates exist. It opens or attributes cycles
oldest first per account, touches no day-log content, and converges to an
empty candidate set.
Refs #1333