Commit Graph
1232 Commits
Author SHA1 Message Date
Marc-André Bombeck a880f764fc chore: keep the handlers this release touched within their size baseline
The register refusal codes, the end of the no-device rest, the
no-recipient annotation and the sign-in shell comment had each grown a
function the size ratchet holds. The codes become constants, the rest
ends through a helper beside the one that starts it, and the
annotation moves into that helper. The settings Web Push card dropped
under the limit when its subscription post moved out, so its entry goes.
2026-11-12 14:16:06 +01:00
Marc-André Bombeck e498a44d70 chore: correct comments that describe the removed clear push and API cache
The dose tag comment still explained the clear push sent for a logged
dose, the session-end wipe comment still named the worker's API cache as
a live layer, and the worker's header and foreign-origin note did not
match what the worker stores and when it resumes.
2026-11-12 14:09:18 +01:00
Marc-André Bombeck 38299f7c6e fix(pwa): clear the dose count earlier releases left on the app icon
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.
2026-11-12 13:56:33 +01:00
Marc-André Bombeck 367ba65bf6 fix(auth): take the push subscription off on every sign-out button
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.
2026-11-12 13:51:44 +01:00
Marc-André Bombeck 031379d966 Merge branch 'release/v1.47.3' into release/v1.47.4
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/test-as-never.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-12 13:27:52 +01:00
Marc-André Bombeck 2af0751b69 test: type the new fixtures instead of casting them to never
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.
2026-11-12 13:03:51 +01:00
Marc-André Bombeck 2d1009136a chore: drop the em dashes this release added to prose and test titles
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.
2026-11-12 12:54:20 +01:00
Marc-André Bombeck cad4c89831 test: mark the visit, dose and address-book deletes restorable
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.
2026-11-12 12:53:36 +01:00
Marc-André Bombeck df47980454 fix(telegram): pick the running course and judge a reminder by its slot
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.
2026-11-11 22:59:53 +01:00
Marc-André Bombeck f76e5cdafd fix(insights): let a read grant read the record's targets
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.
2026-11-11 22:54:34 +01:00
Marc-André Bombeck ce01d455be fix(targets): no adult weight and BMI bands on a child's record
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.
2026-11-11 22:53:31 +01:00
Marc-André Bombeck 4be724fefb fix(ai): keep the picked gateway serving behind an empty chain
A person on the OpenAI-compatible gateway lost every AI surface once
their chain was saved at all: the fallback to the picked gateway asked
whether the chain was the default object itself, so a chain reset to
the default, reordered, or written by the gateway form's reasoning
setting (which appended a disabled entry to hold the value) no longer
counted, and the chain resolved empty. Rows written that way before
this release broke on deploy.

The fallback now reads the chain's content: the picked gateway serves
when nothing else resolves, unless the chain holds the gateway switched
off. A disabled gateway entry that carries a reasoning effort is the
form's holder, not a switch; the chain editor drops the effort of a
gateway it switches off, so a switch-off stays one. Saving the gateway
form with the gateway picked writes its entry switched on. Resolution,
the presence probe, the account payload and the iOS availability flag
all read the same rule.
2026-11-11 22:45:28 +01:00
Marc-André Bombeck 82125815d5 test: settle the parity snapshot and the size ratchet
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.
2026-11-11 22:17:07 +01:00
Marc-André Bombeck aacf06b7b3 fix(coach): say when the attachment picker list is cut
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.
2026-11-11 21:44:42 +01:00
Marc-André Bombeck e68041a3af fix(forms): keep one Idempotency-Key for the remaining create forms
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.
2026-11-11 21:39:19 +01:00
Marc-André Bombeck 0278b6045e fix(pwa): keep the Medications home-screen shortcut
The shortcut is the quickest way to a dose for everyone who tracks
medications. With the module switched off it opens the page that says so
and offers to turn it back on, which is a sensible landing, so the
shortcut stays. The unread `source=shortcut` parameter stays removed.
2026-11-11 21:33:07 +01:00
Marc-André Bombeck 212ff9ee55 Merge branch 'fix/v1474-auth' into release/v1.47.4
# Conflicts:
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-11 21:32:28 +01:00
Marc-André Bombeck 188d21d6ac Merge branch 'fix/v1474-offline' into release/v1.47.4
# Conflicts:
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-11 21:32:11 +01:00
Marc-André Bombeck 0b7afd8460 fix(errors): show the catalogue sentence, not the server's text
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.
2026-11-11 21:29:33 +01:00
Marc-André Bombeck 3385163fd6 fix(demo): stop refusing a visitor's own sign-out, language and timings
On the demo every page logged its own failures. The sign-out answered
403, so a visitor could not sign out at all; the language write answered
403 on every page load, and because its once-per-tab marker is only set
on success it was sent again on the next one; the page-timing beacon was
refused the same way. Five password sign-ins per 15 minutes per address
locked out a group trying the published demo account together.

