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.
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.
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.
Deleting a vaccination, a doctor visit or an address-book entry closed
the sheet with no way back, although all three are soft deletes with a
documented restore route. The vaccination hook had a restore mutation
nobody called; visits and the address book had none.
The two hooks gain a restore mutation, and each delete now confirms
with an Undo toast, the same pattern the illness episode uses. The Undo
runs after the sheet has closed, so it awaits the restore itself and
the hook's own onSuccess refreshes the lists, instead of relying on
callbacks a closed sheet never receives. Restoring an address-book
entry brings the entry back, not the links the delete cleared on the
visits that named it. The three new success toasts are pinned in the
success-affordance guard.
The dashboard snapshot mock still called `buildWeightBandsFromHeight`,
which went with the weight floor change, and kept the old 30 kg lower
edge. It now builds the bands the way `buildTargetBands` does.
personal-record-badge.tsx, intake-history-editable.tsx and
intake-history-preview.tsx had no importer outside their own tests and
the preview, so they rendered nowhere while four guard tests kept them
listed as live surfaces. They go with their tests, their guard entries
and the strings only they used.
GET /api/personal-records stays: the native app reads it. The
resolved-field guard now records its benchmark band as a native-only
field instead of naming the deleted badge as its reader.
A mood, symptom, lab value, custom metric value, unscheduled dose,
allergy, vaccination, visit or measurement saved on a weak signal could
be stored twice: the POST committed, the answer missed the 15 s
timeout, the form said it could not save, and the second tap was a new
call with a new key, so the server ran it again.
useIdempotentPost keeps one key until a save lands and renews it for the
next entry, so the retry replays the first answer. The create forms and
the three create hooks post through it; the comment on the per-call key
in api-fetch now says what that key does and does not cover.
LabForm and MoodForm each gain the one line that mounts the key, so
their function-size baselines go up by one. The intake toast guard now
recognises a dose posted through the hook.
The cycle page, its ring and the summary card took "today" and their
read window from the device clock, while the server keys every cycle
day, today included, in the profile zone. With a phone in another zone
the highlighted day and the day "log today" writes to sat one apart
from the rest of the app around midnight.
One `useCycleWindow()` now dates today in the display zone and derives
the shared window from it; it replaces `localYmd` and its two copies.
A weight target was refused below 30 kg on every save path (the own
targets editor and a managed record's settings parse the same schema),
while a weight reading is accepted from 1 kg. A guardian setting 20 to
23 kg for a child got only "please check your entries". The target
floor is now the measurement floor.
The weight bands on the dashboard and the weight page started their low
red zone at a hard-coded 30 kg, so a target below it lost that zone; the
edge now follows the same floor. Bands are clipped to the chart's own
domain, so an adult's chart looks the same. `buildWeightBandsFromHeight`
had no caller left and goes; the band builder's 30/250 defaults, which
every caller overrode, go with it. The unit-adapter test keeps its
inward-rounding check at the new floor (1 kg is 2.2046 lb, offered as
2.3).
The derived BMI card kept its own copy of the WHO cut-offs beside
`classifyBMI`, which the targets panel, the status text and the
overview use. It now maps that one classifier onto its four-word
vocabulary, so a later change to the cut-offs has one place to land.
Recovery, strain and other computed rows in the measurement list carried
a pencil and a bin. The pencil opened an edit sheet labelled "Value ():"
whose save the server then refused, and the bin deleted a score the
engine owns. Both are gone for COMPUTED rows.
Readings from a device or a provider keep both controls: the server takes
a note or time edit on them, and a wrong scale reading has to stay
removable. The server is unchanged.
The dose reminder printed the slot as the raw stored HH:mm, so it said
"20:00" while the medication card said "08:00 PM". That hit everyone who
set a 12-hour clock, and every en or ko user on the automatic setting,
which resolves to 12 hours for those languages.
The reminder now formats the slot's instant in the owner's zone with the
same formatter the card uses, honouring the owner's hour-cycle choice. A
recipient reading the message in another language gets that language's
clock when the owner left the setting on automatic.
The custom metric, allergy, vaccination, booster, visit, address-book,
workout and quick-add dose forms printed the server's error string as
it came: English in every locale, and for a network failure the
browser's own TypeError text. They now resolve the error code through
localizedApiError like the other entry forms, so a known refusal reads
in the person's language and anything else falls back to the form's
"could not save" sentence. A custom metric whose name is taken keeps
its specific message, now translated.
The workout form's browser test expected the server prose and now
expects the form's sentence.
Typing 720 instead of 72.0 in the measurement edit sheet showed the
English banner "Validation failed" in every locale, with no field
marked. The route refuses a changed value outside the type's band with a
422 whose issue list names the field, and the client dropped that list.
The sheet now checks the same band before sending and says, under the
value input and in the unit it shows, which range is accepted. ApiError
carries the field paths of a refusal, so a value the server still
refuses lands on the field too; any other refusal goes through
localizedApiError instead of printing the server's prose.
Inside a shared or managed record the weight page shaded the band for
the viewer's own height, and the record's own target never arrived
because `/api/user/thresholds` refuses under a switch: a parent saw
their own healthy range behind a child's weights. The BMI page decided
from the viewer's height whether there was anything to show, and the
target gear opened an editor that could neither load nor save there.
The weight page now reads the record's target and height from
`/api/insights/targets`, the payload the reference panel under the
chart already uses, and paints the same wings as before. The BMI page
reads the record's height there inside somebody else's record. The
gear stays hidden in another person's record, as the BMI chart
already draws nothing there rather than a wrong figure.
With the Medications module switched off, dose reminders, missed-dose
rows and low-stock alerts kept coming, a tap on one landed on
"Medications is switched off", and the Telegram buttons still booked
intakes. The reminder tick now drops the medications of an owner with the
module off, one module read per owner and tick, so neither a reminder nor
a missed-dose placeholder is written while it is off. The low-stock pass
skips such a user, as the mood reminder does for its module, and the bot
offers and books nothing.
A medication whose course had ended still counted as live: the low-stock
pass kept warning about its stock, and the bot's /add list offered it and
booked a dose after the end, taking a unit off the stock. The low-stock
pass and every bot path now skip a course that has ended on the person's
clock. A course that has not started yet keeps its reorder warning and
stays in the bot. A paused medication is already active: false, so the
existing filter keeps it out.
The bot's six copies of the medication lookup go through one helper.
The notification matrix listed "Unusual measurement", "Low adherence",
"Withings sync failed" and "Personal record" with a switch per channel,
three of them on by default, yet nothing dispatches any of them: a reading
far outside its range or a week of missed doses produced no message.
eventTypesForRole now leaves them out for every role, next to the
admin-only filter, and the preferences read no longer serves rows stored
for them. Their labels in all locales and their tap targets are deleted.
The types stay in EVENT_TYPES, so the preference write and the stored rows
keep parsing; no migration.
The label and tap-target tests walked EVENT_TYPES and so pinned the four
in place; they now walk the event types that are sent.
"What changed since your last panel" rounded every delta to two
decimals, so TSH 0.004 to 0.009 read "no change", and it printed values
and deltas through `String()`: a full stop in every language and every
digit of a unit conversion (41.999198717948715 ng/mL).
The summary now reads each reading at the precision it was recorded at
(`recordedPrecision`, with the report's printed figure for a converted
value, which the route now selects) and rounds the delta to the finer
of the two, so its direction agrees with the printed figures. The card
prints both through `useLabNumber()`. 5.9 - 5.6 still comes out as 0.3,
so the existing marker-pairing test stands as it was.
A dose date is stored at UTC midnight. The health record PDF, the share
page, the related-records list, the document link chips and both dose
pickers formatted it as an instant in the reader's zone, so across the
Americas each of them showed the day before the vaccination list did.
The booster anchor added the interval to the stored instant, which put
the first due day a day early there too.
The report and share page print dose dates through the calendar-date
formatter, the link surfaces through `useCalendarDate()`, and the
booster counts its interval from the dose's date to the start of that
day in the person's zone.
A save after the session ran out, or into a rate limit, showed the form's
generic "could not save". The server already names both refusals in
meta.errorCode (auth.missing for an ended web session, the status codes
http.rate_limited, http.forbidden and http.invalid otherwise), but the
catalogs had no sentence for them, so the lookup fell through to the
fallback. They have one now in all ten locales, and a refusal that
reaches the client without a code (a reverse proxy's 429) is read by its
status the way the server labels it.
A dose date is stored at UTC midnight and an allergy onset at noon UTC;
neither has a time of day. The day view, the day index, the visit
preparation and the timeline treated both as instants in the person's
zone: west of UTC a dose landed on the previous day, and the day view
printed a clock time nobody entered (01:00 for a dose in Berlin, 13:00
for an onset).
These readers now take the date part, as the vaccination list and the
allergy form already do: the day and the index query the UTC calendar
day (the window documents already use), the events carry no instant,
and the timeline dates both kinds by their key. Correct for midnight
and noon rows in every zone, without touching stored data.
Saving a weight, a mood, a workout, a measurement edit or a quick-add
dose emptied the form and kept the button spinning until every read the
write touches had refetched (snapshot, digest, charts, insights). On a
slow connection that looked like nothing had been saved. The vaccination,
visit, address-book, symptom and check-up hooks did the same through an
onSuccess that returned the refetch, which a mutation waits for.
The forms now report and close first and start the refresh without
waiting for it; the hooks no longer return it. The quick-add loses a
second sweep of the inline compliance charts that the medication bundle
already covers, and its source pin follows.
The date of birth is a calendar date stored at UTC midnight. The PDF
cover and the share page formatted it as an instant in the owner's
zone, so everyone in the Americas read the day before; settings and
the FHIR export, which take the date part, already showed the right
day. Both now print it through a calendar-date formatter (read in UTC,
the owner's date order), which is right in every zone, unlike a noon
anchor that moves the day east of UTC+12.
Confirming a document's facts creates medications, conditions and lab
values, and the profile take-over creates a condition or an allergy.
Both evicted only the roots of those lists, so the dashboard and the
Today digest showed the old medication set until the 120 s poll, the day
and timeline kept the old state for five minutes, and adherence and
nutrient reads were not evicted at all.
Both now spread the bundles of what they create, and the condition paths
pair them with the inactive refetch of the daily reads.
The settings avatar took its fallback letters and its alt text from the
login, while the top bar and the sidebar spell the display name, so the
same account showed different initials on two screens. The three
copies of the initials function become one `nameInitials()` beside
`accountName()`, and the settings avatar reads the same name.
Marking a check-up done on the dashboard updated its tile, while the
Today rail above it kept listing it as due until the 120 s poll. The
reminder writes refreshed the two daily reads only through the inactive
pass, which fetches nothing for a query that is on screen. They are now
invalidated the ordinary way too, so a mounted rail refetches, and the
day root joins them because a check-up marked done is listed on its day.
The test fake pinned only the inactive call; a real query client now
shows the mounted rail refetching.
The report cover, the PDF author field, the FHIR Patient and the share
page printed `fullName ?? username`, and the collector never read the
display name. A managed record has no way to get a full name, so its
generated `managed-<hex>` login landed on every clinical document made
for it; an own account printed the login instead of its display name.
The collector now resolves the name once (`patientName()`: full name,
display name, login, never a managed record's generated handle) and the
five readers print that one field. `patient.username` and
`patient.fullName` leave the report payload; nothing else read them.
Symptoms, workouts, lab results, allergies, documents and questionnaires
evicted only their own lists. An open day, its calendar dots and the
timeline kept the pre-write answer for up to five minutes, so "Capture
for this day" saved a symptom or a workout and the day showed the old
list, and a deleted row stayed on it.
Workouts, labs, allergies and documents get a bundle beside the seven
that already carry the day and timeline roots; the six lab lists become
one (the OCR dialog's own copy of the commit hook's list goes with it).
Symptoms and questionnaires add the two roots to their list. Symptoms
stay off the illness bundle on purpose: nothing on the dashboard or the
Today digest reads them. Custom metrics need nothing: neither the day
nor the timeline reads them. invalidateKeys now drops a key listed twice,
since spread bundles share the two roots.