Commit Graph
9841 Commits
Author SHA1 Message Date
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 59f94683ae Merge branch 'fix/v1474-push' into release/v1.47.4 2026-11-11 21:32:03 +01:00
Marc-André Bombeck 5029b30b2b fix(i18n): drop the translation notice from the sign-in pages
Eight of ten languages opened the sign-in, registration and profile
take-over pages with the "machine translated" strip above the form, the
first thing a visitor read before having an account. The strip stays in
the app shell, where the language can be changed and a correction
reported; its text is unchanged.
2026-11-11 21:27:54 +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 a4766824ef fix(auth): keep the page when a sign-out from an auth page is refused
The sign-out buttons on the registration and profile take-over pages
took every HTTP answer as success. A refused sign-out (the demo's 403,
a 429, a 5xx) wiped the client caches and reloaded the page while the
session stayed valid; on the take-over page a network failure was an
unhandled rejection with no message at all.

Both go through apiFetch now, which throws on a non-2xx answer, and say
that the person is still signed in: inline on the registration page, as
a toast on the take-over page. The message is the one the account
menu's sign-out uses.
2026-11-11 21:18:46 +01:00
Marc-André Bombeck 8fdc238981 fix(auth): replace the sign-in page in the history after signing in
Back after signing in, registering or taking over a profile showed the
form that had just been submitted, because each pushed the next page
on top of it. Password, passkey and second-factor sign-in, registration
and the password take-over now replace the page instead.
2026-11-11 21:15:05 +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 75b879279f fix(auth): leave nothing of the last account behind on any sign-in
Only the password and passkey success path cleared the offline copies
on this device. A session that ended by expiry reached the sign-in page
without any wipe, and single sign-on and both profile take-over paths
started the next account on top of the previous one's IndexedDB
snapshot and worker caches.

