Commit Graph
9936 Commits
Author SHA1 Message Date
Marc-André Bombeck 444e1eaca8 fix(auth): replace the page with the sign-in page on sign-out
The menu's sign-out pushed the sign-in page onto the history. Back then
reopened the page just signed out of, and the edge sent the next person
to the sign-in page with that page as `next`, so their first sign-in on
the device landed where the last person had been. The sign-out now
replaces the page.
2026-11-12 14:02:30 +01:00
Marc-André Bombeck 510ce3128d fix(auth): end the session only on HealthLog's own 401 or 403
A refetch of the account that answered 401 or 403 now signs the person
out and clears the offline copies, which is right for HealthLog's own
answer. But a self-hosted instance behind a forward-auth proxy or a
firewall can get a 401 or 403 that HealthLog never wrote, and that
signed a person out whose session was fine.

HealthLog's error envelope always names a code, so only a 401 or 403
that carries one is read as the session's end; one without is treated
like a network failure and keeps the last known state. The tests now
give the account route the answer the server really sends.
2026-11-12 14:02:13 +01:00
Marc-André Bombeck 37734c9fd8 fix(auth): say why a registration or passkey sign-in was refused
Once the sign-in and registration pages read codes instead of the
server's prose, a taken username or address said only "Registration
failed", an expired invitation said "You don't have permission for
this", and a passkey this server does not know, a passkey sign-in that
took too long, an unreadable passkey answer, a single-sign-on-only
server and an already signed-in visitor all said "Login failed".

The registration route now names its refusals (auth.register.taken for
both 409s, auth.register.invite_invalid for a refused invitation), and
those codes, the three passkey refusals, oidc_only and
auth.already_authenticated get a sentence in every language.
2026-11-12 14:00:18 +01:00
Marc-André Bombeck 94ea48d723 fix(push): make a new subscription deliver for its account at once
Subscribing re-enabled a disabled channel but left the failure counter
and cooldown on one that was still enabled. Failures counted before the
new subscription (no browser left, a push service outage) kept every
reminder skipped until the cooldown ran out, and one more transient
failure switched the channel off. Subscribing now resets them too.

A browser also kept the row of an account whose session ended without a
sign-out, so the next person on it saw that account's reminders. An
endpoint is one browser: subscribing now removes other accounts' rows
for it, and the settings card, which posts its subscription on open,
claims the browser for whoever uses it now.
2026-11-12 13:57:36 +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 1dde3034c6 fix(auth): say a refused sign-in in the sign-in page's own words
The sign-in refusals carry only their status's generic code, so once the
sign-in page read codes, a wrong password, a wrong second-factor code
and a refused security key all said "Your session has ended", and a
lockout said "too many requests just now".

localizedSignInError reads a generic 401 as the step's own failure and a
generic 429 as "Too many attempts. Wait a few minutes and try again.";
a code the step names itself keeps its sentence. The password, passkey,
code and security-key steps use it. The rate-limit sentence moves from
auth.claim to auth.signInRateLimited, since the claim page and the
sign-in page now share it, and the Korean one takes the polite form the
rest of the catalogue uses.
2026-11-12 13:54:34 +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 6914f844be fix(push): send the browser's subscription again when the card opens
The Web Push card reads its state from the browser only. When the server
had lost the row, or the channel had been switched off, the card said
enabled and no reminder came. Opening the card now posts the subscription
the browser holds, which stores the row and turns the channel back on.
A refused post (demo, push off instance-wide) changes nothing on screen.
2026-11-12 13:48:52 +01:00
Marc-André Bombeck d5c0e759b8 fix(push): turn Web Push back on for accounts it was switched off for
Before this release, a send with no browser subscription counted as a
failure, and five of them, or one 410, switched the WEB_PUSH channel off.
Subscribing again stored the browser but left the channel off, so the
card read "enabled", a test push arrived, and no reminder was ever sent.
Subscribing now re-enables the channel, but nothing prompts the people
already caught to subscribe again.

