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.
NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false built a dashboard that read its
tiles from two analytics slices, the widget layout, the mood analytics
and the thresholds instead of the one snapshot cell. It took a rebuilt
image to reach, and the snapshot has been the only path the published
image runs since it became the default. The flag, its readers, the
legacy reads in the dashboard hook and tile context, the slim and thick
merge helper, the build-time toggle notes in .env.production.example and
the self-hosting guide, and the tests that pinned the legacy wiring go.
The presence read is data, never AI, and spans every module: each flag
whose page a switched-off module hides answers false without reading
the rows. Both inventories now say so.
The root layout now reads the public monitoring switches while it
renders, so nothing calls GET /api/monitoring/settings any more (the
native client never did); the route and its unpublished entry in the
route coverage guard go. The Coach feedback card reads the AI quality
card's cell, which left queryKeys.adminCoachFeedback() without a reader.
The Coach memory editor, the managed profile's handover link, the cycle
length fields and the command palette's search set a font size under
16 px on touch screens, so iOS zoomed the page when one took focus. They
now read 16 px on a touch screen and keep their smaller size under a
fine pointer, like the field primitives. With no field left over, the
guard's list of known offenders goes.
A swipe past the close point paints the sheet where the finger let go and
asks the owner to close it. A sheet holding unsaved input asks about
discarding first and stays open, so it sat half pushed off the screen
behind the question. One frame after the close, a sheet whose content is
still open now glides back to its resting place. The function's size
baseline rises by the seven lines this takes.
The illness episode, cycle day, custom metric, target, mood tag, symptom
and life event sheets open filled from a record or a fetch, so the body
check cannot tell typed input from what was loaded, and a stray tap
outside or a swipe dropped the change without asking. Each now keeps
what it opened with (the cycle day once its fetch has hydrated the form)
and passes whether the fields differ to the sheet's dismissal guard. The
custom metric and life event forms report it to their hosts the way the
workout form does.
The size baselines of the six sheet functions and the cycle sheet file
are raised by the few lines the comparison takes.
Carries the targets read at read level, the two new error codes
(`customMetric.name_taken`, `auth.mfa.already_active`) and the
notification categories' description without its em dash.
The Settings Coach entry now follows whether the Coach can answer, as
the sidebar does, with Settings, AI keeping the stored memory in reach.
A managed record has no Settings, AI, so while its Coach could not
answer yet (no operator key, no consent) the guardian lost the only way
to the record's Coach data scope and its switch. For a managed record
the entry follows the module switch alone again.
The Playwright project list still named the deleted onboarding AI
offer spec. The CoachPlan comments said the review writes
`outcomeEncrypted` and surfaces it behind a flag; nothing writes or
reads it now, and the column is marked internal. Comments that named
`resolveProvider` as the resolver now name the chain, and the function
itself says it is test-only.
The fixtures added with these fixes switched the type checker off with
`as never`, which the realism ratchet refuses to see grow. They now carry
their types, three webhook mocks needed no cast at all, and the lowered
counts, with the targets route's one fewer version stamp, are written
back.
The child-band change left the classification dividing by the record's
height where it had checked the adult-band height, which the type checker
refused. Both now read the same value.
Where the composer fills the column (a phone, the drawer, a narrow
column), the New conversation button floats above the composer and
covered the last message's time and actions at 390 px. The thread now
keeps the button's height free below the last message while the button
floats, the way the shell clears the Coach button, and drops back to its
plain gutter once the button sits beside a wide composer.
Since a codeless refusal reads by its status, three answers lost their
meaning. A failed ChatGPT sign-in poll said "Error saving" instead of
asking to start the sign-in again; it now falls back to the sign-in's
own failure sentence. A dose-history import still running when the
card stopped watching said "Nothing was written" although the job may
still write; that case now has its own sentence. Setting up an
authenticator while one is active answered "setup failed"; the route
now sends `auth.mfa.already_active`, which says to disable the active
one first, in every locale.
The tab strip on every Insights page read the whole overview (22 KB),
a workouts page, the ECG list and the analytics summaries to learn which
pills to show. GET /api/insights/presence answers just that, for the
record on screen: the reading count per type after the record's module
switches, and whether it has mood entries in the last 90 days, a tracked
scheduled medication, workouts and ECG recordings. A flag whose page a
switched-off module hides is false and its rows are not read. The strip
now reads this and the saved layout; each page keeps loading its own data.
The cases for the bounded read asserted the query's where clause on a
mocked client and cast their fixtures past the type checker. Against
Postgres they now show what the read promises: a rolling medication last
taken before the window shows the dose that fell due inside it, and a year
of earlier takes answers the same ledger as a short history.
The plan review rule told the Coach to read how a plan went from its
progress line, but a progress line exists only for an active plan, and
the review sets the plan to reviewed the morning its reminder surfaces.
For the plan being discussed there was nothing to read, so the model
could make a result up. The rule now reads a result only from a
progress line the snapshot carries, and otherwise asks how it went.
The notification categories' contract description, which lands in the
published API document, and a handful of new code comments and test
titles used em dashes; the house style keeps them out of new prose.
The three deletes gained an Undo toast that calls the restore route,
but the destructive-control registry still listed them as tombstoned
with no restore, with comments saying they would become restorable the
day a surface offered the undo. The correlation card test's header
still described the disabled "Coming soon" action it no longer tests.
The done screen's "AI is optional" line is a sentence the person reads,
set in muted text, which the UI standards keep for meta. It is body
text now. Its test asserted the absence of strings deleted from every
bundle in the same change, so it could not fail; it now checks that the
line is not muted, that the screen links to the AI settings once, and
that no button sits beside the link.
The form read every 409 as "a metric with this name already exists".
Now that it keeps one idempotency key across taps, a second tap while
the first create is still running also answers 409, and the form told
the person their name was taken. The create and rename routes send
`customMetric.name_taken` with the clash, and the form matches the code.
The vault picker and the Coach's attachment picker each wrote their own
"showing the newest 50" line, which the UI standards leave to
`ListCapNotice`, and with a search running past fifty matches the line
still said "Search to find an older document". Both pickers now share
`DocumentPickerCapNotice` on `ListCapNotice`, which takes the sentence
to show, and a running search asks for a narrower one.
The Russian, Polish and Korean refusals for a removed practice, visit
or check-up said "choose another or none" word for word; they now name
the picker's own empty choice or say to leave the field empty. Brazilian
Portuguese called a check-up a "check". The dose-name refusal names the
vaccination card the way the form does in English, Spanish, Polish and
Dutch, and the oversized workout batch no longer says "at once" twice.
A score, rating, count or ratio has no unit label, so the range
message for a manual pain score read "Enter a value between 0 and 10 ."
with a space before the stop, and the same in every locale. A second
sentence without the unit now answers those types.
The row menu stopped offering edit and delete on a computed score
(recovery, strain, stress), but the list still drew its selection
checkbox and "select all" took it into the page, so the bulk bar could
delete a page of scores, and the tombstones kept the recompute out.
A computed row now has no checkbox on either layout and stays out of
the page selection, and the bulk-delete route skips computed rows if a
client sends them anyway. The iOS app treats these rows as read-only
and does not rely on deleting them.
Two quick deletes in the address book showed the Undo toast for the
second entry only: the callbacks `mutate` takes belong to the latest
call of the list's one mutation, so the first entry's were dropped. The
toast now hangs on each delete's own promise.
The route test that pinned the cached answer replaced the database client
and the engines, so it saw the calls the route makes and not what a reader
gets back. The integration test drives the shipped route: a reading written
past the API leaves the second open on the stored answer, while a
measurement written through the API and a module switch both show it.
The "taken <name>" keyword looked up the first medication of that name
and only then asked whether its course had ended, so an old ended
course beside the current one of the same name answered "not found".
The lookup now reads every match and takes the first that runs.
A reminder button judged the course at the moment of the tap, so the
last evening dose of a course, confirmed after midnight, was refused
although the web still takes it. When the message names its slot, the
course is now judged at that slot. The handler file grows by six lines;
its size baseline is raised to match.
The "Mark done" button re-enables as soon as the satisfy call settles,
while the row kept reading as due until the list refetch landed, so a
second tap stored a second satisfied entry; skip and snooze behaved the
same. The row each of the three calls answers with is now written into
the cached list on success, so the row moves to its next due date at
once, and the refetch follows as before.
Logging the next dose satisfies a matching booster reminder with the
dose date, a calendar date stored at UTC midnight. The reminder engine
reads that instant in the person's zone, where west of UTC it is the
evening before, so the next booster came due a day early. The first
booster's anchor was already counted from the start of the dose day in
the person's zone; the satisfy path now hands the engine the same
instant.
The weight band and the BMI chart read `/api/insights/targets` inside a
shared record, and the route asked for a manage grant, so a read or
write delegate got a 403 and saw no band and no BMI. The targets are
the bands the dashboard snapshot already paints on a whole-record read
grant, computed with no provider, so the read now asks for the same
level. A section-scoped grant is still refused.
The sheet now owns its dismissal guard and the Back handling, and three pages gained one or two lines for the save and refetch order; each branch had recorded its numbers, the merge kept only one side.
Now that the BMI chart draws inside a managed record, a child's normal
BMI sat in the red under-17 band, and the weight page painted the adult
WHO band (about 26 to 35 kg for 118 cm), so a healthy 21 kg read as
underweight. The release's own weight-band test pinned that band.
No band here knows a child's age yet. For a record under 18, or a
managed record with no date of birth, the targets read names no WHO
weight range, no BMI range and no category; the record's own weight
target still shows, and the values and the chart stay. The BMI chart
and the weight band draw only what the server names. The weight-band
test now uses an adult height and gains the child case.
The targets builder and the vitals builder grow by two lines each for
the flag; their size baselines are raised to match.
A create form kept its key until a save succeeded. The server stores a
refusal under the key for a day, so after a 422 or 409 the corrected
retry ran as a different body without any protection, and a timeout on
it could store the entry twice; an unchanged retry after the cause was
gone replayed the stale refusal until the form was closed.
Any answer now settles the key. Only a timeout, a dropped connection, or
the 409 that says the first request under the key is still running
(`X-Idempotent-Replay: false`, now `ApiError.inFlight`) keeps it, so
those retries still replay the first answer.