An unreadable provider key is now reported once: the empty-chain fallback no longer opens the same key a second time. Two files shrank, so the size baseline steps down.
The comment said the series route caps sleep at 365 days; the route
accepts up to 3650, which is what the "All" tab asks for and what the
clamp below applies.
The cycle page's calendar card showed one line of loading text until
its read arrived, then grew to the month grid, so the predictions and
everything below moved down once the page had drawn. The card now
draws the month header and this month's weeks in the grid's own cell
size while it loads, with the loading text kept for screen readers.
`/admin/backups` opens with the recovery readiness card, which showed a
one-line spinner until its seven signals arrived as one block, so the
backups and off-host cards below jumped down by the whole list after
they had drawn. The loading state now draws one placeholder row per
signal in the list's own row shape, with the loading text kept for
screen readers.
A metric page without data offers to ask the Coach about the metric.
44 of the 49 pages passed that question as an English literal, so a
German, French or Korean reader got an English first message; only 5
read it from the catalog. 31 of the 44 were one sentence with the
metric's name swapped in.
`HealthKitMetricPage` now asks that sentence once, translated, around
the page's own title, and the 31 pages drop the prop. The 13 pages
with a question of their own (steps, HRV, sleep, mood, workouts, blood
pressure and others) read it from the catalog like the 5 did, in all
ten languages.
The dashboard snapshot carries the mood tile's series from the same
engine `/api/mood/analytics` answers from, and the snapshot already
seeds the widget layout, but nothing seeded the mood analytics cell the
mood chart reads, so every dashboard open with the mood chart asked for
the series again. Both seed points, the server render and the client
snapshot read, now fill that cell too, under the same rule as the
layout: only an empty cell, and only when the tile has readings, the
one case where its summary is the route's.
Every document load, the sign-in page included, mounted a client
effect that asked `/api/monitoring/settings` for three instance-wide
values (false, null, false on a default install) before it mounted the
analytics script or the error reporter. The root layout is a server
component and now reads them while it renders and passes them in, so
the request and the effect go. The key-mismatch page reads nothing, as
before. The route stays for other callers.
The layout shell around every Insights page read the advisor briefing
(`GET /api/insights/generate`, about half a second on the demo) for the
regenerate button on the tab strip, and once the answer arrived it
loaded the schema chunk that parses it. The strip shows no briefing;
only the overview does, and it reads the same cell itself. On a metric
page both were spent on a button.
The shell's hook now reads the briefing only while its own regenerate
settles. A regenerate from a page that never read the briefing reads
it first, so a timed-out attempt still settles against the briefing it
replaces instead of taking the old one for fresh. The button, its
toasts and the overview are unchanged. The strip's comments that said
only the overview carries the button are corrected.
The history tab asks for a window of at most a year, but the route read
every intake the medication ever had: a medication taken three times a
day for five years read about 5,500 rows on each open. The full read
also served to find the last intake for rolling cadences.
The route now reads the rows anchored or taken in the window, and of
the rows before it only the latest take, which is the anchor a rolling
grid re-starts from and the last intake when none falls in the window.
Earlier takes place no slot in the window, so the ledger is the same.
The read moves into its own function, which shortens the handler.
Insights > Medications sent two requests per medication on entry: the
compliance calendar inside each card and the efficacy read behind the
summary card, which sits below every card and shows one line per
medication. The summary now mounts through `DeferUntilNear`, like the
overview's sections, so the efficacy reads go out only when the reader
scrolls toward it. The cards and what the summary shows are unchanged.
The comment that called the summary "never a second computation" goes:
the efficacy route builds the view on every request.
`/admin/coach` mounts the AI quality card and the Coach feedback card,
and both read `GET /api/admin/ai-quality` with the same unwrap, under
two different query keys, so every open sent the request twice. The
feedback card now reads the quality card's cell.
The version poller asked `/api/version` every 60 s from a plain timer,
also in a background tab or a minimised window, for as long as the
browser let timers run. It was the largest standing poll and the only
one that kept going while hidden, although the poll notes describe it
as the same shape as the others.
A tick now asks only while the tab is in view, and a tab that comes
back after a full interval away checks once at once, so a release
still surfaces on return.
`/api/insights/health-status` recomputed the coincident deviation and
the 180-day changepoint scan over every vital on each open of Insights,
the only read on that page that stayed slow when warm, for a body of a
few hundred bytes. It now goes through the same analytics cell its
sibling reads use. Measurement and mood writes sweep the record's
cells; the reader's day and the switched-off modules are part of the
key, so a new day or a module switch never serves the other answer.
Weight thresholds accept values from 1 kg, the notification preference descriptions name only the events something sends, and the error-code catalogue lists the new codes.
The settings nav filtered its Coach entry by the raw module map, which
follows the operator's switch and Hide Coach but not a missing provider
or missing consent, so it kept an entry the sidebar and the bottom bar
had already dropped. It now reads useNavModules like them. Settings, AI
decides from the same map whether to list the stored Coach memory, so
the memory always has one place in Settings where it can be read and
deleted.
On a phone the tour falls back to a card at the bottom edge, 8 px above
it, so in the installed app its Next and Skip row sat inside the home
indicator strip. The card now adds the bottom safe area, as the bottom
bar and the sheets do.
The alias chips on a biomarker and the attached-document chips of a
share link had 24 and 20 px remove buttons that act on the first tap,
without a confirm. Aiming for one chip's x on a phone could remove the
one beside it. Both keep their look and take the 44 px touch area the
attachment pills already use.
Both dropdowns asked for the trigger's width with the Tailwind 3
shorthand `w-[--radix-popover-trigger-width]`, which Tailwind 4 turns
into a declaration no browser accepts, so they took the width of their
longest row. The v4 form fixes the width, and `max-w-none` lifts the
popover's 20 rem cap, which is shorter than the field on a phone.
Held sideways a phone keeps the bottom bar, and the sheet's 90 dvh cap
is about 350 px there, while the hub needs 410 to 540 px. The grid did
not shrink or scroll, so the last rows (documents, timeline, coach,
achievements) sat below the screen edge. It scrolls inside the sheet
now and stops at its end instead of pulling the page.
The capture picker and the mood page import `sheetBodyHasUnsavedInput`
from the sheet module now, and two tests that replace that module with
a stub failed on the missing export.
The end-to-end setup still seeded the shared-provider offer's account
and captured its session, although the journey that used it is gone.
Three comments still pointed at what was removed: the tour launcher
cited the deleted personal-record badge as its model, the personal
record worker said the badge still renders, and the morning briefing
push said a concerning reading goes out as an anomaly notification,
which nothing sends.
The Coach's "Choose from documents" picker reads the newest fifty
documents and dropped the route's nextCursor, so with a longer vault an
older report seemed not to exist unless one already knew to search for
it. A cut list now ends with the same line the document picker shows:
how many are shown and that the search reaches the rest. The document
row moves into its own component, which takes AttachmentPicker under
the function-size cap.
Every metric page asked `/api/insights/coach-read`, and every call ran
the full 180-day discovery scan over every channel, plus the emerging,
lab and coincident passes, to pick the one driver whose outcome is the
page's metric. The scan does not depend on the metric, so each page in
a row paid over a second for the same answer and the strip stayed a
skeleton long after the chart had drawn.
The strip now reads the drivers through an analytics cell keyed on the
record, the language and the switched-off modules, the same contract
`/api/insights/correlations` uses: every measurement, mood, medication,
illness, custom-metric and pattern write sweeps the record's cells. A
failed scan is not kept. The band and the latest reading stay per call.
On /settings/anamnesis the four note fields were raw textareas styled
by three copies of a class constant with text-sm (14 px), and on
/settings/export the three paste fields used text-xs (12 px) and the
backup passphrase text-sm. iOS zooms the page into such a field on
focus and leaves it zoomed. The notes use the Textarea primitive now
and the export fields keep the small size only under a fine pointer.
The 16 px guard read the five field primitives and could not see a
call site override their size, or a raw field styled from a constant.
It now parses every field call site; four that are still small sit on
a list that only shrinks. The focus guard drops the three records
files, whose fields come from the primitive now.
Inside a shared or managed record the BMI chart drew nothing: it only
knew the viewer's height, and dividing someone else's weights by it
would have given a plausible wrong figure, so it divided by nothing.
The chart now takes the record's height from /api/insights/targets,
the cell the BMI page and the reference panel already read, through a
small useBmiDivisor hook; in the viewer's own record it keeps using the
account's height.
The person's chat bubble took the first two letters of the login, so a
managed record read "MA" for its generated handle and everybody else saw
other letters than in the top bar. It now uses nameInitials(accountName)
like the top bar, the sidebar and the settings avatar. The thread test
that pinned the login's two letters now expects the shared spelling.
The life event hooks returned the timeline and day refresh from
onSuccess, which a mutation waits for, so saving or removing an event
kept the sheet spinning until the whole timeline had reloaded. The
refresh is now started and not returned, like the other record sheets.
Seven places kept information in a hover tooltip, which never opens on
a phone: the measurement-diversity lightbulb on the metric pages, the
sleep-source warning on the dashboard tile, the score coverage
breakdown, the target status pill, the range bar's marker, the colour
hint of the dashboard averages and the full token and user names in
the admin token list. The lightbulb showed a button that did nothing,
the warning a sign nobody could read.
They open on a tap now: InfoPopover for the two icon buttons, a
Popover on the meter, pill, marker, averages and cells. Where the
trigger sits in a link (a dashboard tile, a lab row) its tap opens the
popover instead of the page.
A screening, an illness episode, a life event, a symptom, a custom
metric or an address-book entry created on a weak signal could be
stored twice: the POST committed, the answer missed the timeout, and
the second tap went out under a new key. Their routes already honour
the key; the forms now post through useIdempotentPost like the other
create forms, so the retry replays the first answer.
The screening's save moves into a small hook beside MentalWellbeing,
which sat at the function-size cap and now drops below it; the custom
metric form's create-or-update call moves into one as well.
The illness episode hooks returned the refresh of the illness reads and
the dashboard from onSuccess, so every episode sheet kept spinning until
those had refetched. The mood edit dialog, the mood and measurement bulk
deletes and their Undo toasts did the same with an await, and so did the
dose edit dialog, the log-intake dialog and the ledger's add dialog.
They now answer and close as soon as the write lands and start the
refresh without waiting for it. A scheduled dose lands on its slot, so a
card that is still stale for a moment only repeats it.
The take and skip buttons on a medication card keep waiting: nothing
marks the dose in the cache meanwhile, so releasing them early would
offer a Take for a dose already recorded.
Back on a phone (the Android button, the edge swipe of the installed
app) with a sheet open went back a page under it: the sheet and what
was typed in it were gone, in the edit sheets too. Only the account
form guarded Back, behind an option.
Opening a ResponsiveSheet now pushes one history entry with the page's
URL. Back lands below it and closes the sheet, asking first when it
holds unsaved input or when its owner keeps it open; a sheet closed any
other way takes its entry off again, so the next Back still leaves the
page. A history write made while that Back is under way waits for it, a
link followed from a sheet replaces the sheet's entry, and the page
entry keeps the URL the page has now, so a stripped one-shot `?add=`
does not come back. The vault's document sheet already owns `?doc=` and
opts out.
The visit, vaccination, allergy, address-book and workout forms read a
refusal's code through the catalogue, but nineteen of the codes their
routes send had no sentence: a stale doctor or check-up, a planned visit
that would close a check-up, a dose without a name, a booster that
cannot be planned, an invalid body, a workout batch that is too large.
Each of them read as the form's generic "could not be saved", which
hides what the person can change. They now have their own sentence in
all ten languages.
The eight-step medication wizard, the add sheets on /measurements,
/mood and /labs, and the visit, vaccination, practitioner and checkup
forms closed on a stray backdrop tap or swipe and lost what was typed.
The dashboard quick entry and the capture menu asked first, through two
hand-copied dialogs and a DOM check that read the first sheet body in
the document and counted the date field's prefilled "now" as input.
ResponsiveSheet now takes a dirty flag, or a check it hands its own
body, and asks with the existing discard dialog. Forms that keep a seed
pass whether they differ from it; the wizard compares what a save would
send. Plain add forms pass the DOM check, which skips the date and time
fields again. The three copies of the dialog are gone.
About forty refusal messages still printed the error string the server
sent: English in every locale, and for a dropped connection the
browser's own TypeError text. Mood tags, targets and thresholds, family
history, medication side effects, phases, API tokens and dose imports,
the Coach model picker, the AI provider, fallback chain, timeout and
Codex cards, the notification channel cards, the avatar, passkeys,
security keys, the authenticator card, the re-proof dialog and the
admin pages now resolve the refusal's code through localizedApiError
and fall back to the surface's own sentence.
Two refusals carried something a person needs and had no code: a
private AI base URL (the AI_PRIVATE_ORIGINS hint) and removing the last
passkey. Both now send a code with a sentence of their own. A throttled
re-proof says so in the reader's language. Raw fetch sites throw the
same ApiError as the verb helpers through the exported throwApiError,
which replaces the admin's getApiErrorMessage; the authenticator card's
copy of describeStepUp is gone, and describeStepUp no longer passes a
non-step-up refusal's prose through (its test pinned that).
The admin password reset keeps the server's verdict on a 422: the
server words the strength and breach verdict in the account's language.
Someone whose doctor agreed a target of 140/90 entered it on /targets;
the chart and the dashboard tile followed it, but the BP and pulse status
texts, the general status, the comprehensive overview, the insight-card
alert and `extractFeatures` (and through it the comprehensive text, the
daily briefing and the Coach) still graded every reading against the age
default. The text under the chart could contradict the chart.
All six places now resolve the band the way the charts do since #1261:
`resolveEffectiveBpTargets` for blood pressure, the PULSE override for
resting pulse, the default only where no target is set. The resolved band
rides in each snapshot's targets block, which is part of the input hash,
so the next generation follows a changed target. The Health Score keeps
its clinical targets on purpose.
The nightly plan review read a before/after window of the plan's metric
and stored an English verdict in `outcomeEncrypted` for every plan. No
screen and no route returned it; only the Coach snapshot read it, behind
the `COACH_EXPERIMENT_VERDICT` flag that is off by default. The worker now
only moves an active plan whose review fired to `reviewed`, which is what
takes it out of the standing group. The flag, its snapshot block, the
three eval cases for that block and the compose and env entries go with
it. The Coach prompt keeps its experiment rules and now ties the
read-back to the plan's progress line instead of the removed block.
Existing stored outcomes stay in backups untouched.
The plans page and the Coach reminders card printed the metric as a raw
key in capitals (WEIGHT, BLOOD_PRESSURE) in every language. Both now show
the measurement's name from the catalogue, through the plan metric
resolver moved to `plan-metric.ts`; a key HealthLog does not read shows
as written. The unused `coach.plans.proposalTitle` string goes.
Every new account finished setup on a full AI panel: a gradient badge, a
sample briefing, three ways to connect a provider, a one-tap offer for the
operator's key and a disclaimer, the most prominent block on the screen.
It is now one muted line with a link to Settings, AI, shown under the same
rule as the Insights hint (could be set up here, nothing configured yet,
not opted out). That hint and the consent request on Insights and
Settings, AI stay the places that invite.
The sample briefing card, the `onboarding.ai.*` strings in ten locales and
the end-to-end spec for the removed offer go with it. The consent-grant
guard's floor drops to the two callers left.
The resolver tests queue their fixture rows through one helper instead of
an `as never` per row, the no-provider status case also proves the
single-provider resolver is never asked, and the size and comment
baselines take the lower numbers the change left behind.
Every correlation card carried a permanently disabled button with the
tooltip "Coming soon", which on a phone (no hover) was just a dead
control. The feature it stood for was never built; the button and its
two strings go in all ten locales. The Coach hand-off and the dismiss
action stay.
Eight call sites (Coach turn, status texts, briefing, comprehensive
insights, medication extraction, about-me questions, both lab OCR paths,
the saved-config connection test) answered an empty chain by calling the
single-provider resolver, which ignores the chain's switches. A person who
kept only the local model on and lost its address got the ChatGPT sign-in
or the operator key instead, and every copy tagged that provider as the
operator key, so consent and cost attribution saw the wrong owner.
The rule now lives once in `resolveProviderChain`, and the presence probe
reads the same rule: a saved chain is the person's word, so an empty one
stays empty. Only a chain never saved falls back, to the OpenAI-compatible
gateway picked under Settings, AI, because the default chain has no entry
for it; that entry carries its real type. The callers drop their copies,
and the presence helpers that restated the old fallback go with them.
A person who switched both the Coach and AI analysis off under Settings,
Modules still got invited to set AI up: the Insights hint, its copy in
Settings, AI, the checklist row and the onboarding panel all keyed on
`provider.canConfigure`, which only knows the record kind and the
operator switch.
The flag itself stays as it is, because the AI usage card and the consent
request read it too and still serve document reading, lab photos and
medication extraction. The three invitation readers now also check the
module map through `hasOptedOutOfAi`.
The day view, the day index and the visit preparation each built the
UTC calendar window for a date-only column by hand (documents had a
fourth copy). `dateOnlyWindow()` beside the other date-only helpers
replaces them. The report name select becomes one spread, the share
page builds its calendar formatter outside the view, and the PDF
section reads it off the context, so the functions these fixes touched
stay within their size budget; `DocumentRecordLinks` drops under it.
The picker for linking documents to an illness and for choosing
documents for a share link reads the newest fifty and dropped the
route's nextCursor, so with a longer vault an older report seemed not
to exist unless one already knew to search for it. A cut list now ends
with a line that says how many are shown and that the search reaches
the rest. Paging was left out: the coach attachment picker shares the
picker's cache key with a single-page read.