The migration re-enables every WEB_PUSH channel the dispatcher switched
off (a disabled_reason is set; a channel switched off by hand has none)
whose account still has a subscription, and clears the counter, the
cooldown and the last failure the same way reEnableChannel does.
2026-11-12 13:48:51 +01:00
Marc-André Bombeck a1591c4ea0 fix(i18n): keep one apiErrors.http block per locale
Two branches each added the block, and the merge kept both. A JSON parser keeps the last one, so the sign-in sentence for a rate limit was dead text.
2026-11-12 13:42:34 +01:00
Marc-André Bombeck d3cec13f98 docs(api): regenerate the contract
The test push answers with a reason instead of per-endpoint counts.
2026-11-12 13:29:02 +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 9b4b5a1255 chore(release): v1.47.3 2026-11-12 13:09:39 +01:00
Marc-André Bombeck 8974158c58 Merge branch 'fix/v1473-review' into release/v1.47.3 2026-11-12 13:08:58 +01:00
Marc-André Bombeck bde455e3be docs(api): regenerate the contract
Carries the targets read at read level, the two new error codes
(`customMetric.name_taken`, `auth.mfa.already_active`) and the
notification categories' description without its em dash.
2026-11-12 13:06:18 +01:00
Marc-André Bombeck 5b8543f471 fix(settings): keep the Coach entry for a managed record
The Settings Coach entry now follows whether the Coach can answer, as
the sidebar does, with Settings, AI keeping the stored memory in reach.
A managed record has no Settings, AI, so while its Coach could not
answer yet (no operator key, no consent) the guardian lost the only way
to the record's Coach data scope and its switch. For a managed record
the entry follows the module switch alone again.
2026-11-12 13:05:37 +01:00
Marc-André Bombeck 8b6d9826ea chore: drop stale references to the onboarding offer and old resolver
The Playwright project list still named the deleted onboarding AI
offer spec. The CoachPlan comments said the review writes
`outcomeEncrypted` and surfaces it behind a flag; nothing writes or
reads it now, and the column is marked internal. Comments that named
`resolveProvider` as the resolver now name the chain, and the function
itself says it is test-only.
2026-11-12 13:03:58 +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 315c89d229 fix(targets): classify a weight with the height its band was built from
The child-band change left the classification dividing by the record's
height where it had checked the adult-band height, which the type checker
refused. Both now read the same value.
2026-11-12 13:03:51 +01:00
Marc-André Bombeck 8c9fbbdd2b fix(errors): name three refusals that fell back to a generic sentence
Since a codeless refusal reads by its status, three answers lost their
meaning. A failed ChatGPT sign-in poll said "Error saving" instead of
asking to start the sign-in again; it now falls back to the sign-in's
own failure sentence. A dose-history import still running when the
card stopped watching said "Nothing was written" although the job may
still write; that case now has its own sentence. Setting up an
authenticator while one is active answered "setup failed"; the route
now sends `auth.mfa.already_active`, which says to disable the active
one first, in every locale.
2026-11-12 12:58:52 +01:00
Marc-André Bombeck 31c4440154 refactor(custom-metrics): read a refused save in one helper
Matching the name clash by its code grew the form component past its
size baseline. The refusal's sentence now comes from a helper beside it.
2026-11-12 12:58:37 +01:00
Marc-André Bombeck e71b1b9fd4 fix(coach): leave no plan result to invent once a plan is reviewed
The plan review rule told the Coach to read how a plan went from its
progress line, but a progress line exists only for an active plan, and
the review sets the plan to reviewed the morning its reminder surfaces.
For the plan being discussed there was nothing to read, so the model
could make a result up. The rule now reads a result only from a
progress line the snapshot carries, and otherwise asks how it went.
2026-11-12 12:55:15 +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 55b223d4ed fix(onboarding): show the closing AI line as body copy
The done screen's "AI is optional" line is a sentence the person reads,
set in muted text, which the UI standards keep for meta. It is body
text now. Its test asserted the absence of strings deleted from every
bundle in the same change, so it could not fail; it now checks that the
line is not muted, that the screen links to the AI settings once, and
that no button sits beside the link.
2026-11-12 12:53:12 +01:00
Marc-André Bombeck 706af61cdd fix(custom-metrics): say "name taken" only for a name clash
The form read every 409 as "a metric with this name already exists".
Now that it keeps one idempotency key across taps, a second tap while
the first create is still running also answers 409, and the form told
the person their name was taken. The create and rename routes send
`customMetric.name_taken` with the clash, and the form matches the code.
2026-11-12 12:52:39 +01:00
Marc-André Bombeck 671245c782 fix(documents): one cap line for both pickers, with a search-aware hint
The vault picker and the Coach's attachment picker each wrote their own
"showing the newest 50" line, which the UI standards leave to
`ListCapNotice`, and with a search running past fifty matches the line
still said "Search to find an older document". Both pickers now share
`DocumentPickerCapNotice` on `ListCapNotice`, which takes the sentence
to show, and a running search asks for a narrower one.
2026-11-12 12:51:32 +01:00
Marc-André Bombeck 92e1e24599 fix(i18n): correct the new record and workout refusals in five locales
The Russian, Polish and Korean refusals for a removed practice, visit
or check-up said "choose another or none" word for word; they now name
the picker's own empty choice or say to leave the field empty. Brazilian
Portuguese called a check-up a "check". The dose-name refusal names the
vaccination card the way the form does in English, Spanish, Polish and
Dutch, and the oversized workout batch no longer says "at once" twice.
2026-11-12 12:49:55 +01:00
Marc-André Bombeck 3ed266e281 fix(measurements): drop the stray space in a unit-less range message
A score, rating, count or ratio has no unit label, so the range
message for a manual pain score read "Enter a value between 0 and 10 ."
with a space before the stop, and the same in every locale. A second
sentence without the unit now answers those types.
2026-11-12 12:49:18 +01:00
Marc-André Bombeck 048dbbee9f fix(measurements): keep computed scores out of the bulk delete
The row menu stopped offering edit and delete on a computed score
(recovery, strain, stress), but the list still drew its selection
checkbox and "select all" took it into the page, so the bulk bar could
delete a page of scores, and the tombstones kept the recompute out.