Sign-out, the language write and the timing beacon now pass the demo
block: each touches only the visitor. The language route still answers
and sets the visitor's cookie, but under DEMO_MODE it no longer writes
the shared account's row, so one visitor's choice is not the next one's
starting state. A demo allows 30 password sign-ins per 15 minutes per
address; other instances keep 5.
2026-11-11 21:27:03 +01:00
Marc-André Bombeck 62b9e404ef fix(shell): return to the list's scroll position on Back
Back from a detail page (a biomarker, a medication, a workout) opened
the list at the top. The shell scrolls inside `<main>`, which stays
mounted across routes, and the browser's own restoration only moves
the document. Nothing kept the list's offset, so `<main>` stayed where
the detail page had left it; and the one-shot skip on `popstate` was
cleared by a timer that could run before the restored page rendered,
in which case the forward-navigation reset fired anyway.

The shell now records `<main>`'s offset per address while scrolling,
reads it back on `popstate` and applies it once the page has rendered,
retrying for up to a second while a list is still shorter than the
offset. A forward navigation still starts at the top.
2026-11-11 21:23:17 +01:00
Marc-André Bombeck ecb219f346 fix(auth): say a refused sign-in in the reader's language
The sign-in page (password and passkey), the second-factor step and
registration printed the server's English error text, so a German or
French page said "Invalid credentials" or "Invalid code". They now
resolve the error code through localizedApiError like the other forms
and fall back to the page's own sentence. Registration still shows a
422 as sent: that is the password feedback, which the server already
words in the reader's language.

A 429 carries the generic `http.rate_limited` code, which had no
sentence, so a person locked out by the limiter would have read only
"Login failed". The catalog now has one, reusing the wording of the
profile take-over's rate-limit message.
2026-11-11 21:21:20 +01:00
Marc-André Bombeck 6c5a6eacbb fix(auth): bring a deep link back to its page after signing in
A home-screen shortcut, a notification tap or a bookmark opened without
a session landed on the sign-in page with a bare URL, and after signing
in the person was on the dashboard instead of the page they asked for.
The sign-in page already honoured a sanitised `next`; neither redirect
set one.

The proxy now passes the request's own path and query as `next` (the
bare start page goes without one). The shell does the same when the
account answer ends a session on a protected page, except after a
deliberate sign-out: the marker a confirmed session leaves is gone by
then, so the next person on the device starts from a bare sign-in page
rather than the page the previous one left.
2026-11-11 21:14:04 +01:00
Marc-André Bombeck 9227844f42 Merge branch 'fix/v1473-ai' into release/v1.47.3
# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-11 21:07:27 +01:00
Marc-André Bombeck aa6e617da0 fix(insights): grade blood pressure and pulse against the own target
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.
2026-11-11 21:00:10 +01:00
Marc-André Bombeck ee04c5bcf4 test: register the push unsubscribe DELETE by hand
The DELETE behind "Disable push" moved into the shared
unsubscribeWebPush helper under lib/, which the destructive-control
detectors do not scan. The card's entry stays and says where the
request goes out now.
2026-11-11 20:55:08 +01:00
Marc-André Bombeck af623d12db fix(push): send the test push to every device, in the user's language
The Web Push test went to the newest subscription only. With a phone
and a laptop subscribed, Test pressed on the laptop buzzed the phone
and the laptop looked broken. The text was English in every locale,
and the push was dialled through the library's own transport, past the
pinned safeFetch that real sends use.

