chore(release): v1.46.0

This commit is contained in:
Marc-André Bombeck committed 2026-10-25 21:10:00 +01:00
1 parent c4b300ad78
commit f6c62f6f39
3 files changed
+417 -2

No files matched your search

+415
View File
@@ -1,5 +1,420 @@
# Changelog
## [1.46.0] — 2026-10-25
### Added
- **A database dump every night, and a restore that rebuilds a server.**
Until now every copy the app made was per account and could only roll an
account back on the same server; a lost server needed a dump you had to
make yourself. The app now takes a whole-database dump every night (01:30
Europe/Berlin) and before every update that brings migrations, keeps the
last seven on the new `backups` volume, and also puts it in your off-host
bucket when one is set up. Each dump has a manifest beside it: app
version, migration, Postgres version, key fingerprints and the row count
of every table. `docker compose run --rm db-restore latest` (or
`latest --from-bucket` after losing the host) restores it into an empty
database only, checks the keys and the checksum first and every table's
row count after, and refuses anything else. A drill in CI does exactly
that every night and on every change to the backup path: it builds a
server, fills it, destroys every
volume, rebuilds it from the bucket and the keys alone, and checks that
people can sign in and read their notes and documents.
- **Recovery readiness, pushed to the admins.** One card on the admin
dashboard and the backups page says whether a server could be recovered
today: the nightly dump, the weekly account copies, the off-host copies,
the restore drill, whether off-host copies are set up, and the recovery
kit. When one of them goes wrong, every admin gets a notification, once,
and again daily while it stays broken, and once when it recovers. A
stopped backup used to show only if someone opened the page.
- **A recovery kit instead of "save the active key".** The kit lists every
key a copy still needs, retired ones included, with their fingerprints:
the encryption keys, the backup key and its predecessors, and the token
key. It becomes due again whenever that set changes, for example after a
key rotation. An upgraded server asks once, because the kit now also
lists the token key.
- **Fourteen days to change your mind.** Deleting an account or a managed
profile now locks it at once (every session, token, share link and
connection is revoked) and erases it after 14 days
(`ACCOUNT_DELETION_GRACE_DAYS`, 0 keeps the old immediate deletion). An
admin can undo it in the users list until then. "Delete all data" first
keeps a copy of the account for the same 14 days, and the copy restores
like any other. Nothing in the background touches an account while it
waits.
- **Apple Health write-back for measurement tokens.** A measurement token,
the narrow kind a scale or Home Assistant uses to add readings, now has a
"Write readings to Apple Health" switch, when you create it and in the
token list. With it on, the HealthLog app on your iPhone adds that
token's readings to Apple Health. The readings still show that they came
from an external device, and the token still cannot read or change
anything. The server decides which readings go to Apple Health and tells
the app, so switching it on later also covers what the token already
sent. Readings from before this release are matched to a token only
where the account has exactly one measurement token; the card says so
when older readings could not be matched. (#1272)
- **Recently deleted for documents.** While it holds anything, the
Documents page links to Recently deleted. It lists each deleted document
with its type, its date, where it came from (Paperless-ngx, for
example) and how many of its 30 days are left, and each one can be
restored or deleted for good right away. The import picker now lets you
tick a document you deleted: one still in Recently deleted comes back
with everything that belonged to it, one already removed is imported
again. Automatic imports, such as a Paperless workflow or the import
script, still never bring back a document you deleted. (discussion
#1275)
- **Questionnaires in every app language.** PHQ-9, GAD-7, WHO-5 and the
sleep questionnaire no longer fall back to English where no validated
translation exists. New validated versions: PHQ-9 in French, Dutch,
Korean and Brazilian Portuguese, GAD-7 in Dutch, Korean and Brazilian
Portuguese, WHO-5 in Dutch and Korean, and the sleep questionnaire in
Polish. Elsewhere HealthLog shows its own faithful translation with a
note on every step that the result is for orientation, and one tap
switches to the English original. Each check-in remembers which wording
it was answered in, and the doctor report marks a total from an
unvalidated translation with an asterisk.
- **Your own files import in full.** The JSON import reads every field
the full backup and the JSON export write: a night's sleep stages, range
readings, an ECG's verdict, the recording device, and a mood entry's
note, detailed ratings, time zone, tags, rated factors and day context.
A full backup also brings your own mood tags, factors and groups along.
Missing ones are created within the usual limits, a tag you already
have is never overwritten, and anything that cannot be matched is named
on the import card.
- **GPT-6 models.** gpt-6-sol, gpt-6-luna and gpt-6.1-sol get reasoning
control with a real "off", the request shape reasoning models expect,
and image input, through an OpenAI key and the ChatGPT sign-in alike.
The OpenAI presets offer gpt-6.1-sol and gpt-6-luna.
- **A queued restore says what it is waiting for.** A restore never runs
beside a backup or another large job. The admin console now says which
of the two it waits for and since when, instead of reading like a stuck
worker.
### Changed
- **Postgres 18.** The compose file now runs Postgres 18 instead of 16. An
install from 1.45 or earlier moves its database across by itself on the
first start: a short-lived `db-upgrade` step copies it into a new
`postgres-data` volume, compares the row count of every table, gathers
the planner statistics, and only then lets the app start. If anything
fails, the app does not start and the log says why; a move cut short is
run again from scratch on the next start. The old `pgdata` volume is
left as it was, so going back is the previous image and two lines of the
compose file. On a real 600 MB instance the move took eleven seconds.
HealthLog keeps running on Postgres 16 for installs that bring their own
database, and the tests run on both. (discussion #1282)
- **A quicker, lighter start.** On an account with ten years of data the
app answers its first request about a tenth sooner after a start, holds
about 55 MB less memory right after it, and the background worker is ready
after loading about a quarter of the code it used to need, with the rest
loaded in the background. Within a minute it has tried every job once and
reports any that cannot load, so a broken image shows up at deploy time
instead of when a job fails hours later. Pages and charts answer as fast
as in 1.45, with the same number of database reads; a ten-year chart takes
a few milliseconds longer because it now checks that a trend has enough
days behind it.
- **Chart axes that read cleanly.** Value ticks fall on round steps (60,
80, 100 rather than 57, 83, 109), date ticks fall on a calendar grid
(days, Mondays, months, quarters, years) and keep their place as data
arrives, and the "your average" and target zone labels sit beside the
plot instead of over the readings or clipped to one letter. The dots
under a chart mark only the days the chart itself shows.
- **Tidier code underneath.** Large parts of the code were split up so
the next changes are smaller and safer to make: files over 800 lines
went from 123 to 84, functions over 200 lines from 411 to 346. Nothing
you use should look different. Signing in with a password or a passkey,
adding a weight, logging a mood, importing lab results, changing a
setting and restoring a full backup are now tested end to end against a
real server.
- **TrueNAS guide.** It covers the app in the TrueNAS Community train and
its backups, and the encryption key backup step points at the app's
Encryption Key field.
### Fixed
- **Slow local models get the time you allow them.** The response timeout
reached each model call, but not the work around it: a daily briefing is
up to five calls in a row and had one fixed bound of the timeout plus 30
seconds, so a model needing several minutes per answer was cut off in
its second call. The briefing now gets your timeout for every call, the
Insights page keeps waiting as long as that allows, and the status cards
and the other AI background jobs follow the same setting instead of
fixed limits. (#1274)
- **The Coach no longer shows a model's reasoning as its answer.** With a
model that always reasons, served through an OpenAI-compatible gateway
or a local server, the Coach could show the thought instead of the
reply, cut off, and sometimes skip the tools it meant to call. The token
limit now keeps room for reasoning whatever the model is asked,
reasoning is taken out of the answer in every shape servers send it,
tool calls run whenever the reply carries them, and a model that still
produces nothing but reasoning gets its own message suggesting another
model. A model that does not reason keeps its own answer even when it
is cut off. (#1276)
- **A model that never answers can no longer run up requests.** One idle
hour against a model that reasoned into its token limit could cost well
over a thousand requests. One call now makes two requests at most. A
reply with reasoning and no answer is asked once more without
reasoning, and if that does not help either, that model is not asked
that way again for an hour. Background work leaves a provider that
keeps failing alone: after three failures in a row it waits five
minutes, from five on it pauses for half an hour, doubling while the
failures continue, up to a day. The Coach and "Re-run analysis" still
reach it and show its error, and saving your AI settings, the admin AI
settings or a fresh ChatGPT sign-in ends the pause. A status card whose
generation failed waits from five minutes up to two hours before the
next try, even when new data arrives. Tokens spent on replies without
an answer count toward the daily AI budget, "Index all documents" stops
after three failed calls in a row, and a gateway that refuses the
reasoning setting is noticed once instead of failing every call.
(#1279)
- **"Extract lab values" reads long documents.** On a stored document it
still sent the whole report in one call, which a long report outgrew,
and the answer came back as "Couldn't read the document. Try a clearer
copy." It now reads the way the lab scan does: a PDF page by page and
text in parts, up to 400 readings, with the same timeout and budget. If
the provider stops early, the readings already found are kept and the
review warns that some values may be missing, every time it is opened;
a read that stopped before the first value says so instead of blaming
the copy. An extraction the iPhone app waits for reads in at most three
parts and marks the document incomplete when that was not enough.
(#1257)
- **PDF previews, vision reads and search work together.** The server
loaded two versions of pdf.js, and whichever ran first broke the other:
after a local text read every preview failed and a vision read of a PDF
was refused, after a preview the local text read failed and the document
stayed out of search. The server now ships one pdf.js. Previews and
search entries the clash broke are repaired once on the first start
after the update. (#1277, PR #1278)
- **"End" on a medication works, and ends it now.** The button had failed
with an error on every press since it was added. It now ends the
running course at that moment: a dose taken earlier today still counts,
and a later one is no longer due, reminded or counted as missed. End is
offered only while a course is running, someone who manages your record
can use it too, undoing the end brings the later doses back, and a new
course can start from the next day. (#1280, PR #1281)
- **Memory stays flat under bursts of requests.** Every page the server
rendered stayed in memory for ten minutes after it was sent, about
250 KB each, so a run of automated requests could push the container
towards its memory limit. A render is now released with its request.
- **Fast right after an import, not only once the database caught up.**
Postgres learns what a table holds in the background, some time after
rows arrive. Until it had, on a new install or right after a large
import, several reads chose a plan that grew with the square of the
data: the analytics page over a few thousand blood pressure readings
took seconds, and the long-range charts and all-time highs and lows of
a ten-year history could run past the database's time limit. Those
reads now take one pass over the readings whatever Postgres knows, with
the same results, and take a fraction of a second.
- **Trends need readings on enough days.** Three blood pressure readings
taken in one sitting, the only ones that week, produced a 7-day trend of
thousands of mmHg. A trend is now shown only when the readings fall on
at least three days spread over at least half the window (4 days of 7,
15 of 30), wherever a trend appears.
- **iPad and tablets.** The app fills the window after a rotation and
under Safari's floating toolbars, without a strip of page colour at the
bottom, a cut-off last card or a page that scrolls under the app. In
landscape every sidebar destination is reachable, with a cue when more
are below. A vertical swipe over a row of tabs scrolls the page, and the
area over the home indicator matches the page in Edge, Chrome and
Firefox.
- **The app's own backup and export import again.** The JSON import
refused both, because they write an empty field as `null` or `""`. It
reads them as the app wrote them now, converts display units such as lb
or mmol/L, and skips deletion records. The portable backup also carries
a reading's glucose context, sleep stage, range, rhythm verdict and
device, which it used to drop.
- **Deleted imports stay deleted after a restore.** A backup restored onto
a fresh server let a Paperless workflow or the import script store again
every document you had deleted. The backup now carries which imports
were deleted.
- **ChatGPT sign-in works without a model override.** The ChatGPT backend
rejects every model from gpt-5.5 down, so an install without its own
model list ended in "all configured Codex slugs were rejected". The
default list now starts at gpt-5.6-terra.
- **Workout splits and GPX times.** Splits were missing for every synced
route, and an exported GPX track had no times. Both read the
stored route points correctly now.
- **A hung backup no longer blocks restores.** A backup that hung held the
slot for large jobs until the container restarted, so a restore waited
forever. A job that outlives its time limit now gives the slot up.
- **API answers match the published contract.** Creating or editing a
medication answers with the same fields as reading it, a dose answers
the full entry, medications and doses come back in the same order on
every request, readings opened by day carry their note, and the
contract now declares several answers the server already gave.
- Every page view saved your language to the account again and counted
against your write limit; it is saved once per tab now.
- A page reloaded right after you entered a reading shows that reading
instead of the dashboard from before.
- The daily briefing's chart notes and storyline were dropped from replies
that passed validation; they are kept now.
- A stored AI key that can no longer be decrypted counts as no key instead
of failing the briefing with a server error.
- After a Fitbit or Google Health history import, the dashboard and
analytics show the new data right away instead of after their cache ran
out.
- Averages and streaks take one clock per request, so a reading at the
edge of a window no longer moves in and out, and today's reading counts
for the streak whatever time zone the database runs in.
- A refused request by someone you share your record with shows in your
activity feed with its own wording.
- A stat's value and unit stay on one line beside the docked day view;
the neutral score label is readable in the dark theme; axis labels are
sized correctly when reduced motion is on.
- **Large backups upload without filling the memory.** Uploading a copy
with many documents or ECG recordings held them whole while it was
checked, and a big vault could crash the server. Every reader of a
backup now streams the large parts; a 271 MB file needs about 2 MB.
- **Backup texts say what each copy can do.** The weekly copy is an
account rollback on this server, the personal export is for the person,
the JSON import reads measurements and mood within its stated limits, and
only the database dump rebuilds a server. Nothing claims more anymore.
- **Long Coach answers keep all their steps.** After a reload an answer
with more than 48 steps lost the rest.
- **The hint for reading scans on the device points to the right switch**,
Settings, Modules, Labs, in every language.
- **Help links open the right page** instead of going through redirects.
### Security
- **A crafted PDF or image can no longer take the server down.** A small
PDF with one huge compressed image, or a JPEG with a padded header, made
the server reserve gigabytes and get killed, for every account on it.
Everything pdf.js does now runs in its own process with a memory and a
time limit, image sizes are read by the JPEG rules and an unreadable size
is refused, and a document over the limits is marked as such instead of
being tried again.
- **Rate limits count the real caller.** Forwarding headers are believed
only from a connection that comes from the server's own network or a
proxy on it; a caller reaching the port directly no longer picks its own
address, and the sign-in throttle no longer exempts an address an account
used before.
- **Writes from other sites are refused.** A request that changes something
and comes from another site, including a sibling subdomain, is refused
before it is read. The iPhone app and other token clients are not
affected.
- **The MCP consent page names where the access goes**, and warns when a
self-registered application sends it to a remote host.
- **Switching Labs off keeps lab values out of every AI feature.** The
Coach, the daily briefing, status notes and the MCP lab prompt read no
lab values while the module is off. Before, the Coach still sent them.
- Smaller fixes: share links show no documents while the documents module
is off, a narrow medication token can no longer probe medication names,
MCP tools fence medication and lab text like every other user text,
reading documents takes turns between accounts, so one account's heavy
files cannot hold up everyone else's, the session cookie cannot be
planted from a sibling subdomain over HTTPS, file-like paths under `/api`
go through the request checks, the deploy
webhook can be signed, the error relay to GlitchTip needs a signed-in
session, and database error details are redacted before they leave.
- Raised the minimum versions of body-parser and fast-uri, which the MCP
server runs on (CVE-2026-111181, high; CVE-2026-110180;
CVE-2026-110306), of ip-address, which took exponential time on a
crafted IPv6 zone identifier, and of a development-only copy of undici.
### Upgrade notes
- Migrations 0475 to 0485 (0475, 0476, 0477, 0478, 0479, 0480, 0482, 0483
and 0485; 0481 and 0484 are not used). Most add a column under a
bounded lock wait and retry on a lock timeout. These do more:
- 0476 links existing readings to the measurement token that sent them
where the account has exactly one such token. It sets only that link,
so the readings do not count as changed.
- 0477 builds an index on `measurements` concurrently; writes carry on
during the build.
- 0478 clears the recorded preview failures and 0479 re-queues the PDFs
whose search entry failed, so the first start renders those previews
once more and indexes those PDFs once more. The indexing uses your AI
provider where one is set up and you have allowed it, as an upload
would.
- 0485 drops the unused preview-features column from `users`.
On a copy of a real instance all of them together took under a second.
- If a concurrent index build fails half-way, for example because the
container was stopped, mend the cause and mark the migration rolled
back; the next start drops the invalid index and builds it again. See
`docs/ops/deploy.md`.
- After a deploy, the worker reports any job it cannot load within a
minute; `docs/ops/deploy.md` says what to look for and when to roll
back.
- `/api/settings/preview-features` is retired and answers 410. It served
a switch for pages that never shipped, and no client reads it. A backup
from 1.44 or 1.45 that carries the field still restores.
- The Apple Health switch takes effect with an iPhone app build that
reads it; until then the app keeps mirroring Withings, imported and
hand-entered readings as before.
- If you set `CODEX_MODEL_FALLBACK_CHAIN` only to get past the rejected
ChatGPT models, you can remove it.
- Migrations 0486 to 0489 add the dump ledger and its key column, the
readiness and kit tables and the deletion columns. 0487 also drops the four old key-backup
columns from `app_settings` and carries an existing confirmation over;
as with 0485, do not run the previous image beside the new schema.
- **A new `backups` volume** holds the local dumps. The compose file
mounts it; a stack built without it keeps dumps off-host only and says
so. In the bucket, dumps go under `database/<your host>/`, so two
servers sharing a bucket never mix them up; add `database/` to the
bucket's lifecycle rule, and the backups page warns when no rule covers
it. Dumps keep a deleted account until they
age out, so state that period as your backup retention.
- **New settings:** `DB_BACKUP_ENABLED`, `DB_BACKUP_DIR`,
`DB_BACKUP_SCHEDULE`, `DB_BACKUP_KEEP`, `DB_BACKUP_BEFORE_MIGRATE` and
`ACCOUNT_DELETION_GRACE_DAYS`, all on sensible defaults. If the dump
before a migration fails on a writable directory, the start stops and
names the switch to skip it.
- **Behind a reverse proxy,** forwarding headers are believed only from a
private or loopback address. If your proxy reaches HealthLog from a
public address, set `TRUSTED_PROXY_CIDRS` to that address; otherwise
every user shares one sign-in rate limit. `TRUSTED_PROXY_CIDRS` also
narrows the default when you know your proxy's network
(`docs/self-hosting/reverse-proxy.md`). Behind a proxy that rewrites the
Host header, make sure `APP_URL` is right, or writes from the browser
are refused as cross-site.
- The deploy webhook can now be signed (`X-Deploy-Webhook-Signature`);
`DEPLOY_WEBHOOK_REQUIRE_SIGNATURE=true` accepts signed requests only.
`DEPLOY_WEBHOOK_REQUIRE_TIMESTAMP` keeps its meaning, and the stock
Coolify setup needs no change.
- Over HTTPS the session cookie is now `__Host-healthlog_session`. The old
name is still read and swapped on the next request, so nobody is signed
out.
- The app image is about 6 MB larger: it now carries the Postgres 18
client for the dumps. The supported memory floor stays at 1536 MB for
the app container.
- The first start with the new compose file moves the bundled database to
Postgres 18 (see Changed). Use `docker compose up -d` with the new
compose file; pulling only the new image keeps Postgres 16, which this
release still runs on. The move needs free disk space about the size of
the database. Rollback, the steps for a database you run yourself, and
how to remove the old volume later: `docs/ops/postgres-major-upgrade.md`.
### Thanks
Thanks to @haritowa for two reports that came with their fixes: the clash
between two pdf.js versions that broke previews and search (#1277), fixed
in PR #1278, and the "End" button that never worked (#1280), fixed in PR
#1281. Both pull requests are part of this release as written.
Thanks to @Tio61 for the precise wire captures behind the Coach showing a
model's reasoning as its answer (#1276) and for the exact request counts
that exposed the runaway requests against a model that never answers
(#1279).
Thanks to @gesus14 for noticing that the daily briefing gave up long
before the response timeout allowed (#1274).
Thanks to @asmala for coming back to report that "Extract lab values" on
a stored document still failed on long reports (#1257).
Thanks to @cnrd for the careful proposal that became the Apple Health
switch for measurement tokens (#1272).
Thanks to @frubino for asking how to get a deleted document back, which
led to Recently deleted (discussion #1275).
Thanks to @muhdusama for noticing that the MCP labs tool declared a
narrower answer than it gives, and for the fix with its tests (PR #1284).
Thanks to @sttrebo for asking about Postgres 18, which turned into the
move itself, not just a guide for it (discussion #1282).
## [1.45.0] — 2026-10-23
### Added
+1 -1
View File
@@ -1,7 +1,7 @@
openapi: 3.1.0
info:
title: HealthLog API
version: 1.45.0
version: 1.46.0
description: >-
Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "healthlog",
"version": "1.45.0",
"version": "1.46.0",
"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",