A same-origin auth gateway that answers an expired session with a 200
sign-in page returns HTML for both the navigation and the /api/version
probe, which read as another app. The worker then unregistered and
destroyed the Web Push subscription, which nothing re-creates, so
reminders stopped without notice.
The worker no longer unregisters while the registration holds a push
subscription, or when the lookup is unavailable. It deletes its caches
and passes requests to the network for the rest of its life instead.
The probe now sends same-origin credentials, so a valid gateway cookie
reaches HealthLog.
Refs #847
A reverse proxy with a response-header allowlist strips
X-HealthLog-Page from every real HealthLog page, and the worker then
unregistered on each load and dropped Web Push subscriptions.
A successful page without the marker now only sends requests to the
network while the worker asks /api/version with no-store and a manual
redirect. It deletes its caches and unregisters only when the answer
positively is not HealthLog: a 404, a non-JSON 2xx, or JSON without
the version shape. A failed probe, any other status or the HealthLog
shape resumes normal service. buildSha may be null on a self-built
image, so only its presence is checked.
Refs #847
The worker is registered with scope / and serves /_next/static/*
cache-first. A self-hoster who stopped HealthLog and started another
app on the same port kept getting HealthLog's cached chunks until they
unregistered the worker by hand.
The proxy now marks every page response with a presence-only
X-HealthLog-Page header. When a successful navigation arrives without
it, the worker stops handling requests for the rest of its life,
deletes its own caches and unregisters, and hands the page through
untouched. A network failure keeps the offline fallback, and a
redirect or error status is never read as another app, so a gateway
in front of a stopped HealthLog does not drop the Web Push
subscription.
Refs #847
- exercise signed-in service-worker install at CacheStorage boundary
- require data-free offline navigation and immutable public assets
- require current and legacy session cache cleanup
A browser SSO sign-in landed on /auth/login?error=oidc_failed even though it
had succeeded. Two layers conspired:
- The service worker enables navigation preload, but its fetch handler returned
early for /api/ paths without calling event.respondWith(). For the OIDC
login/callback navigations that left the preload unconsumed, so the browser
also ran its own default fetch and the callback was requested twice. The
first request redeemed the single-use authorization code and set the session;
the second tried to redeem the now-consumed code, failed at the IdP token
endpoint, and that 307 to the error page is what the user saw.
- Even with the double request gone, a legitimate retry (proxy, double
navigation) hit the same wall on the server.
Fix both layers:
- The worker settles /api/auth/* navigations itself, consuming the preload
response (or fetching once when there is none), so exactly one request
reaches the server. These responses are never cached.
- The callback is idempotent. When the single-use state cookie is already
consumed but the request carries a database-validated, unexpired session, the
callback treats it as a duplicate of a completed login and redirects into the
app instead of surfacing an error. The authorization code is never exchanged
on this branch, and a consumed/forged/expired state from a caller with no
valid session still fails closed with the unchanged oidc_state error.
Refs #600
One-shot prettier pass so the new format:check gate starts from a clean
baseline. Formatting only — no behaviour change. Generator-owned and
vendored artifacts stay untouched via .prettierignore.
Self-hosters without an Apple Developer account rely on Web Push to the
installed PWA — there is no Live Activity or lock-screen widget to drive.
Make that path carry the dose lifecycle on its own:
- Stable per-slot tag. The medication dose-due reminder now sets a
`med:<id>:<iso-slot>` Web Push tag (medicationDoseTag), so a re-fired
reminder for the same slot coalesces instead of stacking, and a later
clear can target it.
- Clear-on-taken. When a dose is marked taken or skipped, push a
`type:"clear"` notification reusing that slot tag. The service worker
closes the matching pending reminder and refreshes the badge — the PWA
equivalent of ending a Live Activity. A snooze leaves the dose
outstanding and clears nothing. Wired into the single + bulk intake
routes alongside the existing APNs cross-device sync.
- App badge. The dispatcher carries a server-authoritative outstanding-
dose count; the SW reflects it via the Badging API
(setAppBadge/clearAppBadge), feature-detected and a no-op where
unsupported.
- The SW push handler honours requireInteraction for urgent events and
renotify so a re-fired reminder re-alerts.
Dose-due, preventive-care, and low-stock reminders already cascade to
Web Push when no APNs channel exists; tests pin that an APNs-less
instance still delivers.
`public/sw.js` carries a `|| "..."` fallback literal used for `CACHE_VERSION`
when `importScripts('/sw-version.js')` is unavailable. The literal had drifted
to `v1.4.43`, four-plus minors behind `package.json`; in any environment where
the generated file is missing this keyed every cache to the stale version and
turned the activate-step eviction into a no-op — the exact bug the generator
was written to kill.
Extend the prebuild generator to rewrite the fallback literal in `sw.js` to the
current `package.json` version on every build, anchored on a
`/* @sw-version-fallback */` marker (the build fails loudly if the marker is
gone). The literal and the generated `sw-version.js` can no longer diverge.
The SW test now reads the fallback straight from the source so it tracks the
literal instead of hard-coding it.
With the service worker now registered for every user, the network-first
page cache sees every authenticated navigation. The everyday app routes
are client-fetch-only shells with no server-rendered PII, but the
`/c/[token]` clinician-share view renders health values straight into the
HTML and emits `no-store`. The page cache ignored the header and stored
it, so a revoked or expired share could still render back offline.
Gate `cache.put` in `networkFirst` on a new `isCacheableNavigation`
check: skip any response carrying `Cache-Control: no-store` (the
principled rule for any present or future server RSC that renders user
data) and skip the `/c/*` path outright as belt-and-braces. Ordinary
navigations still cache unchanged.
The medication list fired one compliance request per card every 30
seconds, and each request scanned the medication's full intake history
and expanded the recurrence calendar five to six times. The endpoint now
mints bands once per request, serves the sub-windows from that single
ledger, bounds the event read to a year of selected columns and sits
behind a write-invalidated 15-minute server cache; the cards poll at
five minutes. The dashboard drops two redundant checklist queries by
reading counts from the snapshot, the BMI chart derives from the weight
series instead of refetching it, the detail page no longer serialises
behind the auth probe and seeds its query from the list row. Expensive
analytics reads gain a shared per-user rate limit, and the service
worker stops serving cross-version shells, enables navigation preload
and trims caches in one pass.