The shell now clears those copies on every /auth/* page once
/api/auth/me answers 401 or 403: once per signed-out stretch, never on
an answer without a verdict. Single sign-on and the take-over by single
sign-on clear them before leaving for the identity provider, and
registration and the password take-over start from an empty query cache
like the sign-in page does.
2026-11-11 21:10:38 +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 37aa6052c6 test(pwa): tap a visible dashboard link in the offline journey
The desktop shell renders the bottom bar's links too; pick the first
one that is visible so the tap lands on the sidebar.
2026-11-11 20:47:38 +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 00c56b0dd5 fix(pwa): keep the status bar readable in the light theme
The installed iOS app uses the `black-translucent` status bar, which
always draws white text, and the shell painted the strip behind it in
the card colour. In the light theme that is near-white, so the clock
and battery vanished. The light theme now paints the strip in its dark
ink; the dark theme is unchanged.
2026-11-11 20:43:46 +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 51827f43a6 chore(pwa): cut the service worker's comments to what a reader needs
The worker is served as written, so every browser downloaded its
release history: more than half of the file was comments, many of them
dated notes about earlier states of the code. The comments now say what
the code does and why, without release numbers. No code changes; the
fallback version literal and its rewrite marker are untouched.
2026-11-11 20:42:32 +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 b3b4c9684e fix(pwa): keep the daily briefing text out of IndexedDB
The dashboard snapshot carries the daily briefing and its recall line,
and the persister wrote the snapshot to IndexedDB for up to a day,
although its own comment said narrative text never reaches the disk.

The persister now nulls both fields before the write. The web
dashboard reads the briefing from the Today digest, which is never
persisted, so the restored snapshot paints exactly what it did before.
2026-11-11 20:38:41 +01:00
Marc-André Bombeck 70295f45cb fix(push): bring Web Push back after turning it on again
A channel switched off because the last browser's endpoint answered 410
(app removed from the Home Screen, site data cleared) stayed off when push
was turned on again: the subscription was stored and the test push
arrived, but no reminder ever did. Subscribing now re-enables a disabled
channel through reEnableChannel, the same reset the status card's button
runs.

Turning push off on the last browser deleted the subscription and left
the channel on, and each later reminder was booked as a failed send
(web_push_no_subscriptions) until the channel gave up after five. The
dispatcher now leaves channel state alone when a sender had nobody to
send to; APNs without a paired iPhone and email without an address or
SMTP had the same problem and are covered by the same rule.
2026-11-11 20:38:39 +01:00
Marc-André Bombeck 960f3c4878 fix(pwa): keep the static cache across sign-in and sign-out
The session wipe deleted every HealthLog cache, the static one too,
although it holds only locale catalogs and icons. Each sign-in and
sign-out threw away the 190 KB catalog and refetched it.

The wipe now deletes the page cache and the API caches older releases
left behind, current and legacy versions, and keeps the static cache.
Function name and signature are unchanged.
2026-11-11 20:37:14 +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 aae029d94e Merge pull request #1365 from MBombeck/release/v1.47.2
release: v1.47.2
v1.47.2
2026-11-11 07:21:17 +01:00
Marc-André Bombeck 0a0df4946a chore(release): v1.47.2 2026-11-11 07:01:25 +01:00
Marc-André Bombeck 9ef3331f85 test(pwa): give the sw-version script tests time to start node 2026-11-11 01:56:44 +01:00
Marc-André Bombeck a1cfb570c1 test(browser): wait for the file's own iframe before the first command
Every browser test iframe is named "vitest-iframe" and the Playwright
provider looks the frame up by that name. Once a page runs a second
file, the first file's iframe can still be found until its detach event
arrives, so the second file's first click lands in a frame that is going
away and fails with "Frame was detached". With fifteen browser files and
at most twelve pages, three pages now run two files each, and a full
local run failed this way in four of five tries. The setup now waits
until the frame found by name is its own.
2026-11-11 01:47:48 +01:00
Marc-André Bombeck 13fd7edfd8 docs(changelog): do not promise a unit change on a (calc) marker 2026-11-11 01:20:25 +01:00
Marc-André Bombeck 3745a0207d test(gki): check the chart's series colour without a browser
In the full run the browser version made a neighbouring browser file
fail with a detached frame, while each passed alone. Render the chart in
Node with Recharts stubbed, as the coach result chart tests do.
2026-11-11 01:11:27 +01:00
Marc-André Bombeck 8b607ab501 test: drive the revoke race against Postgres and type the new fixtures
The revoke audit is now asserted in the integration race test where a
rotation commits while the turn-off waits, instead of a route test that
mocked the handler and the client. The other new tests type their
fixtures rather than casting them away.
2026-11-11 01:00:23 +01:00
Marc-André Bombeck 8aa8379861 fix(gki): draw the index chart in a chart token
The custom metric chart drew its line in --primary, which put the index
in the action colour right under the ketone series drawn in a chart
token. Give the chart an optional series colour, default primary so a
custom metric looks as before, and draw the index in --chart-4.
2026-11-11 00:52:41 +01:00
Marc-André Bombeck 35b4e75a53 refactor(reminders): share the satisfiable select with the Telegram button
The select is checked against SatisfiableReminder, so a column the
reschedule needs cannot be left out of a narrowed read again, and the
handler is back under its size baseline.
2026-11-11 00:52:40 +01:00
Marc-André Bombeck 0b3c013af7 fix(i18n): name the period in the forecast style hints
Only the selected style's hint is shown, so "it may start" and "they
may fall" had no noun on screen to point back to. Name the period in
every locale that left it implied, and put the Korean hints in the same
register as the rest of the card.
2026-11-11 00:49:02 +01:00
Marc-André Bombeck b4a5442fb5 fix(labs): leave "/" to the browser when no search field is on screen
The shortcut called preventDefault before checking for the field, so on
a labs or documents page still loading, in error, or with nothing
recorded, "/" did nothing and the browser lost its own use of the key.
2026-11-11 00:48:16 +01:00
Marc-André Bombeck ee1eb921a9 fix(ui): announce a filter bar's result count
The count beside every filter bar was a plain span, so a screen reader
heard nothing while a list narrowed, only the empty state once nothing
was left. Make it a polite status region.
2026-11-11 00:47:27 +01:00
Marc-André Bombeck 128cf55ab6 fix(day): fit the GKI tile's pair line in its column
The single-reading line spelled out both readings with their names and
units and was cut off after the glucose in every layout. Show the two
numbers with one unit, which fits the tile, and keep the worded pair for
assistive tech.
2026-11-11 00:46:30 +01:00
Marc-André Bombeck a7e98b980e fix(cycle): audit the feeds a revoke actually turned off
DELETE audited the feed it read before its two revoke passes. When a
rotation committed in between, that feed was already revoked by the
rotation and the second pass revoked its successor, so the trail showed
the old feed revoked twice and the new one never. Read back the rows
carrying this request's revokedAt stamp and audit each.
2026-11-11 00:45:22 +01:00
Marc-André Bombeck 5ebfac1259 fix(gki): build the index from the glucose shown beside it
A reading entered in mmol/L is stored as whole mg/dL, and the index
divided the unrounded conversion, so a 5.2 / 0.3 pair printed beside a
17.4 rather than 17.3. Round the glucose to the decimal it is shown with
first and divide that. The tiles also printed a two-decimal ketone
reading to one decimal; show it as recorded.
2026-11-11 00:44:29 +01:00
Marc-André Bombeck c8b32ccaea fix(documents): judge the review flag on the unit as stored
A "1 (calc)" unit is stored as "1", the explicit no-unit value, but the
dimensionless check ran on the raw string and missed it, so the read
went straight through instead of to review.
2026-11-11 00:42:05 +01:00
Marc-André Bombeck 6c8458d911 fix(reminders): keep a course window's end through every reschedule
The first due slot of a Coach course honoured endsOn, but the tick's
roll-on, satisfy and skip all built their schedule input without it, so
a seven-day twice-daily course kept rolling on indefinitely and never
reached the null nextDueAt the expired-course cleanup waits for.
Carry endsOn on RollableReminder and SatisfiableReminder and select it
wherever a caller narrows the row.
2026-11-11 00:41:31 +01:00
Marc-André Bombeck f521cdbbaf docs(changelog): correct the checkup repeat wording and note the zlib update 2026-11-11 00:27:46 +01:00
Marc-André Bombeck f20efca500 Merge PR #1370: biomarker filtering and search in the labs list 2026-11-11 00:25:12 +01:00