From fbb3ad78529bf047dbd9f5277ff3706043397469 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 12 Nov 2026 14:24:36 +0100 Subject: [PATCH] chore(release): v1.47.4 --- CHANGELOG.md | 85 +++++++++++++++++++++++++++++++++++++++ docs/api/openapi.yaml | 93 +++++++++++++++++++++++-------------------- package.json | 2 +- public/sw.js | 2 +- 4 files changed, 136 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 520c70801..b2224ad9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,91 @@ ## [Unreleased] +## [1.47.4] — 2026-11-12 + +### Fixed + +- **Web Push comes back when you turn it on again.** After a reinstall of + the Home Screen app, cleared site data, or turning push off on your + last device, the push channel switched itself off for good. Turning + push on again stored the browser and sent a test push, but no reminder + ever followed. Subscribing now turns the channel back on and clears + earlier failures, opening the push settings repairs a lost + subscription, and migration `0497_reenable_subscribed_web_push_channels` + turns push back on for every account that has a subscribed browser but + was switched off this way. A channel that has nobody to send to (no + browser, no paired iPhone, no mail server) now pauses for an hour and + says why on the status screen instead of counting failures. +- **No more invisible push for every recorded dose.** Logging a dose sent + a silent push to every browser to close the reminder. Safari does not + allow silent pushes and could switch push off for the iPhone app + without a word. A reminder already on screen now stays until you + dismiss it or the next one replaces it, and the dose count earlier + releases left on the Home Screen icon is cleared. +- **Signing out signs this device out of push too.** The browser kept + receiving the signed-out account's medication reminders, also after + someone else signed in. Every sign-out button now removes this + browser's subscription once the server confirmed the sign-out, and a + browser only carries the reminders of the account that subscribed it + last. If signing out fails, the app stays where it is and says you are + still signed in, instead of showing the sign-in page. +- **A shared browser no longer shows the previous account's data.** The + dashboard snapshot is restored only once the signed-in account is + known and is its owner, the sign-in pages clear the previous account's + copies as soon as the server confirms there is no session, and single + sign-on and the profile take-over clear them too. The daily briefing + text is no longer written to the browser's storage. +- **Offline, the open page stays.** Without a connection, a tap on a tab + or a link ended on the bare offline page, although the banner promised + your last synced data. The page you have open now stays readable, and + a note says other pages open once you are back online; the banner and + the README say exactly that. The service worker no longer stores API + answers on the device, and answers older releases stored are deleted + on update. +- **Links arrive where they pointed.** A Home Screen shortcut, a + notification or a bookmark opened while signed out takes you to that + page after you sign in. After you sign out yourself, the next sign-in + starts from the dashboard, and Back no longer reopens the page you + left. +- **Back works as expected.** Back after signing in, registering or + taking over a profile no longer shows the form you just sent, and Back + from a detail page returns to where you were in the list. +- **Sign-in errors say what happened, in your language.** A wrong + password or code says the sign-in failed, a lockout says to wait a few + minutes, and registration says when a username or address is taken or + an invitation is no longer valid. Passkey and single-sign-on refusals + say what went wrong. +- **The iPhone push hint.** In Safari on iPhone and iPad the push + settings now tell you to add HealthLog to your Home Screen first, + instead of saying push is not supported. The test push reaches every + device you subscribed, in your language, and says why it failed. +- **A 401 or 403 from a proxy or firewall in front of HealthLog no longer + signs you out.** An account that expires while the app is open now ends + the session, so the next save does not fail with "sign in again". +- Tapping a reminder while the app already shows that page no longer + reloads the app, and setup pages seen while signed in stay out of the + offline page cache. Single sign-on starts even when the browser's + storage answers slowly. +- The clock and battery of the installed iPhone app are readable again in + the light theme. + +### Changed + +- **Demo instances.** Visitors can sign out, each visitor's language stays + in their own browser instead of the shared account, refused language + and timing requests no longer show up on every page, and a demo allows + 30 password sign-ins per 15 minutes per address. Other instances keep + their limits. +- The "machine translated" notice no longer appears on the sign-in, + registration and profile take-over pages. +- The service worker file is half its former size. + +### Migration + +- `0497_reenable_subscribed_web_push_channels` turns Web Push back on for + accounts whose channel was switched off by failed sends while a browser + is still subscribed. A channel you switched off yourself stays off. + ## [1.47.3] — 2026-11-12 ### Changed diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index ce479a2e1..1d4ac94e6 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.47.3 + version: 1.47.4 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. @@ -4016,15 +4016,16 @@ paths: "403": description: Refused before any work. Either the operator runs `OIDC_ONLY=true` (`meta.errorCode` = `oidc_only`), or self-registration is closed and the request carried no invite, or the invite it carried was invalid, expired - or exhausted. The three are distinguishable by prose but not by code. + or exhausted (`auth.register.invite_invalid`). content: application/json: schema: $ref: "#/components/schemas/ErrorEnvelope" "409": - description: The email or the username is taken. Deliberately one message for both, so this is not an account-existence - oracle. Also the answer when two concurrent signups race past the pre-check and the database unique index - catches the second. + description: The email or the username is taken (`auth.register.taken`). Deliberately one message and one code for both, + so this is not an account-existence oracle. Also the answer when two concurrent signups race past the + pre-check and the database unique index catches the second. `auth.already_authenticated` when the request + carries a signed-in session. content: application/json: schema: @@ -24719,8 +24720,9 @@ paths: per minute per user. - Read `ok`, not the status. A push that every service rejects still answers 200, with `ok: false` and a `reason`. - No endpoint is returned: the full endpoint is the routing secret for that subscription. + Read `ok`, not the status. A push that every service rejects still answers 200, with `ok: false`, a `reason`, + and `meta.errorCode`: `upstream_rejected`, or `connection_failed` when no push service answered. No endpoint is + returned: the full endpoint is the routing secret for that subscription. responses: "200": description: The test ran. `ok` says whether the push was accepted; a false here is a real failure reported inside a @@ -39702,42 +39704,43 @@ components: anamnesis.lifeConfirm.invalid, anamnesis.lifeSections.invalid; auth — auth.account.deletion_pending, auth.admin.required, auth.already_authenticated, auth.mfa.already_active, auth.mfa.code_invalid, auth.missing, auth.passkey.last_method, auth.refresh.invalid, auth.refresh.reuse, auth.refresh.revoked, - auth.reproof.failed, auth.reproof.required, auth.reproof.too_weak, auth.scope.insufficient, - auth.session.insecure_transport, auth.session.unavailable, auth.stepup.required, auth.token.expired, - auth.token.invalid; backup — backup.foreign_reference, backup.key.missing, backup.owner.exists, - backup.owner.missing, backup.payload.undecryptable, backup.restore.active, - backup.restore.rows_exist_elsewhere, backup.section.missing, backup.target.admin_account, - backup.target.holds_records, backup.target.managed_profile, backup.target.not_found, - backup.target.pending_deletion, backup.target.unconfirmed, backup.target.username_taken, - backup.upload.too_large; biomarkers — biomarkers.unit.locked; body-site — body-site.invalid; coach — - coach.budget.exceeded, coach.conversation.invalidTitle, coach.conversation.notFound, - coach.fenced.attachmentConflict, coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound, - coach.fenced.attachmentNotIndexed, coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable, - coach.fenced.invalid, coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json, - coach-prefs.body.invalid_shape; consent — consent.ai.confirmRequired, consent.ai.required; customMetric - — customMetric.name_taken; cycle — cycle.bulk.invalid, cycle.bulk.too_large, cycle.calendar.invalid, - cycle.calendar.range, cycle.cycles.invalid, cycle.day-log.conflict, cycle.day-log.invalid, - cycle.disabled, cycle.feed.api_disabled, cycle.feed.cookie_only, cycle.feed.exists, - cycle.feed.fertile_goal, cycle.feed.none, cycle.feed.notFound, cycle.feed.rateLimited, - cycle.period.invalid, cycle.symptom.custom.invalid, cycle.symptom.custom.limit; cycle-prefs — - cycle-prefs.body.invalid_json, cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; - data_wipe — data_wipe.copy_failed; dawarich — dawarich.credentials_rejected, - dawarich.home_place_unknown, dawarich.invalid, dawarich.not_connected, dawarich.rate_limited, - dawarich.unreachable; day — day.invalid; demo — demo.readOnly; documents — - documents.chat.conversationNotFound, documents.chat.invalid, documents.inbound.alreadyConfirmed, - documents.inbound.alreadyPartlyConfirmed, documents.inbound.budgetExceeded, - documents.inbound.decryptFailed, documents.inbound.encounterNotFound, documents.inbound.episodeNotFound, - documents.inbound.extractFailed, documents.inbound.factNotFound, documents.inbound.factNotPending, - documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge, documents.inbound.fileType, - documents.inbound.invalidMetadata, documents.inbound.invalidQuery, documents.inbound.invalidUpdate, - documents.inbound.localOcrDisabled, documents.inbound.notDeleted, documents.inbound.notFound, - documents.inbound.notIndexed, documents.inbound.outputLimit, documents.inbound.pdfRendererUnavailable, - documents.inbound.providerUnsupported, documents.inbound.quotaExceeded, documents.inbound.rateLimited, - documents.inbound.restoreDuplicate, documents.inbound.restoreGone, documents.inbound.sourceAliasLimit, - documents.inbound.uploadBusy, documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound, - documents.sources.authRefused, documents.sources.badResponse, documents.sources.browserOnly, - documents.sources.invalidAddress, documents.sources.linkTargetNotFound, documents.sources.notConnected, - documents.sources.notFound, documents.sources.organizationRequired, documents.sources.originNotAllowed, + auth.register.invite_invalid, auth.register.taken, auth.reproof.failed, auth.reproof.required, + auth.reproof.too_weak, auth.scope.insufficient, auth.session.insecure_transport, + auth.session.unavailable, auth.stepup.required, auth.token.expired, auth.token.invalid; backup — + backup.foreign_reference, backup.key.missing, backup.owner.exists, backup.owner.missing, + backup.payload.undecryptable, backup.restore.active, backup.restore.rows_exist_elsewhere, + backup.section.missing, backup.target.admin_account, backup.target.holds_records, + backup.target.managed_profile, backup.target.not_found, backup.target.pending_deletion, + backup.target.unconfirmed, backup.target.username_taken, backup.upload.too_large; biomarkers — + biomarkers.unit.locked; body-site — body-site.invalid; coach — coach.budget.exceeded, + coach.conversation.invalidTitle, coach.conversation.notFound, coach.fenced.attachmentConflict, + coach.fenced.attachmentLimit, coach.fenced.attachmentNotFound, coach.fenced.attachmentNotIndexed, + coach.fenced.attachmentRequired, coach.fenced.attachmentUnavailable, coach.fenced.invalid, + coach.request.invalid; coach-prefs — coach-prefs.body.invalid_json, coach-prefs.body.invalid_shape; + consent — consent.ai.confirmRequired, consent.ai.required; customMetric — customMetric.name_taken; cycle + — cycle.bulk.invalid, cycle.bulk.too_large, cycle.calendar.invalid, cycle.calendar.range, + cycle.cycles.invalid, cycle.day-log.conflict, cycle.day-log.invalid, cycle.disabled, + cycle.feed.api_disabled, cycle.feed.cookie_only, cycle.feed.exists, cycle.feed.fertile_goal, + cycle.feed.none, cycle.feed.notFound, cycle.feed.rateLimited, cycle.period.invalid, + cycle.symptom.custom.invalid, cycle.symptom.custom.limit; cycle-prefs — cycle-prefs.body.invalid_json, + cycle-prefs.invalid; disable-coach — disable-coach.body.invalid_json; data_wipe — data_wipe.copy_failed; + dawarich — dawarich.credentials_rejected, dawarich.home_place_unknown, dawarich.invalid, + dawarich.not_connected, dawarich.rate_limited, dawarich.unreachable; day — day.invalid; demo — + demo.readOnly; documents — documents.chat.conversationNotFound, documents.chat.invalid, + documents.inbound.alreadyConfirmed, documents.inbound.alreadyPartlyConfirmed, + documents.inbound.budgetExceeded, documents.inbound.decryptFailed, documents.inbound.encounterNotFound, + documents.inbound.episodeNotFound, documents.inbound.extractFailed, documents.inbound.factNotFound, + documents.inbound.factNotPending, documents.inbound.factTypeMismatch, documents.inbound.fileTooLarge, + documents.inbound.fileType, documents.inbound.invalidMetadata, documents.inbound.invalidQuery, + documents.inbound.invalidUpdate, documents.inbound.localOcrDisabled, documents.inbound.notDeleted, + documents.inbound.notFound, documents.inbound.notIndexed, documents.inbound.outputLimit, + documents.inbound.pdfRendererUnavailable, documents.inbound.providerUnsupported, + documents.inbound.quotaExceeded, documents.inbound.rateLimited, documents.inbound.restoreDuplicate, + documents.inbound.restoreGone, documents.inbound.sourceAliasLimit, documents.inbound.uploadBusy, + documents.inbound.uploadTimeout, documents.inbound.vaccinationNotFound, documents.sources.authRefused, + documents.sources.badResponse, documents.sources.browserOnly, documents.sources.invalidAddress, + documents.sources.linkTargetNotFound, documents.sources.notConnected, documents.sources.notFound, + documents.sources.organizationRequired, documents.sources.originNotAllowed, documents.sources.permissionMissing, documents.sources.rateLimited, documents.sources.redirected, documents.sources.tokenRequired, documents.sources.unavailable, documents.sources.unreachable, documents.sources.versionTooOld; encounter — encounter.invalid, encounter.link.invalid, @@ -90798,7 +90801,9 @@ components: - sending_paused description: Server-derived. `auto_disabled` — the dispatcher turned it off after repeated hard rejects (`disabledReason` says why). `manually_disabled` — the user turned it off. `sending_paused` — still enabled - but inside a retry cooldown until `nextRetryAt`. `active` — enabled and sending. + but inside a retry cooldown until `nextRetryAt`, after a transient failure or after a send that had nobody + to go to (no browser subscribed, no iPhone paired, a transport the operator has not set up; + `lastFailureReason` names it, `consecutiveFailures` is not raised). `active` — enabled and sending. disabledReason: description: Why the dispatcher auto-disabled the channel; null when it did not. This is the field that separates `auto_disabled` from `manually_disabled`. diff --git a/package.json b/package.json index bcc7d9f4b..4a741829c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.47.3", + "version": "1.47.4", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index 2fe77d88e..7b7df52cf 100644 --- a/public/sw.js +++ b/public/sw.js @@ -16,7 +16,7 @@ try { } const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.47.3"; + /* @sw-version-fallback */ "v1.47.4"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // Every HealthLog cache, current and legacy, and nothing another app on the