chore(release): v1.47.4

This commit is contained in:
Marc-André Bombeck committed 2026-11-12 14:24:36 +01:00
1 parent 02f4f8116c
commit fbb3ad7852
4 files changed
+136 -46

No files matched your search

+85
View File
@@ -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
+49 -44
View File
@@ -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`.
+1 -1
View File
@@ -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",
+1 -1
View File
@@ -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