The done screen moves focus to its heading like every other screen; the
confirm screen's preference list holds only dt/dd pairs and the settings
link stands beside it; Back from Q1 is not offered once the flow has
been entered, because the front door would resume past the welcome and
bounce straight back; the medication task records no target on the
ledger, as the contract says, and keeps the id on the screen; and the
two success affordances go through the outcome module rather than a
hand-rolled tick. The error-code catalogue lists the answers and restart
codes and no longer the retired step ones, and the session-surface guard
learns the one new deriving helper the onboarding pages read through.
The getting-started checklist reads the answers: a visit named within a
month adds a "prepare the visit" row that is done once one is on the
calendar, and the data-source row names the wearable that was chosen.
Its per-browser state moves to one module so the done screen, the skip
and Settings write the same keys.
Settings → Account gains the two ways back in: "Set up again" through
the restart route, which resets the ledger, keeps the answers as
prefill and touches no module; and "Show the checklist again", which
forgets what this browser hid.
A page whose module is off now answers with the shared notice and the
one action that helps, in one place for every module page: the shell
maps the route to the module through the navigation model's own gate,
so a page and its nav entry cannot disagree about which switch they
follow. And the tour's two dead stops get their anchors on the
integrations and health-record group headings.
The needs-based onboarding has no read of its own — its state rides
GET /api/auth/me — but every screen that persists an answer, the confirm
screen, the disclaimer acknowledgment and the Settings restart card are
mutations, and the queryKey-factory rule allows no bare key at a call
site. One module names them, and one read-back key for the reading the
first-result step logs, under the measurements prefix so a later write
evicts it.
A managed profile could be created and deleted and never changed. A name
typed as a placeholder was permanent, a household that moved kept the
timezone the record was created in, and there was no way to correct a date
of birth somebody finally had (#939).
PATCH /api/managed-profiles/{id} takes the fields creation takes, every
one optional and at least one required — an empty body is a refusal rather
than a save that audits a change nobody made. It carries the gate the rest
of the family carries: a cookie session with a fresh second factor, an
active Guardian grant on the record, and the same byte-identical 404 for
"no such record" and "not yours". A GET beside it answers the record as it
stands, because the account payload names a managed record and nothing
else, and a form filled from that would have had blank fields to send.
The new field is gender. Leaving it out of v1.37.0 meant every managed
record started with the column NULL, which is what the cycle module
derives its default from: a guardian creating a record for a child could
neither record the answer nor turn the module off from there. It reuses
the column and the three values every other surface writes, so there is
one word for one column rather than two.
Refs #939
crypto.randomUUID() is [SecureContext] in the Web Cryptography IDL, so a
page served over plain HTTP never gets it. Measured in Chromium against
http://<lan-ip>: isSecureContext false, randomUUID and subtle undefined,
getRandomValues still a function.
The document vault minted its queue id and Idempotency-Key that way, inside
the file-input change handler. On a LAN self-host the call threw before a
queue row existed, so picking a file produced no upload, no error and no
trace — the picker simply looked inert, and the only way through was to
reach the app over HTTPS. The client transport has minted a default
Idempotency-Key the same way since v1.37.18, which put every apiPost and
apiPatch on the same footing: on such a host nothing wrote at all.
Add randomId(), which prefers the platform call and otherwise assembles a
v4 UUID from getRandomValues — the same CSPRNG, the same 122 bits, no
secure context required — and route every browser-side call site through
it. Nothing in an upload needs one: the bytes travel over multipart XHR and
every cryptographic operation happens on the server. Passkeys, web push and
home-screen install still need HTTPS; browsers withhold those outright.
A structural guard fails a direct crypto.randomUUID() anywhere outside the
two server-side files that may keep it, so the class cannot return one call
site at a time.
Also point the CSV and JSON import cards straight at the import guide. They
resolved through a redirect under the integrations tree, which is the wrong
place for a guide that is not an integration runbook. The suite that
covered those links pinned the old spelling rather than the guarantee; it
now asserts an absolute docs-site URL, no internal path, and new-tab
hygiene.
`User.glucoseUnit` has existed since v1.2 and around thirty surfaces read
it — the dashboard tiles, the glucose page, the targets panel, the CSV and
FHIR exports, the doctor report, the Coach snapshot, the low-reading push
copy. Nothing could ever write it. No route, no schema, no field on any
form, so every account resolved to the mg/dL default and the mmol/L half
of the app was unreachable code. Anyone who reads glucose in mmol/L
converted in their head on every screen.
This is the missing end: a `GET`/`PATCH /api/auth/me/glucose-unit` scalar
on the pattern the unit-preference endpoint next door established, and a
dropdown in the profile form beside it. Deliberately its own control
rather than a branch of metric/imperial: metric countries are split on
which unit they read glucose in, so one dropdown would get half of them
wrong.
Storage is untouched and stays untouched. Every reading is written in
mg/dL by every ingest path and the unit only selects a display branch, so
nothing already recorded is reinterpreted. The entry form moved with the
preference, which is the half that could have hurt: it asks in the chosen
unit and inverts through `toCanonicalMgdl` on submit, so a 5.3 typed by an
mmol/L reader is filed as 95 mg/dL rather than as 5.3 — a value the
plausibility band accepts in silence and every surface reads back as a
severe hypo.
The contract gains the endpoint and nothing else; the account payload
already carried the field.
The per-user retry ledger has recorded every AI delivery outcome for a
long time, but no surface ever read it for the operator: a central
provider could fail on every call for weeks and the admin console said
nothing. The AI settings section now carries a health card, backed by a
new admin-only endpoint that folds the ledger into one row per provider
type: users tracked, users currently failing, the worst uninterrupted
failure run, and the last success and failure instants. Operator-managed
types sort first because their failure takes the shared fallback down
for everyone whose chain relies on it. Counts and timestamps only; the
response never names a user.
The restore dialog asked for its typed confirmation on nothing but a
filename and a date; the counts only surfaced in the audit row after the
rows were already replaced. A read-only summary endpoint now decrypts and
schema-validates the stored file through the same helpers the restore
itself runs and answers the summarizeBackup counts, and the dialog shows
them — per section, with an other-records line so a future summary key
can never silently vanish from the preview. A failed preview states its
own absence and never blocks the restore.
Refs A3-8. The per-item PHQ-9 / GAD-7 / WHO-5 / SCI answers have been
stored encrypted since the assessments shipped, and nothing ever read
them back — the clinical detail (item 9 included) was invisible. A new
by-id read decrypts them for whoever holds the mind read grant (the same
grant that already serves the mood diary's prose and the item-9 flag),
degrades to an honest items-unavailable state on any decrypt or shape
problem, and the history rows gain an expandable breakdown rendering the
official instrument texts the check-in flow already carries — nothing
retranslated, no alarm styling on item 9 beyond what the house already
does.
Rides along: the guard inventories learn the branch's new routes (frozen
counts moved with reasons), the reminder-engine test harnesses carry the
ledger delegate, and the custom-metric restore toast keys sit in their
right namespace in all six locales.
Refs #223. A due Vorsorge item could only be done, deleted or ignored;
Telegram's postpone moved the date with no trace. Both decisions are
first-class now, and both are honest:
- POST /{id}/skip (no body): the interval restarts from the skip instant
through the recurrence engine, lastSkippedAt is stamped, skipCount
increments, any snooze clears. lastSatisfiedAt is NEVER touched — done
and skipped stay distinguishable forever, enforced in the primitive and
pinned by tests.
- POST /{id}/snooze ({until: YYYY-MM-DD}): the server resolves the day to
the notifyHour in the profile timezone and moves snoozedUntil and
nextDueAt to the same instant, so every due-state consumer follows
without knowing snooze exists. At least tomorrow, at most five years.
- GET /{id}/history: the new append-only completion ledger
(MeasurementReminderEvent), one row per satisfy from every engine path
and per skip, onTime derived at write time against the due instant that
was current when the event landed.
Two anchor fixes close the backdated-event gaps: the auto-resolve floor is
now the later of last satisfy and last skip (an old reading synced late no
longer resolves the cycle the user just skipped), and a backdated real
result re-anchors the next due at the later of its own instant and the
skip decision.
Screenings stay skippable and snoozable — the satisfy-side 409 guards
claimed fulfilment; a skip claims the opposite. Appointment reminders 404
on all three routes, in the lookup, like every by-id sibling. /checkups
gains the postpone sheet (quick chips, date field, the skip zone with its
own confirm) in both view branches, with muted meta lines and no card
colour change. The MCP preventive-care tool carries the resolved fields;
the OpenAPI contract publishes all three paths and the widened DTO.
Ten client mutation sites forgot a consumer: a coach-accepted checkup
stayed invisible for up to five minutes, a threshold or target edit left
the dashboard bands stale, a score-config save marked the daily reads
stale but never refetched them, a booked visit missed the Today rail, a
source-priority reorder kept every chart on the old canonical picks, an
adopted self-context note never reached the Settings read. Each site now
invalidates what its write changed and forces the unmounted snapshot +
digest reads to refetch through refetchInactiveDailyReads.
New illnessDependentKeys bundle carries the daily reads (Rest Mode
frames both), dailyDigest joins encounterDependentKeys, and the
chart-data prefix gains a factory accessor for whole-cache flushes.
The daily-reads guard learns the cycle / encounter / vaccination /
illness bundle families and a second matcher for direct snapshot/digest
invalidateQueries without a forced refetch, each with a frozen reasoned
allowlist. Both matchers were proven live by reverting a fix and
watching the guard go red (see the test doc comment).
GET/PATCH /api/anamnesis/emergency, wrapped in apiHandler. The PATCH
validates a partial body with per-field length caps, builds the Prisma
data field-by-field from the parsed input (never a spread), narrows the
user from requireAuth rather than the body, encrypts the free text on
write, and audits the change. The GET decrypts fail-soft for the form
prefill. Registers the OpenAPI operations and schema. An integration
test reads the row back to prove the write lands.
- from the record: a document link picker on the capture form, gated on the
documents module (blanked when off, never post-filtered)
- from the upload: a VACCINATION scan offers to file against a dose recorded
within the shared ±7-day window; a single candidate pre-selects, two offer a
picker with nothing pre-selected (WR-8 seen red on the pre-select rule,
reverted by content hash), zero shows nothing, and it never blocks the upload
- the suggestion rule reuses the visit moment's window constant rather than
forking it; it registers with the shared filing module when that lands
- suggestion and link copy in six locales
- rung 2: a prefilled, user-confirmed, editable, declinable prompt writes an
ordinary VORSORGE reminder keyed on the dose's primary antigen; the
published origin enum and reminder DTO are unchanged
- idempotent by antigen — a second confirmation re-anchors the existing
reminder rather than minting a second (WR-9 seen red on the reminder count,
reverted by content hash)
- rolling day cadence so logging the next dose re-anchors the due date through
the Phase 1 satisfy matcher; the whole loop proven through real routes
- booster prompt copy in six locales, plural-aware
- one sheet for create and edit, saving with a date and one identity arm
(catalogue pick or free text); nothing else can block the save
- catalogue picker searches localised names and generic synonyms over the
static antigen list, never overwriting the free-text arm
- optional dose/series, lot, site, practice combobox, visit suggestion and
note, all reusing the encounter-form primitives
- write hooks fan out through the dose dependent-keys and the reminder reads
- form/site/sheet copy in six locales
- read the immunization log through the query-key factory, unwrapping the
envelope; the series arrive resolved per component antigen from the DTO
- render one group per antigen, a combined dose appearing in each of its
component groups with that component's own position; free-text and dead-slug
rows fold under their verbatim name
- neutral cards, meta in muted, no due status on the page
- add the dose-list dependent-keys bundle, evicting the preventive-care root
alongside the dose list so a satisfied booster never lingers on checkups
- series/lot/error copy in six locales
The visit stage's second half landed while this was in flight and both
touched the same three places. Resolved as one set: the link facade
carries the reverse document-to-visit direction and the new dose pair
side by side, the sharing literals hold both sets of admissions with the
count re-added on top, and the language bundles take the shorter module
description the trunk settled on.
The dead-code gate is right: a bundle with no caller is dead code. The
capture surface brings it back with the mutation that needs it, and the
key module says what that bundle will have to evict.
The bundle landed a release before the surfaces that read it and was removed
on trunk as dead. Both mutation hooks in this branch use it, and the merge took
trunk's deletion, so it comes back here together with the writes that need it.
The comment now names its consumers, so the next dead-code sweep can see them
without reading two files.
Research Mode was the opt-in behind the estimated GLP-1 drug-level curve:
turn it on, read a disclaimer, get the chart. The chart stopped consulting
the flag several releases ago and has painted for every account since, so
the switch on Settings → Advanced changed no screen in either position. An
inert control that promises to hide a chart is worse than no control.
The curve is simply part of the medication page now. Gone with the switch:
the acknowledgment dialog, `/api/auth/me/research-mode`, the shared status
DTO and its gate helper, the disclaimer-version stamp, the query key, and
the three user columns the acknowledgment wrote (migration 0321). The
Privacy page's second button pointed at the retired control and goes too.
Settings → Advanced is now what it reads as: reset the data, delete the
account.
- a query-key module and a dependent bundle that also evicts the
preventive-care root, because logging a dose settles a booster and the
checkups list goes stale in the same tick
- the OpenAPI module publishes the resolved series with the contract
written into the description: render text from these numbers, never
recompute them
- the health-background consent sentence now names the immunization
history in all six languages, so the widening is stated where it is
agreed to rather than inferred from a code comment
- no server-side cache helper: nothing cached reads a dose, and a sweep
over caches nobody reads would look like coverage without being it
A lab panel files itself against the visit it came from, and never has to. The
OCR review and the manual form both offer the suggestion through the one shared
field, anchored on the draw date rather than on now, so backdating a reading
moves the offer with it. The commit writes one link PER RESULT ROW, which is
why the join is m:n: a marker re-run on a different day belongs to its own
visit. Best-effort by design — the readings are the write that matters and must
not be lost to a link that could not be made.
The condition side gets a Visits about this card with an action that pre-links
the episode. From the episode side only, and that is a decision: an episode
spans weeks and a visit is a point, so matching the other way would be wrong
often enough to teach a person to ignore every suggestion in the product.
A due checkup gains a second action beside erledigt. Filing the visit as done
satisfies the checkup through the same forward-only, idempotent primitive the
checkbox uses, so doing both is one satisfaction and no error. The two actions
answer different questions — one says it happened, the other says where.
An appointment now appears where a person looks, and NEITHER surface relaxes
the reminder-table exclusion that keeps it out of the preventive-care lists.
Both read the visit table: a quiet line under the dashboard checkup block for
anything inside two weeks, and one rail item for today or tomorrow. A diff in
either that touches origin is the wrong fix.
A visit write now also refreshes the reminder reads. Closing a checkup moves
its next-due date on three surfaces that would otherwise keep showing it due
until their own poll.
OpenAPI regenerated in the same commit.
The timezone resolver and the dependent-keys bundle shipped ahead of
their consumers, and the dead-code gate rightly went red on the trunk.
The surfaces that need them arrive with the next visit stage and bring
them back together with their callers.
Every arm here is the same shape as the one that already exists for
conditions, because the vault's filing model is settled and a second shape
would be the thing to explain rather than the thing to use: encounterIds on
upload and on PATCH with replace-set semantics, linkEncounter / unlinkEncounter
beside the episode pair in the bulk action, and an encounterId list filter.
Both directions ride ONE table. The visit's sheet writes encounter_document_links
from its end and the document reads it from the other, declared as a second
direction in the link service rather than a fourth table. A visit's kind
crosses the wire as the enum constant rather than a rendered name: unlike a
condition label, which is the person's own words, a kind is one of eight closed
values whose human name the reader's own bundle owns.
The suggestion is offered where the document is being looked at, resolved by
the shared rule rather than by this surface: one candidate pre-selects with an
undo, two offer a picker with nothing pre-selected, none offers nothing at all.
It resolves with no AI provider configured, and it renders nothing once the
document already names a visit.
The bulk verb has a caller — the vault's selection bar files a whole selection
against a visit, mirroring the condition verb and hiding itself on an account
with no visits. Nothing here is one-ended.
OpenAPI regenerated in the same commit.
/checkups gains a second section rather than a nav entry. The page already
owns the right sentence — when do I have to do what, where — and is
deliberately not module gated, which is the same reason a visit carries no
module key of its own: what a visit may point AT follows the modules that own
those things, so the gate sits on the three link pickers and blanks the block
rather than rendering an empty one.
A visit saves with a date and nothing else. No required practitioner, no
required kind, no required link, and the only thing that can disable Save is a
date that will not parse. The status is derived from the date on a new visit
and only becomes a field on an existing one, because somebody booking an
appointment should not also have to tell the app that it is in the future.
The address book is record content, not configuration, so it sits under
/checkups rather than in Settings — and it inherits that page's route-family
classification with it. Deleting an entry says in one sentence that the old
visits stay, because a person deleting a doctor has every reason to fear
otherwise.
Two defects closed on the way:
- /checkups carried sharedRecord: true and appeared in neither route family,
so a scoped grant could not present it at all. It was invisible while a
whole-record grant admitted every path; the visits section makes it load
bearing, and the page is now listed under both domains whose content it
shows. Deduplicated, or a grant holding both would offer the page twice.
- the address-book search read the name only, so looking a practice up by the
name on the door returned nothing and read as "not in the book".
The practitioner search description in the OpenAPI module moved with it.
A document arriving in the vault and a lab panel being committed both ask the
same question — does this belong to a visit? — and the answer has to be the
same question asked once. The rule lives in one module with one exported
window constant: the caller's live DONE or PLANNED visits within seven days of
an anchor date, resolved into a verdict rather than a list.
One candidate pre-selects, visibly, with one undo. Two or more offer a picker
with nothing pre-selected: pre-selecting the first of two is what teaches a
person to stop reading suggestions, and the return type has no field on that
arm a caller could mistake for a choice. Zero offers nothing.
Cancelled visits and no-shows are not candidates. Neither happened, so neither
produced the letter or the panel being filed, and admitting them would widen
the set with rows that are wrong most of the time — which pushes the honest
single match into the picker branch.
The verdict is served over HTTP because all three moments live in the browser.
Handing the client a windowed list and letting it decide would put the rule in
two places, and the copy in the browser is the one nobody re-reads.
- suggest-window.ts with the window as an exported constant, not a literal
- GET /api/encounters/suggest, profile domain, reviewed into the frozen literal
- OpenAPI regenerated
- fixtures are relative to the run instant; a fixed date slides out of a
relative window and rots the test into a false green
Two readings, side by side, never merged. The person's own rating leads
— larger, first, and returned exactly as it was stored — and the
comparison sits beside it with its range and the number of days it was
built from. The deviation is measured against the range, not against its
midpoint: inside it is a day that went as the account's own past days
suggest, and only outside it is worth a sentence.
The sentence is a counterfactual and says so. Never "your mood is 5.4",
always "a value around 5.4 would have been expected". The explanation
names what the comparison weighted for that day, out of what the job
stored for that day, in the words the capture sheet already uses — a
person reads "Workload" here because that is what the slider they moved
was called.
The route declares the whole record rather than the mind section, and
the reason is written at the declaration: the comparison reads the day's
context AND the figures the sleep, activity and recovery modules hold,
and it names them, so a delegate holding only the mind section would
receive a derived view of sections the owner declined to share. A scoped
grant is refused outright rather than served a contribution list with
the linked features quietly removed, which would read as "only the mood
fields mattered" and be false.
The causal-verb ban is a test now rather than a review habit: the copy
guard reads all six bundles and fails on a causal construction in any of
them, and on a statement that quotes a value without the days behind it.
Both halves were watched failing.
Query keys land in the central factory before any surface reads them,
because the lint rule that bans a bare array means the first component
listing visits cannot compile without a slot. There is deliberately no
server-side cache invalidator beside them: nothing cached reads a visit
yet, and a sweep over caches nobody reads would look like coverage
without being any. The module says so at the place a reader would go
looking.
The OpenAPI table publishes seven paths with everything resolved — the
practitioner as an object rather than an id, each link with its label
and date, the appointment's next due instant computed. What it does not
publish is the reminder origin enum, which still reads with two members:
the third value exists in storage and reaches no response, because every
preventive-care read excludes it. Keeping that contract closed is why
the exclusion is worth a guard.
Classifying visits under the existing health-background domain widens
what that domain grants. Somebody who shared it so a relative could see
an allergy list now also shares every appointment and the address book.
That is stated where it is agreed to rather than left in a code comment:
the consent screen's sentence names visits and practices, in all six
locales, and the activity feed gains a verb line for each of the eight
new delegated actions so an owner reads what was done rather than
'made a change'.
- one resolver reads the day's figures from the modules that own them; nothing
is copied onto the mood row, so correcting a sleep session corrects it here
in the same moment
- absence answers { present: false } rather than zero, because a night nobody
recorded and a night of no sleep are different facts
- a module switched off blanks its whole block instead of filtering down to an
empty one
- keyed by the local day so the capture sheet can ask before an entry exists
The scope mirror is watched so a switch this browser did not begin still
holds every shell until /me reconciles it: another tab on an older
bundle, the referential action behind an account deletion, an operator.
It recognised its own by comparing the mirrored scope against the scope
it was switching to, and that is not a true test. fetchMe mirrors
whatever record the server currently reports and it runs DURING a switch,
both when postSwitchWithReconcile reconciles a lost compare-and-set and
when a second tab reloads beside the initiator. At that moment the server
still reports the record being left, so the write did not match and was
read as somebody else's switch.
What followed was silent. A foreign write publishes a new transition id,
and every terminal call opens by checking the id it was handed, so the
commit that followed was dropped without a trace and the expected scope
was left naming a record no /me would ever confirm. Nothing could release
the hold, and both tabs sat behind the hydration gate until the stored
transition aged out thirty seconds later.
That is the intermittent browser failure that has been carried through
three rounds as a canary and never explained: a peer reload racing a
switch, a gate that never lifts, and a run forty-six seconds long made of
a fifteen-second wait and a thirty-second one. On the selection that
provokes it, eight runs failed three times before and twenty failed none
after, and the runs that used to take fifty-seven seconds now take
twenty-five.
A write is foreign only when nothing of this browser's own is in flight.
Ignoring it while held costs nothing, because a held browser is already
in the state that branch would install. The rule is a named function so
it can be checked in a suite that has no DOM, and the property the guard
exists for is pinned beside it: a response that disagrees with a
committed scope is stale and still must not release.
- add a managed-profile card to Settings -> Shared access, between the
invitation card and the standing-state cards
- collect exactly the four fields the create route accepts, with the date
of birth genuinely optional and never derived from a year
- refresh the account payload on success, so the new record reaches the
switcher and the banner without a reload
- assert the section is reachable for an owner and refused for a Guardian
inside a profile and for an adult manager
- attach the asserted context to every same-origin request inside the
three fetch entries, so no call site classifies itself
- discard a response whose echoed context contradicts the adopted one;
serve one that carries no echo, which is what keeps the public poll
and every actor surface working
- adopt from /api/auth/me and the switch response, and from nothing else
- send the epoch on every switch and retry once after reconciling
- hold and reconcile on the 409 instead of leaving the record
- Wait for a validated account scope before warming record reads
- Abort scoped preloads and discard handoffs across record transitions
- Cover malformed scope reloads and fresh own-record recovery
- Render a record-keyed status view for managed profiles\n- Refuse shared Settings links before actor controls mount\n- Localize intentional unavailable states
Each behaviour here was verified by removing the code that provides it and
confirming the test went red, then putting it back. Two of them did not, and
both were rewritten rather than kept:
The restore-side persister check asserted that the delegate's own dashboard
could not be read back inside a shared record. Deleting the guard left it
green, because the record-scoped hash already makes a snapshot written in one
record unreadable in another. It now counts what landed in the cache, so the
guard it names is the thing it measures — and the weaker line is kept beside
it with a note saying why.
The switcher's account rows were asserted from server-rendered markup. Radix
portals do not render server-side at all, so an earlier draft was measuring
the renderer. Those rows moved to the browser suite; what stays here is the
gate, which fails sharply — with the `canSwitch` bail removed the two cases do
not return different markup, they throw.
Two more shapes worth naming. The cache dimension is exercised through a real
QueryClient built from the app's own defaults, because "the hash string
changed" and "one record's data cannot be read while inside another" are
different claims. And the invalidation is a plain function over a client with
three live entries seeded, with a test that asserts they start valid — without
it, a client that failed to seed and one where everything was invalidated look
the same from every other assertion.
Every cached read in this app is about one health record, and nothing in the
key factory said which one — there was only ever the signed-in account's. Once
a browser can act on somebody else's, `["dashboard","snapshot"]` names two
different answers and the cache cannot tell them apart. That is this repo's
known cache-poison class, on health data, across two people.
One line at the root rather than a dimension threaded through twenty-three key
modules: a `queryKeyHashFn` that folds the record id into the cache identity of
every entry. Key arrays are untouched, which is the part that matters — prefix
invalidation and the import-time dependent-key bundles both match on the array
and keep working. Prefixing the arrays would have broken every bundle silently.
The hash is defence in depth. Switching is a full page reload, so the
in-memory cache is empty on the other side of it; the layers that survive a
reload are the IndexedDB snapshot and the service worker's data cache, and
those are handled by refusing to write either while a switch is on, and by
skipping the restore. Nothing of the owner's record ever reaches the disk of a
delegate's device, so a stale scope marker can cost an empty restore and never
another person's data.
The persister's per-account binding read the account payload's id, which under
a switch is the delegate in both states — it could not tell the two apart. It
now asks the record scope, which can.
A dedicated settings page lists every scorable pillar grouped by the area
of health it speaks to, so the fact that blood pressure, glycaemia and
lipids are three pillars of one area is visible rather than buried. Each
row shows the three things that were never separable before: whether the
pillar is being recorded, whether it is shown, and whether it counts
toward the score. Only the last is writable here, and the page says so.
A pillar whose tracking is switched off is not listed at all, because it
is not waiting for anything. A pillar this build cannot score yet is
listed, disabled, and says why. Wellbeing under safety signposting is
named as safety guidance and never as a mistake in the selection. The
refusal for a selection too narrow to produce a score comes from the
server and is shown in the reader's own language.
The score's foot disclosure gains one link into the page.
The guard read a parameter as required whenever it carried no question
mark and no default, so `Locale | undefined`, `Locale | null`, a locale
widened to `any`, and a default sitting on the destructuring pattern all
walked straight past it. Each of those lets a caller say nothing and the
sentence come back in English, which is the bug the guard exists to stop.
A counter-case pins the other direction: a plainly required locale still
reads clean, so the guard stays quiet on the shape the chain is meant to
have. The correlation read now also asserts the language it was asked in;
nothing else in that file would have noticed it going back to English.