The route now calls sendViaWebPush, so the test reaches every
subscription, reaps endpoints the push service reports gone, and goes
out through the same dial as a reminder. Title and body come from the
account's locale. The response keeps ok and latencyMs and gains reason;
sent and perEndpoint are gone, nothing read them.
2026-11-11 20:52:36 +01:00
Marc-André Bombeck 926c427907 fix(coach): stop computing the plan review verdict nobody sees
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.
2026-11-11 20:51:18 +01:00
Marc-André Bombeck 7f8ba0ae98 fix(push): tell iPhone Safari users to open the Home Screen app
iOS offers Web Push only to a web app opened from the Home Screen. In a
Safari tab the push card said the browser does not support push
notifications, and the step that fixes it was only in the docs. On an
iPhone or iPad outside the installed app the card now says to add
HealthLog to the Home Screen, open it from there and turn push on.

WebPushCard grows by two lines for the second sentence; its baseline
goes from 201 to 203, still below the 215 it had before the previous
change.
2026-11-11 20:46:58 +01:00
Marc-André Bombeck 32911fea37 fix(push): remove this browser's subscription on sign-out
Signing out ended the session and wiped the caches but left the Web
Push subscription in place, in the browser and on the server. The
browser kept showing the signed-out account's medication reminders,
also after someone else signed in on it, and the next account's
Settings showed push as already on.

