The note field imported its length limit from the validation module,
which pulled zod and the medication request schemas into the client and
put the medications list and detail pages over their bundle budget. The
limit lives in a zod-free module now, and the validation re-exports it.
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 averages on a dashboard tile open their colour hint on a tap now,
which made them touch targets of 16 px height. On a touch screen the
trigger is 44 px tall, and a negative margin keeps the tile's line
where it was.
Opening a sheet pushed a history entry, and closing it took that entry
off again with a history back. The browser answers that back later, so
when the close started a navigation it landed after the router's push
and undid it: the medication wizard stayed on the list instead of
opening the new medication, a section picked from the settings list on
a phone stayed on the account page, and a link followed from a sheet
could put the page under it back.
The sheet history goes, with its wiring in ResponsiveSheet, the
document sheet's opt-out, the account form's deference to it, Back
answering the discard question, and their tests. Back behaves as
before: it leaves the page and the sheet goes with it, and the account
form's own Back guard answers every Back again. A tap outside, Escape,
the X and a swipe still ask before dropping typed input. The two size
baselines return to their earlier numbers.
A badge call from the service worker stalled Chromium's worker thread
while it activated, awaited or not: the worker reached "activated" but
never claimed its pages, so no page was ever controlled and every
offline path was gone. The worker no longer touches the badge. The
installed app clears a leftover count from the page once the worker is
registered; a browser tab leaves it alone.
The filter-sync test no longer reads `history.length`, which the
browser caps; the recorded push without a replace already proves the
new entry.
The two ketone cases repeated the glucose cases with one type changed; they are now one parametrized case each, which keeps the mocked client casts at their count.
The Fitbit guide still called the integration experimental and spoke of
the 30 October 2026 turndown as ahead. Three new files carried em
dashes in a doc comment and two test titles.
- The About button opens the release notes and now says so in all ten
languages; six of them still showed the English word "Changelog".
- The discard question now also covers edit sheets, where "Discard this
entry?" read like deleting the target or episode being edited. It
asks about the changes in every language; Russian also loses the
ambiguous "Отменить" on both the title and the button.
- The 7-day trend figure on a dashboard tile had an English screen
reader label in every language.
- Outlier terms in the new checkup and ketone strings follow each
catalog (ru "Кетоны в крови", "Коуч"; es "Desactivado"; nl
"rusthartslag"), the French steps question reads naturally, and the
shared Polish and Russian Coach question no longer assumes a
masculine speaker.
The case with a year of earlier takes proved the ledger stays the same,
which the old read of every intake satisfied as well, so a revert of
the bounded read passed every test. It now also counts the rows the
route read and expects only the window's.
- README: the integrations line now names Strava, Garmin and the Health
Connect import, the reminder channels include email and the webhook,
and the last two feature labels say what they cover.
- UI guidelines: the password form was said to carry a hidden username
field it does not have; the line now states when one helps.
- CONTRIBUTING: strings go into every locale file, not only English and
German.
- 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 git and docker ignore files and the lint config listed the folders
and notes of particular editors and tools. Local workspace folders
belong in a contributor's own global excludes. The image copies only
named paths, so nothing local reaches it, and the lint config now skips
every hidden top-level folder, which covers local worktrees under any
name; no tracked hidden folder holds code.
The commit-msg hook listed the tools and the session link format it
refused, so the file read as a description of how commits are written.
It now states the rule: commits are authored by the maintainer's
identity, and a co-author trailer is only for a contributor. It refuses
a generator line, any "<Word>-Session:" trailer and a co-author trailer
whose address is not a GitHub noreply address, so a contributor's
credit still passes.
Several test titles and comments said whose readings a fixture came
from, with a host name beside them: a blood pressure average, a month
of paired readings with dates, a cached insight about weight, cycle
history and dose timing. They now describe the fixture itself (a
profile averaging 134/87, a ten-pair month, a long history); the values
and what each test proves are unchanged.
Four comments still named `queue-runtime.ts` without its folder after it
moved under src/__tests__/helpers/, and the v1.45.0 boot page described
two dumps that are no longer kept.
Water, caffeine and the nutrient channels feed the correlation scan
behind /api/insights/correlations and the Coach read line on the metric
pages, but the water and nutrient batch writes evicted only the
dashboard snapshot. After logging water both could serve the read taken
before it until the cell went stale. Both writes now sweep the record's
analytics cells, as measurement and mood writes do.
The tab strip shows the Workouts pill from the presence read, which
stays fresh for five minutes and was not part of the workout write's
invalidation. After the first manual workout, or after deleting the
last one, the pill stayed missing or stayed on for up to five minutes.
Inside a managed child's record, or as a delegate with manage access,
the tab strip showed Weight, Pulse, Blood pressure and the other metric
pills, and every one of them opened an error. The strip now reads the
record's own presence answer, but the metric pages read /api/analytics,
which serves only the signed-in account's own record and refuses a
switched session. The strip again shows those pills only in the
account's own record, as before; mood, medication, workout and ECG
pills keep following the presence answer.
A "Measure blood ketones" checkup kept sending its push, Telegram or
email with the Glucose module switched off, while the glucose checkup
beside it went quiet. The checkup job kept its own copy of the module
map, which knew glucose but not ketones.
The job now asks the one ownership table the rest of the app gates on.
That table did not list the four mental-wellbeing screenings, which only
the job's copy knew, so they join it under the mental-health module; a
screening score row now also follows that module in the day view and
search.
- The Coach thread kept 80 px free under the last message for the New
conversation button while the keyboard hid that button.
- The remove buttons on lab alias chips and attached-document chips
reached sideways into their own label and the next chip, and an
alias is removed at once. Their tap area now stays inside the chip's
gaps sideways and keeps its height.
- A capture menu comment described a dialog that is gone.
The symptom form and the dashboard's medication intake asked "Discard?"
on every close, untouched. The sheet's fallback check read the input
Radix keeps for the intensity slider (value "0" before anyone moves
it) and the dose the form fills in from the preselected medication as
typed input.
The check now skips a slider's own input and counts a slider moved off
unanswered instead, and compares a field against the value its form
says it filled in (`data-seed`). A controlled input's defaultValue
follows its value, so it cannot tell the two apart. The dose field
names the medication's dose, one line more for the form's baseline.
Back on a sheet with unsaved input asks whether to discard it. A second
Back while that question was open put the sheet's entry back and asked
again, so the Android button could never get past the question. It now
closes the question and keeps the sheet and its entry, like Cancel.
Three ways the sheet's history entry could still cost a Back:
- A close that also writes the URL (the day's date picker, the life
event sheet opened by ?add=lifeEvent) let the router's answer to that
write land on the page entry with the closed sheet's mark. The next
sheet close then stepped one entry past the page, and on an installed
app opened from a notification that closed the app. A write that
carries the mark of a sheet that is gone now loses it.
- A sheet's entry started empty, and landing below it copied that empty
state onto the page entry, so an open day or document lost the key it
keys its own Back on. The sheet's entry now starts as a copy of the
page entry's state.
- The account form's Back guard heard the sheet's own Back first when
it was mounted before the first sheet of the visit, and asked to
discard the form's edits for closing the section list. The guard now
leaves a popstate the sheet history owns to it.
The browser tests run against Chromium's session history and fail
without each fix.
The new worker cleared a leftover app badge inside its activation and
waited for it. A browser that never settles that call keeps the worker
activating, so it never takes over its pages and every offline path is
gone. The badge is now cleared beside the activation, best effort.
The sign-in journey expects the page's own sentence for a wrong
password, not the server's text.