A computed row now has no checkbox on either layout and stays out of
the page selection, and the bulk-delete route skips computed rows if a
client sends them anyway. The iOS app treats these rows as read-only
and does not rely on deleting them.
2026-11-12 12:48:03 +01:00
Marc-André Bombeck a4b14609bf fix(practitioners): offer an Undo for every delete, not only the last
Two quick deletes in the address book showed the Undo toast for the
second entry only: the callbacks `mutate` takes belong to the latest
call of the list's one mutation, so the first entry's were dropped. The
toast now hangs on each delete's own promise.
2026-11-11 23:00:43 +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 d79060e26b fix(checkups): show a marked-done check-up as done before the refetch
The "Mark done" button re-enables as soon as the satisfy call settles,
while the row kept reading as due until the list refetch landed, so a
second tap stored a second satisfied entry; skip and snooze behaved the
same. The row each of the three calls answers with is now written into
the cached list on success, so the row moves to its next due date at
once, and the refetch follows as before.
2026-11-11 22:56:32 +01:00
Marc-André Bombeck 46906916d8 fix(vaccinations): re-anchor a booster on the logged dose's own day
Logging the next dose satisfies a matching booster reminder with the
dose date, a calendar date stored at UTC midnight. The reminder engine
reads that instant in the person's zone, where west of UTC it is the
evening before, so the next booster came due a day early. The first
booster's anchor was already counted from the start of the dose day in
the person's zone; the satisfy path now hands the engine the same
instant.
2026-11-11 22:55:59 +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 9683daf4f2 fix(api): take a new idempotency key once the server has answered
A create form kept its key until a save succeeded. The server stores a
refusal under the key for a day, so after a 422 or 409 the corrected
retry ran as a different body without any protection, and a timeout on
it could store the entry twice; an unchanged retry after the cause was
gone replayed the stale refusal until the form was closed.

Any answer now settles the key. Only a timeout, a dropped connection, or
the 409 that says the first request under the key is still running
(`X-Idempotent-Replay: false`, now `ApiError.inFlight`) keeps it, so
those retries still replay the first answer.
2026-11-11 22:50:42 +01:00
Marc-André Bombeck 0c77eaa085 fix(errors): name the refusals behind the AI test and a passkey proof
Three refusals answered without an error code, so the web printed the
status's generic sentence. A connection test with nothing configured
read "Some of the entries were not accepted", and a mistyped password
while adding a passkey read "Your session has ended. Sign in again",
which sent people to sign out for nothing.

An empty chain now resolves to no provider, which the test route
answers with `ai.provider.none`. The passkey registration and the
step-up mint answer a failed or stale proof with `auth.reproof.failed`.
`ai.provider.none`, `ai.budget.exceeded` and `auth.reproof.too_weak`
get sentences in every locale.
2026-11-11 22:49:20 +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 17dbb225d4 fix(profile): read and store the date of birth as a calendar day
The iOS app sends the date of birth as the instant its date picker
returned, and the profile writers stored that instant as sent. Since the
report cover and the share page read the date part of the stored value,
an entry made just after midnight in Berlin printed the day before, and a
Californian entry made in the evening printed the day after.

Both profile writers now store the calendar day the value names in the
owner's zone, at noon UTC like other date-only values, so an iOS client
reading the instant back in its own zone keeps the date. Older rows keep
their instant: dateOfBirthKey reads a date-only anchor (UTC midnight or
noon) by its date and any other instant in the owner's zone. The report
collector, the targets age, and the web forms that send the date back
read it through that helper.
2026-11-11 22:37:57 +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 5ea829df8b docs(api): regenerate the contract
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.
2026-11-11 21:58:18 +01:00
Marc-André Bombeck f6d11e7e37 Merge branch 'fix/v1473-rest' into release/v1.47.3 2026-11-11 21:57:54 +01:00
Marc-André Bombeck 0d41abcda4 fix(settings): list the Coach entry only while the Coach can answer
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.
2026-11-11 21:50:28 +01:00
Marc-André Bombeck ce377ef56d chore: remove leftovers of the deleted onboarding AI offer and badge
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.
2026-11-11 21:47:54 +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 24cca975da fix(charts): draw the BMI of the record on screen
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.
2026-11-11 21:43:07 +01:00
Marc-André Bombeck b85989f3df fix(coach): spell the bubble's initials from the display name
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.
2026-11-11 21:41:39 +01:00
Marc-André Bombeck b5c97062b8 fix(timeline): close the life event sheet before the timeline refetch
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.
2026-11-11 21:40:02 +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