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.
Moving the navigation into the shared helper took both sign-in hand-offs
out of what the headerless-transport guard walks, which only reads the
component and page trees. The helper now only clears the offline copies
with the capped wait, and each page navigates itself. The device
registration test checks the end of the no-device rest inside the test
that already pairs an iPhone instead of a copy of it.
Merging the previous release in kept this branch's older numbers for
telegram-webhook-handlers.ts (1335 lines, handleCallback 590), although
the file is unchanged at 1319 and 582, the numbers the previous release
recorded. The gap sat inside the ratchet's slack, so nothing flagged it.
The register refusal codes, the end of the no-device rest, the
no-recipient annotation and the sign-in shell comment had each grown a
function the size ratchet holds. The codes become constants, the rest
ends through a helper beside the one that starts it, and the
annotation moves into that helper. The settings Web Push card dropped
under the limit when its subscription post moved out, so its entry goes.
The sign-in page and the profile take-over page each cleared the offline
copies with the same capped wait before leaving for the identity
provider, with the limit written down twice. leaveForSingleSignOn does
it once for both.
A send with no browser subscribed, no iPhone paired or a transport the
operator has not set up no longer counts as a failure, but nothing else
replaced it. The mood reminder and the daily briefing retry every
minute until delivered, so such a channel wrote a skipped row each
minute, for good; the dispatch still counted as an attempt, so the
watchdog and status alerts took it as announced; and the status screen
said "active" with no hint why nothing arrived.
The channel now rests for an hour after such a send, with the reason on
its status, and the send does not count as an attempt. Subscribing a
browser or pairing an iPhone ends the rest at once.
The record fence journey called the page cache its positive control,
but signed-in pages are never stored, so the entry it counted was the
install precache, and the half inside the record repeated a claim that
holds by construction now that the worker keeps no API answers. The
comment now names the precache, and inside the record the journey reads
the IndexedDB dashboard snapshot, which must be missing or still the
caller's own.
Sign-out removes the push row first, through the raw fetch helper, which
has no default timeout. On a connection that is up but carries nothing,
the sign-out waited for the browser's own timeout before the logout
request even started. The removal now gives up after five seconds.
The dose tag comment still explained the clear push sent for a logged
dose, the session-end wipe comment still named the worker's API cache as
a live layer, and the worker's header and foreign-origin note did not
match what the worker stores and when it resumes.
The light-theme fix painted the strip behind the white status bar only
in the signed-in shell. The sign-in, registration and take-over pages
and the onboarding shell padded the strip without painting it, so after
a sign-out in the installed iOS app the clock was white on near-white.
They now use the same shell-safe-area band.
The Web Push test answered a refused push with `ok: false` and the
sender's `reason`, but the Test button reads `meta.errorCode`, so it
only said "Test failed". The answer now carries `upstream_rejected`, or
`connection_failed` when no push service answered.
The test push used the administrator's test title, which put the brand
first in several languages; it has its own title now.
The German sentence dropped its article, and the Spanish one read as if
the sign-out had been the session's own state ("Tu sesión sigue
abierta"); it now says the person is still signed in.
The hint for iPhone and iPad Safari ended with "turn on push here", but
the button reads "Enable push"; it now names the button in every
language. The hint is two sentences of steps, so it is shown as content
rather than muted, and the French one uses the typographic apostrophe
like its neighbours.
The offline strip joins two sentences, and both ended on "once you're
back online"; in French it ran to four lines on a phone, and the
Russian one was ungrammatical. The first sentence now only says that
changes can't be saved.
On Back, the shell set the list's saved offset again on every frame
while the list was still shorter than that offset. Someone who started
scrolling in that time was pulled back, and a list whose data took more
than a second ended up where the skeleton allowed. The restore now stops
at the first wheel, touch, key or pointer input and keeps trying for up
to three seconds.
The sign-in page and the profile take-over page waited for the offline
copies to be cleared before leaving for the identity provider, with no
limit. WebKit can leave an IndexedDB open unsettled after the installed
app returns from the background, and the button then did nothing. The
wait is now capped at one and a half seconds.
The menu's sign-out pushed the sign-in page onto the history. Back then
reopened the page just signed out of, and the edge sent the next person
to the sign-in page with that page as `next`, so their first sign-in on
the device landed where the last person had been. The sign-out now
replaces the page.
A refetch of the account that answered 401 or 403 now signs the person
out and clears the offline copies, which is right for HealthLog's own
answer. But a self-hosted instance behind a forward-auth proxy or a
firewall can get a 401 or 403 that HealthLog never wrote, and that
signed a person out whose session was fine.
HealthLog's error envelope always names a code, so only a 401 or 403
that carries one is read as the session's end; one without is treated
like a network failure and keeps the last known state. The tests now
give the account route the answer the server really sends.
Once the sign-in and registration pages read codes instead of the
server's prose, a taken username or address said only "Registration
failed", an expired invitation said "You don't have permission for
this", and a passkey this server does not know, a passkey sign-in that
took too long, an unreadable passkey answer, a single-sign-on-only
server and an already signed-in visitor all said "Login failed".
The registration route now names its refusals (auth.register.taken for
both 409s, auth.register.invite_invalid for a refused invitation), and
those codes, the three passkey refusals, oidc_only and
auth.already_authenticated get a sentence in every language.
Subscribing re-enabled a disabled channel but left the failure counter
and cooldown on one that was still enabled. Failures counted before the
new subscription (no browser left, a push service outage) kept every
reminder skipped until the cooldown ran out, and one more transient
failure switched the channel off. Subscribing now resets them too.
A browser also kept the row of an account whose session ended without a
sign-out, so the next person on it saw that account's reminders. An
endpoint is one browser: subscribing now removes other accounts' rows
for it, and the settings card, which posts its subscription on open,
claims the browser for whoever uses it now.
The server set the Home Screen badge only through the clear push sent
for each logged dose, and that push is gone. A device whose last clear
left a count (one forgotten dose, an update in the middle of the day)
kept that number on the icon for good, since iOS never clears a web
app's badge by itself.
The worker now clears the badge once on activate. The badge handling in
the push handler, the unused outstanding-dose counter and the test of a
payload no server sends are removed, and the self-hosting guide no
longer promises the clear push or the badge.
The sign-in refusals carry only their status's generic code, so once the
sign-in page read codes, a wrong password, a wrong second-factor code
and a refused security key all said "Your session has ended", and a
lockout said "too many requests just now".
localizedSignInError reads a generic 401 as the step's own failure and a
generic 429 as "Too many attempts. Wait a few minutes and try again.";
a code the step names itself keeps its sentence. The password, passkey,
code and security-key steps use it. The rate-limit sentence moves from
auth.claim to auth.signInRateLimited, since the claim page and the
sign-in page now share it, and the Korean one takes the polite form the
rest of the catalogue uses.
Only the menu's sign-out removed this browser's push subscription. The
sign-out buttons on the registration page and the profile take-over page,
which exist so someone else can use the browser next, left it behind, and
the next person kept seeing the last account's reminders. All three now
go through one signOutRequest.
The browser's own subscription is now removed only once the server has
confirmed the sign-out; the server row still goes first, while the
session can name it. A refused sign-out stores the row again, so the
person who is still signed in keeps push. The logout test pins the new
order and the refusal.
The helper file is renamed web-push-subscription.ts, since it now also
saves a subscription for the settings card.
The Web Push card reads its state from the browser only. When the server
had lost the row, or the channel had been switched off, the card said
enabled and no reminder came. Opening the card now posts the subscription
the browser holds, which stores the row and turns the channel back on.
A refused post (demo, push off instance-wide) changes nothing on screen.
Before this release, a send with no browser subscription counted as a
failure, and five of them, or one 410, switched the WEB_PUSH channel off.
Subscribing again stored the browser but left the channel off, so the
card read "enabled", a test push arrived, and no reminder was ever sent.
Subscribing now re-enables the channel, but nothing prompts the people
already caught to subscribe again.
The migration re-enables every WEB_PUSH channel the dispatcher switched
off (a disabled_reason is set; a channel switched off by hand has none)
whose account still has a subscription, and clears the counter, the
cooldown and the last failure the same way reEnableChannel does.
This reverts commit 0d41abcda. With the entry hidden, Settings, AI showed
the stored Coach memory instead, and that card reads the conversation
list: an account without any AI provider then made a Coach request on a
page that should make none. Settings lists the Coach while its module is
on, as before.