Sign-out now runs the same steps as "Disable push" before the logout
call: DELETE the server row while the session is still valid, then
unsubscribe in the browser. Both callers share one helper. A failure
there does not block signing out. With the dispatcher no longer
counting "no subscription" as a failed send, signing out does not
start the five-failure countdown on the account's channel.
2026-11-11 20:44:24 +01:00
Marc-André Bombeck 72dcd32f7a fix(pwa): drop the Medications home-screen shortcut
The manifest is one static file for every account, so the Medications
shortcut was offered with the module switched off and opened its
"switched off" notice. Only shortcuts into pages no module can turn off
remain: Add measurement and Today. The `source=shortcut` parameter goes
too; nothing read it, and the measurements filter discarded it as an
unknown data source. The install `id` keeps its `source=pwa`, which is
an identity string only.
2026-11-11 20:43:23 +01:00
Marc-André Bombeck 6d8f533deb fix(auth): keep the page when sign-out is refused
The sign-out mutation took every HTTP answer as success. A refused
logout (403 from the demo guard, a 429, a 5xx) wiped the client caches
and showed the sign-in page while the session cookie stayed valid, so
the record pages opened again on the next visit. The call now goes
through apiFetch, which throws on a non-2xx answer, and the error toast
says the person is still signed in instead of naming a network error.
2026-11-11 20:42:25 +01:00
Marc-André Bombeck a824484d4a refactor(pwa): drop the unreachable precached-shell fallback
Without a connection a navigation also looked for itself in the static
cache, which only ever holds icons and catalogs; `/` is never
precached, and the install step deleted a leftover root document only
so that this lookup could not find it. Both go; the page cache and the
offline page remain.
2026-11-11 20:41:05 +01:00
Marc-André Bombeck 09f3280c1c fix(pwa): keep signed-in onboarding pages out of the page cache
The proxy treats /onboarding/* as public, so those pages are not sent
no-store, and the worker stored the documents a signed-in account saw
during setup, against its own rule that only public pages land in the
page cache. The worker now skips them by path, like the share view.
2026-11-11 20:40:32 +01:00
Marc-André Bombeck 61f9447887 fix(pwa): focus the open window on a notification tap instead of reloading it
Tapping a reminder called `navigate()` on the first app window even
when it already showed the target page: a full document load that
dropped the query cache and any open sheet. The rejection of an
unawaited `navigate()` on an uncontrolled window went unhandled.

A window already on the target is now only focused, and preferred over
the first window. Otherwise the window is focused, then navigated; if
that fails a new window opens.
2026-11-11 20:39:59 +01:00
Marc-André Bombeck 25d087aaa2 fix(push): stop handling the clear push in the service worker
The worker answered a `type: "clear"` push by closing the dose
reminder for that slot and showing nothing. Browsers do not allow a push
without a visible notification on a subscription that promised one:
Safari removes the subscription after a few, Chromium shows its own
"updated in the background" notice. The server no longer sends it, so
the branch and its tests go; a reminder stays until it is tapped or the
next reminder with the same tag replaces it.
2026-11-11 20:39:08 +01:00
Marc-André Bombeck 31f296a03e fix(push): stop the invisible push sent for every recorded dose
Each taken or skipped dose sent a `clear` push to every subscription of
the account. The service worker closed the pending reminder and showed
nothing. A subscription made with userVisibleOnly may not receive
pushes that show no notification: Safari revokes it after a few, the
next reminder meets a 410 and the channel is switched off, with nothing
in the app saying why.

The clear push, its three call sites and the badge count only it fed
are removed. A reminder already on screen now stays until it is tapped,
dismissed or replaced by the next reminder for the same dose. The
sender's badge field went too: nothing else ever set it. The parity
snapshots lose the two recorded effects.
2026-11-11 20:39:07 +01:00
Marc-André Bombeck b793be3f84 fix(onboarding): shorten the AI pitch on the last setup screen
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.
2026-11-11 20:38:37 +01:00
Marc-André Bombeck 1d5236a326 test(ai): settle the ratchets after the empty-chain change
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.
2026-11-11 20:37:48 +01:00
Marc-André Bombeck 433b92a67f fix(pwa): restore the dashboard snapshot only for its own account
On a cold start the IndexedDB snapshot was restored before
/api/auth/me had answered, and the owner check was skipped while the
account was unknown. On a shared browser the previous account's
dashboard could be put into the next account's cache until the refetch
landed.

The restore now waits until the signed-in account is known and runs
again when it changes. A snapshot whose owner is not that account, or
that was written before any account was known, is discarded and wiped.

Because the restore now lands after the page has mounted, it fills
queries that have no data yet instead of skipping every query the cache
already knows; data the page holds is still never overwritten.
2026-11-11 20:36:25 +01:00
Marc-André Bombeck 0df65ce396 fix(pwa): keep the open page when a link is tapped offline
Without a connection the strip said "Showing your last synced data",
but one tap on a tab or link traded the readable page for the worker's
bare offline page: signed-in pages are no-store, so the document
navigation Next falls back to has nothing to load.

While the browser reports offline, a tap on a link to another page of
the app now stays on the open page and repeats the hint in a toast. The
strip says what is true: this page stays readable, other pages open once
the connection is back. The README stops promising offline support.

A cold start or a typed URL without a connection still gets the offline
page; the e2e spec keeps that and adds the tap.
2026-11-11 20:33:57 +01:00
Marc-André Bombeck 1d7169b266 refactor(pwa): drop the service worker's API data cache
The worker buffered every allowlisted API read, the 60-second version
poll included, and wrote it to CacheStorage before the page got it. No
page could ever be opened offline to read those answers back, since
every signed-in document is no-store. What the cache did do: keep one
account's readings on a shared device, answer a refetch that failed
right after a save with the copy from before it, and let a late write
recreate the cache after the sign-out wipe.

API requests and the hashed /_next/static/ chunks now go to the network
untouched; the HTTP cache already holds the immutable chunks. The
cache-name pattern keeps `data`, so caches older releases left behind
are still deleted on activate and by the session wipe.

The TypeScript copy of the allowlist and its test go with it, and the
record-switch e2e spec now checks that nothing under /api/ is kept on
disk before or after the switch.
2026-11-11 20:31:15 +01:00
Marc-André Bombeck f519df4433 Merge branch 'fix/v1473-record' into release/v1.47.3 2026-11-11 20:18:59 +01:00
Marc-André Bombeck 6a02cda210 Merge branch 'fix/v1473-writes' into release/v1.47.3 2026-11-11 20:18:58 +01:00
Marc-André Bombeck da671f74aa refactor(day): share one window for date-only columns
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.
2026-11-11 20:12:44 +01:00
Marc-André Bombeck 53f4ecc2c5 fix(documents): say when the document picker list is cut
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.
2026-11-11 20:08:24 +01:00
Marc-André Bombeck d204b4344b fix(records): offer Undo after deleting a vaccination, visit or entry
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.
2026-11-11 20:06:02 +01:00
Marc-André Bombeck 5449933c4f chore: remove three components nothing renders
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.
2026-11-11 20:03:00 +01:00
Marc-André Bombeck f761500247 fix(forms): send one Idempotency-Key per opened create form
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.
2026-11-11 20:02:40 +01:00
Marc-André Bombeck 02856e7f37 fix(targets): accept a child's weight target below 30 kg
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).
2026-11-11 20:00:39 +01:00