The unreleased changelog entry said the image moved to Wolfi. It now names the official Node image, why (Raspberry Pi 4 rendering, Node security releases arriving with the image), that the image carries no package manager, and that psql is no longer in the app image. The README and the getting-started prerequisites say the same.
1.3 MiB
Changelog
[Unreleased]
Added
- Blood pressure by time of day. The blood pressure page has a card
with the morning, midday and evening averages side by side, each with
its count and pulse, and the morning minus evening difference once both
have at least three readings. Morning runs 04:00 to 11:59, midday 12:00
to 17:59 and evening 18:00 to 03:59 in your time zone, wide on purpose,
because home measurement guidance ties the two readings to waking and to
bedtime rather than to a clock. A control under it narrows the chart to
one part of the day, kept in the URL, and the stat strip names the part
while it is narrowed. The server computes the split, so the app can show
the same numbers (
GET /api/measurements/blood-pressure/time-of-day, andpartOfDayon the daily series). (#1331) - Russian. HealthLog speaks Russian, its tenth language: the whole interface, the AI output language, the safety screen's Russian bank, Russian month names in the coach's fact check, and the questionnaires with the "show original" switch, marked as not yet validated. (#1322)
- A lab reading has its own id over MCP.
fetch labresult:<id>returns one reading with the report and visits it came from, and the links on documents and visits now point at the reading that report holds instead of the analyte's latest one.lab:<analyte>still answers with the latest reading. (#1323) - The intake history shows where an injection went. A dose logged with an injection site already stored it, but the per-medication history only showed when it was taken and where the entry came from. Each injected dose now carries a chip with the site next to its status, in the table and on the mobile cards, so the rotation can be read back from the history. Doses without a site look as before.
- Garmin, directly. Garmin Connect now offers a Personal API, and
HealthLog reads it: create a key in Garmin Connect (Settings, Personal
API), paste it into the new Garmin card, and steps, distance, floors,
active calories, resting heart rate, sleep stages and sleep score,
overnight SpO₂, breathing rate and HRV, weigh-ins with body composition,
activities and logged period days sync every hour, with the whole
history filled in behind. Body Battery and Garmin's all-day stress level
arrive as types of their own (
BODY_BATTERY,STRESS_LEVEL), never mixed into the computed scores. Garmin keys expire after 30 to 365 days; the card warns a week ahead and asks for a new key when Garmin stops accepting the old one. Accounts on Garmin's China service work too; the key goes only to the service you pick. The key is stored encrypted and only ever sent to Garmin. (#1333) - Training Readiness and acute training load. The Personal API does
not carry them, so they are new types (
TRAINING_READINESS,ACUTE_TRAINING_LOAD) a Home Assistant bridge can push through an ingest token, and they chart on the recovery page next to Body Battery. The Home Assistant guide has the automation. (#1333)
Changed
- The image runs on the official Node 22 image (Debian slim) instead of
Alpine. The runtime is glibc now, so the native page renderer,
password hashing and the Prisma engine run their glibc builds. The
published arm64 image renders PDF pages on a Raspberry Pi 4 again, where
the Alpine build of the renderer needs CPU features an ARMv8.0 core
lacks. Node security releases now reach the image with the official
Node image, usually within days. The image carries no package manager:
npm, npx, corepack and yarn are removed.
pg_dumpandpg_restoreare PostgreSQL 18.6, the bundled database's version;psqlis no longer in the app image, so run it in thedbcontainer. A health check that still callswget, from a compose file copied before this release or a platform template, keeps working: the image carries a smallwgetthat answers health checks and nothing else. The currentdocker-compose.ymlprobes with Node directly, and that is the one to switch to. (#1311) - The app connects to the database with less power. On Postgres 17 or
later, with a database user that may create roles (the bundled compose
file's user may), the app and the worker now connect as a role of their
own that reads and writes rows but cannot change or drop tables, create
functions, read server files or create roles. Migrations, the dump
before them and restores still run as the owner, and so does the queue
schema, which the role may only write rows into. Nothing needs setting:
the role is created at start, with a password derived from an existing
secret, one per database. Where the role cannot be set up (Postgres 16,
a managed database without the right,
DB_APP_ROLE=off), the app runs as before and says why at start.docs/self-hosting/database-role.mdhas the details.
Fixed
- Medication adherence reads faster. Every rate walked a schedule from the day the medication was created, so it slowed down as a medication aged, about 4 ms per rate after two years. It now starts just before the window it reads, with the same result.
- A PDF the server cannot render is read by its text. On a host without the page renderer (an x86 CPU without AVX2), an AI provider that takes pages as images could not read a PDF at all, and the message asked for a Claude provider without saying why. A PDF with a text layer is now read as text there, and a scan says plainly that this server cannot turn its pages into images. A PDF that fails to render on a capable server is reported as that, not as a provider problem. (#1311)
- A marker that started with a text result takes the unit of its first number. A marker created from "see note" had an empty unit that locked once it held a reading, so the first number, a unitless ratio included, was refused. While a marker holds only text readings, its first number now sets the unit. (#1313)
- The AI safety screen catches more medication instructions. A negated take ("don't take your metoprolol tonight"), a pause, an extra amount without a target ("add 4 more units of insulin") and a liquid dose of a syrup or solution are now held back in all ten languages, while advice such as drinking water or taking a break from screens passes. A Dutch reminder not to stop a medication without a doctor was held back before and passes now.
- Least-squares fits keep r² within 0 to 1. Values far from zero with little spread could give an r² a hair above 1.
Thanks
- @FloMaetschke for the injection site in the intake history (#1330).
- @l1137l for the Russian translation, the Russian safety bank and the patience through two reviews (#1322).
- @johnnyrepo026 for finding the text-first marker that refused its first number (#1313).
- @mbreitkreuz for testing the MCP links down to two reports on one day (#1323).
- @Oguz3003 for staying with the Raspberry Pi 4 until it reads PDFs (#1311).
- @mills1975 for asking how blood pressure differs over the day (#1331).
- @kruemelro for asking for a direct Garmin connection on Garmin's new Personal API (#1333).
[1.46.6] — 2026-11-02
Added
- Lab values without a unit. Ratios and indices such as INR,
cholesterol/HDL or albumin/globulin had nowhere to go: manual entry, the
CSV import and the document review all required a unit. Entry, the
scanned-report rows and the document review now offer an explicit "No
unit" choice, stored as the UCUM unity
"1", and the CSV import reads1in the unit column the same way. An empty unit is still refused, so a unit the reading lost is not mistaken for none, and a document read that answers "no unit" by itself is held for review. Such a value shows as the number alone, a marker kept in a real unit refuses it, and the FHIR export codes it as UCUM1. (#1313) - MCP reads follow their links. Lab readings in
get_labsandfetch lab:list the documents and visits they are linked to, with the linked records once per answer instead of on every reading of the same report.fetch med:andget_medication_schedulelist theirs, andfetch document:now also names the lab results and medications filed against it. Rows ofget_visits,get_medication_scheduleandget_illness_recoverycarry an idfetchtakes. An imported document names its source system and id, never the address of the instance it came from. (#1323)
Fixed
- A refused lab value can be corrected in the review. A value that Finish review refused, such as a ratio printed without a unit or a unit that disagrees with the marker, showed the reason but could only be discarded. The row now offers the same Check control as a low-confidence value. (#1313, #1327)
- The doctor report prints every language. The PDF used a built-in font that covers Western European letters only, so Polish letters such as ą, ł or ś, the whole Korean report, and any medication name or note in another script printed as empty boxes. The report now embeds an open Unicode font family covering Latin, Greek, Cyrillic and, when the report needs it, Korean, and arrows, ≤ and ² print as themselves instead of ASCII stand-ins. The file is compressed, so a report stays around 65 KB. Chinese, Japanese and Arabic text are not covered yet.
- PDFs read on a Raspberry Pi 4. The native canvas build for arm64 Alpine needs CPU features an ARMv8.0 core lacks, and loading it ended every PDF read with an illegal instruction. The CPU check now covers that case, and PDF reading carries on without the canvas, so the text layer and the index work; thumbnails keep their existing fallback. (#1311)
- The display name greets you. The dashboard and Insights greetings, the top bar and the sidebar showed the username even when a display name was set. They now use the display name and fall back to the username. (#1309)
- Summaries count every reading right after an import. A type whose history was not folded yet counted as covered as soon as one day was, so a summary could show one day of weight readings out of three. A type now counts as covered only when its folded days reach back to its oldest reading.
- The repeated hour of a clock change stays two hours. On the night the clocks go back, a dense pulse or glucose chart folded both runs of the repeated hour into one point. Each hour is its own point now.
- Google Health heals a connection that lost its account id. The 1.46.4 update cleared a placeholder id on existing connections. After a clean sync such a connection now reads its real id back, so the one-account-per-profile check covers it again.
- WHOOP refuses a sign-in without a user id instead of storing the text "undefined" as the account.
Changed
- Next.js 16.4.3 and the weekly dependency updates. node-forge, reached through the APNs client, is raised to 1.4.2, which closes five high advisories.
Thanks
- @johnnyrepo026 for the precise report on unitless lab ratios (#1313), with the cause already traced to the right two files, and for the pull request that fixed the review half of it (#1327).
- @mbreitkreuz for the MCP linked-records request (#1323), a careful spec with the real case that made date matching fail.
- @Oguz3003 for tracking the Raspberry Pi 4 crash down to the CPU and confirming the workaround (#1311).
- @angusmaul for noticing the greeting ignored the display name (#1309).
- @mills1975 for the heart-rate sync report and the diagnostics that pinned it to the iPhone app (#1326); the fix follows in an app update.
[1.46.5] — 2026-10-30
Fixed
- Azure OpenAI reasoning deployments answer through the gateway. An
OpenAI-compatible gateway pointed at an Azure OpenAI v1 endpoint sent
max_tokensandtemperature, and a reasoning deployment (gpt-5 on, the o-series) refused the request with a 400. A deployment named after its model now getsmax_completion_tokensand no sampling controls from the first request, as onapi.openai.com. A deployment with a name of its own, or any other gateway that refusesmax_tokensthe same way, is asked once more withmax_completion_tokensand keeps that for the endpoint and model. GPT-4 deployments and other gateways keep the wire they had. Reported in #1304.
[1.46.4] — 2026-10-30
Fixed
- Google Health connects on every profile of an instance again. The connect stored the same placeholder as the Google account's id for every account, so since 1.44.0 a second profile on the same instance was turned away with "This Google account is already connected to another HealthLog profile", also when it only reconnected. The connect now stores the account's real id. An update clears the stored placeholder, and a profile that was turned away connects with its next attempt. Instances with one Google Health connection were not affected.
[1.46.3] — 2026-10-29
Fixed
- Codex picks from what the account is offered. OpenAI retires Codex
models for ChatGPT sign-in in steps, and every retirement left Codex
refusing every request until a release changed the built-in list. Each
sign-in now reads its account's model list, the way the Codex CLI does,
and chooses from it in HealthLog's order of preference. Whether a model
reads images comes from the same list. If the list can't be read, the
built-in order applies, and
CODEX_MODELandCODEX_MODEL_FALLBACK_CHAINkeep their meaning. The working model is remembered per sign-in instead of in one slot for the whole server, so two accounts on different plans, or the shared central connection, no longer overwrite each other, and a sign-in reconnected to another account starts fresh. (#1287) - A first Google Health sync no longer stalls on the sleep history. Each night's clean-up of re-scored sleep carried every segment id of the whole fetch, so a backfill of a few years sent one query per night with tens of thousands of values, over the database's limit, and kept the server busy building them for many minutes before each one failed with a warning. Each night now protects only the segments inside it, which is all the clean-up can reach. (#1195)
- A gateway that refuses the reasoning field is asked without it for a day. The lesson held for an hour, and every expiry cost one refused request, a few a day on a gateway that runs the briefings unattended. Lessons read from a single reply keep the hour. (#1279)
Added
- Lab values on the timeline. The values menu lists the lab markers the record has, and a chosen marker draws its readings as their own points, never averaged, with the reference range behind them. A reading outside the range is drawn as a triangle and named in words. A tap shows the day, the value, the range and the report it came from. Readings in a unit that cannot be converted are left out and the timeline says how many. Up to 24 markers at once; the choice is remembered like the other value lines. (#1299)
- Restoring the backup of an erased account. An upload whose account
no longer exists used to be refused with nowhere to go. Admin, Backups
now offers to restore it into a new account created for it, or into an
existing account that holds no records beyond what sign-up and setup
leave, confirmed by typing its username. Admin accounts are not offered.
Nothing names the erased account afterwards. A new account has no
password until an admin sets one with Reset password. Restoring the same
file into a second account is refused with an explanation instead of a
generic failure. The runbook in
docs/ops/backup-restore.mdhas the details.
Thanks
@haritowa for taking the Codex retirements apart in #1287 and proposing the live list. @mills1975 for the timeline idea in #1299. @Tio61 for three days of request counts that showed what the hour still cost (#1279).
[1.46.2] — 2026-10-29
Fixed
- Large accounts get their summaries. On an account with tens of
millions of heart-rate readings the job that rebuilds the summary tier
after an upgrade ran one aggregate over five years under the database's
60 second limit, never finished, and left the dashboard cards reading the
raw table until they timed out too. It now works through the history one
calendar year at a time, every step gets five minutes of its own, and a
run that is out of time hands the remaining years to a follow-up instead
of starting over. Finding the accounts that need the rebuild no longer
reads every reading. If you raised
DATABASE_STATEMENT_TIMEOUT_MSas a workaround, you can set it back. (#1298) - Lab values from a document land on the day of the report. A report usually prints the collection date once, in its header, so confirming it filed every undated value under the day you confirmed it. An undated value now takes the report date, or the filing date when the report names none, and an impossible date such as 30 February counts as no date. Values filed under the wrong day before are moved once after the update: only those that came from a confirmed document, had no date of their own, still sit at the moment they were confirmed, and whose document names a different day. The server log says how many moved. (#1293)
- Deleting a blood pressure reading deletes all of it. Deleting the systolic or the diastolic value used to leave the other half and the pulse behind. It now removes the whole reading, Undo brings the whole reading back, and deleting only the pulse still removes only the pulse.
- Daily briefings work with models that always reason. Background work asks for medium reasoning at most, and a model that cannot be told to stop thinking used all of that room and returned nothing. Once HealthLog has seen a model answer with nothing but its reasoning, background work gives it the room the Coach gets on High. (#1276)
- Single sign-on accounts can confirm it's you. Creating a token, connecting an assistant or resetting a password asks for a fresh confirmation, and an account with no password, passkey or second factor had nothing to offer. It now confirms through its identity provider with a fresh login, accepted only for the linked identity and a sign-in from the last few minutes. The same confirmation can add a passkey. (#1296)
- A personal baseline no longer depends on the wall clock. Reads that ask for a baseline as of a given moment saw a different window through the summary tier than through the raw readings.
- Signed out, the language switch keeps your choice in the browser instead of asking the server to store it.
- A new account is greeted with a welcome instead of "welcome back".
- The Coach chat, the document chat and the profile settings accept every language the app ships.
- The French translation says "vous" throughout.
Added
- Admins hear about a background job that keeps failing. After three failed runs in a row every admin gets a push on their system alert channels, a reminder at most once a day while it keeps failing, and one message when it works again. Retries of one run count once, and a job that runs per account counts per account, so one account in trouble is reported once and not every hour. (#1298)
- Heart rate from Google Health and Oura is folded like Apple Health's. Readings older than 90 days become one value per hour, and a later sync does not bring the folded readings back. The first fold of a large history works through the backlog over a few nights and waits until that account's summaries are rebuilt; afterwards the index rebuild under Admin, System status gives the space back. (#1297)
- Admins can delete another account from the users list, through the same grace window as deleting your own. Your own account, the last admin and managed profiles are excluded.
- Crisis lines for the Netherlands and Belgium on the support card.
- A section on large accounts in the self-hosting guide.
- Tests that restore every release's own backup file, restore the backup of an erased account, and hold the header height in every app language.
Thanks
@Cnote43 for #1297 and #1298, written up so precisely that the fix mostly followed the report. @haritowa for finding, fixing and live-testing the lab date bug (#1293) and for the Codex model checks and tests (#1288). @Tio61 for testing every step of #1276. @sposschip for #1296.
[1.46.1] — 2026-10-26
Fixed
- Charts on a phone are their old size again. 1.46 moved the labels for your usual level and for a target zone out of the chart and beside it. On a phone that room took about a third of the chart's width, and the legend took more of its height. On a narrow screen the labels sit inside the chart again, as they did before; a tablet or a wide window keeps them beside it.
- The date axis shows more than one date on a phone. A 90-day chart is labelled with the first of each month again, and the All range with the years.
- The value axis writes whole numbers when its steps are whole: 57, 58, 59 bpm rather than 57.0, 58.0, 59.0.
- The All range shows your whole history. Long ranges are read from summaries the server keeps for the last five years, and readings older than that did not appear: a weight history going back to 2015 started in late 2021. The older readings are now read directly and lead the chart.
[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
backupsvolume, 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(orlatest --from-bucketafter 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-upgradestep copies it into a newpostgres-datavolume, 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 oldpgdatavolume 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 29 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
nullor"". 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.
-
A directory setting below
/procno longer freezes the server. Node's way of creating a folder with its parents loops forever there on Linux, so aDB_BACKUP_DIRorGEOLITE2_DIRpointing below it stopped the process. Such a directory now reads as not writable. -
The last hour of a long DST day keeps its doses. On the day the clocks go back, the list of today's doses that the apps offer from ended an hour early, so a late evening dose was missing from it; on the day they go forward it reached an hour into tomorrow. It now spans the real local day, as the dashboard already did.
-
A dose logged for a two-day plan lands on its slot in the week the clocks go back. A plan with doses on two neighbouring weekdays, started shortly before the change, could file a later dose as unscheduled, and logging it again gave back the previous one. Whether a plan counts as daily or weekly no longer depends on the stretch of days it is read through.
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
/apigo 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
measurementsconcurrently; 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.mdsays what to look for and when to roll back. /api/settings/preview-featuresis 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_CHAINonly 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_settingsand carries an existing confirmation over; as with 0485, do not run the previous image beside the new schema. - A new
backupsvolume 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 underdatabase/<your host>/, so two servers sharing a bucket never mix them up; adddatabase/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_MIGRATEandACCOUNT_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_CIDRSto that address; otherwise every user shares one sign-in rate limit.TRUSTED_PROXY_CIDRSalso 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 sureAPP_URLis 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=trueaccepts signed requests only.DEPLOY_WEBHOOK_REQUIRE_TIMESTAMPkeeps 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 -dwith 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
- Brazilian Portuguese. The whole app, the Coach and the reports are available in pt-BR, with Brazil's crisis lines (CVV 188, SAMU 192) next to the international ones. Portuguese readers outside Brazil see 112 and Portugal's SNS 24 and SOS Voz Amiga as well.
- Translate in the browser. Translators can edit any language on the web with their GitHub account and send the change as a pull request; the contributing guide links the editor and explains what reviewers check.
- Dengue and tuberculosis in the vaccine catalogue. Dengue is listed with the travel vaccines (two doses three months apart) and tuberculosis with the childhood ones (one dose at birth); neither has a booster. Each cites its WHO position paper, and searching "bcg" finds tuberculosis.
- The Coach knows your whole lab history. Asked about a marker such as LDL or PSA, it now sees your recent values and a yearly summary going back ten years, instead of stopping at twelve months. Other readings keep the one-year window. (#1263)
Changed
- Faster. On an account with ten years of data the dashboard loads in well under half the time, a ten-year chart about five times faster and a one-year chart about three times faster. The values are the same: the new reads are checked against the old ones across time zones, daylight saving changes and multiple sources.
- Steadier start and less memory. The app no longer freezes for most of a second while it starts, holds only the language you use in memory, and pauses heavy background work when the container gets close to its memory limit instead of being restarted. Stopping the container lets running jobs finish.
- The questionnaires follow the validated originals. PHQ-9, GAD-7, WHO-5 and the sleep questionnaire now share one flow: the question's time frame is always shown, answers are a proper choice list you can use with the keyboard, and a review step lets you check every answer before it is saved. The result explains the score in plain words and whether it moved meaningfully since last time. The German, Spanish, Italian, Polish and French wording now matches the published validated versions word for word; where no validated translation exists, the original English is shown rather than an unvalidated one.
- Re-sent Apple Health data no longer counts as new. When the iPhone sends values the server already has, nothing is rewritten and the Apple Health card no longer claims fresh data arrived; it says when data was last re-sent without anything new. (#1259)
- Fonts are now part of the app instead of being fetched from Google when an image is built.
Fixed
- A workout synced by two services counts once everywhere, including when the two copies fall on either side of a five-minute mark, and opening either copy shows the same session.
- A backup restored for disaster recovery keeps the original creation dates of medications, courses, ingredients, nutrient days and mood and symptom links, and keeps sealed labels and notes sealed. Deleted family history entries are part of that backup too.
- The Coach refuses more ways of telling you to change a dose, such as halving it or taking "the same as before" when that is not your plan, in every language. Several of its safety checks never matched words with accents; they do now.
- A Strava connection whose refresh token expired now asks you to reconnect instead of failing quietly.
- Ending a session from the session list records correctly whether it was your own.
- Values that round to zero no longer show as "-0".
- Clearing a filter on the documents, measurements or mood pages, closing a document or zooming the timeline no longer brings the old address back a moment later.
- The nightly measurement maintenance no longer reports every run as failed: it recorded the table's bloat as a decimal where only whole numbers were accepted, although the maintenance itself had run. (#1271)
- Mood dimensions logged between local and UTC midnight count for today again instead of going missing until the next day.
- Sheets on a phone open without a slide when your device asks for reduced motion, and every control's accessible name now matches its visible label.
- A large import from Apple Health, Health Connect, Withings or Oura refreshes the charts that depend on it straight away.
Upgrade notes
- Migration 0471 adds one column for Apple Health bookkeeping.
- Single-container installs need nothing else. If you run the web and
worker roles in separate containers, start the worker first: the job
queue updates its schema on start, and a web-only process waits for it.
See
docs/ops/deploy.md. - The supported memory floor stays at 1536 MB for the app container.
Thanks
Thanks to @dareoon for the Brazilian Portuguese translation (#1251), to @asmala for adding dengue and tuberculosis to the vaccine catalogue (#1266) and for asking the Coach to look further back in the lab history (#1263), and to @mills1975 for spotting the failing maintenance job (#1271).
[1.44.2] — 2026-10-22
Changed
- Sheets that come up from the bottom can be pulled. On a phone every sheet now follows your finger: pull it down to close it, flick it away, or let go early and it springs back. Content that is scrolled scrolls first, sideways gestures such as sliders keep working, and the X, Escape and a tap outside still close it. The day view opens at half height as before and pulls up to almost the full screen. The page behind no longer reloads when you pull inside a sheet, and closing a sheet puts the focus back where you were.
Fixed
- Your own targets shape the dashboard charts. Blood pressure, resting pulse and body fat drew the standard band on the dashboard even when you had set your own targets; they now draw yours, and the tile colours follow them. The oxygen saturation page uses your target too. Without your own targets nothing changes. (#1261)
- Local thinking models finish the briefing, and regenerate waits for them. A thinking model such as Gemma 4 on Ollama could spend a long briefing thinking and hand back no answer, which was logged as a connection problem and shown as a timeout. A reply with nothing but thinking is now asked once more without thinking, and that model is asked that way for the next hour, through the Local provider and an OpenAI-compatible gateway alike. The regenerate button waits as long as your AI response timeout allows instead of giving up after 45 seconds, and so does saving your notes about yourself; a local model allowed more than five minutes is no longer cut off at five. A failed call is logged with what actually went wrong, such as a refused or dropped connection, an empty reply or a timeout, and the briefing only suggests a longer timeout when time really ran out. The provider's reasoning setting now says that the daily briefing thinks at its own depth. (#1267)
Thanks
Thanks to @mills1975 for spotting that the targets never reached the chart (#1261).
Thanks to @Tio61 for the precise report that traced the regenerate timeout to its line and ruled out the transport with their own requests from inside the container (#1267).
[1.44.1] — 2026-10-22
Security
- Next.js 16.4.2. The framework fixed two critical advisories that
allow unauthenticated remote code execution, one in
use cachefunctions that return typed arrays (GHSA-673q-mjm6-r5xr) and one in the image optimizer when it handles SVG (GHSA-2464-fhr4-w9xw), and a high-severity denial of service through Server Actions (GHSA-p3m5-59mp-jv5r). HealthLog uses neitheruse cachenor the image component, and it defines no Server Actions and refuses requests that call one. Every instance runs the same framework, though, so it updates anyway. - The image optimizer is off. Nothing in HealthLog renders through it,
so
/_next/imagenow answers 404 instead of processing whatever image a request names. Icons and logos load as before. - nodemailer 10.1.7 fixes several advisories in how email addresses are parsed (GHSA-8c8m-p3m8-cjg8 among them).
Fixed
[and]move the open day on every press. Pressed quickly in a row, one press could be lost and the day ended one short. A step now starts from the day in the address bar and waits until the previous one is shown.- Back closes a document opened from the vault. Pressed right after opening a document, Back could leave its sheet open. The sheet now follows the address bar directly.
Upgrade notes
- None. Pull the new image and restart.
[1.44.0] — 2026-10-22
Added
- Import lab results and medications from a spreadsheet. Settings > Export & Import has a card that reads the head of a CSV file, suggests a column for each field from its header, and lets you correct the separator, decimal mark, date order and time zone. A check run lists every refused row with its line and reason before anything is written, and importing the same file again only finds duplicates. Templates for both kinds download from the card. (#1215)
- An activity calendar on the workouts page: a week or month of steps, active minutes, workouts by sport and training load, each day measured against your own usual range and each period against the ones before. Weekly and monthly goals are optional. Today counts as in progress until it is over.
- Fitness, fatigue and form above the workout list, from the training impulse HealthLog already stores. A day with activity data but no training counts as rest; a day without any data is left out instead of counted, and the card says how current the figures are.
- Where a value comes from. Each metric page links to a view that lists every source with readings for a day, its place in your source priority and which one the charts use, with a side-by-side comparison of two sources over 30, 90 or 365 days.
- Lab trends. A marker with at least three readings over a year shows its slope, and when the straight line would leave the reference range within three years. A series whose newest reading is more than a year old is shown without a projection.
- The change at a medication start, as a level and a trend change with intervals, next to the existing before and after comparison. Days with missed doses, pauses and other medications that changed in the window are accounted for, and the view says when the data are too thin.
- More in the doctor report: sleep duration, HRV as SDNN and RMSSD, daily steps, a workout summary per sport and the medication effect, each only when its module is on. The medication effect is included only when the medication list is shared too. The report names the window it analysed.
- More correlations: water, caffeine, micronutrients, alcohol days, workout minutes, training impulse, activity ring minutes, State of Mind and room climate against that night's sleep. State of Mind, mindful minutes and alcohol days stay on the correlations page and are never handed to the Coach or the narrative cards.
- Level shifts in thirteen vitals over 180 days instead of five over 90. Slow drifts and plateaus no longer count as a shift.
- Mental health screeners show whether a change reaches the threshold that counts as meaningful.
- Apple Health: exercise, stand and mindful minutes, stand hours, State of Mind and AFib burden. Workout GPS tracks are kept only when you turn that on (Settings > Data & privacy); tracks already stored stay, and accounts that already kept tracks start with it on.
- Oura: workouts, intraday heart rate and a full history backfill that resumes after a restart.
- Indoor climate (CO2, temperature, humidity) through the measurement API, with a Home Assistant blueprint.
- A guardian can connect a managed profile's integrations (Withings, WHOOP, Oura, Polar, Strava, Google Health), and each card says whose record it connects for.
- FHIR: Observations as a time series by
dateandcode, with RMSSD and workouts coded.
Changed
- Settings are sorted into nine groups: Account, Tracking, Arrangement, Notifications, Health profile, AI, Connections, Data and System. Every old address still works and forwards to the new place. On a phone one button names the current section and opens the list. Each card saves the same way and asks once before anything destructive, and German wording uses one word per thing.
- All OAuth integrations refresh their tokens the same way, so a refresh that races another one no longer signs you out of Strava, Google Health or Withings. One provider account can be linked to only one record.
- Imports from Apple Health insert large batches faster, and the health check answers "starting" while the background jobs come up, so a slow boot no longer marks the container unhealthy.
- The metric pages load about 90 KB less JavaScript on first visit.
Fixed
- Oura no longer imports the equivalent walking distance as distance.
- A workout synced from two sources counts once in strain, the briefing and the day view.
- The Coach's provider and model switch is back, and notification channel switches act on the first tap again.
- Jobs queued during start-up wait for the queue instead of being dropped.
Upgrade notes
- 1536 MB is the supported memory floor for the app container. This is
the existing default of
APP_MEMORY_LIMIT; if you lowered it, raise it back. Below the floor the app still starts, but the boot log, the support report and Admin > System status say so. Plan for 3 GB of RAM on the host. - Migrations 0424, 0430 to 0434, 0440 to 0442 and 0470 run on start. They add columns and settings, turn the GPS track setting on for accounts that already have tracks, and drop a cached copy of the AI insights text, which is rebuilt on the next run. Workouts with a track but no elevation get their climb filled in by a background pass after the first start.
- If you use Fitbit, connect Google Health before 30 October 2026, when Google turns the old Fitbit API off.
Thanks
Thanks to @Cnote43 for the case for a spreadsheet import of twenty years of lab results (#1215), and to @alondin and @haritowa for staying with the Oura connection until it held (#1204).
[1.43.11] — 2026-10-21
Fixed
- Long lab reports scan completely. A report is now read page by page instead of in one request, so a hospital report with several panels comes back whole, up to 20 pages and 400 readings. Where the AI provider stops before the end of a page, the readings it already returned are kept and the review screen names the pages to check. A scan that hits your daily AI budget stops there and says so, and a reading whose name or unit is too long is marked on its row instead of blocking the save. Models that think before answering no longer use up the room meant for the readings. When a PDF carries a text layer, it helps the model tell similar characters apart, but the page image always wins. (#1257)
- An upcoming appointment opens itself. Tapping it on the dashboard, in Today or in its reminder now opens that visit instead of the check-ups list. The visit preparation and the doctor report offered for an appointment start at your last visit with the same practice, otherwise the same specialty or kind of visit, and say which; with no match they cover the last 90 days. (#1258)
- Withings sleep stages arrive the next morning, not weeks later. One failing night no longer loses the other nights of a sleep sync. Steps that Health Mate copies in from another app are skipped only when that other app already sends them to HealthLog for the same day.
- Deleting a WHOOP recovery no longer removes that night's sleep.
- Polar no longer asks you to reconnect when your device simply lacks a data type, and Strava keeps calories and progress when its rate limit hits.
- Fitbit and Google Health HRV is filed as RMSSD, apart from the SDNN that Apple Watch and Withings report, and takes part in correlations and illness views. Readiness, HRV balance and stress keep using SDNN sources only; the dashboard tile, the briefing and the doctor report show RMSSD, labelled, when you have no SDNN. Restoring a backup from before this release keeps that split.
- The notification settings show the mood reminder as it is delivered.
- Settings behave the same everywhere: delete, revoke and remove confirmations are red again, ending a session's kept access asks first, cards no longer stay stuck saving after a network failure, and quiet actions (passkeys, security keys, disconnecting an integration, share links, injection sites, local OCR, check-up reminders) tell you how they went.
- Readings entered through Telegram, the assistant API or Health Connect now rank properly against device estimates in source priority.
Changed
- Fitbit stops on 30 October 2026, when Google turns its old Fitbit API off. From then HealthLog no longer polls Fitbit, keeps everything already imported, and the Fitbit card explains how to move to the Google Health integration, which reads Fitbit and Pixel data. A day recorded by both counts once. Accounts that never used Fitbit no longer see the card after that date.
- Saved AI keys can be removed, the display name can be changed in your profile, and emergency fields can go back to "Not recorded". Setting or removing an AI key is recorded in the audit log.
Security
- Hardened the markers that keep document and user text apart from instructions sent to an AI model. Nested markers could re-form after cleaning; cleaning now repeats until nothing changes. This covers lab scans, the Coach and the MCP server.
Upgrade notes
- Migrations 0420 and 0421 run on start. 0420 refiles Fitbit and Google Health HRV as RMSSD and clears their rollups, which are rebuilt in the background on the first start; expect a few minutes of extra work then. 0421 adds one column for Withings sleep bookkeeping.
- If you use Fitbit, connect Google Health before 30 October. Tokens cannot be carried over, so it is a fresh sign-in.
Thanks
Thanks to @asmala for pinning down both limits the lab scan ran into (#1257), and to @mills1975 for the appointment link and the report window idea (#1258).
[1.43.10] — 2026-10-21
Changed
- Range and view switches look the way the chart tabs used to. Since 1.43.8 every row of mutually exclusive choices shares one control, and it drew them on a grey track with the chosen option raised. Above a chart that read heavier than the chart itself. The shared control now draws what the dashboard's range tabs always had: no track, the chosen option filled in the highlight colour. The range rows above charts are back to their smaller type. Keyboard navigation and the touch size the shared control brought stay as they are.
[1.43.9] — 2026-10-21
Fixed
- Oura's resilience level syncs. Resilience sits behind Oura's stress permission, which HealthLog did not ask for, so it was refused even with every other permission granted. HealthLog now asks for it. When Oura refuses a single data type for a missing permission, the Oura card names exactly that type, the refusal no longer triggers a token refresh and never counts toward the reconnect warning. After updating, enable Stress for your app in Oura's developer portal and connect Oura once more. (#1204)
- Connecting an integration survives a repeated return link. Some browsers open the provider's return link twice. The second visit spent the same one-time code again, and Oura answered by revoking the connection the first visit had just made. Every connect flow (Oura, Polar, Strava, WHOOP, Withings, Fitbit, Google Health) now claims the return link once; a second visit waits for the first and lands on the same page. (#1204)
- Two syncs no longer spend the same Oura sign-in. Syncs that renewed the Oura sign-in at the same moment each used the one-time renewal token, which Oura can treat as reuse. Renewals now take turns.
- Apple Health rows show the newest reading. A type HealthLog also writes back to Apple Health, such as weight entered here, showed the age of the last sample Apple Health sent while the status above it already counted the newer entry. The row now shows whichever is newer. (#1247)
- An expired ChatGPT sign-in waits for you. When the sign-in is refused as expired, the connection is marked as needing a reconnect after the first refusal and stays parked until you sign in again, instead of being retried on every insight run. Signing in again puts it back in front at once. Two renewals at the same moment take turns, so a working sign-in is never parked by mistake.
- The demo instance no longer shows the setup checklist, whose steps cannot be done in a read-only account.
Changed
- Faster syncs. Withings syncs, full syncs included, write a batch in one pass and skip readings that did not change, so a sync that took half a minute finishes in about a second. Every connected service refreshes its daily charts in one pass after a sync instead of one day at a time.
- Faster dashboard and insights. The all-time reading counts on the dashboard and the all-time figures for weight, blood pressure and pulse are read from the daily summaries instead of every reading, which makes them many times faster for accounts with a lot of watch data. The all-time figures fall back to the readings wherever the daily summaries do not match them. The integrations settings card answers sooner on the first visit.
- Lighter server. Each signed-in request loads only the small account fields it needs, so the profile picture and stored credentials are no longer read into memory on every call. The server loads each page's code when it is first used rather than all of it at start, so it starts with less than half the memory and without a pause in its first minute; a page's first visit after a restart takes a few milliseconds longer.
- The hourly sign-in location lookup no longer retries addresses it cannot place on every run; it tries such an address again after a week.
- Requests that probe for server actions, which this app does not use, are turned away with a 404 and counted in one warning line instead of filling the error log.
- The periodic memory log line breaks the heap down by area, so a large heap can be explained from the logs.
Security
- The
honodependency floor is raised to 4.13.11 (GHSA-5r4p-p66f-jhc7).
Upgrade notes
- Three additive migrations run at start (0417 to 0419). The first start after the upgrade refills the daily summaries once in the background; until that finishes, the all-time figures are read from the readings as before.
- Oura: enable the Stress permission for your app in Oura's developer portal, then connect Oura once more and leave every permission ticked.
Thanks
@haritowa for tracing the Oura resilience refusal to the missing permission and for timing the repeated return link, @alondin for staying with #1204 through every round, and @mills1975 for #1247.
[1.43.8] — 2026-10-20
Fixed
- Numbers and units read in your language. Decimals follow your language everywhere: fever, cycle and mood figures, custom metrics, nutrient totals, chart tooltips and the sentences the server writes show 37,5 instead of 37.5 where your language uses a comma. Units such as breaths per minute, years and minutes read in the app's language instead of the stored English token, and the cycle and mood comparison boards use the same unit names as the rest of the app (bpm, not S/min). File sizes read the same in the document vault and the admin area.
- Titles wrap instead of being cut. Tile and card titles wrap onto a second line with hyphenation instead of ending in an ellipsis, with room between the lines, and tiles in a grid keep their values on one line when a long title wraps. Dashboard trend tiles keep their full label on up to two lines. Long health-score area names, mood tags, document titles, lab names and token names show in full, and a long field label keeps its colon on the last word.
- One look for the same control. Range, filter and choice toggles (time ranges, card or table view, recent or all) look and behave the same everywhere. Small text links and buttons such as "Open schedule", "Select all" and "View all" are easy to tap on a phone and show a focus ring. Confidence, score and range chips share one look, and medium confidence has the same colour on every card. Empty lists and charts say "nothing here yet" the same way across the app. Page titles on the insights pages, the medication detail and the check-in screens match every other page title, and compact cards keep their tight spacing on wide screens.
- The blood-pressure target card labels its systolic bar as well as its diastolic one, and the sleep-stage legend lists only the stages the visible nights contain.
- The Dawarich card shows one action row; set your home place from the card menu.
- Every published GET response is checked against its OpenAPI schema.
Thirteen operations that disagreed now publish what they send, the single
mood entry and custom metric reads carry the same fields as their lists, and
the document chat list declares its
fenced,attachmentsanddocumentTitlefields. - Lists say when they were cut. Practitioners, vaccinations and encounters
report a cut list and accept
offset, and the web reads them to the end.hasMoreon symptom events is no longer true for a page that is exactly full. MCP tools say when a list was cut and answer a failure with a stable code instead of internal error text. - A deleted practitioner stays named where it still matters. Visits and doses that still show a practitioner removed from the address book keep the name, in the app, the MCP read and appointment reminders, and the portable export carries such a practitioner along so the visit keeps its reference after a restore.
- Renouncing a Guardian role while that Guardian's account is being deleted no longer answers "add another Guardian".
Changed
- Deleted records are removed for good after 75 days. Deleted lab results, visits, practitioners, allergies, vaccinations, conditions, life events, cycles and the other soft-deleted records can be restored for 75 days and are then purged by a nightly job. A deleted record that a live record still points to is kept for as long as it does.
- Expired sessions, tokens, trusted devices and sign-in challenges are cleaned up nightly.
- Writes have one per-account limit of 300 a minute, edits and deletes included; bulk sync endpoints and the chat routes keep their own limits. A temporary database problem with this limit lets the write through instead of failing every save; the sign-in limits stay strict.
- Cheaper writes. Deleting a medication or an account no longer scans whole tables for related rows, redundant indexes are gone, and writes to measurements are cheaper. An unused measurement index is dropped at upgrade when that can happen at once and otherwise by the nightly maintenance, so upgrading no longer waits on a long-running database read. The unused per-day medication compliance rollup table is removed; nothing changes in what you see.
- Migrations are steadier. They run in a UTC session regardless of the
database's default time zone, so a migration that mixes
timestampandtimestamptzno longer writes shifted values on a non-UTC database. A migration marked safe to re-run that loses a deadlock is retried like one that timed out on a lock, and the container health check allows up to ten minutes for a slow migration before reporting the container unhealthy. /api/feature-flagsannounces its deprecation and its sunset on 2027-03-31.- The document vault and the Coach load less JavaScript.
[1.43.7] — 2026-10-20
Added
- Dutch. HealthLog speaks Dutch now, the eighth language, translated and contributed by @robdegraaf (#1246). Friends in the Netherlands are always welcome here. Cheers with a Grolsch! 🍺
- Readings you enter here count for Apple Health freshness. Weight, blood
pressure and glucose are no longer called quiet while you keep entering them
in HealthLog for the phone to pick up, and the card shows when you last did
(
lastEnteredHereAt, Refs #1247).
Fixed
- Error states say what happened. When a session has ended, a card offers to sign in again instead of a retry that cannot work; when there were too many requests just now, it says so without a pointless retry; a section you are not allowed to see on a shared record stays quiet. Some thirty cards that used to show a bare red line, a "still learning" note, an endless skeleton or nothing at all when their data failed to load now say so and offer a retry, and charts follow the same rule. Every read failure goes through the same row, card and chart state, deciding by the error code first and the status second.
- A short database outage no longer signs you out. A session that could not
be checked because the database was unreachable, out of connections or
otherwise failing answers 503 (
auth.session.unavailable) and the client keeps its credential and tries again. A malformed session cookie is treated as signed out (401) without a database lookup. - A nap is not a night, but it is sleep. A day whose only sleep is a short
daytime nap has no main night (
main: nullon/api/sleep/night). A day's sleep in the list, the export and the correlations counts its naps, the same total the chart and the sleep score show; the main night alone ismainAsleepMinutes. The night view opened without a date shows the last night with main sleep. The sleep list, the night view, the chart and the sleep score name the same day for every night. A REM segment of zero minutes no longer drops the night around it. - Deleted entries stay deleted for the AI. Deleted mood entries and readings no longer shape the status texts written about you, and the general overview looks at every metric again instead of the newest few thousand rows of the densest one.
- Days logged for a chronic condition count in the symptom correlations.
- Inviting someone whose name or e-mail matches no account says so
(
sharing.invite.no_account) and is never confused with a profile you no longer look after. - Old nights open, and "All" means all. The night view answers any date, however old, and the sleep series reaches back to the first night. The sleep status read the last few weeks of stages; it now reads the last year of nights.
- Days are your days. The dashboard streak and the day a dose counts for were placed on the wrong side of midnight for readings close to it; both follow the profile timezone now. Skipping a dose from Telegram snoozes until the end of your own day, not the server's.
- One number for one thing. The spread of a series is the same in the app,
the API, MCP and AI tools. Correlations on the insights overview and in AI
data appear only when they are statistically sound (at least 20 pairs,
p < 0.05), as everywhere else; below that the value is
null. A chronic condition no longer fills the symptom series with zeros. - Medication adherence on the cadence chips counts pauses, courses and missed doses like the detail page; a week's pause read 77 % instead of 100 %.
- A workout Apple Health sends twice is stored once, and every workout reader uses the same source order.
- Provider histories do not end early without saying so. A WHOOP, Oura, Withings, Fitbit or Strava history that stops at its page limit is no longer marked as fully imported. A Strava history of more than about 4,000 activities imports in stages from where the last stage stopped, one stage per rate window, instead of re-reading the newest activities after every restart. WHOOP and Oura no longer read the first page twice when a list ends with an empty page token. Imported values from every provider and from Health Connect are rounded the same way, with halves rounded up (a weight of 72.345 kg now stores as 72.35, not 72.34).
- A long Google Health backfill keeps its place. Only attempts that end at the same edge count toward the final end, and a restart during the 24-hour wait no longer starts a second chain.
- The recovery page says in one sentence why it is still empty, decided on
the server (
recoveryEmpty), and always links to the integrations. - Lists that stop at a limit say so (
meta.truncated,meta.limit) instead of passing the oldest loaded entry off as the beginning, and the web shows a note under each such list. The activity feed keeps itsdata.truncatedfor existing clients; it is deprecated in favour ofmeta.truncated. - Reconnecting Strava, even to a different account, reads that account's full history again instead of continuing where the previous one stopped.
- The day view's "highest since" notes pick up a new or corrected reading right away.
- Old days and long histories are read whole. The sleep summary covers the whole history, the pulse assessment reads at least a month of pulse instead of a few hours of a dense stream, and the mood fallback reads the same five years as the rollup path.
- The coach's daily limit on follow-up questions resets at your own midnight, and the weather and air-quality refresh includes today east of UTC.
- Deleting a large account no longer aborts after five seconds.
- Workout dates follow your date format in the list, the detail and the
dashboard tile. "Not enough nights yet" names the number actually needed.
groupBy=daytogether withvalueMin/valueMaxanswers 422 instead of ignoring the filter.
Changed
- Faster reads. The latest value per type is read through one bounded query instead of a sort over every reading (about 60 ms down to 1 to 2 ms on a large account), the dashboard summaries share one cache cell, and paging through the day view computes the two-year comparison once.
- One heavy job at a time per process. Imports, restores, backups, rollup
backfills, provider backfills and the nightly runs queue behind each other
instead of stacking up in one heap. A job that finds the lane busy goes back
to the queue and starts once it is free, so a backup waiting behind a large
import no longer runs out of time while it waits. A long Google Health
backfill makes room at its next slice boundary for a job waiting behind it,
and a job that waits more than two hours is reported as a warning
(
job.heavy_lane.waiting_long). Building rollups for a large account no longer runs a second time when it takes longer than fifteen minutes. The six per-metric status crons are gone (the nightly batch already covers them), and a queue fed only by its cron polls every 30 seconds. measurementsstays small. An update that changes nothing keeps its timestamps, so it stays on its page and is not sent to the phone again; the table leaves room on each page and autovacuum starts at 5 % dead rows. A nightly check warns when dead rows or bloat grow past that (seedocs/ops/measurement-maintenance.md).- Migrations that wait for a lock no longer hold writers back or stop the
boot.
prisma migrate deployruns a migration file statement by statement, not in one transaction, so the lock-sensitive part of migration 0412 runs as one block that gives up after ten seconds when another process holdsmeasurements. The container then retries a migration marked safe to re-run, with a growing pause, instead of stopping at boot; any other migration failure still stops the boot. See "A migration that timed out on a lock" indocs/ops/deploy.md. - The day view's "highest since" notes cache fixed 90-day blocks and use less memory when paging back through older days.
- An insights card whose refresh keeps failing stops polling after its usual number of attempts.
- Failed provider syncs and refused uploads are logged as signals instead of audit entries; a provider's error text no longer lands in the audit log in plain text.
- The cached AI health report is encrypted at rest, existing rows included.
- Every API error carries a stable
meta.errorCode; a route without its own code gets anhttp.*code from the status. - Logs group requests by route template (
/api/medications/[id]) instead of by concrete path. - Failed inputs no longer fill the audit log; only attempts by someone with a grant appear there.
- The browser bundle no longer pulls in server modules, and a test keeps it that way.
- The Coach stops polling a document that will never be indexed.
- On an out-of-memory crash the process writes a diagnostic report to stderr
(
--report-on-fatalerror, without environment or network details), and the heap monitor adds a 95 % tier.
[1.43.6] — 2026-10-19
Fixed
- A long measurement history no longer loads in one response. The list for
cumulative types (steps, active energy and similar) loaded every day since
the first reading in one request and drew one table row per day; past 5000
raw rows the oldest days were cut off without a word. The server now pages
over days with a correct total, folds the days in SQL instead of reading raw
rows into the process, and the list shows 25 days per page. The sleep list
pages over nights the same way and no longer stops at one year; a night older
than a year opens its detail. For
groupBy=day,limitcounts days. Thepartialflag and thewindowFromfield are gone because nothing is cut off any more (Refs #1245). - A Google Health backfill that keeps moving no longer stops at a page
ceiling. The ceiling counted pages across every attempt and was never
reset, so a dense type such as heart rate reached it after roughly two and a
half years and every later slice stopped at the same wall. A walk now stops
only when it has not moved back in time for 20 pages in a row, or when it
reaches the horizon (
GOOGLE_HEALTH_BACKFILL_HORIZON_YEARS, 20 by default). A type that ends says where its history ends and why, on the integration card, instead of showing "loading" forever. A first end without progress is only provisional: the type keeps its place and gets a second attempt a day later before the end is final (Refs #1220). - Consistency in the sleep score no longer reads 0 after a day without a
night. A day on which the ring recorded only a nap was placed on the clock
by that nap, hours away from the other nights, and one such day in thirty
pushed the spread past the scale. A day whose only sleep is a short daytime
nap now leaves the rhythm (the nap still counts toward that day's sleep); a
short night stays on the clock. Every reader of the sleep midpoint follows the
same rule. When there are not enough nights to judge a rhythm, or the latest
day had only a nap, the row says so instead of showing a number. The sleep
score value, including
rhythmNightsandgap, is now published in the OpenAPI schema (Refs #1204). - An empty recovery page no longer guesses. It asks for a reconnect only when a missing permission is recorded, and otherwise says that no level has arrived yet and points to the Oura card (Refs #1204).
- The MCP endpoint answers GET and DELETE with 405 and
Allow: POST. A client that probed for a server-sent event stream was retried in a loop, which produced hundreds of thousands of requests per week. - No empty frame under the next appointment. The "since the last visit" card drew its border before knowing whether it had anything to show. Optional tiles on the insights pages no longer flash a skeleton that collapses to nothing, and a failed read on them now says so with a retry instead of vanishing (Refs #1241).
- Workout dates in the list, the header and the dashboard tile follow the profile timezone instead of the browser's.
Added
- Heap diagnostics. The process logs
runtime.heap.sampledevery five minutes (every minute above 70 % of the heap limit) and, once, aruntime.heap.pressurereport above 85 % that lists the oldest requests and jobs in flight and the busiest routes. WithHEALTHLOG_HEAP_SNAPSHOT=1it also writes one heap snapshot intoHEALTHLOG_DIAG_DIR; a snapshot holds plain health data, seedocs/ops/heap-diagnostics.md. - A nightly run of the full multi-year Google Health backfill test; the run on every pull request uses a tenth of the pages.
Changed
- Dependabot keeps
next,eslint-config-nextand@next/bundle-analyzerout of the weekly groups, so a red Next bump no longer holds the other updates red. - The timezone guard also matches
getDateTimeFormat(locale, options)calls that leave the zone out, and comments and test fixtures no longer carry personal names.
[1.43.5] — 2026-10-18
Fixed
- Backups of accounts with many documents no longer run out of memory. The weekly in-database backup, the manual backup and the restore held the document vault and ECG traces in memory several times over (bytes, base64, the serialised section and the preview check), and sealed one oversized chunk; an account with a large document vault crashed a 524 MB heap. Document bytes are now read and written in 1.5 MB slices, ECG traces page by page, every section is written element by element and no chunk exceeds the chunk size. The restore reads documents without their bytes first and writes each one on its own. The file format is unchanged (Refs #1239).
- A failed tombstone purge names its phase and condition (e.g.
statement_timeout) instead of a bare error.
Added
- The admin support report and System status flag a heap limit below 700 MB
and point to
APP_MEMORY_LIMIT;/api/admin/statuscarriesmemory: { heapLimitMb, low }.
[1.43.4] — 2026-10-17
Fixed
- An illness shows only the symptoms filed under it. The illness detail view merged the symptom events linked to the illness with every event inside its date range, so overlapping illnesses showed each other's symptoms. It now shows linked events only, and every symptom row on the illness page names its illness. The symptom edit sheet has the illness field, with the current link preselected; an event can be filed under an ongoing illness or an ended one whose dates contain it, and POST and PATCH accept the same set. The day view no longer links a symptom to a deleted illness (Refs #1237).
[1.43.3] — 2026-10-17
Fixed
- Every action behind a second-factor check opens the confirmation
dialog. Handing over a managed profile, creating, editing or deleting
one, adding or removing a guardian, turning off TOTP, regenerating recovery
codes, removing a security key or passkey, changing the password, deleting
data or the account and the admin key rotation only showed a sentence when
the second factor needed confirming. They now open the confirmation dialog
and retry once confirmed, also after a single sign-on login (Refs #959).
The refusal names the methods the account has in
meta.methods. - A passkey counts as a second factor for managed profiles. An account whose only strong sign-in is a passkey was told to set up a second factor before it could create, edit, hand over or delete a managed profile or manage its guardians. A passkey sign-in, or confirming the passkey in the dialog, is now enough; the admin key rotation follows the same rule. Password sign-in, the native app and the actions that ask for a fresh factor only from accounts with TOTP or a security key are unchanged.
- An account that signs in only through single sign-on is no longer offered a password field it cannot fill when it sets up a second factor; it is asked to sign in again.
[1.43.2] — 2026-10-16
Fixed
- AI consent is only ever given by the person. Opening the AI settings in
the web app, and the iOS app's launch sync, minted an
ai_fullconsent receipt without anyone agreeing, so background AI work could reach the operator's shared key without consent.POST /api/consent/aiandPOST /api/consent/ai/webnow write a receipt only with"confirm": trueand answer 400consent.ai.confirmRequiredotherwise. Where AI is blocked only by missing consent, the web shows a consent card that names the service and the data, with an explicit Agree button. Existing receipts stay valid; the AI settings show since when consent is given and how to withdraw it./api/auth/mepublishesai.consent(status,since,recipients).
[1.43.1] — 2026-10-16
Fixed
- Lab values keep their precision in the doctor report, the share view, the day view, the scan review and the Labs page: a stored value keeps every digit it has, a converted value keeps the precision of the printed one. Reference ranges follow the same rule, and so do GLP-1 doses and the measurement rows of the report (Refs #1228).
- Every illness episode stays reachable. An ended episode that belongs to a condition now shows in both the card and the list view, flares nested two levels deep included, and the illness page loads past the first fifty episodes (Refs #1224).
[1.43.0] — 2026-10-16
Records that link to each other, supplement composition as a nutrient source, life circumstances with AUDIT-C, AI usage levels, marker merging, Dawarich, single sign-on handover and an error id on every failure.
Added
- Relation links. Five link tables (migration 0410): lab result and
document, visit and medication, visit and vaccination, medication and
document, medication and condition.
POST/DELETE /api/medications/{id}/linksand/api/labs/{id}/links; visit and vaccination link routes take the new target kinds. Six detail responses carryrelated[], filtered by module and sharing scope; a scan committed from a document links its lab results. Backed up both ways, portable restores included. - Supplement composition (migration 0404).
PUT /api/medications/{id}/ingredients(MANAGE-delegable),POST .../ingredients/extract(label photo through the document AI, owner only, proposal only),GET /api/nutrients/catalogwith input units and EFSA upper limits (with the form each covers). Taken doses writeSUPPLEMENTnutrient rows per local day;/api/nutrientsand/dailysplit by source and judge the upper limit. DHA and EPA join the catalogue. - Life circumstances (migration 0405). Six sections with a valid-since
date and per-section AI release (
/api/anamnesis/life-circumstances,ai-sections,confirm); AUDIT-C (/api/anamnesis/audit-c) in validated en/de/ko/pl wording with per-locale cut-offs and sources, English withvalidatedLocale: falsefor es/fr/it. Read by the Coach (released sections only), the doctor report and FHIR. Screening hints by account language. - AI usage levels (migration 0402). Frugal, standard and full; admin
ceiling and default, user choice within (
ai.usageon/api/auth/me,PUT /api/auth/me/ai-usage). Sync gap by source, compact JSON in prompts, a stable Coach prompt prefix.AI_SYNC_REGEN_GAP_HOURSsets the instance's hours between card refreshes after a sync. - Marker merge and aliases (migration 0403).
POST /api/biomarkers/{id}/mergewith adryRunpreview (the web always shows it first), alias editing, alias choice on scan review, catalogue shorthands; link pickers search a year or a month. - Dawarich (migration 0406). Daily distance, car and train km, places
count, time away from home and travel days; no places or coordinates
stored. Private hosts only through
DAWARICH_PRIVATE_ORIGINS. Movement block in the day view, two correlation channels, travel suggestions. - Managed profile handover through single sign-on
(
POST /api/auth/claim/sso), also underOIDC_ONLY. - Supportability.
meta.requestIdon every error envelope and an error id with copy button in the web; "Copy support info" under Settings, About; a separate problems ring in/api/admin/app-logs; an admin support report with accounts anonymised. - Server-resolved fields for clients. Direction verdicts and trend
sentiment on the snapshot, summary, targets and series; chart reference
lines; personal-record benchmarks; mood patterns; cycle
absorbed/restored; hourlystats:rows for summed types andsyncTrigger: "backfill"withlastBackfillAt(migration 0408). - Pulse day mean v2 (migration 0407). Sparse hours weigh
min(1, n/3); admin dry run, apply and rebuild.
Changed
- zod 4.6.7. Request instants must carry seconds (
isoDateTime()); a refusal says so and is counted asvalidation.datetime.no_seconds. OpenAPI writes nullable fields as type arrays. - The legacy integration status fields are deprecated and go in v1.44; read
health. - Below a default band, blood pressure, resting pulse and body fat read as
hold; steps and SpO2 above their band never read asup-bad. Targets judge the seven-day mean, like the dashboard. - Migration 0409 drops the plaintext location columns once the v1.42
backfill has sealed every row; recovery steps in
docs/ops/migration-0409-plaintext-location.md. - Push notifications to guardians and in discreet mode open the app root.
- Zod left the first load of
/,/measurements,/insights/moodand/settings/[section].
Fixed
- A backup with a qualitative lab value (empty unit) restores.
- Free weekdays reach a prompt only with the work section released; a concurrent about-me save no longer re-enables a released section.
- Hour buckets and the nightly drain cut days in the same timezone; both starts of the repeated fall-back hour count.
- Dawarich and Nightscout responses are read with a size bound; a Dawarich window edge no longer overwrites a complete day.
- Supplement nutrient rows are recomputed when the nutrients module returns or the timezone changes.
[1.42.4] — 2026-10-16
Reliability: one integration health model with a watchdog and a shared card, a resumable Google Health backfill, no fixed list cut-offs.
Added
- Connection health. Every integration status route (and
/api/integrations/status,/api/integrations/healthkit,/api/record-settings/integrations) carrieshealth: state (ok | limited | degraded | action_required), reason, since, one action, unavailable types, last success, last data and redacted diagnostics (integration_statuses.recent_calls, migration 0400). - Watchdog. Hourly and daily passes flag a continuous source that goes silent (24 h; 48 h for Apple Health, Oura and WHOOP; never for scales, BP cuffs or Strava) and pages the admin once per incident for action needed, provider errors or silence, with one recovery note; provider-wide incidents are aggregated; the first run records standing incidents without paging.
- Integration cards. One card for every connection and the managed
profile view, rendered from
healthonly, with a three-dot menu and a collapsed, copyable diagnostics section. - HealthKit arrivals. Medication doses record arrivals as
MEDICATION_DOSE; every type carrieslastBackgroundAt(migration 0401 turnshealthkit_type_syncs.typeinto text). GET /api/labs/overview: one row per biomarker with its latest reading.- The container logs pending migrations before
migrate deploy.
Fixed
- Google Health backfill (#1220). Progress is saved per page inside a collection; slices of five minutes or 500 pages; server errors and timeouts are retried; a long rate limit ends the slice and keeps the place; a refused resume filter falls back to a client-side skip that keeps its own place. Every admitted import runs in its own background event. The hourly sync waits for a running slice and stays inside its expiry.
- Fixed list limits (#1222). Visit link pickers open on the record's date
with search and paging (
/api/labsq; illness episodesfrom,to,offset,sortDir,q; encountersoffset); the lab list reads the overview; side effects, GLP-1 medications, drug-level intakes and symptoms page through all entries. A guard rejects new fixed client limits. - Extra Apple Health dose (#1221). A dose on another local day than the held take is stored unscheduled instead of overwriting it; a same-day entry converges onto the slot; dose history lets the slot's own take claim it first.
- Sleep score. A night's midpoint comes from its main session; Oura
restanddeletedperiods are skipped and earlier imports retired. - Recovery page empty state; aging test fixtures made relative with a guard.
API
healthon integration status responses;metricFreshness[]gainsMEDICATION_DOSEandlastBackgroundAt;POST /api/medications/intake/bulkacceptssyncTrigger; GLP-1 timeline addsmedications[].
[1.42.3] — 2026-10-15
Withings API plans, lab unit conversion, chronic conditions and rest mode, Oura scopes.
Added
- Lab unit conversion (#1095). A registry of convertible analytes with a
hub unit per analyte, derived from molar masses (glucose, cholesterol,
creatinine, urea and BUN, uric acid, calcium, bilirubin,
vitamin D, HbA1c in both directions, power-of-ten pairs). Markers carry
analyteKey, the catalog slug matched from name and unit and re-derived whenever either changes.POST /api/labs, the OCR commit and the document commit convert a reading into its marker's unit when the pair is known (201 withconverted: true) and keep the printed value, unit and range text; unconvertible pairs still answer 422labs.unit.mismatch, now with areason. Converted ranges are stated in the marker unit everywhere (note, Coach, MCP, doctor report, FHIR); the printed text is shown as printed, with its unit. Contributed by @BernhardBuckel.
Fixed
- Withings API plans (since 2026-10-12). A request for a data type
outside the account's Withings API plan fails whole with status 277, so
every measure sync failed. Syncs now drop the documented advanced types
on a 277, bisect any remainder, remember unavailable types per connection
(
withings_connections.api_plan_state, migration 0398) and re-probe weekly. ECG and sleep-summary fields outside the plan are skipped quietly. A 277 never counts as an auth failure; connections parked by it are released by the fallback cron. The Withings card lists the types the plan excludes (planUnavailable). - Oura scopes (#1204). The authorize request adds
spo2andheart_health. A 401 on single collections next to successful ones is recorded as a missing scope (users.oura_scope_denied, migration 0398), counts as success and shows a reconnect hint (scopeReconnect) instead of an error state. - Rest mode and chronic conditions (#1216). Only active episodes that
are not
CHRONIC_ONGOINGtrigger rest mode; Day N, the nudge pause, the streak freeze and the banners follow. Chronic conditions stay context for the Coach and for coincident vital deviations. - ntfy quick answers (#1203). Every refusal on
/api/notifications/action/*is logged with a reason; a non-POST request gets 405 withAllow: POST. - Lab form (#1214). The draw date gets a full-width row.
- Lab edits keep the conversion record unless the value changes.
API
LabResult:converted,sourceValue,sourceUnit,printedReferenceText;sourceReferenceTextis null for a converted reading.Biomarker.analyteKey.- Integration status: Withings
planUnavailable, OurascopeReconnect.
[1.42.2] — 2026-10-11
Actionable notifications, record search in the command palette, automatic intake recording, and fixes for Oura, non-UTC databases, documents and Google Health backfills.
Added
- Notification tap targets (#1203). One map resolves a target path per
event type; ntfy carries it as
Click, Telegram as an "Open in HealthLog" button on public https (text link on plain http), web push asurl, webhook and Gotify as a URL field, email as a link line, APNs asurl. No target for guardian deliveries, discreet cycle mode or the silent sync push. ntfy titles and action labels outside ASCII are sent RFC 2047 encoded. - Notification content preference.
notificationPrefs.content.detail(genericdefault,detailed) onPATCH /api/auth/me/notification-prefs; withdetailedthe morning briefing title carries the score. - Mood quick answers on ntfy. Three actions post to
POST /api/notifications/action/[token]: an HMAC token bound to user and answer, valid six hours, single use per reminder, rate limited before verification, recorded once in the idempotency table so a purged entry cannot be answered again. Mood entries record sourceNOTIFICATION. A reminder tapped while acting in another record opens the mood page without the entry sheet. ntfy messages are published as JSON. - Record search (#1117).
POST /api/search(query in the body, so it never appears in a URL or proxy log; read-level access, one search at a time per account, 20 a minute) searches visits and procedures, conditions, documents, vaccinations, lab results, medications, symptoms, workouts and notes, decrypting encrypted text in bounded batches with a shared time budget, and reportstruncatedwhen a bound stopped a source. Matching is case-, accent- and umlaut-spelling-blind and shared with the MCPsearchtool. The palette lists hits under "In your records"; each hit deep-links to its entry. Migration 0397 adds a partial index on live readings that carry a note. - Open in new tab (#1202) for PDF and image originals in the document vault.
- Record doses automatically.
Medication.autoMarkTaken(withautoMarkTakenSince, migration 0395): a job every 15 minutes records each due dose as taken from the moment the option is switched on, through the schedule engine, never over a dose taken, skipped or deleted by hand, with the same stock and rollup follow-ups as a manual take. Intakes carryautoRecorded; no reminders are sent for such a medication. List and detail responses carry the resolvedremindersActive(false also once a course has ended). Rolling cadences step past slots the job will not record, and the card shows the same next slot.
Fixed
- Oura token exchange (#1204). Codes are exchanged at
moi.ouraring.com/oauth/v2/ext/oauth-token; a code issued by that server is never sent to the legacy endpoint. Refresh falls back toapi.ouraring.com/oauth/tokenonce. The callback validatesiss(RFC 9207). - Non-UTC database time zone (#1200). Every connection (Prisma, pg-boss,
lock and maintenance clients) runs with
TimeZone=UTC, with aSETfallback when session options are disabled; raw SQL reads the clock asNOW() AT TIME ZONE 'UTC'; boot warns when the session zone is not UTC. - Google Health backfill (#1194). A slice that saved progress enqueues
the next slice and ends without
job.run.failed; a slice without saved progress still fails, and the chain stops after collections + 2 slices. - Admin AI spend card sums today's spend on each account's local day.
- Daily AI budget is keyed on the account's local day.
- Day view: Escape inside the date picker no longer closes the day.
Changed
- Docked strips run the full height, the header search is a magnifier icon,
g ptoggles the day panel. - Without a zoom in the URL the timeline fits its window to the record: three months, clamped to the first entry (at least 28 days) for a young record, and everything when the last three months are empty. An empty window offers "Show everything" when older entries exist.
- The explanatory line under day-linked charts is gone.
API
- New:
POST /api/search,POST /api/notifications/action/{token}. - Medication:
autoMarkTaken,autoMarkTakenSince,remindersActive; intakes and sync intake upserts:autoRecorded. notificationPrefs.content.detailon the notification preferences.- APNs payloads carry
urlon every visible push.
[1.42.1] — 2026-10-10
Fixes for Google Health syncing and the measurement maintenance run.
Fixed
- Google Health parked after a reconnect (#1194). A backfill walk used
one access token throughout; after Google's one-hour expiry the walk saw
401s and the connection was parked at
error_reauthalthough the refresh token was valid. Resources now take a token source that renews before expiry and once more on a mid-walk 401; a 429 is waited out (Retry-After, RetryInfo or backoff) and stays transient; parking follows only a refused grant at the token endpoint, and an account parked by a data-call 401 recovers on its next run. One sync per account at a time: the hourly run skips, a manual sync answers 409sync_in_progress, backfill and sleep repair wait. An interrupted backfill resumes frombackfill_progress(migration 0393). - Overlapping Google Health sleep segments (#1195). A batch keeps one
reading per natural key (the longer segment), a remaining collision is
settled through
reconcileExternalMeasurement, and overlapping sessions no longer clear each other's rows. Night totals use the union of segments. - Measurement maintenance on Docker (#1192). The maintenance connection
sets
max_parallel_maintenance_workers=0and needs no/dev/shmsegment; the bundleddocker-compose.ymlsetsshm_size: 256mbfor the database. - Dashboard spacing. The tile row sat flush against the top card; a
display: contentswrapper had swallowed the gap.
API
POST /api/google-health/syncmay answer 409sync_in_progress.
[1.42.0] — 2026-10-09
A day view behind every dated value, an optional timeline with life events, air quality and pollen per day, a Health Connect import, and the handover of a managed profile to the person it describes. The measurement table drops the samples the nightly fold used to keep, after a one-time repair of means an earlier fold computed from part of a day.
Added
- Day view (#613).
?day=YYYY-MM-DDopens one local day over the current page: docked from 1280 px beside a fixed strip at the right edge that opens and closes it and keeps it, with the last day, across pages; a side sheet below that and a bottom sheet on a phone. On/coachit sits next to the conversation list, each with its own strip. It shows the value it was opened from, what ran through the day (one entry per medication with the day's dose, the cycle day and phase), eight curated readings against the 30-day usual range (the rest behind "All values"), the day's scores (health score, readiness, recovery, sleep score, strain) and the day's entries in clock order. Readings and scores link to their pages, and the date in the header opens a calendar with Today. Every chart drawn in days opens it (on touch the tooltip offers the day first), as do data tables, the measurement, mood and intake lists, heatmaps, lab readings, documents, workouts, symptom and illness days and the days a Coach answer read. Back and Escape close it, Alt with an arrow steps a day. A planned visit shows what changed since the last one fromGET /api/day/notable. Not offered: the dashboard's today area, tile charts, the cycle calendar, model text, future dates, the clinician share link and the doctor-report PDF. - Timeline module (#613). Opt-in. One SVG time axis from 768 px with lanes for life events, conditions, allergies, medications, vaccinations, visits and procedures, labs and documents (one row per medication, dose changes and pauses as segments of its bar), a year, month or week grid or any range, and as many value lines as the record has types, as quarterly, monthly, weekly or daily means by span (a short gap dashed, a longer one left open, a mean from fewer than three readings drawn hollow, each point with its own tooltip); a chronicle, newest first, on a phone. Switching the module on opens a readiness inventory that rates each lane and links each gap; it stays available as Data coverage.
- Life events. Title, kind (family, home, work, loss, other), a start at day, month or year precision, optional end and note; title and note encrypted at rest (migration 0387). Owner only in this release: no share level or MANAGE grant reads or writes them, and no model ever receives them. They travel in both backup purposes and are wiped with the account.
- Air quality, pollen and UV (#615). The environment fetch adds the
Open-Meteo air-quality feed (CAMS,
domains=auto): PM2.5, PM10, NO2, SO2, CO, the ozone 8-hour high, European and US AQI, UV, dust, aerosol depth and six pollen groups, folded into local days (means and maxima need 18 of 24 hours) and stored as null where the feed has no value (migration 0381). Pollen exists only in the European domain. A nightly gap fill covers up to 90 days per account. PM2.5, the ozone high and the highest pollen join the correlation channels. An account switch under Location & weather (on by default), attributions beside the values, and a dashboard chip on a day with high pollen, a warm night or very poor air. - Air quality for the past. A background job fills air quality, pollen and UV for earlier local days with an entry: after the update, when the home is set and when air quality is switched on. A day inside a location period is fetched at that place, a day from the home's effective date on at the home; a day before it with no period stays empty rather than borrowing a place the person was not at. The source reaches back to 2013 in Europe and to August 2022 elsewhere, pollen from 2021 in Europe only; older days stay empty. Nearby days share one request of up to 90 days, a run holds itself to 300 of the account's 400 daily calls and half of each instance window, and a long history spreads over several nights. Location & weather shows the progress (migration 0391).
get_environmentandget_dayfor the Coach and MCP.get_environmentis module-gated and carries no coordinate or place name;get_dayleaves out life events, notes and document names and honours the person's Coach exclusions.- Health Connect import (#972). Upload the ZIP Android's Health Connect
exports under Export & Import. It is read in the background (read-only
SQLite, schema version 9 or later, 1 GiB upload cap, one import per
account): daily totals from the top-priority app, per-minute heart rate
for 90 days and hourly means before that, sleep stages, workouts, cycle
days, water and nutrients, under the new source
HEALTH_CONNECT(migrations 0379, 0380). Apps already connected directly are skipped and a second import of the same file writes nothing. - Managed profile handover (#959). A guardian with a fresh second factor
creates a one-time link (1, 7 or 14 days, shown once with a QR code) and
proposes no access, view only or manage per guardian. The person opens
/claim/<token>, sets a username, email and password, and decides each guardian's access after signing in. Pending invitations are withdrawn; disclaimer, onboarding and AI consent reset. Refused onOIDC_ONLYinstances (migrations 0383, 0390). - Custom vaccines (#1005). A record defines its own vaccine with the antigens it covers, series length and booster interval; its doses count into each antigen's series and settle boosters (migration 0386).
- Workout editing (#1162). A hand-entered workout opens its form from the detail page and saves as an overwrite of the same row.
- HRV RMSSD from Apple Health (#1110). iOS 27's
HKQuantityTypeIdentifierHeartRateVariabilityRMSSDis stored asHRV_RMSSD(1 to 300 ms) with its own source-priority ladder, charted apart from SDNN. - Apple Health arrival per type (#1173). The Apple Health card shows
when each type last arrived, by which trigger (including the new
manualfor Sync all) and when it last brought a new value (migration 0384). - Documents layout. Preview cards or a compact list, months flowing side by side (the default) or stacked, per account (migration 0388). Only a field the person picked is stored, so an untouched one follows the default.
- Measurement maintenance. An admin card and
POST /api/admin/maintenance/measurementsrunVACUUM (ANALYZE)and aREINDEX INDEX CONCURRENTLYper index, largest first. - Query statistics. The bundled database loads
pg_stat_statements;CREATE EXTENSIONonce to read it (docs/ops/query-statistics.md). - Score history.
/insights/health-scorecharts the daily health score with its usual band, broken where the score's recipe changed, fromGET /api/insights/score-history; readiness, recovery, sleep score, stress and strain share one 7, 30, 90 days or all range picker. - Top card switch. The dashboard layout settings can hide the top card
(
todayCardVisible). - Keyboard shortcuts.
gthend,m,l,t,i,corsopens a page,nthe add menu,[and]step the open day, and?(or the account menu) lists them; ignored while typing in a field. - Command palette. Cmd+K (Ctrl+K) or the search field in the top bar finds pages, Insights pages, every Settings card by its anchor, module switches and actions (new entry, the Coach, a backup, today, the shortcuts), filtered by modules, AI availability, role and a shared record's grant; typo-tolerant, with recent places when empty.
Changed
- Correlations remove season and trend. Every daily pair is correlated on residuals after a linear trend and one annual harmonic are taken out of both series (trend only under 120 days), with p-values on the Pyper-Peterman effective sample size. Weather channels compare the mean of the day before and the day itself with the same day's outcome. Some earlier findings no longer qualify.
- Fold leftovers. The nightly folds delete the raw samples they fold
instead of keeping them for 75 days. A re-sent raw sample whose hour or
day a live
stats:row covers is a duplicate with reasonfolded_window, in the batch route, the export import and the Health Connect import. A background purge removes the backlog in batches of 5,000 per account; the sync feed and a restore leave these deletions out. Health Connect heart rate is folded like Apple Health's. - Duplicate batches. A batch of samples the server already holds is answered without a write and queues no record detection or reminder sweep; caches are evicted only for a foreground or manual sync or a hand-entered reading, and marked stale otherwise.
- Locations encrypted. The home location, travel periods and stored environment days keep their coarse location sealed (migration 0382); a background job seals existing rows.
- Lighter client. Settings sections load on their own (611 to 403 KB gz) and the shared shell no longer carries Zod (199 to 136 KB gz).
- Wellness scores show their course on each tile and page; the sleep
score and readiness reads add
series. The proxy caveat line is gone. - Empty states and add actions. An empty list carries the one add action; the Insights pages add through the header plus.
- Touch and small screens. 44 px targets on touch screens, a sidebar that fits short windows without a native scrollbar, the phone shell on a phone held sideways, 16 px fields, safe areas in the installed app and sheets that stay above the on-screen keyboard.
- Provider pause. From five consecutive hard failures a provider is skipped for a window that starts at 30 minutes and doubles, capped at a day; a dead Codex refresh token reads as a sign-in to renew.
work_memis set per app connection fromDATABASE_WORK_MEM(16MB);DATABASE_SESSION_OPTIONS_DISABLEDsends no session options for hosts behind PgBouncer.- Volume queues keep finished jobs for 96 hours instead of seven days.
Fixed
- Means folded from part of a day. The folds cut at a fixed offset instead of the local day boundary, so a second run replaced a mean-type day, and the boundary hour of heart rate, HRV and SpO2, with the mean of its later part. Folds now stop at the local day boundary, and a one-time repair per account recomputes each mean from its samples and writes only values that differ (migration 0389). It covers the last 75 days of daily means and the 75 days before the 90-day raw window of hourly means, where every sample of the day is still kept. Older days stay as they are: their remaining samples can be a fragment of the day, and a mean over them would be worse than the stored one. The nightly folds follow the same rule for a day or hour that already has a mean. The purge waits for the repair.
- Push after an update. Reload on the new-version hint unregistered the service worker, which ended the Web Push subscription; it now updates the worker instead.
- Discord and Slack webhooks. A Discord webhook URL answered the test
and every reminder with "Cannot send an empty message", and a Slack
incoming webhook refused the body too: the generic format carried neither
Discord's
contentnor Slack'stext. It now sends both as plain text (title and message, cut to Discord's 2000 characters), tells Discord to resolve no mentions, and keepstitle,message,eventTypeandpriorityunchanged for Home Assistant and n8n rules. Discreet mode covers the new fields. - MCP deep links.
/insights?metric=and/labs?analyte=open the metric's page and the marker's page. - Apple Health status no longer reads "waiting for first data" after an export import.
- Strain falls back to the device's day strain when no computed value exists.
- Failed briefing runs name each account's outcome, a briefing held back for a causal claim gets one repair attempt instead of hourly retries, and the tiered context converts stored times to the reader's zone.
- Google Health re-reads two hours of intraday heart rate per sync, and the first cycle of each local day a full day (#1023).
- The capture sheet opens from the dashboard's add button on phones; the installed app follows its own theme in the status bar and toasts.
source-map-js1.2.2,postcss-selector-parser7.1.6 andsmol-toml1.9.0 close CVE-2026-93749, CVE-2026-104844 and GHSA-r4xh-jqrq-34v2 in the build and development toolchain.
Removed
- The "took it twice" and "skipped a dose" achievements.
API
All changes are additive.
- New routes:
/api/day/{date},/api/day/index,/api/day/notable,/api/timeline,/api/timeline/readiness,/api/life-events,/api/vaccinations/custom,/api/environment/preferences,/api/documents/inbound/layout,/api/import/health-connect-export. - The measurement source enum gains
HEALTH_CONNECT. /api/auth/meaddstimezone,glucoseUnitandfeatures.trackIntake; module availability liststimeline.POST /api/measurements/batchacceptssyncTrigger: "manual"; a re-sent sample whose hour or day is already condensed returnsduplicatewith reasonfolded_window.GET /api/integrations/healthkitaddslastReceivedAt,lastTriggerandlastNewSampleAtper type.- The sync feed lists only deletions a person makes, not those of the condensing step.
GET /api/environmentaddsairQuality(withhistory, the progress of the past-days backfill),latestDayandattributions.GET/PUT /api/auth/me/source-priorityaddinUse.- A dose for a record-only medication is refused with 422
medication.intake.notTracked; failed passkey sign-ins answer withpasskey.*codes instead of a 500. /api/medications/complianceaddsmeta.aggregateand?days=;/api/mood/insightstakes?days=and returnsstabilityWindowDays.- Coach steps can name the tools
get_dayandget_environment, the domainsdayandenvironment, and adaydate. - Vaccination doses carry
customVaccineId, correlation findingsnEff, workoutsstoredAvgHrandstoredMaxHr, and the sleep score and readiness reads aseriesof past values. /api/life-events*answer only the record's owner; a delegate gets 403, the day'slifeEventssection readsnot_sharedand the timeline has nolifelane.POST /api/environment/backfillanswers 429environment.backfill_rate_limited(three an hour per account) and 409environment.backfill_pendingwhile one is queued.GET /api/environmentmay returnlatandlonasnull.- The handover claim preview no longer carries
grantId; a guardian'sdisplayNamemay benull. - The restore report adds the catalog
environmentLocationCiphertext. get_daynames documents and visits by kind only.
Thanks
@lutzkind for #613, #615 and #586, @CritLoren for #972 and #1005, @ckaotik for #959, @BernhardBuckel for #1110, @mills1975 for #1173 and @NebuPookins for #1162.
[1.41.2] — 2026-10-07
More room for a Coach turn, prompt caching that holds across its rounds, and MCP tool results that match their schemas.
Changed
- Turn budget. A turn on the person's own provider gets 165k tokens, 200 s and 16 rounds (was 120k, 150 s, 12); a turn on the operator's budget 55k tokens, 120 s and 8 rounds (was 40k, 90 s, 6). The final answer's reserve keeps its share of each (1,650 tokens), the concurrent-turn slot follows the longer wall time, and a turn shows up to 64 steps.
Fixed
- Cached input in the turn budget. Codex requests carry a stable
prompt_cache_keyand session id per conversation, as the official client does, so later rounds of a turn read the prefix from the provider's cache instead of paying for it again;api.openai.comgets the same key, gateways do not. Anthropic usage now counts cache reads and cache writes in the turn's total (they are reported besideinput_tokens), so a cached round is weighted at a tenth instead of almost nothing and a cache write in full. Local and OpenAI-compatible servers that reportprompt_tokens_details.cached_tokenshave it counted. - MCP output schemas (#1170).
list_metricsdeclares theavailabilityits rows already returned, the Coach-backed reads declaresearchedWindowandavailable, andget_intraday_pulsedeclares each bucket'sminandmax. The advertised schemas close every object, so a strict client refused these results. Every tool's real output is now validated against its advertised schema under a strict 2020-12 validator. Thanks to @RyRyMason for #1170. - Deleted readings in MCP discovery. The
list_metricsdiscovery rows and thesearchclinical-signal probe count live rows only: a measurement type whose readings were all deleted is no longer listed as present, and the probes use the live-rows index instead of walking every deleted row.
[1.41.1] — 2026-10-07
A calmer thinking row in the Coach.
Changed
- One thinking row. While the Coach works, a single row shows a brain icon and the current step in body text with a soft moving highlight and animated dots ("Reading blood pressure…"); no running seconds. The interim table line under it is gone; tables appear with the answer. Afterwards the row reads "Thought process · N steps", stays closed unless opened, and closes again when a turn finishes. Opened, the steps stand on a thin line, each with its own icon and the reasoning summary under its title.
Fixed
- Coach in the demo. A refused chat on a demo instance says that the
Coach doesn't answer in the demo instead of reporting an unreachable
provider; demo refusals carry
meta.errorCode: "demo.readOnly".
[1.41.0] — 2026-10-07
The Coach reasons before it answers, shows what it is doing while it works, asks when a question is ambiguous, and remembers what it is told.
Added
- Reasoning. Codex, OpenAI, Anthropic, OpenRouter, gateways and local
servers receive the requested reasoning in their documented dialect;
reasoning summaries stream to the Coach, signed or encrypted reasoning state
is carried between tool rounds and never stored, and a model that rejects a
reasoning parameter is retried without it (the downgrade is learned per
provider, endpoint and model for an hour, only from a 400 that names the
parameter). Thinking depth per person under Coach settings (Off, Low,
Medium, High; Medium by default); an operator switch and maximum under
Admin, Assistant; turns on the operator's budget are capped at Medium.
/api/auth/mepublishes the resolvedcoachReasoning. - Budgeted tool loop. A turn runs against a token, time and round budget
(own provider 120k tokens, 150 s, 12 rounds; operator 40k, 90 s, 6 rounds)
with a no-progress brake and a reserved final round that keeps the tool
definitions with
tool_choice: none. Every round is reserved against the daily budget beforehand and settled afterwards. At most two concurrent turns per person. - Live trail. One quiet status line per answer (
activityframes), never expanded by itself; the screened reasoning text is stored encrypted (coach_messages.trail_encrypted) and served lazily byGET /api/insights/chat/{id}/messages/{messageId}/trail. - Comparisons.
compare_series(two periods or two metrics) and acomparechart; tables arrive as interim results before the answer. - Clarifications as suggested replies in the thread, at most one in six turns and three a day; a declined or limited question becomes a stated assumption with a reply that changes it.
- Memory and plans. Every turn carries an egress-checked memory block;
remember_factsaves preferences, goals and context with a visible note and undo, health facts only after the person agrees;propose_plansuggests a plan from a stated goal. Both are written only when the answer that shows them is stored. Background extraction runs once a conversation is quiet for 30 minutes; plan progress reaches the daily briefing. The memory list in settings shows each fact's source and accepts edits. - Background reasoning for the daily briefing (scheduled and on request) and the weekly and monthly narratives; status cards never reason.
Fixed
- Dose-change screen. The outbound screen blocks the dose-change class across all seven languages, including the person's own medication names, drug classes, spelled-out amounts and "extra / one more"; plan texts go through the same screen.
- Glucose by time of day. The usual-range line, the MCP baseline and the signals of the day compare glucose with the same hours of earlier days instead of setting a morning reading against whole-day means.
- Usual-range line names the date of a latest reading that is not from today, and a steady value as one figure.
- Memory refresh asks the Coach capability before deferring, so a switched-off Coach does not re-queue it.
- Dependencies. The MCP SDK moves to 1.32 (GHSA-6qxp-vccf-f47h) and
proxy-addrto 2.0.8 (GHSA-jqcg-44mw-7w3h).
Changed
- Answer voice. The first sentence answers; 40 to 120 words; at most one next step.
- Keyboard focus is drawn in a neutral tone everywhere.
Migrations
0376_ai_reasoning_controls,0377_coach_message_trail,0378_coach_fact_source: additive columns with constant defaults or nullable; no table rewrite.
[1.40.6] — 2026-10-05
The Coach reads long series again; "today" only from today's readings; the dashboard card fits a phone; text fields lose the purple focus ring.
Fixed
- Coach series without numbers. Since 1.21.0 every Coach tool read one
shared per-turn snapshot capped at 24,000 characters; on accounts with many
series whole blocks became a placeholder and the tool still answered
present: true. A trimmed block is now re-read for the tool's own source, a Coach tool's read of one source is condensed instead of emptied (a summary from the daily values with the latest week against the first, the newest days, monthly means, each block in its own fields), a block without numbers answersretrieval_failedto the Coach, and the Coach calls the table tool itself for long histories. MCP reads are unchanged. The step list shows the reading count; no stray comma on copy. - "Today" from yesterday's readings. Signals of the day, the vitals fact, the lead line, the health status and the morning push only speak about today from readings taken today in the profile timezone; a stored briefing from an earlier day is no longer served.
- MCP schemas for strict clients (#1170).
tools/listadvertises every input and output schema as JSON Schema 2020-12 without a$schemakey, so clients that only accept 2020-12 load the tools; tool results are unchanged. - Briefing for the local day. The first read of a local day and the first reading of a signal metric that day warm the briefing once (within the existing daily limit), so users outside Europe and readings taken in the morning get a current briefing; every surface resolves it the same way.
- Deltas use each metric's display precision ("+34 bpm").
Changed
- Dashboard card on a phone. A compact score ring at the top right beside the lead; Today facts as label over value, full width. Desktop unchanged.
- Text-field focus. No purple ring on inputs, textareas, selects and date
fields; focus shows as a neutral border (
--input-focus). Invalid fields keep their red border. - Coach panel no longer has a search field (the full conversation list keeps its search).
[1.40.5] — 2026-10-05
The Coach's conversation panel runs the full height of the window.
Changed
- Coach panel. Docked, the panel runs from the top of the window to the bottom, beside the top bar instead of below it, and its header line continues the top bar's. Shut, nothing of it remains on screen.
- Top bar on the Coach. It names the page and the open conversation, and the panel toggle sits at its right end, against the panel.
- New chat. A round button with a plus at the panel's bottom right.
[1.40.4] — 2026-10-05
The Coach's conversation panel docks to the right edge; model and tokens move behind an info icon; no view chip for a result that already toggles.
Changed
- Coach panel. Flush against the right edge, full height below the top
bar, its own scroll area; the shell renders
/coachwithout its container gutter. The toggle uses mirrored panel icons and moves with the panel. Its buttons and rows go compact only beside a mouse or trackpad; with touch they keep the 44 px target at any width, a tablet included. - Answer row. Copy, read aloud, retry, an info icon and the time in one left-aligned group; the info icon shows model and token count on hover, focus or tap, and Enter or Space keeps it open.
- Follow-ups. No "show as chart/table" chip when the answer's result already offers that toggle; also for stored answers.
[1.40.3] — 2026-10-05
Small fixes to the Today overview and the numbers behind it.
Added
signalLineon the daily digest. The muted line under the lead is now decided on the server: when the lead already talks about the top signal's metric, only the delta remains, and the line is null when nothing is left. Every client shows the same line.basison health-status deviations.dayfor the usual whole-day range,sameHoursfor the usual range at this time of day.
Fixed
- The Today lead is shown as whole sentences. A long sentence is no longer cut mid-word; the headline or the deterministic sentence takes its place, and only as a last resort is text shortened, at a word boundary.
- Chart trend lines, legends and tooltips name every metric in the reader's language instead of showing its internal name (resting heart rate among them).
- A personal usual range never collapses to a single value ("61–61 bpm"): it keeps at least the measurement's own day-to-day spread, so the next ordinary reading is not counted as outside it.
- Glucose today is compared with the same hours of earlier days, up to the time of today's latest reading. A fasting morning no longer reads as below the range of whole days, and the range named is the usual one for this time of day.
- Encrypting stored text in place or rotating the encryption key no longer makes old Coach conversations look recently active, and the Coach memory summary no longer moves a conversation either. Measurements, mood entries and every other encrypted record keep their sync position through the same kind of rewrite.
- Conversations an earlier release made look recently active move back to their last message (migration 0375).
[1.40.2] — 2026-10-05
A calmer Coach with conversations in a side panel and settings in place; the top of the dashboard says what matters today; documents filter by procedure.
Added
- Coach panel. Conversations in a collapsible right panel (a sheet on a
phone), grouped by day, with rename, delete with undo and a new-chat
button; a gear opens quick settings (provider, model and reasoning where
they are the user's, and the lookback).
/coach?settings=dataopens it. - Coach lookback as a real limit. 7, 30 or 90 days, 12 months
(
lastYear, new) or all; it limits the snapshot and every Coach tool, a read beyond it answersoutside_reach. MCP keeps its own windows. - Today overview. The digest gains
lead,today[],restModeandscore.steadyWeeks/steadyAtLeast: a lead line with content (deterministic without AI), up to five facts (medications, an appointment today or tomorrow, last night, vitals, cycle, Rest Mode), the ring with how long the score has been steady. The morning push line carries no vital values. - Procedure filter in the document archive (#1154).
linkedProcedureson the usage endpoint; the list already acceptedencounterId.
Changed
- Coach answers. One action row (copy, read aloud, retry, time; tokens and model); web thumbs and the evidence block removed; remember is an icon; follow-up chips in the answer column; a scroll-to-bottom button; the composer's "+" menu removed.
- Settings → Coach summarises what the Coach sees and links into the overlay.
- Coach preferences parse field by field, so one invalid value no longer drops the others.
Fixed
- The medications fact counts the whole day on a 25-hour DST day.
- Providers without credentials are no longer offered in the Coach overlay; an operator-managed provider shows as such.
- Document filter controls match the search field's height on a phone.
[1.40.1] — 2026-10-04
Restore opens older encrypted values again; faster pages on a long-used server; a day of pulse is the mean of its hours; Apple Health imports leave out HealthLog's own samples; more memory for the app by default.
Fixed
- Restore key probe. A legacy unprefixed ciphertext was read as a base64 string and refused a restore the key opens; legacy values open as strings, probes come only from sections that count, binary probes use their AAD. The drill checks every account of the newest date.
- Daily pulse (#1107, migration 0372). A day is the mean of its local hours' means and a window the mean of its days, in every reader; stored pulse rollups are recomputed. Count, min and max stay over every reading.
- Apple Health import (#1109). Records carrying HealthLog's origin marker or a HealthLog id are left out and counted; cycle samples too.
- Blood pressure series. One source per day for both values.
- Backups. Heartbeat on both backup jobs; per-account attempts recorded (migration 0373), interrupted accounts go last; the preview is built from the writer's counts.
- Document previews. A permanent render failure is recorded (migration 0374) instead of re-queued at every start.
- Umami. Events from a tracker built with a custom endpoint are proxied to the configured Umami host.
- Display. Rest Mode shows its start date; the favicon is transparent; the sidebar no longer flashes scrollbars while it expands.
Changed
- Performance. One Prisma client per process instead of one per bundle;
the rollup coverage probe uses a loose index scan and concurrent readers
share one probe; timezone offsets are memoised per hour; chart text is
measured outside
<body>; Insights mounts lower sections near the viewport; a nav prefetch is not re-sent on mount; MCP single-metric reads build only the requested metric; the backup schema loads on first use. - Lab list (#1057). The range bar and name use a wide card.
- Compose.
APP_MEMORY_LIMITdefaults to1536m(792 MB heap). - Dependencies: next 16.3.8, eslint-config-next 16.3.8, zod-openapi 6.0.2, testcontainers 12.2.0, p-limit 7.3.3.
[1.40.0] — 2026-10-04
Back up the encryption key and refuse a key that cannot open the data; several courses and custom categories for medications; custom symptoms; document reads in the background; sick notes; lab unit refusal and charts that follow the source priority; blood pressure no longer loses its diastolic value; document previews work again.
Added
- Encryption key backup step (migration 0365). Admin step with key id,
fingerprint and per-platform guidance (
HEALTHLOG_PLATFORM), an optional "check my copy" that compares and discards the pasted key, a reminder that returns when the key changes. - Boot key check. A canary per key id, probed oldest values first across
string and Bytes columns; a proven mismatch answers 503
encryption.key_mismatcheverywhere except/api/healthand/api/version, MCP included, and starts no worker; an unproven result serves, warns and re-checks.ENCRYPTION_KEY_CHECK=warnas a last resort. - Medication courses and custom categories (#1024, #1041, migration
0368). Several courses per medication; compliance, streak, cadence and
dose history count every course and never the gaps; existing dated
medications become one course each. Own categories as
custom:<id>withcategoryLabel; the runtime category table is now in the schema. - Custom symptoms (#981, migration 0369). Up to eight definitions, events with a 0 to 10 intensity, a capture kind, one correlation channel per symptom, a Coach block; gated on the Illness module.
- Document AI as background runs (#1090, migrations 0366 and 0370).
Index, summary, suggestions and extraction accept
Prefer: respond-async; the lab scan always runs in the background;GET /api/ai-runs/{id}. SICK_NOTEdocument kind.- Natural durations (#1101) in the sleep field.
- Reasoning effort per provider (#1126). The Local and OpenAI-compatible
forms gain a Reasoning select (Default, Off, Low, Medium, High), stored on
the provider's chain entry; Default sends nothing, Off sends
reasoning_effort: "none". A rejection of that field no longer switches the endpoint's JSON mode off.
Fixed
- Blood pressure saved without its diastolic value (iOS #15, migration
0371). The iPhone app sends the two halves of a manual reading as two
requests under one
Idempotency-Key, and the server answered the second with the first one's cached response. The replay cache now stores a SHA-256 fingerprint of method, path and body (JSON canonicalised, a multipart boundary normalised) and replays only the same request; a different body under the same key is saved. Readings saved before this stay incomplete;docs/ops/blood-pressure-missing-diastolic.mdlists them. - Document previews and scanned PDFs (#1124). The image hoisted an older
@napi-rs/canvasbinary next to the pinned loader, so every thumbnail failed; the build now hoists the pinned version and fails on a mismatch. PDFs render with pdfjs font, cmap and wasm data and a registered fallback font instead of absent system fonts, and an all-blank render reportsraster-failed. The thumbnail job logs why a preview is missing. - Portable backups. Cycle free text, custom mood labels, visit notes and vaccination notes are carried readable and re-encrypted on restore instead of as the source server's ciphertext; custom mood tags restore without mood entries; a round-trip guard fails on any ciphertext in a portable file. A medication update and its course share one transaction.
- Lab unit mismatch (#1094). A reading in another unit than its marker is
refused (
labs.unit.mismatch) on every write path; a marker's unit is locked while it has readings (biomarkers.unit.locked). - Source priority in series (#1108). Pulse, HRV and SpO2 series read the ladder's source per day; unranked types keep every source.
- Sign-in over plain http (#1097). Refused with
auth.session.insecure_transportbefore a password, code or challenge is used; the sign-in page says so first. - Chart window (#1102). Ends at the end of today in the profile time zone.
- Workouts. The insight cap survives a delete (migration 0367); a changed
manual:resubmit updates the workout and its paragraph. - Display. Clinician decimals in the owner's locale, stated dates east of UTC+12, archive month grouping, lab list columns without a range, icon picker touch targets and labels, symptom entry load errors.
Changed
- Quick-entry forms load on demand; the four eager route baselines are lowered to the new measurements.
[1.39.9] — 2026-10-03
Workouts can be logged by hand, the body fat tile opens its own page, and every AI call honours the response-timeout setting.
Added
- Manual workouts (#1085). A short form (sport, start, duration, optional
distance and energy) from the dashboard "+ Add" menu, the capture picker and
the workouts page, saved through
POST /api/workouts/batchwithsource: "MANUAL"and a per-formexternalId. NewDELETE /api/workouts/{id}for manual workouts only (409workout.delete.synced_sourcefor synced ones). - Body fat page (#1084).
/insights/body-fat; insights layout idbody-fat. Tile destinations live in one table gated on the module surface map. - Wide lab scan review (#1059). The review stage uses a 72 rem dialog on large screens, with a container-query row layout.
Fixed
- AI response timeout (#1090). Document reading, summaries, extraction,
lab OCR, filing suggestions, medication extraction, the About-me questions,
the document chat and the provider test run under the user's setting
instead of a fixed 60 s; browser aborts derive from
ai.provider.responseTimeoutMson/api/auth/me. The lab OCR text mode no longer hits the 15 s client default. The settings placeholder shows the real defaults. - Dashboard "+ Add" menu. Mood and medication intake follow their modules, like the capture picker.
Checks
- Tile destination guard, AI call timeout guard, menu and picker parity.
- node-forge advisory GHSA-86w9-cpqp-85rv ignored until a patched release exists; only the unused APNs certificate path depends on it.
[1.39.8] — 2026-10-02
New ECGs from the iPhone get through, a resting heart rate can be logged by hand, and glucose ranges, decimals, calendar dates and chart axes follow the reader.
Fixed
- ECG ingest limit (#1060). 600 recordings a minute instead of 60; the app restarts a sweep after a 429, so a sweep larger than the limit never finished and the newest recording never arrived.
- Glucose bands (#1067). Time-in-range and band labels, the target line, the glucose page description and the fasting explanation in the reader's unit, from one set of thresholds with the guidelines' SI values; the unit literal guard now reads the locale bundles.
- Decimal separators. Range bars, lab values, reference ranges and the glucose panel's indices use the language's number format.
- Calendar dates. Date-only lab values and stated document dates show the stated day in UTC+12..+14.
- Chart axes. Unit-suffixed ticks no longer wrap (no-break space); date field buttons have a 44 px hit area.
- Bridge rate limit (#1054).
workouts:writebatches count in their own per-account bucket.
Added
- Resting heart rate in the entry form (#1079). Gated on the Recovery module like its chart; the surface map carries its summary key, page, trend slot and correlation channel.
[1.39.7] — 2026-10-02
The units a person chose, the day they are in, the phone they hold and the backup they rely on are now right everywhere, and each kind of mistake that kept recurring has a check that fails when it comes back. Account settings travel in backups, ECGs from the iPhone are accepted again, and four columns that did not survive a restore now do.
Fixed
- Units everywhere (#1067). The clinician share view prints every
statistic with its unit and glucose in the owner's unit. Metric status
notes, the period narrative, the glucose thresholds editor, the health
score glycaemia labels, workouts (distance, pace, elevation), the trends
row, fever entries and basal temperature follow the glucose unit and
metric/imperial preference. The briefing, weight status, Coach snapshot,
reference bands, cycle contrast, metric table and reaction line hand the
model figures in the reader's units; crisis floors are named in both units.
A unit change regenerates the stored texts. One server resolver
(
getReadingTransform) serves both preferences. - Days in the user's zone. Dashboard summary mood instant, Coach GLP-1 injection days, document and lab dates (noon UTC for date-only values), GLP-1 timeline side effects, the adherence storyline's last course day, the lab OCR duplicate check and environment date defaults; 84 further UTC day cuts replaced.
- Phone layouts. The lab list, values table and add-value sheet fit a phone; notification settings, admin app logs, AI settings, dashboard layout settings, the health record scope picker and the page header no longer overflow at 390 px; the lab chart axis starts at zero.
- ECG ingest (#1060).
averageHeartRateandsamplingFrequencyare accepted as numbers and rounded before storage; a refused body logs the refused fields (never values). - Backups. Account settings and the module selection are backed up and
restored column by column (
accountSettingssection; never credentials, identity or operational state).MenstrualCycle.absorbedIntoId,Workout.externalSourceVersion,NutrientIntakeDay.externalSourceVersionandNutrientIntakeDay.createdAtnow survive a restore. - Served, not re-derived. Blood pressure target zones come from the
server for the record being viewed and honour a custom target; log-intake
preselection uses
intakeActionable; late/missed thresholds come from the user's settings; Settings → Coach follows the record's module state. - Nothing dropped silently. Mood writes report
droppedTagKeys/droppedFactorKeys; OCR reportspageCoverage; unknown Google Health sleep stages are counted. - Dense reads. Targets (glucose, pulse), cycle insights, HRV and SpO2 series, the doctor report's HRV and SpO2 and the sleep list fold or bound their reads.
- Job queues. Every queue declares its runtime; long passes hold an advisory lock, an explicit expiry and a budget stop, so a retry after expiry cannot run beside the original.
Checks
- Unit literal sweep over display and AI code, a resolved-field consumer
guard,
healthlog/no-utc-day-keylint rule with reasoned exceptions, a time-zone harness (Los Angeles, Kiritimati), a dense-read guard over request handlers and raw SQL, a queue runtime guard, a mobile overflow sweep over every signed-in route, a full-column backup round-trip over 69 models plus theUserrow, and a meta-test requiring every guard to assert a non-zero match floor.
[1.39.6] — 2026-10-01
A critical security fix for the web framework, a workout token for devices and bridges, a lab scan that no longer drops incomplete readings, and a restore preview that appears at once. A full-history import can no longer run beside its own retry, and the nightly clean-up steps around a restore.
Security
- Next.js 16.3.6. Fixes a critical advisory against 16.2.0 to 16.3.5. The floors of fast-uri (3.1.8), ip-address (10.7.1) and dompurify (3.4.16) are raised for newer advisories; development-only copies of @grpc/grpc-js, brace-expansion and undici 7 too.
Added
- Workout token. Settings → API → Workout tokens mints a token with the
workouts:writescope (POST /api/tokens/workouts, recent confirmation required, at most ten live tokens). It reachesPOST /api/workouts/batchonly; workouts written with it carry the sourceEXTERNAL, and a body that names a source is refused with 422. (#1054)
Fixed
- Lab scan review. Saving is all or nothing over the selected readings; incomplete ones are marked with a summary and focus on the first, instead of being left out silently. (#1055)
- Restore preview. The counts a backup holds are stored with it (migration 0364) and the preview answers from them; older backups are read once and then stored. The query no longer retries, and only a real schema failure is reported as one. (#1031)
- Dashboard quick-add follows the server's
intakeActionableand no longer offers an ended course. (#1040) - Dense glucose inside 90 days is bucketed per local hour for the series when the window holds more than 10 000 readings; statistics cover every reading. The doctor report keeps glucose raw inside 90 days.
- Full-history import lane. Admission jobs expire after 16 hours instead of pg-boss's default 15 minutes, each run holds an advisory lock on its kind and account so a retry cannot run beside it, and the Google Health import stops at its job budget as incomplete. (#1023)
- Tombstone purge deletes per account under the restore lock and leaves an account being restored for the next night. (#1031)
[1.39.5] — 2026-09-28
A server with a per-minute heart-rate history stays inside its memory: the Google Health import writes page by page, and charts, insights and the doctor report read such a history in buckets. A restore of a large account holds far less in memory and no longer collides with a background job right after a start.
Fixed
- A per-minute heart-rate history crashed the server. The Google Health full-history import read the whole collection into memory and mapped it into a second list, and kept one entry per reading for the later rollup pass. On an account with 1.3 million readings that passed the default 1 GB heap within seconds. Each page is now written before the next is requested, and the rollup list holds one entry per type and day (Google Health and Fitbit). The heart-rate walk stopped at 1000 pages, which a per-minute stream passes in under two years; it then counted the import as unfinished and started over on every retry and start. It may now run to 10 000 pages. (#1023)
- Dense pulse inside 90 days. A pulse window with more than 10 000
readings is bucketed per local hour in the series route (the iPhone app's charts and API clients) (mean with low and high,
hour:ids), read per day for the doctor report, and the resting-pulse estimate on the insights page is computed per day in the database. Sparse pulse data stays raw. (#1023) - Doctor report readings. Where a type is drawn from day buckets, the summary counts its readings rather than its points, and the health-record export's latest observation is the newest actual reading, not the last day's mean.
- Restore memory. The measurement bulk insert writes at most 100 rows per statement. The database client keeps the parameters of its last hundred statements, so 1000-row statements held about 100 000 readings (about 60 MB) for the whole write; now about 7 MB. The Apple Health export import uses the same insert. (#1031)
- Restore right after a start. A restore could deadlock with the step consolidation folding the same account after a boot. The consolidation passes now take the restore's account lock in shared mode for each day and leave an account being restored for their next run. (#1031)
Dependencies
- pg-boss 12.34.0 (applies its own schema update 37 to 42 on the first start), undici 8.11.2, @tanstack/react-query 5.103.2, @types/node 26.6.2, CodeQL actions.
[1.39.4] — 2026-09-27
The Coach shows what it reads while it works, puts the figures behind an answer in a table you can copy or view as a chart, and offers follow-up questions. Workout GPS tracks and the phone numbers and addresses of your doctors are encrypted at rest, and the weather location is stored more coarsely. Restoring a backup of a very large account works again. An ended medication course no longer offers a dose, mental health is a medication category, and editing or restoring a medication keeps its category. The All range of a chart means one thing at every length.
Added
- The Coach shows what it is reading. While a reply is being worked out, a line above it names the current read ("Checking: Blood pressure, last 90 days"). Once the answer is in, the line collapses to "Looked at 3 sources" and opens to a list with the window and the number of readings of each read, or why it found nothing (no readings, none in this window, module switched off). The list comes back after a reload. It never contains a health value.
- Result tables under an answer. When the Coach reads a series it keeps the figures as a table: one row per day, week or month, the same numbers the app's charts show, at most 400 rows. The latest twelve rows show first, with Show all for the rest. Copy for spreadsheet puts the table on the clipboard as tab-separated text and as an HTML table (cells that a spreadsheet would read as a formula are quoted); Copy as text gives aligned columns. Tables the answer relies on show under it; the others sit under "Data used" in the evidence panel. A table copied from an earlier answer says so and names its date. Workouts by sport, sleep by night, adherence by day and the latest lab results come out as tables too.
- Charts for result tables. The server picks the chart from the table: a line for levels such as blood pressure, weight or pulse, bars for totals such as steps and for counts by category (horizontal above six categories, the top eight plus "Other"), and a histogram when you ask how often a value came up (at least ten readings). A table with fewer than two rows gets no chart. A toggle switches between chart and table.
- Follow-up suggestions. Up to three buttons under the latest answer: show as a chart or as a table, compare with the period before or with a year earlier, look further back, or a related metric the Coach found a connection to. Each is offered only when your record holds what it asks for. "Show as a chart" and "Show as a table" are answered from the stored table, without a request to your provider and without using tokens. A suggestion tapped after a newer answer arrived is sent as a plain question. Settings, Coach, "Suggest follow-up questions" turns them off.
- Clarifying questions. When a question could mean more than one metric or time window ("my pulse" with resting, walking and spot pulse all recorded), the Coach may ask back with up to four choices above the message box, or you type your own answer. Metric choices are only ones your record holds, the Coach never asks about doses or diagnoses, and it never asks twice in a row.
- How this was worked out. The evidence panel has a line naming the sources, windows, reading counts and how they were combined (daily averages, weekly totals, latest reading), written by the server. When you question a figure, the Coach reads it again and corrects the answer if the figures differ.
- Keep looking. When the Coach reached its limit of reads for one answer and had to stop, a Keep looking button continues the same question, reusing what it already read. An answer is continued once.
- Mental health as a medication category for antidepressants, anxiety and ADHD medication and mood stabilisers, in the add and edit form and on the card, in all seven languages. Thanks to @Cnote43 for #1041.
- API.
GET /api/insights/chat/{id}/messages/{messageId}/resultsreturns a reply's tables, owner only and not gated on the Coach being available; a table whose module is switched off comes back as{ ref, withheld: "module_disabled" }, one that cannot be read aswithheld: "unavailable".POST /api/insights/chattakes optionalfollowUp: { messageId, id }andclarification: { messageId, choiceId? }. The stream has four new frames,step,result,followUpsandclarification, in the orderstep*,token*,provenance,result*,suggestion,suggestedAction,clarification,followUps,done; older clients ignore them.metricSourcegainssteps,method,results(metadata only),followUps,clarification,forcedFinalandcontinuationOf. A reply answered from a stored table hasproviderType: "reuse". Coach settings takefollowUpChips(absent means on). The medication list and detail reads carrycourseStatus(UPCOMING,CURRENT,ENDED) andintakeActionable. The stream frames are now in the OpenAPI document, includingsuggestedAction, which was missing.
Changed
- The All range of a chart shows the average day of each week or month. A long history is drawn from weekly or monthly figures. For steps, energy, distance, flights and daylight such a figure was the whole week's or month's total, while a short history showed a daily average, so a month of steps read about thirty times higher once the history was long enough. Every length now shows the daily average, and the caption says "Daily avg per week" or "Daily avg per month". For levels such as blood pressure each day weighs the same, so a day with many readings no longer outweighs the rest.
- The sidebar collapse control is at the bottom. It is a full row labelled Collapse above Admin, or above Settings without Admin. The saved choice carries over, and the width change respects reduced motion.
- A booked appointment reminder reads the practice address when it is sent, so changing the address in the address book also reaches a reminder that is already booked.
- A restore that cannot fit its time limit is refused up front and the message names both the readings in the backup and the readings the account holds now, since clearing those takes time too.
- An edit of a practitioner's phone number or address is logged by field name, without the old or new value.
Fixed
- Restoring a backup of a very large account failed. On an account with close to two million readings, most of them folded into hourly averages and waiting for the nightly clean-up, the restore stopped while clearing the current data because every database request is limited to 60 seconds. Nothing was lost, since a restore is all or nothing. Deleting the readings was slow because the database checked personal records and ECG recordings once per reading without an index; both are indexed now, which also speeds up the nightly clean-up and deleting an account. The restore now runs under its own time limit, which grows with the readings in the file and in the account, and the background job may run for six hours instead of two. On a test account of 1.89 million readings with the database held to half a processor core and 40 MB/s of disk writes, the restore finished in about a quarter of an hour with every reading identical to the backup. Thanks to @mills1975 for #1031.
- Charts were empty after a large restore until the next restart, because rebuilding the chart summaries ran into the same 60-second limit. The rebuild has its own allowance.
- Restores skip deletion markers older than 75 days. They are what the nightly clean-up removes anyway. Younger markers are restored as before and the backup file is unchanged. On the test account, with 1.68 million old markers, the restore took 5.4 minutes instead of 15.
- A failed restore now says why: it ran out of time, waited on records something else was changing, the database rejected a record, the database ran out of disk space or memory, or the connection dropped, each with what to do next. The full error stays in the server log and the audit log.
- An ended medication course offered taken and skip. The table, the cards and the take-all button now leave it out and show an Ended badge; a course that ends today keeps its buttons for the whole day on your clock, and no overdue marker is raised for an ended course. The detail page called the last day of a course ended while it was still running. A missed dose can still be added from the history. Thanks to @Cnote43 for #1040.
- Editing a medication on the web reset its category. The form had no choice for thyroid, digestive, skin or sleep medication, so such a medication opened on Other and saving any change stored Other. The form lists every category now, and an edit sends the category only when you changed it.
- Backups did not carry a medication's category, so after a restore every medication read as Other. Both backup formats carry it now and a restore writes it back. Backups made before this release still restore without categories; take a fresh backup after upgrading.
- Long-range charts named weeks and months one early west of UTC. Weekly and monthly points are now placed inside their own week or month in every zone.
- East of UTC the Coach could name a night by the day before, because it dated a night by its wake-up time in UTC. It uses the day the night ends on in your zone, the same day the sleep table shows.
Security
- Workout GPS tracks and practitioner phone numbers and addresses are encrypted at rest. Both were stored readable, and a track usually starts and ends at home. Existing rows are encrypted in the background after the first start and again every night for anything an older server wrote meanwhile; until then they are read as before. The map, splits and GPX download of a workout and the address book behind visits and vaccinations look the same. Name, practice and specialty stay readable, because the picker searches and sorts on them. The same pass removes the readable copies left in activity log entries of earlier address book edits and in booked appointment reminders. Key rotation covers the new columns. A disaster-recovery backup carries the phone number and address encrypted, a portable export carries them readable, and a restore of either stores them encrypted. Workout tracks are still not part of a backup.
- Coach result tables are encrypted at rest like the conversation. Only their titles, sources and row counts are stored readable. They are deleted with the conversation or the account, carried in both backup formats (encrypted in a disaster-recovery backup) and covered by key rotation.
- The weather location is stored to about 11 km instead of about 1 km. Home, travel periods and the daily weather rows are rounded to one decimal of a degree on every write and on restore, and the upgrade rounds what is stored. The weather data comes in cells of 9 to 25 km, so readings stay practically the same.
- A German reply telling you to change a dose could pass the dose check. "Erhöhe auf 2,4 mg" was caught, "Erhöhe deine Dosis auf 2,4 mg" was not. Both are caught now, in the Coach and in the other written texts that pass the same check.
- An earlier table of a metric you have since excluded from the Coach, or whose module you switched off, is not shown to the Coach again, and the list of earlier tables it sees leaves them out.
- Paired apps start over after a restore or after deleting all data.
The change feed answers
cursorExpiredto a bookmark issued before a restore, Delete all data or the admin Wipe all data, so an app loads the account afresh instead of missing what the restore replaced.
Upgrade notes
- Take a database backup before upgrading.
- Migrations:
0360adds the encrypted column for Coach result tables,0361adds encrypted columns for workout tracks and practitioner phone and address (the readable track column becomes nullable) and rounds stored weather coordinates to one decimal,0362indexes the two columns that point at readings,0363addssync_reset_atto users. Only0361changes data, by rounding coordinates. - Going back to 1.39.3 is not supported once the server has started: it cannot read tracks and contact details that are stored only encrypted. Restore the database backup from before the upgrade instead.
- The background encryption of existing tracks runs after the first
start. A large archive of outdoor workouts takes a few minutes. Keep a
retired
ENCRYPTION_KEYSentry until Admin, Encryption lists nothing for it. - A restore job may now run for up to six hours. No new settings.
- Backups made before this release restore without medication categories. Take a fresh backup once the server is up.
- iPhone app 1.0.3 and 1.0.4 keep working; the new stream frames and fields are additive. The app does not show the Coach's steps, tables or suggestions yet. Editing a medication in the app still stores Other for a category the app does not list (diabetes, antibiotic, mental health).
[1.39.3] — 2026-09-27
Documents can be picked straight from Paperless-ngx and Papra. Exports, share links, tokens, invitations and new second factors ask for a recent confirmation, and sign out everywhere reaches every way into the record. Medication days west of UTC, long-range figures and synced daily totals land on the right day. Deleting an account removes its off-site copies, restores check the keys they need, and the privacy wording says what is and is not encrypted.
Added
- Document picker for Paperless-ngx and Papra. Settings, Integrations
has a Document archives card (address, API token, for Papra the
organization id); the connection is tested before it is saved, the
token is stored encrypted and never shown again, and it is sent only to
the origin it was saved for. The Documents page and the document link
dialogs on visits, vaccinations and conditions get an Import button:
search by name, filter by tag or date, tick documents, and each is
copied with its title and date and linked where the picker was opened.
Documents already imported are marked and linked instead of stored
twice; documents deleted in HealthLog are marked and not brought back.
Picked documents follow the owner's automatic AI reading setting. The
archive is contacted only on search or import, never in the background.
Off until the operator sets
DOCUMENT_SOURCE_ORIGINS. Paperless-ngx 2.16 or later; a Papra key needs read access to documents and tags. Seven cookie-only routes under/api/documents/sources(a Bearer token gets403 documents.sources.browserOnly). Thanks to @AntonPalmqvist for #1038 and to the self-hoster behind #938. - Source instance for imports.
POST /api/documents/inboundandGET /api/documents/inbound/sourcetake an optionalsourceInstance(the archive's address, normalised to its origin), so the same id in two archives is two documents. Keys stored without it match any instance, as before. The import script sends it and skips files larger than the server accepts once it has learned the limit.sourceIdrefuses.and... - Body site in the FHIR export. A condition's site goes out as
Condition.bodySite.text("knee (left)"); a recorded side also goes out as a containedBodyStructurewith an active SNOMED CT laterality qualifier (7771000 left, 24028007 right, 51440002 both). Applies to the health-record download,Patient/$everythingand the clinician share link. Thanks to @Cnote43 for #1025. - MCP search and fetch cover visits, conditions, documents and
vaccinations. Ids
visit:<id>,condition:<id>,document:<id>andvaccination:<id>;fetchreturns fields inmetadata(unrecorded valuesnull) and linked records inmetadata.links. Matching covers visit type, reason, outcome, body site and side, practitioner and specialty, condition name, document title, file name and type, and vaccine name, with German and English body-part names treated alike. Documents are searchable by their indexed text andfetchreturns a short excerpt, never the file, a link or the AI summary. A document held back from AI reading answersmetadata.reason: "ai_read_deferred"with no excerpt. Condition and vaccination notes are never sent. The consent page says all of this. Thanks to @Cnote43 for #1025. - Low-supply notice for as-needed medications. Sent once when two
doses or fewer are left, re-armed by a refill, naming the units left
rather than a date. Push metadata carries
asNeeded: true,runwayDays: null,triggerDays: null. - Switches for the remaining outbound requests.
PASSWORD_BREACH_CHECK_DISABLEDstops the HaveIBeenPwned range check (known leaked passwords are then no longer refused);UPDATE_CHECK_DISABLEDstops the daily GitHub update check, and/api/version/check-updatesanswersstatus: "unknown"withreason: "disabled". Both default to on and are on the compose whitelist. - Admin, Encryption lists the keys backups still need. Per key, the stored backups and off-site copies written under it; the "safe to drop" badge appears only when backups are clear too. Backups written before this release show as unrecorded.
BACKUP_ENCRYPTION_PREVIOUS_KEYSkeeps retired off-site backup keys readable, soBACKUP_ENCRYPTION_KEYcan be rotated.POST /api/auth/reproof(cookie only) takes the step-up body and counts as a recent confirmation for five minutes.
Changed
- Sign out everywhere ends everything that can open the record. Besides
browser and phone sign-ins it now revokes connected AI assistants, API
tokens (except the caller's own access token), clinician share links
(kept with
?keepShareLinks=1, a checkbox in the dialog) and invitations nobody has accepted. Accepted access is kept and listed on the sessions card with End access.DELETE /api/auth/me/sessionsaddsaccessTokensRevoked,connectorsRevoked,shareLinksRevoked,pendingInvitesRevokedandgrantsKept. - A password change revokes connected assistants and share links too.
- Turning on the first second factor signs out other devices. A second key added later does not.
- Password sign-in is limited per account without locking it. From a
place the account has not signed in from, five wrong passwords a day are
free, then each doubles the wait from 30 seconds up to 15 minutes. A
remembered browser, a phone whose
X-Device-Ida login of this account was issued to, and an address used by this account in the last 30 days are never held back. The per-address limit of five in fifteen minutes is unchanged. Answers are429withRetry-After. - Request bodies are capped at 1 MB before authentication (was 512 MB).
Apple Health archives, backup uploads, documents, CSV and dose history
imports, lab scans, profile pictures and the app's measurement, workout,
ECG, mood, medication and cycle batches read their own bodies after
authenticating, with their own limits enforced while reading (
413). X-Real-IPis read only withTRUST_PROXY_HOPS=1(the default) and only whenX-Forwarded-Foris absent. With0, or2and more, it is ignored. The log says once when a proxy header arrives that the setting does not allow reading, naming the header and the setting.- CSV re-import. An unchanged row with an
externalIdis left alone and reportedskippedwith reasonduplicate(wasupdated); lookups and writes run in small batches. GET /api/analytics/range. For step-like totalsmeanis the average daily total andcountthe number of days; forSLEEP_DURATIONit is the average night asleep,countthe number of nights,granularitylive.- Synced daily totals from Withings, Fitbit, Google Health, Polar and
Oura are stored at noon in the user's zone (was noon UTC). The
externalIdis unchanged; rows the next sync rereads move and reach the change feed as updates. Fitbit weigh-ins without an offset are read in the user's zone. - Check-up
anchorDateaccepts a bareYYYY-MM-DD, read as that day in the profile zone; the web form sends it. Responses keep the date-time. - "Send raw data" is now "Send detailed history" and says what it sends: daily averages for 90 days, weekly for the year before, monthly up to five years, each with its date and reading count.
- Encryption wording. The app, the Coach and the README said all health data is encrypted at rest. Notes and other free text, documents, questionnaire answers, AI-written text, the Coach conversation and every access token are; measurement values, lab values, medication names and mood scores are not. The wording now says so. Storage did not change.
- Local lab OCR is served from the instance. The engine and its German
and English data (about 17 MB) come from the server instead of a public
CDN, which the page policy blocked;
/labs,/documentsand the OCR worker may run WebAssembly. - The image ships
scripts/reset-password.mjsand the intake repair script from the operator guide. Key rotation is documented as Admin, Encryption, Rotate now. IP_GEO_LOOKUP_DISABLED, like every on/off setting, accepts1,true,yesandon..env.production.exampleis the complete settings list;.env.exampleis a starter and no longer lists the unusedWITHINGS_CLIENT_IDandWITHINGS_CLIENT_SECRET.
Deprecated
ALLOW_LOCAL_AI_PRIVATE_HOSTS=true. It now covers only the admin AI key and AI settings saved on an admin account, over a pinned connection without redirects, and will be removed in a later release. Other accounts' private endpoints needAI_PRIVATE_ORIGINS. The host-list form (ollama.lan,10.0.0.5) keeps working with the same pinning. The boot summary names the setting, and the admin AI settings page shows theAI_PRIVATE_ORIGINSline for the endpoints saved on the instance.
Fixed
- Medication days west of UTC. A Monday plan reminded on Sunday, a course starting on the 14th had a dose on the 13th, and an evening dose on the last day could be dropped; monthly and one-time doses were a day early too. Start and end dates are read as calendar days and the times placed on them in the user's zone, for the list, today list, reminders, history and adherence. History for affected weeks can show the wrongly named day as an off-schedule entry beside the planned day.
- Weeks on and weeks off count whole weeks from the start day instead of Sunday-based UTC weeks; the pause of a plan that did not start on a Sunday can move.
- A snooze holds only the dose it answered. Skipping the morning dose from Telegram no longer silences the evening dose. A snooze set while no dose is open holds the next one.
- Check-up reminders with several times of day are sent at each. The evening half of the morning-and-evening blood pressure course waited until the next morning.
- Supply estimate. Honours
INTERVALon daily, monthly and yearly rules, several days of the month or months of the year, every weekday a monthly rule names (every Monday counts four or five times; the first Monday once), the pause of an on/off plan, and as-needed schedules as not consuming. An injection every third month with two doses left reads about 180 days, not 60. Whole-dose counts no longer lose one to rounding (2.4 mL at 0.8 mL is three doses). - The doctor report keeps same-named medications apart, in the PDF, the FHIR export, the clinician link and the AI summary.
- A doctor report for a past period stays inside it and compares course dates by day in the report's zone, so west of UTC it no longer drops a course ending on the first day or lists one starting after the last.
- Weekly, monthly and yearly figures pick the source per day instead of once per bucket, so mixed manual and synced readings and a device switch mid-month count every day. Affects year and All ranges, baselines, the one-year change and assessments.
- "vs. last year" is the average of the 30 days starting a year and a month ago, not whichever month began inside that window.
- All-time average, minimum and maximum count one source per day for histories older than five years, and so do the all-time figures the assessments and the Coach see for weight, blood pressure and pulse.
- The logging streak was one day short west of UTC in the evening.
- The activity part of the health score uses the 28 completed days up to yesterday, so it no longer dips every morning.
- Heatmaps, digest milestones, assessments and mood and cycle comparisons use the user's day instead of the server's or Berlin's.
- Days that start at a skipped midnight (Chile, Cuba) or on a clock change in New Zealand and eastern Australia start at their first real minute.
- Synced daily totals from UTC+12 and beyond showed a day late. The next sync fixes the recent days, about the last month for most services; reconnecting Fitbit or Google Health rereads the whole history. Oura cycle phases are fetched over the user's days as well.
- Apple Health export import split a day on which the phone or watch updated and kept only the larger half of its steps. Importing the export again fixes stored days.
- Step and sleep period comparisons compare average days and nights instead of single readings, and end with yesterday.
- Long ranges showed no data when all readings fell in the first, partial year; they fall back to monthly figures.
- Assessments refresh after readings from every source (assistant, Telegram, CSV, backup import, Apple Health export), not only in-app entry.
- CSV re-import of a few thousand rows timed out. A re-import that moves a reading recalculates the day it left, and a failed import recalculates the days it wrote; uploading the same file again finishes it.
- A queued Apple Health import lost its upload when it waited longer than seven hours; the cleanup keeps files a queued or running import needs.
- Empty settings from the compose file count as not set: weather and
daylight lookups, the login-location lookup,
TRUST_PROXY_HOPS, and the Withings, Fitbit and Google Health redirect URIs. - The first due date of a check-up no longer moves a day when the browser is in another zone than the profile.
- The password reset command failed with "Cannot find module" in the published image.
- FHIR export codes. Body water and bone mass use the body-composition codes; step length, walking speed, walking asymmetry and VO2 max go out under their Apple Health names; mood (now outside vital signs), estimated A1C and average cycle and period length go out as named values without a code; LDL, vitamin D and eGFR use the method-less codes; mean glucose is coded (in mg/dL) only from a continuous monitor; allergy and injury use current SNOMED CT concepts; a medication code no longer carries the user's name as its display; resting heart rate and oxygen saturation add the required vital-signs codes; an export without vital signs has no empty vital-signs report. No longer sent: 73704-9, 73708-0, 41955-6, 41957-2, 91557-1, 96402-2, 76542-6, 41995-2, 64700-8, 64698-4, 18262-6, 1989-3, 33914-3, SNOMED CT 106190000.
- MCP tools follow the module switches. With Medications, Labs,
Conditions, Documents or Vaccinations off, search and fetch skip them and
get_labs,get_medication_schedule,get_medication_complianceand the lab and medication resources answerpresent: false,reason: "module_disabled";get_visitssetsconditionsReason: "module_disabled".
Security
- Recent confirmation for actions that outlast a session. Whole-record
exports (full backup, encrypted backup, JSON and CSV of everything, the
doctor report, FHIR
$everything), share links, API and MCP tokens, invitations, connecting an AI assistant, and the admin backup download, upload, restore, delete all data and reset password need a sign-in or confirmation within five minutes, with the second factor on an account that has one. Otherwise401 auth.reproof.requiredwithmeta.methods, and the app shows a confirm dialog and continues. The encrypted backup now asks accounts without a second factor too. On a token,GET /api/export/full-backupandGET /api/export?type=allneed anX-Step-Upelevation andPOST /api/export/encrypteda second-factor one on accounts that have a second factor; the doctor report (POST /api/export/health-record) and$everythingkeep taking the token. Migration0359. - Adding a second factor needs a recent confirmation. Without a second factor: a sign-in within five minutes or the password. With one: that factor or a passkey, never the password alone, also for passkeys and on the token path. An authenticator never confirmed with a code no longer counts as a factor anywhere.
- Confirmation attempts share one limit of five per fifteen minutes per account, including password change and email change; each wrong attempt is in the security activity and a correct one is not counted. Second factor attempts at sign-in are counted before they are checked.
- Single sign-on links an existing account only after its own sign-in
(password and second factor, or passkey) within ten minutes, also on
SSO-only instances. The app receives
oidc_link_required. Existing links stay. - Changing the email address needs the current password or a sign-in
within five minutes; on an account with a second factor, that factor or
a passkey. The rest of the save still lands; the address is reported in
rejectedFields. - Private AI endpoints by exact origin. New
AI_PRIVATE_ORIGINS(comma-separatedscheme://host[:port]). Granted endpoints are called over a pinned connection without redirects; metadata, link-local and unspecified addresses stay refused. A failed lab scan no longer logs the endpoint's answer. - Coach conversation titles and custom-metric notes are encrypted at
rest. Existing rows are encrypted in the background after start;
reads fall back to the old column until then. Migration
0357. - Deleting an account, a managed profile or all data removes its
off-site backup copies, right after and again the next night; refused
deletions are retried and shown on Admin, Backups, Off-host, which also
warns when the bucket has no lifecycle rule. Admin Wipe all data leaves
the bucket alone. Migration
0358. - Restores check the keys a backup needs. Preview, restore, upload and
the monthly drill refuse a backup whose encrypted content needs a key the
server lacks (
backup.key.missingwithkeyIds), before changing anything. A restore that would reference another account is rolled back (backup.foreign_reference). - Off-site copies are bound to their account and date (envelope version 4 with a key id); older copies still restore.
- Apple Health imports. An archive may inflate to 200 times its size on
disk (64 MiB to 8 GiB); one import runs per account, and a second one
answers
409import.apple_health.busywithmeta.jobId; the upload is removed on every exit and swept every fifteen minutes; a job that cannot be queued answers503and is marked failed. The admin backup upload stops at 100 times its compressed size (256 MB to 4 GB) with413. - Every credential the server issues, invite links and the sign-in handoff code included, is kept out of logs and the replay cache.
- A passkey or security-key challenge is used up when checked.
- A device-bound refresh token is refused without
X-Device-Id. - Upload routes that read their own body send the same security headers as the rest.
Upgrade notes
- Take a database backup before upgrading.
- Migrations:
0356adds document archive connections and a nullablesource_instanceto the document import tables,0357adds encrypted columns for Coach titles and custom-metric notes,0358records the keys each backup needs and adds off-site purge requests,0359addsreproof_atto sessions. None deletes anything. - Going back to 1.39.2 is not supported once the server has started: it cannot read Coach titles stored only encrypted. Restore the database backup from before the upgrade instead.
- Off-site backup: the bucket credential needs
DeleteObject(an R2 Object Read & Write token has it). To rotateBACKUP_ENCRYPTION_KEY, move the old key intoBACKUP_ENCRYPTION_PREVIOUS_KEYS. - Key rotation does not reach inside earlier backups. Keep a retired
ENCRYPTION_KEYSentry until Admin, Encryption lists nothing for it. ALLOW_LOCAL_AI_PRIVATE_HOSTS=true: nothing changes for the admin key or an admin account. If other accounts use an AI endpoint on your network, copy theAI_PRIVATE_ORIGINSline from the admin AI settings page, restart and remove the old setting.TRUST_PROXY_HOPS: with a proxy that sends onlyX-Real-IP, set1(unset means1); a host reachable directly wants0. Check the log for a line starting[getClientIp] requests carry.- Request bodies over 1 MB are refused outside the upload routes. Keep any upload size limit in your reverse proxy as it is.
- New settings, all on the compose whitelist:
DOCUMENT_SOURCE_ORIGINS(empty, picker off),AI_PRIVATE_ORIGINS(empty),BACKUP_ENCRYPTION_PREVIOUS_KEYS(empty),PASSWORD_BREACH_CHECK_DISABLEDandUPDATE_CHECK_DISABLED(unset, checks on). - Users are asked to confirm before exports, share links, tokens, invitations and connecting an assistant when their last sign-in is older than five minutes. After a password change or sign out everywhere, share links have to be made again.
- The image is about 17 MB larger for the local OCR engine.
- Past medication history west of UTC is not rewritten; a wrongly named day stays as an off-schedule entry and can be deleted by hand.
- iPhone app 1.0.3 and 1.0.4 keep working. Reminders, supply figures, aggregates and synced rows change value without a schema change.
[1.39.2] — 2026-09-26
Documents can be brought over from Paperless-ngx and Papra, conditions get a body site with a view of everything filed at one place on the body, open check-ups stay due after their reminder, and backups of very large accounts work in the standard 1 GB container.
Added
- Document import from Paperless-ngx and Papra. Settings, API &
Tokens has a Document tokens card. A document token (scope
documents:write, 365 days, at most ten live per person) can upload to your own vault and do nothing else; its upload answers only{ id, duplicate }.scripts/import-documents.mjscopies every document with a chosen tag from Paperless-ngx 2.16 or later or from Papra, with title and date, and supports--since, a dry run and--ai-read. A Paperless-ngx 3.0 workflow can post new documents directly. Each document remembers its source system and id, shown on the document as "Imported from … · ID …". Re-sending a known source id answers200as a duplicate; a source id whose document you deleted answers200withdeleted: trueand stores nothing, also after the document is purged. A file you already had is recognised by its content, and up to 20 extra source ids are remembered per document (409 documents.inbound.sourceAliasLimitpast that).GET /api/documents/inbound/sourceanswers whether a source id is known without sending the file; it and a source id in the upload's query string share an allowance of 5,000 an hour. Uploads by a document token have their own hourly limit,DOCUMENT_UPLOAD_LIMIT_PER_HOUR(default 120, 1 to 1000), so an import never holds up your own uploads. Thanks to @AntonPalmqvist for #1038 and to the self-hoster behind #938. - Hold AI reading back for imports. An upload with
aiRead=defergets a preview and a local text index but is not summarised, not staged for lab import and not sent to a provider by any automatic path, including the summary catch-up after switching auto-read on and Index all for search. Read with AI or Generate summary on the document ends the hold. The import script defers by default. - Body site on conditions. A condition takes a body site in your own words, stored encrypted, and an optional side (left, right, both), with suggestions from sites already used. Its page links the site to the new Body sites tab.
- Body sites tab in Checkups. Lists every body site across visits and
conditions, grouped regardless of spelling case. Picking a site, and
optionally a side, lists the visits and conditions there with their
linked documents, lab results and visits. Left or right includes both
sides. Conditions appear only with the illness module on and, for
someone you share with, only with Illness in their access; links they
cannot open show as placeholders.
GET /api/body-sitesserves it. Thanks to @Cnote43 for #1025. - Body site in the doctor report, share page, Coach and MCP. The
conditions table gets a Body site column when at least one condition
has a site; the clinician share page, the MCP doctor-visit summary,
get_illness_recoveryand the Coach's illness context carry it too.
Changed
- Stored backups are kept in encrypted pieces. The weekly and manual
backup is stored as sealed pieces of about 1 MiB, each checked before a
restore, preview or download releases anything, so the size of a backup
no longer depends on the app's memory. The limit derived from the heap
(about 105 MB in a 1 GB container) is gone;
BACKUP_MAX_STORED_MB(default 2048) is the only one left. On a test account of 2.6 million readings in a 1 GB container: backup 87 s, preview 20 s, download of the 1.4 GB file 25 s, restore about 4.5 minutes, every reading matching. Thanks to @mills1975 for #1031. - A backup is not replaced while it is being restored. The weekly and
manual backup skip an account whose restore is queued or running; the
next run catches up. Two backups of the same account at once run one
after the other. A backup replaced while it is read answers
409 backup_changed("replaced by a newer one while it was being read") instead of an error that looked like tampering. - Key rotation converts older backups. Backups stored as one value before this release, uploaded ones included, are converted into pieces under the new key, one at a time and with little memory, keeping their date. A backup that fails its check is left unchanged and counted as an error.
- SimpleWebAuthn 14 and React 19.3. The passkey algorithm list is pinned, so existing passkeys keep working.
Fixed
- A reminder moved an open check-up to its next date. Sending a
reminder advanced
nextDueAtby a full cycle without the check-up being done, so a questionnaire every 14 days read as due in two weeks and a yearly check-up as due next year, and neither showed on the start page. A reminder that repeats less often than weekly, once only, or has no occurrence left now stays due until a qualifying reading, done, skip or snooze, and is resent at most once a week while open (newlastNotifiedAt). Weekly or more frequent reminders roll on as before. Migration0355returns reminders moved this way in the last 90 days to the day they were reminded, only where the notification record shows it unambiguously. - Editing a check-up rescheduled it. Saving any change recomputed the due date from now, and the web form resent its first date shifted to the browser's midnight. Only a change to the interval, rule or first date (compared by calendar day) reschedules now; a notify-hour change keeps the day, and switching a check-up off and on keeps an open slot.
- The start page missed check-ups and visits due today. Check-ups due today show by name (up to three, then "+N") from the morning of the due day; every visit booked today stays for the whole day, with the next day within 48 hours as a second item; both always keep a place among the three items. Layouts saved before visits were a start page item get them switched on once.
- "Overdue by 1 days" reads "Overdue since yesterday".
- A restore changed Apple Health daily averages. Restoring a backup marked daily averages from the nightly clean-up as coming from an unknown older source; they now come back exactly as saved.
- Key rotation counted every weekly backup as an error. Since 1.38.6 rotation could not re-encrypt backups written as one stream, so it never finished clean.
Upgrade notes
- Take a database backup before upgrading.
- Migrations:
0352stores backups in pieces,0353adds the source of imported documents, the deferred-reading marker and the tables that keep deleted imports deleted,0354adds body site and side to conditions,0355addslast_notified_at, repairs check-ups a reminder had moved on and switches visits on for older start page layouts. None deletes anything. - Going back to 1.39.1 is not supported: it cannot read backups stored in pieces. Restore the database backup from before the upgrade instead.
- 1 GB is the minimum memory for the container that runs background jobs; in 512 MB no backup can run.
- If a key rotation reported errors for
data_backupsbefore, run it again after updating, before retiring the old key. - The API changes are additive, and iPhone app 1.0.3 and 1.0.4 keep
working. A condition edit that leaves out
bodySiteorlateralitykeeps the stored values.
[1.39.1] — 2026-09-25
Medication history stops counting misses from before a medication existed, and large accounts can run their nightly jobs, export and restore again. A restore now runs in the background with progress. New: a procedure history, medications kept as a record without tracking intake, and readings from home-automation bridges marked as coming from an external device.
Added
-
Keep a medication as a record. Every medication has a new Track intake switch at the top of its Schedule tab and on the last page of the medication form. With it off, the medication keeps its dose, start and end dates and schedule, and you can still read and change them, but nothing is due: no reminder on any channel, no dose on the doses card or in the today list, no Taken and Skip buttons. It is left out of adherence on the medication card, the dashboard, the doctor report and in the Coach, and the list marks it "Record only". Doses you logged before stay in its history. Switched back on, it is tracked from that moment and the days it was off never count as missed. Every medication starts with tracking on. Thanks to @Cnote43 for #1033.
-
A history of procedures and surgeries. A visit can be of the new kind "Procedure or surgery", with a body site in your own words and an optional side (left, right or both). The body site is stored encrypted. Checkups has a third tab, Procedures, listing every procedure newest first, with a search by body site that also looks at the reason, so an operation recorded earlier as a general visit still turns up. An existing visit can be switched to the new kind without typing anything again. The doctor report has a new Surgical history section covering your whole history; the standard selection includes it, while a selection you saved earlier keeps what you chose. A share link shows it when the link includes it. Thanks to @Cnote43 for #1025.
-
Sleep stages can be sent by name. A bridge such as Tasker or Home Assistant can post one stage to
POST /api/measurementswithtype: "SLEEP_DURATION", asleepStageofIN_BED,ASLEEP,AWAKE,CORE,DEEPorREM, and eitherstartDateandendDateormeasuredAt(the end) andvalue(minutes). It is stored and deduplicated like the stages the iPhone app sends, and a repeat answers200withstatus: duplicate. Sent with the ingest token, the stage is stored with the sourceEXTERNAL. Thanks to @ocurero for #972. -
Long background jobs show up in the app log. A job that runs for more than a minute writes a line naming its queue when it finishes, and one every two minutes while it runs, with how far it has got and how much memory the app uses. A job past its time limit writes a warning, and after a restart the log names every job the restart cut off.
-
Out-of-range upload entries say which limit applied. An entry in
/api/measurements/batchskipped asvalue_out_of_rangenow carriesconvertedValueandrange(min,max,unit), so a value sent as a fraction where a percentage was expected is easy to spot. -
A rejected measurement upload names its reason. The validation
422from/api/measurements/batchcarriesmeta.errorCode: "measurement.batch.invalid", so a client can tell the batch will never be accepted as sent. -
Units per dose like 1½. Units per dose used to take a whole number or one of ¼, ⅓, ½, ⅔ and ¾, so one and a half tablets could not be saved. Any value above 0 and up to 100 with up to four decimal places now works, for the medication, for a single dose time and for the packaging settings on the supply tab, which before could not be saved for a medication taken as half a tablet. The form keeps its buttons and adds Other, which reads 1.5, 1,5, 1 1/2 or 1½ and shows what will be saved. The card shows 1½. Thanks to @gesus14 for asking in #1034.
Changed
- Readings sent with the ingest token are marked as coming from an
external device. They used to be stored as manually entered, so later
nobody could tell a reading from the scale from one typed in by hand.
They now carry the source
EXTERNAL, show an "External device" badge and can be picked out with the source filter in the measurements list. You can still correct them. A request that names anysourceat all is refused with422; the server decides the source from the token, and the Home Assistant example sends none. Readings pushed before this update stay manual, because nothing tells them apart. On a day with both a typed and a bridged reading, the daily totals, charts, health score and doctor report count the typed one; before, both counted and could inflate a running total. A re-push of entries sent before the update can create duplicates once. Thanks to @Antiheld86 for #892. - A restore runs in the background. Restoring a large account takes a few minutes, and a reverse proxy that closes a request after 60 seconds (nginx does by default) showed an error while the restore finished on the server. After you confirm a restore, the backups page shows the current step and how many readings have been written, and the progress survives leaving or reloading the page. The account shows its current data until the restored data is written in one go. Only one restore per account runs at a time. A failure says why and what to do next, and the backup stays on the list so you can start again without uploading the file. A restore interrupted by a server restart before its data is written starts again once by itself; a second interruption is left for you to restart. A failure after the data is written, while charts are rebuilt, is never run again: the data is restored and the charts catch up by the next night.
- A job that runs out of time says more on the admin page. It names how long the job had and that it was either too slow for that limit or the app restarted while it ran, instead of only "job timed out".
- Backups of several hundred megabytes can be uploaded. The admin page
compresses the file before sending it,
.json.gzfiles are accepted as they are, and the limit is 512 MB. The off-host restore script can write a compressed file for this.
Fixed
-
All-time averages and counts no longer count some days twice. Since 1.37.19 the all-time figures combined the stored daily summaries with the raw readings older than five years, and a day that had both was counted in each. Long histories were affected most, and one more day every day, as days aged past the five-year mark. The all-time mean and count on a metric's insights page, the device and resilience tiles, the glucose reading count and the dashboard summaries now count every reading once, so some of these figures change after the update. Nothing needs to be repaired in the stored data.
-
Medication history before a medication was added. If you added a medication at 4 pm and then logged the 2 pm dose, "Last 90 days" showed it as off-schedule and due at the evening slot, and listed a morning slot from before the medication existed. The dose now counts for its own slot, and slots from before you added the medication appear only if you logged something for them. The adherence rate, the calendar, the doctor report and the Coach follow the same rule; the doctor report and the Coach used to count those earlier slots as missed. The morning slot no longer turns into a forgotten dose a day later and breaks your miss-free streak, and migration
0350puts back the ones already marked that way. Thanks to @sreeramachandramurthy for the report and the screenshots (#1028). -
A portable restore keeps when a medication was created. It used to date every medication to the day of the restore, and missed doses in the restored history dropped out of the adherence rate. Backups exported from 1.39.1 on carry the date. For older ones the restore uses the earliest date the backup records for that medication, such as its first dose or the start of its first schedule, and never a date after the restore.
-
The adherence calendar no longer counts a skipped or missed entry that belongs to no scheduled time as a dose taken.
-
A dose attributed to a slot by hand reads the same as one logged there. A dose taken shortly before the slot's window counted as on time on its own but as late once attributed. The confirmation now says what attributing does.
-
Nightly jobs finish on large accounts. On an account with more than a million readings, for example after a multi-year Apple Health import, the nightly clean-up of heart-rate, HRV, blood-oxygen, step and energy samples and the backup could fail with "job timed out": the clean-up loaded a type's whole history into memory and the backup built its whole copy in memory, and the app ran out of memory and restarted. The clean-up now works one day at a time with about a third of the memory, and when it runs short of time it stops and continues where it left off. The backup saves its copy piece by piece and keeps the previous one if a run fails. On an account of 1.25 million readings in the standard 1 GB container both finish in a few minutes while the app keeps answering. Thanks to @mills1975 for the report (#1031).
-
Large accounts can export and restore. Downloading the full export of such an account, with or without a passphrase, ran the app out of memory, and its weekly backup could not be restored, opened or downloaded; the admin page wrongly said it "could not be decrypted". Exports now stream out as they are written and backups are read in small parts. On the same account the export takes about 25 seconds and a restore about two and a half minutes, and the restored readings match the backup.
-
An Apple Health export import needs much less memory. An import of 300,000 readings held 12 MB instead of 53 MB and ran in 35 seconds instead of 55. After an import, chart summaries are rebuilt only for the period the import covered.
-
Smaller memory fixes for large accounts. The half-hourly check for personal records no longer loads a type's whole history. Rotating the encryption key no longer reads every measurement at once, and its report counts only rows that hold encrypted data. The one-time repair of step, energy and distance records can no longer lose them when interrupted. A retried off-host backup continues with the accounts it has not reached yet.
-
The cycle calendar shows intercourse and other logged entries. A day with intercourse carries a small diamond, explained in the legend. A pregnancy, progesterone or ovulation result, contraception, spotting, a temperature, a mucus or cervix reading or a note shows as a small ring, so such a day no longer looks empty. Screen readers name each entry. Someone who sees your cycle through a shared account sees these marks only if they could already open the day. Thanks to @Nazza01 for the report and the screenshot (#1032).
-
Editing a vaccination dose no longer removes its documents. Ever since documents could be linked to doses (1.37.3), a dose opened for editing did not know its linked documents: it showed "Link a document" with nothing under it, and saving any change, such as a batch number, quietly unlinked every document. That was our bug. The dose now shows its linked pages and saving leaves them in place.
-
Document links are easier to tell apart and harder to remove by accident. On a document, each linked dose or visit shows its date, so three COVID-19 doses no longer read as three identical chips. Clicking a chip opens that dose, visit, condition or document; only the small x removes a link, and you can undo right after. Thanks to @Cnote43 for writing up every step (#1024).
-
/api/user/profilereports the time zone the server uses. When the stored zone is unusable it shows the instance default, as/api/auth/mealready did. -
upsert_failedin the nutrient and cycle uploads means "send it again". The API description said to drop such an entry. A retry with the sameIdempotency-Keynow writes again instead of getting the earlier failure back.
Upgrade notes
- Take a database backup before upgrading.
- Migrations:
0346adds theEXTERNALmeasurement source,0347adds the table that tracks restore jobs,0349adds body site and side to visits,0350returns misses wrongly stamped on slots before a medication's creation to pending (nothing is deleted), and0351adds the Track intake switch, on for every medication. POST /api/admin/backups/{id}/restorenow answers202with ajobIdand astatusUrl.GET /api/admin/backups/restores/{jobId}returns the job's status and result,GET /api/admin/backups/restoreslists recent restores, and a second restore of the same account answers409withmeta.errorCode: backup.restore.active.- Everything else in the API is additive, and iPhone app 1.0.3 and 1.0.4 keep working. For a record-only medication the shipped app stops reminding, but its card still shows Taken and Skip, and a schedule change made on the phone is not applied. Change that schedule on the web for now.
[1.39.0] — 2026-09-25
AI is optional everywhere now: your data loads whatever the AI settings say, and each AI switch stops the work it names instead of hiding it. A module you switch off leaves the whole app, and there are fixes for heart-rate streams, charts, the cycle calendar and labs.
Added
- The weight tile judges a change against your own target. Below your target band a gain counts as progress, above it a loss does, and inside the band staying steady is progress while a small move either way is shown neutrally. The weight note, the daily briefing, the overview analysis and the Coach know the target too, and changing the target refreshes the weight note without a new weigh-in. Without a target the tile works as before. Thanks to @COBO5671 for #1006 and the screenshots.
- A document can be linked to every visit and vaccination it covers. The document's own view in the vault lists the visits and vaccination doses it belongs to and lets you add or remove them, so a childhood vaccination record can be filed against every dose it lists in a few taps. Links made from either side are the same links. Thanks to @Cnote43 for #1024 and @CritLoren for #1005.
- The vaccination page shows the booster you planned. Next to each vaccine's heading it says when the next booster is due, marked once it is due within a month and again once it is overdue. The app shows the boosters you planned and does not work out dates of its own (#1005).
- A new "Reading documents" switch for people who run a server. It sits under Admin, Coach and covers AI reading of stored documents, lab report scans and turning a typed medication description into a schedule. It is on by default.
- Reading documents has its own consent. Sending a document, a lab scan or a medication description to an AI service outside your server now needs consent for reading documents, including with your own OpenAI or Anthropic key or your own ChatGPT account. A self-hosted model needs none. The app asks at the place you start the read and grants exactly that; the consent has its own withdraw button in Settings, AI. It does not open the Coach or AI analysis, and consent for AI analysis alone does not cover documents. A full AI consent given before this release still covers both.
- What the Coach stored stays yours with the Coach off. Your conversations, the facts it remembered, your plans and the reminders you asked it to keep can be read and deleted in Settings whether the Coach is switched off on the server, hidden by you, or has no provider. Confirming, finishing or moving a plan or reminder waits until the Coach is back.
- One short hint instead of a message on every card. While no AI provider is set up and you could set one up, Insights and Settings, AI show a single note that AI is optional, with a way to set it up and a "Not now" that hides it on both pages. With AI switched off on the server, setup and the getting-started list do not bring AI up.
Changed
- Your data never depends on AI. Readings, charts, scores, correlations, ECG recordings, heart rhythm events, the breathing and lab-change cards, intraday pulse and the baseline strip on each metric page load the same with AI on or off, with or without a provider, and with or without your consent. Apple Watch ECG uploads are stored in every case. Before, several of these reads sat behind AI switches, and the Insights overview could fail with "Failed to load insights". Thanks to @CritLoren for the log in #975.
- AI parts are left out when AI is off for you. The daily briefing, the sentence under your greeting and under the trend charts, the notes on metric pages, lab markers and workouts, the line after a new reading, the Coach button and menu entry, and the "describe it in words" button in the medication wizard only appear while they can work. Text a model wrote earlier is hidden from the next page load and returns when AI is back; a switch or a missing provider deletes nothing. A summary the app composed from your numbers alone is not AI text and stays.
- Nothing is written by a model at night unless AI is on for you. The nightly briefing, status notes, period summaries, arrival lines, workout notes and Coach memory upkeep now check the server's switches, your AI analysis switch, your provider and your consent for each person as the run reaches them. Switching AI off during a nightly run stops the rest of that run, and a reply that arrives after you withdrew consent is thrown away. Week and month summaries still appear, written from your numbers, and Coach nudges arrive in their plain form when only consent is missing.
- The assistant switches stop AI work instead of hiding it. When a switch in Admin, Coach is off, nothing it covers is sent to a model, and that now includes document reading, lab scans and medication text, which the switches used to leave running. A read queued before the switch was turned off stops when it runs. "Status cards" is renamed "Status notes". If the server cannot read the switches for a moment, every AI feature stays off for that request.
- The Coach has one switch for people who run a server. It is the Coach switch under Admin, Coach. The Coach row under module availability now follows it and links to it; an operator who had turned the Coach module off there has the Coach switch turned off.
- The "Insights" module is now "AI analysis". It decides whether a model writes the briefing, status notes and workout notes for you. With it off, the Insights tab stays in the menu and the bottom bar, and the Today summary and the readiness, recovery and sleep score rings stay on the dashboard. Breathing screening now follows the Sleep module and "What changed since your last panel" follows the Labs module.
- Hiding the Coach hides only the Coach. It used to stop the nightly briefing and status notes as well, which the setting never said. If you had the Coach hidden, AI analysis is switched off for you in this release so nothing new is written; switch it back on under Settings, Modules if you want the notes.
- The morning briefing notification keeps coming with AI analysis off. It carries your health score or the day's reminders without the first sentence a model would have written.
- Someone managing your record sees the stored AI text you would see. Your briefing, notes and summaries show for them whenever they show for you, but opening your record never starts AI work on it.
- Coach reminders wait while the Coach is unavailable. They were surfaced into a conversation nobody could open and dismissed after a few unanswered rounds. They now wait and appear once the Coach is back.
- Withdrawing AI consent deletes the notes written under it. When no consent for AI analysis is left, the status notes, the daily briefing, the model-written period summaries, the arrival lines and the workout notes are deleted in the same step. Your Coach conversations, the facts it remembered, your plans and reminders, and your document summaries are your records and stay. The confirmation says which of these applies to you. When the consent card cannot read your consent, it says so with a retry instead of showing it as withdrawn.
- Switching a module off removes it everywhere. Mood, Sleep, Labs and the other modules now leave the Insights tabs, the trends row on the overview, the add menu (including the measurement types they own, such as sleep and glucose), the dashboard tiles, the overview layout settings and the score rings. The trends row moves on to the next chart so it still shows three.
- Correlations only use what you track. The pattern finder, and the patterns the Coach, the status notes and the weekly and monthly review look at, leave out mood, sleep, glucose, medication adherence, symptoms, weather or lab results once that module is off. Turning it back on brings them back, and patterns you had dismissed stay dismissed. Because fewer pairs are checked, a weak pattern between two things you still track can appear or disappear when you switch a module.
- A link to a switched-off page says why. A bookmark to a page whose module is off shows a short notice with a button to Settings, Modules when the switch is yours. When the page is outside what someone shared with you or the server turned the module off for everyone, it says so and leaves the button out. The Nutrients page uses the same notice and its own "turn on" button is gone.
- Clearer messages when AI is refused. The Coach, document reading, lab scans and the medication reader tell a missing consent, AI switched off on the server, a record someone shared with you and a missing provider apart. A spent daily budget in the medication reader no longer shows "too many requests". Settings, AI names what the server has switched off, and "Test connection" says AI is off on this server instead of calling the model or showing an error code.
- Status notes live in their own encrypted storage. They used to be kept as plain text in the audit log. They are now encrypted like the rest of your health data, covered by key rotation, left out of backups because they are rebuilt each night, and removed by "Delete All Data".
- A year of per-minute heart rate reads in about half the time when a chart asks for it.
- If you write against the API,
GET /api/auth/megainsai: { capabilities, provider }, which says per feature and for the record in use whether AI is available and why not. Routes that serve data no longer answer403for an AI reason; routes that mix data with model text answer200with the model text asnulland anaiobject giving the reason. This coversGET /api/insights/comprehensive,/api/insights/derived,/api/insights/narrative, the status note routes,/api/workouts/{id},/api/daily/digestand/api/dashboard/snapshot, among others. AI refusals carrymeta.errorCodewithmeta.capability,meta.reasonand, where it applies,meta.module; the new codes areai.record.notPermitted,ai.provider.noneandai.unavailable. Document reading, lab scans and medication text refuse withassistant.disabled.documentAi, and/api/insights/generatewithassistant.disabled.briefinginstead ofassistant.disabled.coach. The consent routes accept a new kind,ai_extraction.aiAvailableonGET /api/user/ai-provideris false while the master switch is off, and every card fromGET /api/insights/cardssaysprovider: "rules". - More for API clients. New additive fields are
tiles.weightTrendonGET /api/dashboard/snapshot(directionisup-good,up-bador the newhold),renewalsonGET /api/vaccinations,vaccinationLinksonGET /api/documents/inbound/{id}andvaccinationIdson itsPATCH, andcycleDayandperiodEndableon each day ofGET /api/cycle/calendar.GET /api/medications/{id}/dose-historyreportsfromas the requested start rather than the medication's creation time.GET /api/auth/mereports the server's default time zone when the stored one cannot be used.
Removed
- The "Correlations" assistant switch. It only ever turned off statistics, which no model writes, so correlations now always load.
Fixed
- The worker no longer runs out of memory on a watch that streams heart
rate. On an account with heart rate every minute or faster, the worker
grew until it crashed with "JavaScript heap out of memory" within 15 to
40 minutes, because the heart-rate status card read three months of
readings one by one. The card and the other background summaries,
including the Coach's cycle summary, now read one total per day. If you
raised
NODE_OPTIONS=--max-old-space-sizeto keep the worker up, you can leave it or remove it. Thanks to @ckaotik for the report, the logs and the patient testing in #1023. - Daily totals stay on the same day across chart ranges. Steps, active energy, flights climbed and walking distance could move to the neighbouring day between the 7-day and 30-day ranges, because the longer ranges cut days at midnight UTC. Every range of single days, up to a year, now cuts them at your own midnight; longer ranges still group weeks, months and years by UTC days. Thanks to @mills1975 for #1026 and the screenshots.
- The 7-day chart shows the whole week on a streaming source. It read individual readings up to a limit that per-minute heart rate reaches after about three and a half days. Every range now reads one value per day, and the 7-day and 30-day ranges agree (#1026).
- Charts on the medication page, in the Coach and in recovery use your
time zone. They cut days in Berlin for everyone. When a stored time
zone cannot be used, the server and the app both fall back to the
server's default. A bare offset such as
+05:30is no longer accepted as a time zone, because the database would cut days on the wrong side of UTC; pick a named zone such as Asia/Kolkata (#1026). - Recent doses appear under "Last 90 days" in a medication's history. A dose logged for a time before the medication was added, a dose taken shortly before its scheduled time, and a dose added while the tab was open were missing until later or until a reload. Thanks to @sreeramachandramurthy for #1028 and for spotting the time pattern.
- Past dates in the cycle calendar show their own cycle day. The log sheet labelled any date with today's cycle day, and "My period ended this day" only appeared while today was a period day. It now shows the day of the cycle the date belongs to and offers the period end on the first days of any logged cycle. Thanks to @sreeramachandramurthy for #1004 and the step-by-step notes.
- Save in the cycle log needs something to save. An empty Save stored an empty entry the calendar could not show, which looked like a failed save. It now asks you to pick a flow, a symptom or another sign (#1004).
- Cycle history entered out of order lands on the same cycles. Logging the second day of a period before the first no longer opens a separate one-day cycle; the earlier day becomes the start, and days logged before a back-filled start move to the right cycle. If you already have such a one-day cycle, open the real first day and tap "My period started this day" again. A start tapped by mistake a few days early can be removed again, and the start it replaced comes back (#1004).
- Selecting a cycle symptom no longer moves the list. Symptoms are one per line with the optional 1 to 4 intensity beside them. Thanks to @sreeramachandramurthy for #1001.
- Labs "Save and add another" keeps your biomarker. The second save said "Pick a biomarker first" although one was shown. The form now keeps the biomarker, the date and the visit and clears only the value and its note. Thanks to @sreeramachandramurthy for #1029.
- Manual entry has the rest of the scale's readings. Visceral fat, fat mass, fat-free mass, lean body mass and muscle mass can be entered by hand, in kilograms or pounds as you set. French, Italian and Polish labels that gave two of these one name are distinct. Thanks to @ocurero for #1013.
- The "Link a document" picker offers your whole vault. It said "No documents yet." on vaccinations and visits even with a full vault, because it asked for more documents per page than the server accepts. Documents dated within a week come first, the rest are listed by month and searchable, and a failed load shows a retry. The condition picker on visits had the same problem (#1024, #1005).
- The Insights overview no longer fails as a whole. Each section loads on its own and a failed one offers a retry. "Today's signal", the heart rhythm events and the mood patterns card say when they could not load instead of hiding or spinning, a score page no longer says "not enough data" when it could not read the score, and with the cycle module on but no cycle in progress the scores row no longer leaves a gap (#975).
- Restoring instance settings from a backup made before this release keeps the Coach off when the backup had the Coach module switched off.
Upgrade notes
- Take a database backup before upgrading. Three migrations run.
0343adds the "Reading documents" switch, moves the old Coach module setting into the Coach switch, switches AI analysis off for everyone who had the Coach hidden and removes the Correlations switch. It cannot be undone automatically; going back to an earlier version means restoring that backup.0344creates the encrypted status note storage and deletes the old status note rows from the audit log without copying them, so the next night writes every note again, about one normal night of extra model use.0345adds one column to cycle starts and changes nothing else. - The Correlations switch is gone from Admin, Coach. Correlations always load now.
GET /api/feature-flagsis deprecated. It still answers, with aDeprecationheader pointing toGET /api/auth/me, and the web app no longer reads it. The iPhone app and other clients should readaionGET /api/auth/meinstead, expect200withnullmodel text on the mixed reads listed under Changed, and handle the three new error codes. The route is removed in the first release after the next iPhone app.
[1.38.25] — 2026-09-20
Three readings that never arrived, and one import that turned away a perfectly good export.
Fixed
- Oxygen saturation and body fat from Apple Health reach the server again. The app sent both as a percentage while the server scaled them a second time, so a reading of 97 % arrived as 9700, failed the plausibility check, and was dropped. Worse, the phone treated that as final and moved past the reading, so it never tried again. The importer now takes either spelling, which means current app versions stop losing those readings without waiting for an app update. Thanks to @mbreitkreuz for tracing it across both sides.
- A cycle symptom can be logged without an intensity again. Picking a symptom and saving without tapping the 1 to 4 selector was refused with "Could not save. Try again.", and removing the symptoms made the save work, which pointed at the wrong thing entirely. An unrated symptom has always been a valid entry; only the check at the entrance disagreed. Thanks to @sreeramachandramurthy for the report and the log.
- The Apple Health import accepts your export whatever iOS named it.
The importer looked for
export.xmlin lowercase only. iOS writes that name in the language of the phone and some versions capitalise it, so ordinary exports were refused with a message claiming the archive was invalid. It now matches in any capitalisation, takes a translated name when it is unambiguous, and when it really cannot find the file it lists what the archive does contain. Thanks to @mills1975 for the report.
[1.38.24] — 2026-09-16
Insights loads when the assistant is switched off, and the bundled database no longer ages behind its own security fixes.
Fixed
- Insights loads with the assistant switched off. An operator who turned the assistant off on the server got an error in place of the whole Insights overview. Its main read and several tiles were tied to the assistant switch even though none of them uses a model, so the derived tiles, the ECG list and the device-flagged rhythm events stayed dark, and a watch ECG sent from the phone was refused. These reads follow the Insights module now, and the parts a model writes still disappear when the assistant is off. Thanks to @CritLoren for the log in #975 that showed which requests failed.
- The trends row leaves mood out when the mood module is off. Switching the mood module off already hid the Mood tab on Insights, but the overview still drew a mood chart in its trends row. It skips mood now, the same way the tab strip does.
Changed
- The bundled database moves to PostgreSQL 16.15, and stays current from here. The compose file pinned 16.14, and the pin is deliberate: a database change should be something you can read before it happens. What was missing was anything watching it, so it sat five weeks behind a release that carried security and data-corruption fixes. New PostgreSQL minor releases now arrive as an update we review and ship. Upgrading within PostgreSQL 16 needs no dump or restore. If you run your own PostgreSQL instead of the bundled one, nothing here affects you. Thanks to @tarantila for #987.
- Supply chain. The container scanner's installer is fetched from a commit and checked against its hash before it runs, PostgreSQL major versions no longer arrive as automatic update proposals, and the app moves to nodemailer 10 along with updates to sax, sonner, jose, the canvas renderer and the virtual-list helper. Nothing changes in how the app behaves.
[1.38.23] — 2026-09-15
Connecting an AI assistant through MCP works in Chrome and Edge again, and lab-report scans can be raised past six an hour.
Fixed
- Connecting an AI assistant through MCP works in Chrome and Edge again. After you pressed Allow on the consent screen, the grant went through but the browser stayed on the page and never returned to the assistant, so claude.ai and other clients waited forever. The consent page carried a security policy that only allowed its own address as a form target, and Chromium-based browsers apply that to the redirect back to the client as well. The consent page now allows exactly the return address it has checked against the connecting client, and nothing else. If you added a reverse-proxy rule to work around this, you can remove it. Thanks to @Mark-Milkis for tracking it down in #983.
- A lab scan that never reached the AI no longer uses up one of your scans. A file that was too large, not an image or PDF, a PDF that could not be rendered, or a scan the daily AI budget turned away still counted against the hourly limit. Only scans that are actually sent to the provider count now.
Added
- If you host this yourself, you can raise the lab-scan limit. Scanning a
stack of reports after a doctor's visit ran into a fixed six scans an hour.
Set
LABS_OCR_LIMIT_PER_HOURto allow more (default 6, between 1 and 1000), the same wayDOCUMENT_AI_LIMIT_PER_HOURworks for documents. The daily AI budget still bounds the cost. Thanks to @rkerian for #985.
Changed
- If you write against the API, the redirect after the MCP consent
decision is now
303 See Otherinstead of302. The 429 fromPOST /api/labs/ocr/extractnow also carriesmeta.retryAt. - Prisma moves to 7.10 for the app, its client and the migration tool in the image, together with updates to Playwright, lucide-react, pdfjs and the CI actions. Nothing changes in how the app behaves.
[1.38.22] — 2026-09-13
A channel test no longer runs on settings you have not saved.
Fixed
- The Test button waits until you save. On the webhook, ntfy, email and Telegram cards, Test always sends through the settings that are saved, not what is in the form. After you changed a field, the card still showed "Saved" from the last save, and Test quietly used the old settings. That is how a webhook switched to the Gotify format kept failing with the old message body. Now the "Saved" note goes away as soon as you change something, and Test stays off with a short "Save your changes first" until you save. Thanks to @sreeramachandramurthy for the screenshot in #947 that showed it.
[1.38.21] — 2026-09-13
A channel test says why it failed, a Gotify server gets the message it expects, and Settings is back inside a profile you look after.
Added
- The webhook can send in Gotify's format. Gotify expects the priority
as a number and refused HealthLog's messages, which carry it as a word.
Choose Gotify under Payload format on the webhook card and the message
arrives the way Gotify wants it: routine events with sound, medication
reminders as a banner on Gotify's Android app, urgent events at the top
of the scale. Existing webhooks keep sending exactly what they sent
before. The notifications guide now shows both message formats and how
to set up Gotify, with the URL ending in
/messageand the token in theX-Gotify-Keyheader. Thanks to @sreeramachandramurthy for #947.
Fixed
-
Pressing Test on the webhook, ntfy or email card now tells you what went wrong. When the other end refused the message, the card said "Test failed" and nothing else, although the server knew the answer; one self-hoster had to dig through the container log to learn that their relay had answered 400. The card now names the cause, such as a refused token, a wrong address, a relay that rejected the message or a mail server that did not answer in time, with the status code. For webhook and ntfy it also quotes the first part of the relay's own error message, unless it looks like a token or contains one of your saved secrets. Real deliveries were never affected; only the test button hid the reason. The notification test in the admin area shows the same cause for each channel, and a relay refusing a test no longer counts as a server error in the operator's log. Thanks to @sreeramachandramurthy for #947.
-
The hint under the webhook address no longer says it must be public. A target on your own network works when the server operator lists it in
NOTIFICATION_PRIVATE_ORIGINS, which has been possible since 1.38.17. -
Settings is back in the menu while you are inside a profile you look after. Switching into a managed profile removed Settings from the menu entirely, on the desktop and on the phone, even though the profile's own settings page existed and worked: which modules it tracks, its targets and where its notifications go. The only way there was typing the address. The menu now offers Settings inside that profile and opens the profile's own page. If someone gave you manage access to their own account, Settings opens their medical history, the one settings page that access covers. Notifications stays out of the menu while you are inside someone else's record. Thanks to @sreeramachandramurthy for asking in #939.
-
The note on a manage invitation says when a second factor is needed. It said offering manage access always asks for your second factor. It asks only if you have one set up; that it works only in a browser is true either way.
-
A different app on the address where HealthLog used to run now loads normally. If you stopped HealthLog and started something else on the same address and port, HealthLog's offline worker in your browser kept answering: the other app got HealthLog's cached files and your changes to it never showed up, until you removed the worker by hand in the browser's developer tools. The worker now checks whether the page still comes from HealthLog, and when it does not, it clears its cache and removes itself. When HealthLog is only down, or a reverse proxy in front of it answers with an error, nothing changes and the offline page still appears. If you had push notifications switched on in that browser, the worker stays registered and keeps them, and only stops serving its cached files. Thanks to @el-abcd for describing this in #847.
-
If you ship logs to Loki, a Loki on your own network works now, and a failed push says so. Only public addresses were allowed, so a Loki on the same Docker network or on your LAN was refused without a word, and events were dropped.
LOKI_ENDPOINTcomes from your server environment, so the address you set there is now trusted as it is; link-local and cloud metadata addresses stay refused. It also takes either the base URL or the full push URL ending in/loki/api/v1/push; pasting the full URL used to break shipping just as quietly. When a push fails, the container log gets one line with the Loki address, the reason and how many events were lost, at most every five minutes per reason, and never the password. Thanks to @sreeramachandramurthy for the empty Grafana in #947.
Changed
- If you write against the API, a failed channel test answers 502 with
the cause.
POST /api/settings/webhook/test,/ntfy/testand/email/testused to answer 500 for every refusal from the other end. They answer 502 now, withmeta.errorCode(one ofcredentials_rejected,endpoint_not_found,rate_limited,upstream_error,upstream_rejected,redirected,timeout,connection_failed),meta.upstreamStatus, and for webhook and ntfymeta.upstreamBodywith at most 200 characters of the relay's reply; email carriesmeta.smtpCodeinstead. The 422 for a private address that is not allowed is unchanged, and 500 is left for failures the server cannot name.GET /api/settings/webhookgainsformat(genericorgotify); aPUTwithout it keeps the stored value.
[1.38.20] — 2026-09-11
Signing in no longer tells anybody which addresses have an account here.
Security
- Signing in now takes the same time whether or not the account exists. A wrong password and an address nobody has registered here have always given you the same answer on screen, but the server used to reach that answer much faster when there was no account to check, and the same when the account signs in with a passkey and has no password at all. Someone probing the sign-in page could read that difference and learn which addresses are registered, and which of those have a password. Every refusal now does the same work before it answers.
- Changing your email address is limited to ten changes an hour. The
server tells you when the address you typed already belongs to another
account, which is the only sensible thing to say to someone who typed one.
Nothing stopped a signed-in caller from asking that question over and over
about addresses that were not theirs. Ten an hour per account is far above
any real use of the field, and a save that leaves your address as it is
does not count against it. If you hit the limit while saving other things
on the settings page, those are saved and only the address is held back,
with a note under the field. If you write against the API, a request that
asked for nothing but the address gets a 429 on
PUT /api/auth/profileandPATCH /api/user/profilewithmeta.errorCodeset toprofile.update.emailRateLimitedand aRetry-Afterheader saying how long to wait; a request that carried other fields gets 200 withemaillisted inrejectedFieldsunder the coderate_limited.
Removed
- The endpoint that answered whether an address has an account here is
gone.
POST /api/auth/check-usertook an email address or a username and said, to anyone who asked and without any sign-in, whether an account existed and whether it had a passkey or a password. It was built to decide which field a sign-in screen shows first, which is not worth telling the world who has an account here. The path now answers 410 and says so in the API reference rather than disappearing, so anything still calling it gets a clear "this is gone, stop retrying" instead of a 404 that looks like a broken server. If you wrote something against it, the sign-in request itself is the replacement: send the credential and read the answer.
[1.38.19] — 2026-09-11
The first run tells you what is actually true about your account, an invitation link behaves, and the AI you ask a question to has budget left when you ask it.
Fixed
-
An invitation link opened while you are already signed in no longer offers to register. It showed a registration form, and completing it quietly swapped the session you were in for a brand new account. The link now says you are signed in and what you can do about it, and the server refuses the registration outright rather than relying on the screen to hide the button.
-
The setup step that connects a device says what is true about it. Every provider was described as "connected, first sync running", including ones that had never synced and ones nobody had connected. A provider with no history of its own says so now, and the step offers to connect it.
-
The last screen of the setup no longer claims the AI works when nobody has asked it anything. The offer appeared, and said insights were ready, as soon as an instance had a provider configured. It appears only when that provider has actually answered, and a freshly installed instance says its provider has not been tried yet instead of announcing a failure.
-
A page whose module is switched off says which switch it is instead of showing an empty page, and the getting-started checklist no longer asks for things that are already done.
-
Background insights can no longer use up the day before you ask a question. The daily AI limit counted every request against one number, whoever was paying for the model behind it, so a day of automatic insights could leave your own question refused. It is counted per payer now, and a fifth of it is held back for what you do by hand: on a plan you bring yourself the automatic work runs up to 1 600 000 tokens a day and your questions keep 400 000. Nothing changes for an instance whose AI is only used by hand.
Security
- The demo account can no longer be taken over through the setup step. The step that asks for date of birth, height and sex went through a route that accepts eleven fields, including the contact address of the account everyone shares. It writes those three and nothing else now, refused on the server rather than left out of the form.
Changed
-
GET /api/integrations/statuscan answerunknownfor a provider with no sync history, next to the existing values; that case used to be reported asconnected. The native client does not read this field. -
GET /api/user/ai-providergains three fields describing the instance's shared AI: whether it is healthy, untried or failing, whether it may be offered, and whether the person already agreed to it. -
When the AI limit stops a request, the answer says which ceiling was reached and whose budget it was.
-
Two migrations run on start (
0340_coach_usage_operator_tokens,0341_arrival_reaction_cost_owner), both adding one column with a default.
[1.38.18] — 2026-09-10
The first run asks what you need and switches on exactly that; the old step-by-step wizard and the columns it wrote are gone.
Added
-
A needs-based setup replaces the wizard. A few questions (who the record is for, the areas to watch, medication on a schedule, where readings come from, an upcoming visit), one about units only when an area needs it and the account does not hold it yet, then a confirm screen that shows what the answers switch on, one first task with its form right there (connect a source, add the first medication with a reminder, or log one reading), and a done screen with the checklist open. Every answer is saved as it is given, so leaving and returning resumes at the same step; every question but the first can be skipped, and "Skip for now" on the welcome screen leaves the app usable with nothing switched on. "Set up again" and "Show the checklist again" live under Settings → Account. The server works out the modules from the answers once, the way v1.38.16 introduced; a module the person switched on by hand, or one that already holds data, is never turned off by an answer.
-
Answers given for someone you look after go to their record. With "someone I look after" the questions run in your own record, their record is created on the confirm screen, and the modules go to that record while yours stay as they were. Finishing without creating it changes nothing on your record either. The checklist gets a row for that record instead of reading those answers as yours. If you write against the API:
POST /api/onboarding/completetakesmanagedRecordId; a record the caller does not manage is a 404, as on the managed-profile routes. -
Module pages say which switch is off. A page whose module is off shows an empty state that names the switch instead of a blank page, and the two tour stops that had nothing to point at (integrations, the health record) have their anchors.
-
The setup is available in every language the app has.
Changed
- The dashboard order is arranged from the answers the first time, and only while it has not been arranged by hand, so an order somebody set themselves is never overwritten.
Removed
POST /api/onboarding/step, the goal slugs, and theusers.onboarding_stepandusers.onboarding_goalscolumns (migration0339_retire_wizard_columns). The iOS app never called the route or read the columns;POST /api/onboarding/disclaimer,POST /api/onboarding/tourand the disclaimer and tour fields onGET /api/auth/meare unchanged.
Upgrade note
- Take a backup before deploying, and expect no image rollback. The
migration drops the two columns the v1.38.16 and v1.38.17 images read on
every sign-in, so an older image against a migrated database fails at
login, not only in the wizard. Rolling the image back means restoring the
database, or re-adding the columns with their defaults, which loses
nothing because nothing at this version writes them:
ALTER TABLE users ADD COLUMN onboarding_step integer NOT NULL DEFAULT 0, ADD COLUMN onboarding_goals text[] NOT NULL DEFAULT '{}';
[1.38.17] — 2026-09-10
The second of two close doses can be logged from the card again, and a webhook or ntfy target on your own network can be reached again with the operator's say-so.
Fixed
-
The second of two close doses can be logged from the card again. On a schedule whose neighbouring doses sit closer than twelve hours (08:00 and 12:00, say), taking the first dose made the card offer tomorrow's first dose, and a skip pressed next landed on tomorrow. A taken, skipped or auto-missed dose counted as settling every scheduled time within a window sized for the twice-daily gap, so the first dose swallowed its neighbour and the card walked past it. A logged dose now settles exactly one scheduled time, the nearest one within that window, and the adherence ring on the card follows the same rule, so the due slot and the ring cannot disagree. The window still covers a dose logged a little off its scheduled time, or across a clock change.
-
You can allow a webhook or ntfy target on a private network (#947). The senders look the target up at send time and refuse a private, loopback, link-local or CGNAT address; that is the DNS-rebinding protection from #217, and it stopped a Gotify behind a LAN reverse proxy exactly as designed, with no way to allow it.
NOTIFICATION_PRIVATE_ORIGINSis that way now: a comma-separated list of exact origins such ashttps://gotify.example.com,http://ntfy.lan:8080, read from the server environment only. A listed origin is still looked up, the address it resolved to is the one the connection uses, and redirects are forbidden. Link-local, the metadata range and the unspecified address stay refused even when listed; loopback can be listed for a relay on host networking. A malformed entry is logged once, without its query string or credentials, and allows nothing. Listing a name does not cover its sub-hosts or other ports, because a private name cannot be told from a public one at save time. The grammar is the same asNIGHTSCOUT_PRIVATE_ORIGINS, which now shares the parser and the address rules; existing Nightscout entries, loopback ones on host networking included, behave exactly as before. Reported by @sreeramachandramurthy. -
The test button says why a private target was refused. Saving a private address that is not listed, or testing it, now names the variable to set instead of an internal error that sent the operator to the log. Saving, testing and the scheduled delivery all take the same decision, and an allowed send is marked in the request log with its channel and origin. A link-local, metadata or unspecified target says that no setting can open it, since telling the user to ask the operator would only send them in a circle. The refused save or test is a 422 with
meta.errorCode = private_origin_not_approved, orprivate_origin_not_grantablefor a target no setting can open.
[1.38.16] — 2026-09-10
Mostly plumbing. Two things you may notice yourself: someone you gave cycle-only access to sees less, and the admin page tells you whether last night's off-site backup worked for every account. The rest is for people who host the app or write against its API.
Added
-
If you host this for others, Admin → Backups now shows which accounts have a recent off-site backup. The nightly job used to report one number, so an account that kept failing was invisible. There is a row per account now with the time of its last copy and whether that copy is on schedule, due, overdue or has never happened. On the first day after this update every row says "No record yet"; the next night fills them in.
-
The server side of the new first-run setup. Your record can store its setup answers (who it is for, which areas to watch, medication on a schedule, where readings come from, an upcoming visit, units), and the app derives which modules to switch on from them. It never switches off a module you turned on yourself or one that already holds data. The screens that ask the questions come in a later release; the first run looks the same for now. For API clients: the answers appear as
onboardingonGET /api/auth/me;PATCH /api/onboarding/answerssaves one answer,POST /api/onboarding/completefinishes,POST /api/onboarding/restartstarts over.
Fixed
-
Someone with cycle-only access no longer sees the rest of the record. The cycle insights page compared luteal and follicular averages for weight, resting heart rate, HRV, sleep, steps, glucose and mood, and showed them to anyone whose access covered the cycle, whether or not it covered those areas. It now shows only what the access covers.
-
The app no longer offers buttons that would have failed for someone with limited access. Add, edit and delete on the vaccination pass for read-only access, an editable visit sheet, capture forms and the quick entry on the start page suggesting kinds outside the access, and a handful of pages that opened for anyone with any access at all. Each now checks its own permission. A booster reminder is also no longer created for a record the access does not cover, and a wipe, an account deletion and an encrypted export state in the API reference that they refuse while you are looking at someone else's record, which they always did.
-
The error-reporting card on the admin page says what it actually does. It showed a green "Configured" as soon as an address was set, even with sending off, and never said where reports go. It is green only when a report left within the last fourteen days, amber with the reason otherwise, and it names the target host. There is no default target: errors never leave your server unless you point them somewhere, and
docs/ops/deploy.mdsays how.
Changed
-
If you write against the API, every validation error now lists the fields it rejected in
details.issues. Some routes used to answer with a status code and one sentence. The message and the status did not change, so nothing that reads them today breaks. -
A request body that is not JSON at all answers 400 on every route. Seven preference routes said 422; the
meta.errorCodethey carried is the same, and the native app already handles both. A body that parses but fails validation is still a 422.PUT /api/auth/me/report-selectionnames the ids it does not know inmeta.unknownLeavesinstead of failing with a bare code. -
Routes that create or edit a single row share one write limit of 300 writes a minute per account. A 429 from it carries
Retry-After, theX-RateLimit-*headers andmeta.errorCode = record_write.rate_limited, so a client can tell it from a route's own limit. Batch endpoints keep their own. -
The GLP-1 log and the custom cycle symptom accept
Idempotency-Key, so a retried request returns the first answer withX-Idempotent-Replay: trueinstead of creating a second row. -
Every error code the server can send is listed in the API reference by area. A new code cannot be added without appearing there, and no existing code changed.
[1.38.15] — 2026-09-10
The rest of #939 for records you keep for someone else, one adherence figure everywhere, and better API manners toward a client that retries.
Added
-
You can edit a record you look after now, and record its sex (#939). Name, date of birth, language, timezone and sex can be changed after creation, and the creation form asks for sex so cycle tracking can follow from it. Both need the fresh second factor that create and delete already ask for, and edits are capped at ten an hour.
PATCH /api/managed-profiles/{id}answers 404 for a record you do not manage and 422 with the field list for a bad value. Requested by @sreeramachandramurthy. -
Modules are switched per record. The Modules card and the navigation follow the record you are looking at, so while you are looking after someone else's record you see what that record has open. Someone you gave access to only certain sections no longer learns whether you track your cycle, screeners, illness episodes or supplements. On the API side,
GETandPATCH /api/record-settings/modulesread and write the modules of the record you are acting on, andmodulesandcycleTrackingEnabledonGET /api/auth/medescribe that record; the payload says which fields describe the caller and which the record. -
The module-off notice says why. It names the reason and offers the switch only when it is the record's own choice.
GET /api/auth/mecarries amoduleAccessmap besidemodules, saying per moduleenabled,disabled(the record's choice),not_granted(outside the access you were given) orunavailable(switched off for the instance);modules[key]ismoduleAccess[key] === "enabled", so the booleans keep their meaning.
Fixed
-
A skipped dose is a pause, not a miss, on the dashboard too. The medication card, the dose history and the doctor report have left a skip you chose out of the count since v1.15.9; the dashboard tile counted it as missed, so one day could show two rates. They agree now.
-
Saving the Modules card no longer freezes a record's cycle switch against a sex recorded later. Every save wrote the cycle flag over the one that follows from sex; it now goes only when it changed.
-
A refused second-factor action no longer hides its reason behind the dialog. Regenerating recovery codes or turning off the authenticator kept its dialog open when the server said no (usually an expired second-factor window); both close now.
-
The edit form for a record you look after keeps what you typed when the list refreshes, and a timezone-only edit no longer writes your language into a record that had none.
-
The chip saying your phone owns the medication reminders can appear now. It could never render before.
GET /api/auth/mepublishesnotificationPrefs, includingnotificationPrefs.medication.clientManaged. -
A restore from the admin backups page no longer rewrites instance-wide settings unless you tick the box. It used to whenever the snapshot carried them; the checkbox sits next to the typed confirmation and is off by default (
restoreInstanceSettingsfor API clients). The runbook says what a restore replaces (rows added after the snapshot are removed) and describes the console path. -
A stored copy that will not open is refused with a clear message, not an internal error, whether it was tampered with or written under a key the instance no longer holds, and whether you restore, download or preview it. A client sees a 422 with
meta.errorCode = backup.payload.undecryptable. -
The admin status rows stay readable when the worker is absent or crashed. The text used to switch to a colour that fell below the contrast floor in exactly the state an operator most needs to read.
-
The admin reminder check can name one account.
POST /api/admin/notifications/reminder-checktakes an optionaluserId; without it the check stays instance-wide as before. -
If you write against the API, every 429 now says when to come back. Most rate-limited routes, the batch endpoints,
/api/sync/changesand/api/auth/refreshamong them, sent no rate-limit headers. Every 429 from the limiter carriesRetry-Afterin whole seconds plusX-RateLimit-Limit,X-RateLimit-RemainingandX-RateLimit-Reset; a 429 relayed from an AI provider keeps its own shape. -
Pages of a history pull never overlap or skip. Lists ordered by a timestamp that can tie (measurements, labs, the measurement drill-down, custom-metric entries, medication intake, mental-health assessments) break the tie on
id. -
The API reference allows what the server sends. The documented error response allows
details.issues, which the multi-field 422 always sent, and the sync response declarescycleDaysandcycles. -
Generic authentication errors carry
meta.errorCode(auth.session.missing,auth.mfa.code_invalid, ...), documented on the 401 and 403 responses; a route's own credential check may still answer with prose alone.
Changed
- New end-to-end checks run before each release. Medication adherence from setup through take and skip to the card, the history and the dashboard tile; the doctor report with its text read back; the second factor from enrolment through step-up, a wrong code, a replayed code and a recovery code that works once; backup and restore through the settings pages; and notification preferences through to what was delivered. Several fixes above were found while writing them.
[1.38.14] — 2026-09-09
Two bugs reported by @mbreitkreuz, both fixed at the root (#943, #944), a first-run step that used to swallow an error, and quieter checks before a release.
Fixed
-
Walking distance from an Apple Health
export.zipimport was stored a thousand times too small (#944). Apple writes every record in the archive in the unit the phone displays, kilometres or miles, and the import read the number without reading the unit, so a 2.5 km day landed as 2.5 metres. The import now converts units the way the workout import already did, which also caught a workout archived with its energy in kilojoules. Importing the archive again repairs the stored rows. -
Blood glucose readings without a meal-time tag now count everywhere (#943). A meter synced through Apple Health does not say whether a reading was before or after a meal, so an account could hold hundreds of readings and see no Blood Glucose tile, no glucose target and an empty glucose section in the doctor report, with the module on and no hint anywhere. Untagged readings are a group of their own now: they light the tile with a value and a trend, get a target card judged against the random-reading range, and appear in the report and in the FHIR export. Tagged readings keep their per-meal breakdown.
-
The first-run profile step no longer throws away what the server told it. A height outside the accepted range, or a date the server would not take, was dropped on save while setup moved on and marked itself finished; the value was gone and nothing said so. A refused field now says why underneath the input, in the interface language, and the step waits. The profile card in Settings behaves the same way. The iOS app has the same gap and is tracked there.
-
If you host this yourself and someone no longer has their Apple Health archive,
scripts/repair-apple-health-distance.tsrepairs exactly the rows the import wrote, once per account; it only reports what it would change unless told to write. Runbook atdocs/ops/apple-health-distance-repair.md. -
A retried medication write no longer lands twice. Adding a medication, logging a side effect and registering a supply container now honour
Idempotency-Key, so a replay after a lost response, which is what the iOS app's offline queue sends, returns the first answer markedX-Idempotent-Replay: trueinstead of creating a second row. Documented per route.
Changed
-
scripts/synthetic-journey.mjswalks through an instance after a deploy: it warms the instance, checks that/api/versionmatches the tag just published, signs in, writes a reading, reads it back and deletes it; thesynthetic-journeyworkflow runs it by hand against an instance of your choice, anddocs/ops/deploy.mdsays what a failed step means. -
The off-site backup variables
BACKUP_S3_REGIONandBACKUP_RETENTION_DAYSare in.env.production.example, and the backup runbook no longer claims the worker deletes old copies: it never does, the bucket's lifecycle rule does. -
POST /api/nightscout/connectis in the API reference, where it was missing. -
The checks that run before a release had been failing for reasons unrelated to the app: browser tests that recognised the wrong element or raced the server, bundle limits with no headroom, a dependency check that failed pull requests over advisories that changed nothing. Those are fixed, and new browser checks cover blood glucose in both units and an Apple Health import end to end.
[1.38.13] — 2026-09-09
While looking after someone else's record you get the add, edit and delete controls the server has accepted since v1.37, decided per area and published by the server. Reported by @sreeramachandramurthy in #939.
Fixed
- Looking after someone else's record, the controls the server accepts
are shown again. While looking after a record for a child, or a record
shared at the manage level, the pages for mood, wellbeing check-ins,
visits, allergies, family history and labs showed no way to add
anything, although the server had accepted those entries since v1.37.
The app had hidden every such control until the server side was
confirmed, and that hold was never lifted. Lifting it with one switch
would have been wrong too: documents cannot be handed to anyone else at
any level, so the documents page would have offered upload and delete
and refused each one. The app now asks per area, so the controls appear
for measurements, medications, labs, profile and illness from the write
level up, and for mood and cycle at the manage level; the documents page
says in one sentence that only the record owner can add documents. On
the API side, every entry in the account access list on
GET /api/auth/mecarrieswritableDomainsandmanageableDomains, naming the parts of the record that access may write to and may manage; both are additions. Switching modules per record, editing a record after creation and asking for sex at creation are still open in #939 and follow as one piece.
[1.38.12] — 2026-09-09
A dependency release: the image and the lockfile stop carrying the
advisories a container scanner reported, and next build learns to
type-check the application without the test tree.
Security
- Scanner advisories cleared, none in this code. next moves to 16.3.4 (two critical advisories, and it pulled a vulnerable sharp), nodemailer to 9.1.1 (one high, three medium), js-yaml to 4.3.2 (high), hono under the MCP SDK's node server to 4.13.5 (three medium), vitest to 4.1.11 and baseline-browser-mapping to 2.11.0 (medium). The packages HealthLog does not depend on directly (sharp, js-yaml, hono and baseline-browser-mapping) move through pnpm overrides limited to the vulnerable ranges, so exactly the copies the scanner named change and nothing else does.
Changed
next buildtype-checks the application, not the test tree. Next 16.3 checks the whole TypeScript project during the build, which reaches the unit tests and their fixtures undertests/; the image build leaves that folder out on purpose, so it failed with a missing fixture module. The build now readstsconfig.build.json, which extends the main config without the test tree, andpnpm typecheckkeeps covering everything. If you build your own image there is nothing to do; if you runnext buildfrom a checkout withouttests/, it now works.
[1.38.11] — 2026-09-08
"Sign out everywhere" now means everywhere, a renewal cannot outrun a sign-out, and a retried request can no longer be answered from a cached response that a different credential earned.
Fixed
-
"Sign out everywhere" signs out the other phones, not the one pressing it. A phone login is two credentials, a refresh token and the access token paired with it, and the server checks only the second on each request. The button revoked the refresh tokens, so every other device kept working until its access token ran out, up to a day. And a phone pressing the button described itself to the server as a browser, so the exception meant to spare the caller matched nothing: the phone revoked its own refresh token and was the one device signed out, at its next renewal. The button now revokes the paired access tokens of every other device in the same step, and a phone is recognised by its access token, so its own login is the one kept. Long-lived API tokens from the settings page are untouched, as before. The iOS app's confirmation text will go back to saying "everywhere else" once a build against this release is out.
-
A renewal that overlaps a sign-out loses. Renewal read the presented refresh token as live, issued a new pair, and only then marked the old one used, with a check that asked "not yet used" but not "not yet revoked". A sign-out-everywhere or a credential rotation landing in that window was overtaken: the pair issued a moment after the sign-out was the one live login left. The check now includes revocation and the pair issued inside the window is retired. The client hears
revoked, which it answers with a fresh sign-in, rather thanalready_used, which it would retry. -
A retried request can no longer be answered from a cached response that a different credential earned. When a client retries with the same idempotency key, the saved answer is returned before the route's own permission check runs, so what the cache may answer has to match what the route would allow. Two gaps: someone acting on a record shared with them, whose access had since been narrowed or edited, still got the answer cached under the wider access, because the replay checked only that some live access existed; and a narrow-scoped API token could read an answer cached by a full-access credential of the same account on a route outside its scope. Cached answers for access to someone else's record are now tied to that access, its level and its scope, and a narrow token's answers to the token, so any change of authority sends a retry back to the route to decide afresh. Browser logins and full-access tokens acting on their own record are keyed exactly as before; no client changes.
Changed
- The README's record list still said immunization records were out of scope; the vaccination record has shipped since v1.37.3.
[1.38.10] — 2026-09-06
A busy phone is no longer signed out at the moment it renews its credentials, and the nightly off-site backup streams and fits a small container.
Fixed
-
A busy phone could be signed out at the very moment it renewed its credentials. The app renews its access token about once a day, and the old token used to be revoked the instant the renewal went through. A phone with several requests in flight had sent some with the old token a moment earlier; those came back as revoked, the app read that as "sign in again", renewed again with the refresh token it had just spent, and that looks exactly like a stolen token being replayed, so every credential for that device was withdrawn and the person had to log in again. The outgoing token now stays valid for fifteen seconds, or until its own expiry if that is sooner: long enough for requests in flight to finish, short enough to gain nothing worth having. Replay protection is unchanged; a refresh token presented twice still withdraws the device's credentials on the spot, and so does signing out. The iOS app tolerates the race on its side too, so the two halves close it together.
-
The nightly off-site backup no longer takes the instance down with it. The first time it was pointed at a real destination it ran out of memory within seconds and restarted the container for everyone on it, because the job runs inside the app process. The weekly backup had the same problem in v1.38.6, and that fix had only half reached this job: the upload still built the whole backup as one piece, compressed it whole, encrypted it whole and sent it whole, four full copies of the record at once. Now the record goes out table by table straight into compression, then encryption, then an upload in parts, so no complete copy ever exists and what the process holds no longer depends on how much you have recorded. Measured under the real memory ceiling with a large record, the old way dies while still building the copy and the new way finishes with room to spare; the stored copy for such a record is about 9 MB. If you set
BACKUP_S3_ENDPOINTand its companions on an instance with a large record, you can leave them set after this. -
A nightly run that uploaded nothing for anybody now reports failure. It used to say it was fine. Bad credentials, a missing bucket or a refused connection upload nothing, leave no half-finished parts behind, keep the server up, and fail the run with the storage provider's own reason. A run where some accounts got a copy still succeeds, with the rest counted, because retrying everyone over one object would re-upload everyone's. The runbook lists what the bucket grant needs, including permission to abort a multipart upload.
Changed
- Off-site copies carry their authentication tag at the end. The tag can only be produced once the last block is in, so a leading one meant the whole object had to exist before its first byte could be sent, which is the thing that could not be streamed. The encryption itself is unchanged; the tag still covers every byte and is checked before any plaintext comes back. Copies written by earlier versions still restore without a flag: an old and a new copy of the same record came back with identical row counts, deleted rows included.
[1.38.9] — 2026-09-05
The date order you choose now reaches every date on screen, a gateway that refuses now says why, and a plain-HTTP self-host can write again. Two of the reports came from @foxbcx (#922) and @arisalthaus (#492); thank you.
Fixed
-
The date order you pick under Settings only reached the entry form. @foxbcx switched away from MM/DD/YYYY, watched the date picker follow, and found the chart axes and the measurements list still spelling every date the old way. The setting had reached the form and nothing else; the rest fell back to the language default without saying so. It now reaches the chart axes and their tooltips, the measurements list, the medication charts, the doctor-report PDF, and the page a practice opens through a link you gave them, which spells dates the way you read them rather than the way the practice does. That page also picked up your 12- or 24-hour preference, which it had been ignoring while the PDF used it. Month names stay in the interface language: the setting decides the order of the numbers, not the language of the words, because the alternative is a German month name in an English report.
-
A gateway that refused the call reported nothing but "empty content". With OpenRouter as the OpenAI-compatible gateway, a refused request showed up as "empty content" with no reason, because OpenRouter answers with success and puts the error inside the body. The reason now comes through in the gateway's own words, a missing balance as a missing balance and a data-policy block as a block, and the provider health card and the fallback chain treat each as they would the same answer from any other provider. The local provider had the same blind spot, including for an error that arrives before the first streamed token, and is closed too.
-
The import guide links under Settings → Import went the long way round. Both the CSV card and the JSON card now link straight to the guide instead of through a redirect.
-
On a self-host served over plain HTTP, nothing could be saved. @arisalthaus, on a LAN, found that uploads did nothing and worked out that the transport was the reason. Nothing in a save needs HTTPS, but since v1.37.18 the app labelled every upload and every save with an id from a browser call that is only offered on a secure page. On an http:// address the save failed before a request left the page, and nothing said why: recording a measurement, editing a medication, restoring a backup, all of it. The id now comes from a source plain HTTP allows, so writes work over HTTP on a LAN, a VPN or Tailscale as they do over HTTPS. What a browser still withholds from a page without HTTPS is passkey sign-in, push notifications and installing the app to the home screen; no application can hand those back, and the self-hosting docs now say so.
[1.38.8] — 2026-09-03
The heart-rate-variability tile now works for accounts whose HRV comes from a ring or a strap, and an account can no longer pile up measurement tokens without limit. Both from @Antiheld86.
Changed
- You can hold up to ten measurement tokens at a time. Past that, creating another is refused until you revoke one, so a script stuck in a retry loop meets a wall instead of leaving you hundreds of live credentials to clear out by hand. Only live tokens count: revoking one frees a slot at once, an expired one stops counting on its own, and the tokens your browser or phone uses to sign in are counted separately. A household with a scale, a watch bridge and a couple of scripts sits well under the limit; it is there to catch a runaway, not to ration.
Fixed
-
The heart-rate-variability tile now appears on the dashboard for people whose HRV comes from a ring or a strap. Turning it on under Settings did nothing and said nothing, while the same readings charted fine on the HRV page, which made it look as though the layout was broken. HRV is stored two ways that happen to share a unit: Apple Health, Fitbit and Google Health record SDNN, while Oura, Polar, WHOOP and most watch bridges record nightly RMSSD. Every other place HRV appears already accepted either one; the tile looked only for SDNN. It now uses whichever series you have, titled "HRV (RMSSD)" when that is the one on screen so a reading of 40 is not mistaken for a collapsed SDNN figure. An account with both keeps seeing SDNN, and the two are never blended. The tile's freshness caption was also read off SDNN while RMSSD was displayed, which would have marked a current tile as stale; it reads the series on screen now.
-
Turning the Recovery module off now takes RMSSD off the dashboard in every case. The dashboard has two ways of loading its tiles, and the one used when the snapshot is switched off did not drop the series, so a disabled module still showed up there.
[1.38.7] — 2026-09-03
The weekly backup produces a backup again. v1.38.6 stopped it taking the instance down when it failed, and also stopped it succeeding.
Fixed
-
The weekly backup refused every account because the server had been up a while. The check added in v1.38.6 watched how much memory the whole server process was using, garbage included, and gave up above a line that a long-running server sits above on uptime alone. So every account was refused on the first chunk it wrote, the run finished in seconds, and it reported success. Size had nothing to do with it: an account whose entire stored backup is about a megabyte got the same message as one holding hundreds of thousands of readings, each told its record was too large and that the operator should buy memory. An instance in that state can go a month and a half without a usable copy. That check is gone rather than tuned. The backup is measured against the one thing that really grows with a record, the encrypted copy it produces, against a fifth of the server's memory limit. An account whose backup really does not fit still fails as one job rather than taking the instance down, and the message says what was measured and what it crossed. Measured with a large record under a cap well below the default container, the backup finishes with room to spare; built in one piece, the old way, the same record still dies at that cap.
-
A backup run that wrote nothing for anybody reported success. Accounts present and none backed up came back as a completed job, so the failing-jobs panel stayed empty and the backups page listed the copies it already had. A run that protected nobody is now a failed run, and the backups page shows it as one. A run that writes some accounts and not others still succeeds, with the failures counted per account, because failing the whole run over one record would redo everyone's backup on every retry. Still open: on an instance with several people, an account that fails every week while the others succeed is invisible, because the age shown is the newest copy across all accounts. That needs a per-account view and text in every language, so it waits for a later release.
[1.38.6] — 2026-09-03
Blood glucose can be shown in mmol/L, which it never could before, and the weekly backup finally sits inside key rotation. Text the server writes now arrives in the language you actually read the app in.
Changed
- The rotation tool says what it did not look at. A zero that means "nothing left" and a zero that means "never looked" read the same, and an operator acts on that zero by dropping a key. Each run now ends with how many columns it walked out of how many exist, names any it skipped, and exits with an error when the two disagree. The runbook's retire-the-key step is rewritten around that count, and it tells an operator who already rotated on an older release how to get their backups back.
greetingHouris no longer part of the dashboard snapshot response. A client that decoded it as required needs the field made optional or dropped; thetimezonebeside it is unchanged and is what the greeting should come from.
Fixed
- Blood glucose can be shown in mmol/L. It never could before. The dashboard tiles, the glucose page, the targets panel, the CSV and FHIR exports, the doctor report, the Coach's notes and the low-reading alert all read the setting, and nothing set it. The unit is now a dropdown in Settings under your profile, beside the metric/imperial one and not folded into it, because metric countries are split on which unit they read glucose in. Readings are stored in mg/dL and stay that way; the unit only picks how they are shown. The entry form, the measurements list and the edit sheet ask in your unit and convert on the way in, so a 5.3 typed by an mmol/L reader is filed correctly instead of as a number that passes every check and reads back on every screen as a severe hypo. Editing a value entered in mg/dL does not convert at all, so correcting a typo cannot rewrite a reading into the other unit.
- The same reading converted two different ways. Import and display used slightly different factors, so a value imported in mmol/L did not come back out as the number it went in as. Both ends take the same factor now.
- Text the server writes arrives in the language you read the app in. The interface takes its language from a cookie; the nightly briefing, the Coach nudge and the reminders take it from your account. A single write on page load kept the two in step, and when it was lost (a paint before sign-in, an offline moment, a tab closed in flight) nothing retried, so one English paragraph could sit on an otherwise German page for the life of the account. That is how it was reported: the sentence above the health score came back in English on an account read entirely in German. The server keeps the two in step itself now, correcting the account once when a request arrives in a language it does not agree with. It cannot be dropped in transit, it repairs accounts that have been out of step for months on their next page load, and only a language the app was actually being read in can reach the account.
- The phone app's dashboard greeting no longer falls back to English on a German instance. That request carries no language cookie, so an account that never opened the language picker got English instead of the instance's language. It now follows the same order every background writer uses: your preference, then the operator's default, then English.
- The greeting no longer contradicts the rest of the page. Separate pieces of code answered "what hour is it" for the salutation, and one of them fell back to the device's clock whenever the configured timezone would not resolve, so the header could say good morning while the page said good evening and the dates belonged to a third zone. Both greetings read one helper now, which stays on the configured zone and falls back to the app's default rather than to the device. The server value is gone: it was a clock reading served from a cache for up to an hour, long enough to name a salutation the clock had already left behind.
- An Apple Health import records which export it came from. The import status had always promised the instant the Health app stamped on the archive, and the parser had always skipped it. It reads it now and writes it the moment it goes past, so a run that later fails still says which export it was working from.
- The weekly backup no longer takes the instance down with it. Last release's compression was measured on a machine with a multi-gigabyte heap; the container it runs in is capped at 1 GB, and there the same pass still died, and because the job shares the application process every signed-in user on the host went with it. The document is now written piece by piece: the tables that grow without bound (readings, doses, mood entries) are read a page at a time straight into the compressor and the cipher, every other section is released as soon as its own JSON exists, and nothing is held whole. A record doubled in size now finishes well inside that heap, and the writer stops itself before the limit, so an account too large for its host is one failed backup in the run's summary, not a restart for everybody. Backups written under either older shape still restore.
- The provider health card says what a failing AI provider sent back. The card could report a provider as failing without saying whose problem it was; the status of every failure had been recorded since v1.11 and nothing read it. It is shown beside the failure now, which tells a dead key from a provider having a bad afternoon, and blank where there was no status to record.
- Backups were outside key rotation. The weekly snapshots and uploaded
packs are encrypted like everything else, but under a column called
data, and the rotation tool recognised encrypted columns by their name. So it never read a backup, reported zero rows remaining, and the runbook told you that zero meant the old key was safe to retire; following it made every stored backup permanently unreadable. Rotation covers it now, in bounded batches, and re-seals the ciphertext without reading the plaintext, so both stored forms come back byte-identical. The idempotent-replay response cache was outside rotation for the same reason and is rotated too. - Rotation crashed part-way through and said nothing. Running the tool
against a seeded record showed it dying on the mood day-context note, a
table keyed on the entry it belongs to rather than on an
id. Everything after it, the backups included, was never reached. The walk now takes the table's actual key. - The document index's verbatim text was rotating but never appeared in the summary, so the new coverage count read it as skipped. It has its own line now.
- A misspelled field in a request is no longer accepted and thrown away.
Send
disableCoachhwhere the server expectsdisableCoachand the answer used to be 200 with the key dropped. The settings toggles, the restore and bulk-delete endpoints, message feedback, the admin user update, the diabetes and Coach switches, the onboarding acknowledgements, the provider chain, the chart overlay preferences and the two client-side reporters now refuse a field they do not recognise, and the 422 names the key. A key name is bounded and stripped of anything that is not an ordinary identifier before it is echoed. - Sync, layout, import and the read filters keep accepting a field they do not know, on purpose. The phone app ships optional fields ahead of the servers that will read them, and a self-hosted install runs whichever release its operator last pulled, so a server ignoring a field it has never heard of is what keeps an updated app working against an older install. On a read filter a stray parameter discards nothing, the OAuth endpoints are required by their standards to ignore unknown parameters, and an import file comes from another version or another tool by definition.
Internal
- A column holding ciphertext is now recognised by what the code writes into it, not by what it is called, so a column like the backup blob cannot sit outside rotation again.
- The check that asks whether every column has both a reader and a writer had been wrong in both directions: every column it reported was a lookup key, and a column no code touched was the one thing it could not see. It works now, and its first real run found the glucose unit and the Apple Health export date (read and never written, both fixed above), the provider-health status (written and never read, now shown), and the known-device first-seen date, which no page or export asks for yet.
[1.38.5] — 2026-09-03
The weekly backup costs a fraction of the memory it did, the dashboard stops scanning the two largest tables, and several settings now do what they say.
Security
- Two dependency minimums raised. A URI parser that arrives under several dependencies, the connector SDK among them, was below the fix for advisories about host confusion and request forgery; a query-string parser was below the fix for an array-limit bypass and a denial of service. Both are raised past their fixes.
Fixed
- The weekly backup uses far less memory. On a real instance it had not produced a copy since mid-July: the whole record sat in memory several times over and the process died within seconds. The stored copy is now compressed before it is encrypted, and each stage is released before the next is built. A record of a few hundred thousand readings, on a generous heap, finishes in under half a minute. The pass has a two-hour window of its own now, so a slow disk cannot be mistaken for a broken run. The nightly off-site copy is built the same way, and older backups still restore.
- A failed backup is visible now. It went unnoticed for five weeks because on the backups page an old copy looks exactly like Sunday's. The page now says how old the newest scheduled copy is once it is past due, and how the last run ended when it ended badly, with the reason the queue recorded.
- The dashboard opens faster on accounts with years of readings. Two reads behind the metric tiles scanned every row instead of using an index. Both ask per metric now, and neither grows with the archive; the tiles show the same numbers.
- Mood correlations no longer call a handful of days a finding. The mood page painted "Strong" or "Moderate" on a card after a few paired days. It now needs the twenty paired days and the significance test the rest of the app does. Fewer cards show a coefficient, and a card without one says whether you are still collecting days or nothing stood out.
- A medication with no dose time no longer reports perfect adherence. It showed 100 percent in the insights list, the blood-pressure read and the compliance summary, and lifted every average. It is left out until it has a schedule, as the doctor report always did.
- A day you never opened the app is no longer a skipped dose. The blood-pressure medication chart counted every day without an intake record as a zero, so a week away read as a week off your medication. Only days with an intake record count now; a dose you marked as skipped still does.
- A lab note this instance can no longer decrypt survives an export. Such a note or biomarker context used to come out empty, and a restore finished the job. Both export as a marker saying the text was encrypted under a key this instance no longer holds, and the marker survives a restore. The text itself is still unrecoverable without the old key; you can now see something was lost.
- A deleted mood entry or custom-metric reading counts as deleted in the AI features. The correlation channel built from a custom metric kept using deleted readings, and the check that decides whether an AI assessment is still current could not see a deletion, so yesterday's text was re-dated and served as today's. Both skip deleted rows now.
- A dismissed correlation finding comes back when its evidence has really moved. The check compared two numbers instead of the stored fingerprint, so a dismissal restored from a backup that carried the fingerprint but not the numbers could never be released. It reads the fingerprint now, and a dismissal still survives ordinary sampling noise.
- If you host this yourself, settings you could set but not change. The
environment:block indocker-compose.ymlis a whitelist, so a value in.envlooked configured and never reached the app. Now forwarded, each with a line in.env.production.example:DEMO_MODE;LOG_LEVEL,LOG_SAMPLE_RATE,LOG_SLOW_THRESHOLD_MSandLOG_INCLUDE_STACK;LOKI_ENDPOINT,LOKI_USERNAMEandLOKI_PASSWORD;DATABASE_STATEMENT_TIMEOUT_MS;COACH_MESSAGE_RETENTION_DAYS,HOST_METRIC_RETENTION_DAYS,DENSE_INTRADAY_RETENTION_ENABLEDandINTEGRATION_FAILURE_ALERT_THRESHOLD;TELEGRAM_WEBHOOK_SECRET;NIGHTSCOUT_PRIVATE_ORIGINS;WITHINGS_REDIRECT_URI,FITBIT_REDIRECT_URIandGOOGLE_HEALTH_REDIRECT_URI;OPENMETEO_BASE_URLandOPENMETEO_GEOCODING_URL;ADMIN_AI_BASE_URL_ALLOWLIST,CODEX_MODEL,CODEX_MODEL_FALLBACK_CHAINandCOACH_EXPERIMENT_VERDICT;APNS_KEY,APNS_KEY_FILEandAPNS_PRODUCTION; andDASHBOARD_SSR_PREFETCH. The Data & Privacy page now quotes the Coach retention window you actually set, and a Codex slug rotation is something you can ride out by setting a model rather than waiting for a release. A variable the server reads can no longer be left off that list without a written reason. APNS_KEYandAPNS_KEY_FILEwork where the documentation always said they did. OnlyAPNS_KEY_B64ever reached the container, so choosing either of the other two got a silently disabled APNs and no explanation.- The Web Push key loader no longer advertises names it cannot read. The
WEB_PUSH_*aliases andNEXT_PUBLIC_VAPID_PUBLIC_KEYwere never on the compose whitelist or in any example file, so a value under those names could not arrive. They are gone, and the self-hosting guide no longer mentions them. The example file also said the realVAPID_*names were absent from the whitelist; they have been on it since v1.17.1. - The AI switches save when they are sent to the general admin settings endpoint. That endpoint accepted them, answered as if saved and stored nothing, so a script had no way to tell a written switch from a dropped one. They are written now and echoed back. The admin console's own endpoint was never affected.
Security
- A notification channel the operator switched off stops delivering. Turning Telegram, ntfy or Web Push off instance-wide only hid the setup card while every account that had configured the channel kept receiving on it. A channel that is off delivers nothing now, the skip shows in the notification diagnostics with its reason, and switching it back on restores delivery without anyone re-enabling anything. Turning such a channel on from the user side is refused with a message that says why.
- Every module's API routes are now declared with the module itself, so a module that is switched off cannot leave a route reachable by being forgotten. Nothing was reachable this way before, and nothing changes for you today.
- Every webhook verb that checks the shared secret is rate-limited now. The Withings, Telegram and Coolify hooks limited their POST but not the GET and HEAD a webhook UI sends to confirm a URL: a free way to guess a callback token. Each verb runs the same limit before it looks at the secret; the real services send one or two probes.
[1.38.4] — 2026-09-02
The shared AI key reaches the provider its address names, and the image carries no flagged dependency versions.
Fixed
- The shared AI key reaches the provider its address names. If you
filled the server key with an Anthropic key and
https://api.anthropic.com/v1, every AI call failed and nothing in the error said why: the request was always built for OpenAI's API and posted at Anthropic's, which speaks a different shape, and the address field accepted the value happily. The base URL now picks the client. Anthropic's host, and any subdomain of it, is called on Anthropic's own Messages API and defaults to a Claude model; every other host keeps the OpenAI-compatible path, which is also the right one for a local server or a gateway. A base URL that is not a URL at all behaves as it did. The operator card says which providers the field accepts. Found in a fork by @sweidinger, who worked around it there; this fixes it at the source.
Security
- The published image no longer ships flagged dependency versions. Some minimum versions had gone stale: the sanitiser behind the doctor-report PDF sat below the fix for GHSA-55q2-fjhq-7xh7, the CSS processor the build runs on sat below the fix for CVE-2026-69153, and a schema library that arrives through two unrelated parents had no minimum at all (CVE-2026-59952). All are raised inside their existing major, so nothing changes shape for anyone.
- The Prisma command line inside the image gets the same versions as the app. The image carries a separate install of it, and that one does not read the project's version overrides. It was quietly resolving an HTTP server (GHSA-frvp-7c67-39w9, CVE-2026-39406) and the same schema library (CVE-2026-59952) at the flagged versions while the application itself was already safe. Both are set to the safe versions on that side too. The comparison between the two files had been skipping every package whose name starts with an at sign, so a version set in one place and not the other went unseen; it sees them now.
- The automatic dependency merge runs with less privilege. That workflow handed a write token to every job in the run; the default is read-only now and the write scopes sit on the one job that spends them. Which updates merge automatically has not changed.
[1.38.3] — 2026-09-02
An account with two-factor can be deleted from the phone, and every data source is called by its name.
Fixed
- An account with two-factor can be deleted from the phone. Deleting the account, and erasing the record while keeping the account, both ask for a fresh proof of your second factor once you have one enrolled. That proof lived on the browser sign-in, which the app does not have, so anyone with an authenticator app or a security key was refused from the app no matter what they sent, and a browser was the only way through. The app now re-proves the factor the same way it already does when you manage two-factor. Nothing about the web path changed, and a mistyped confirmation does not use up the proof. One thing to know when erasing the record from the app: API tokens are part of what that request deletes, so the app signs itself out and signs in again.
- Every data source is called by its name. In Settings, Sources, some
entries in the priority ladder showed their internal name,
GOOGLE_HEALTHinstead of Google Health, and Oura, Polar and Strava the same way. The label list was written when there were far fewer sources, and every provider added since fell through to the raw value. The same thing happened in the measurement list's source filter and column, and in the medication intake history, where a dose synced from Apple Health read "via APPLE_HEALTH". Each is complete in every language now, and a new provider cannot ship without its name. - If you write against the API, the deletion and erasure routes accept a
single-use elevation from
POST /api/auth/step-up(a code, security key or passkey) sent back in theX-Step-Upheader. A token on its own still gets nowhere, a password-proved elevation is refused here as a password login never satisfied this check on the web, and the proof is spent only when the erasure is about to run.
[1.38.2] — 2026-09-02
Anthropic keys work on current Claude models again, and the daily briefing stays in the language you read the app in.
Fixed
- Anthropic keys work again on current Claude models. Everything that asks the model for JSON (the daily briefing, the status cards, lab and document extraction) had been failing on the Claude 4.6 family upward, on Opus 5 and Sonnet 5, and on the Fable models. The app steered the answer by starting the model's reply with an opening brace and gluing it back on afterwards; those models reject that outright, so anyone with a current model got a provider failure on each of those calls while plain chat kept working. The trick is gone on every model. The request asks for a single JSON object and nothing else, and an answer wrapped in backticks or prefixed with text still parses. Reported independently by @sweidinger, whose fork retried without the prefix.
- Lab and document OCR no longer refuse a current Claude model. The
image check knew the Claude 3 and 4 families by name, so a key set to a
newer model was told the provider cannot read images even though it can,
and a lab photo was never sent. Any Claude from the 4 family upward, and
every Fable model, now counts. The model presets in Settings also offer
claude-sonnet-5andclaude-opus-5. - The daily briefing stays in the language you read the app in. Someone using the app in German could find the dashboard lead and the Today briefing in English, and no amount of reloading changed it. The nightly writer resolved an account that had never picked a language to English rather than to the instance's language, and the briefing cache kept no record of the language it was written in, so every reader got whatever the last writer left there. The refresh only replaced it once the cache was a day old and the provider answered, which with a provider at its usage limit is never. The cache now records its language: if you read in another language you get the "preparing" state and a fresh briefing in your own, and briefings written before this release are served as before until the next write.
- On a server you run yourself, every background writer (the nightly briefing, the status cards, period narratives, reaction lines, the morning refresh, the briefing push, coach nudges, reminders, and the document and workout summaries) now resolves a missing account language to your instance's default first and only then to English.
[1.38.1] — 2026-09-01
You can push readings in from your own hardware again, and a lab reading shows where it sits against its reference range at a glance. @Antiheld86 built the measurement ingest token (#881, from their proposal in #878); @TimonBed built the reference-range bar and the lab-list alignment (#860, #880) and fixed lab OCR for GPT-5 models (#861).
Added
- A token for pushing readings in. Since v1.30.17 API tokens keep to their scope and the generic token button is gone, so a scale or a watch bridge through Home Assistant had no way in. Settings → API & Tokens now mints a token that adds readings to your own record and nothing else; it cannot read, change, delete, export or mint another token. Shown once, valid for a year, revoked like any other. Contributed by @Antiheld86 in #881, from their proposal in #878.
- The docs site has a guide for wiring one up, with a worked Home Assistant example, the measurement types a scale or a watch bridge tends to report, and what each error means.
- A range bar beside every lab reading, from @TimonBed in #860 and #880. The lab list, its mobile cards and each analyte's history carry a compact bar: the window in green, the reading as a marker, blue below, amber above. A one-sided range gets a bar with its one bound labelled, and a reading far outside its window widens the scale so the marker stays visible. Badge, bar and sparkline line up in columns and share the colours. A qualitative reading, or one without a range, shows no bar.
- The token posts to
/api/measurementsand to/api/measurements/batch, and the settings page lists both beside the medication endpoint.
Changed
- Readings pushed with the token count as manually entered, so you can correct one in the app if a sensor sends something wrong.
- The below-range and above-range badges on a lab reading carry the same light tint as the bar, blue below and amber above; in range stays neutral. Part of #860.
- Routine dependency updates ride along: the Prisma Postgres adapter (see Fixed), the Argon2 binding, the WebAuthn server library, Radix, Lucide, the form resolvers and the dev tooling.
- A push that claims to come from Apple Health is refused rather than relabelled, because that label is half of how the iOS app recognises its own rows, and such a push no longer moves the phone's sync checkpoint.
Fixed
- Lab OCR works with a GPT-5 model on the OpenAI path, fixed by @TimonBed in #861. The list of models trusted to read an image stopped at the GPT-4 generation, so a GPT-5 model was never sent the photo. The extraction budget doubles because these models spend part of it on reasoning, and a provider failure now says so instead of blaming the photo.
- A lab report imported as a document could leave a reference range whose low bound sits above its high one, and for a new analyte that pair went into its catalog entry too. The pair is now dropped rather than swapped, the range text as printed survives, editing or confirming a staged fact applies the same rule, and the sparkline no longer paints such a reading green (#882).
- The OAuth callback warning added in v1.38.0 never appeared in the
published image, and the copyable callback address showed only a path;
both were baked in at build time. They now come from the server and
reflect the
NEXT_PUBLIC_APP_URLor*_REDIRECT_URIyou actually set. - A Prisma adapter update had broken the image build before any release
carried it, because two copies of
pgdrifted apart. It is a runtime dependency now with one copy in the lockfile (#872).
Security
mysql2is updated past GHSA-3f6p-5ww8-9rcr in both the application tree and the Prisma CLI install inside the image. It reaches the tree only through Prisma's MySQL driver, which this app never opens.- The ingest token is confined to its owner's record; pointing it at a record someone shared with you is refused before the sharing grant is consulted.
- Only a web sign-in can mint one. No API token can, because a phone's sign-in lasts a day and this token lasts a year.
- A repeated request with an idempotency key could be answered from the cache after a check of the sharing grant alone, not of the credential presented. Nothing could be written that way, but a cached answer about a shared record could be returned to a token the endpoint itself would have turned away.
[1.38.0] — 2026-08-30
The health score works with the record you actually keep.
Until now it needed three separate areas of health plus one measured value before it would grade at all. Someone who had tracked blood pressure and sleep for months got the same blank card as someone who had just signed up, and the card never said what was missing. Three areas is now a recommendation: one area scores, the arithmetic is the same at every width, and the card says what the number rests on.
Added
- Below full breadth the card says what the number rests on. One line, "Based on {n} of {recommended} areas of health", and the number stays full size in its band colour. A narrower score is a smaller claim, not a worse one. The areas themselves are one tap away in the composition list.
- When an area stops counting, the card says which one and why. If a window runs out of recent data or a source goes quiet, the card names the area that left, in the same words that area uses when it is short of data. You can dismiss it, and it only comes back if the composition moves again. It is not a notification: worth noticing, not worth being woken for.
- Tailscale is a documented way to serve HealthLog, from a self-hoster's suggestion in #847.
- For anyone writing against the API, every score response carries an
optional
scoreBasisblock with the area count, the recommendation, the tier and whether a measured physiological value is among them. It is additive, so an older client keeps working.
Changed
- A blank card now means one thing: nothing in your record can be scored yet. Everything else gets a number.
- The pillar selection in settings saves from one pillar upward. The page still recommends three, because a one-area score is thin.
- The demo seeds every area, so a fresh demo shows the score at full breadth instead of narrowing to a cardiometabolic view after a few weeks.
- For API clients,
scoreVersionmoves from 3 to 4. Stored days keep the version they were written under, and deltas are suppressed across the change as before.
Fixed
- The coverage line no longer says the score "needs" three areas.
- The integration cards warn when the configured OAuth callback address does not match the address the app is served from, from @TimonBed in #855. Self-hosted OAuth otherwise fails with nothing on screen to explain it.
Security
- Every scored band cites a clinical threshold or a published guideline. Nothing is made up from your height or your age beyond guideline bands documented as such; the BMI-22 default that shipped before v1.34 is exactly what that rule keeps out.
[1.37.33] — 2026-08-29
A community day: a contributor's first code fix, a self-hoster's suggestion turned into documentation, and housekeeping.
Added
- Tailscale is now a documented way to serve HealthLog, suggested by
@el-abcd in #847. For a single Docker box,
tailscale servestands in for the reverse proxy entirely: trusted HTTPS on your tailnet, no extra container, the free plan covers it, and the iOS app with Apple Health sync works over the same path.tailscale funnelis covered as the explicit public opt-in. The guide lives in the self-hosting docs and on docs.healthlog.dev, next to the reverse-proxy guide it complements.
Fixed
- A password shorter than the minimum showed its "too short" label in the strength palette's colour instead of reading as invalid. Fixed by @TimonBed in #848, their first contribution here: the label now uses the destructive colour while the bars stay muted.
- If you host this for others, the admin notice about login locations resolving through the online geo lookup repeated on every worker restart, because the marker that should have shown it only once lived in the process's memory. The marker now lives in the notification history, survives restarts and retention pruning, and resets only when the configuration state changes, so the notice fires once when the condition appears and once more only if it goes away and comes back (#851). A host that has disabled IP lookups outright is no longer notified at all; there is no egress to warn about.
- Restored backups briefly wrote decrypted health data with default file
permissions; the restore script now creates its output owner-only.
TRUST_PROXY_HOPSjoined the composeenvironment:whitelist so a.envvalue reaches the container without editing the compose file. - The HTML stripper the notification senders share now withstands multi-character bypasses: one helper, applied until nothing changes, replaces the per-sender copies of a single-pass pattern.
- The operator docs were checked line by line against the current code: stale claims corrected, invented features removed, and the API reference brought in line, here and on docs.healthlog.dev.
Security
- Dependency overrides lift
flatted,js-yamlandminimatchpast their published advisories; the Prisma CLI install in the production image is fixed to an exact version; every open code-scanning alert was either fixed or dismissed with a written reason.
[1.37.32] — 2026-08-28
Korean joins as a seventh interface language, contributed by @aucun6352 in #839, together with the fixes that had to land before it could work.
Added
- Korean (
ko) ships as a seventh interface language, community-maintained alongside French, Spanish, Italian and Polish. The whole app speaks it: dashboard, settings, share views, the clinician report, notifications and the AI prompts. The PHQ-9, GAD-7 and WHO-5 questionnaires carry their officially published Korean wording so a score stays comparable to the Korean literature; the sleep questionnaire stays in its English-validated form with a note saying so. The Coach and the daily briefing take the reviewed English body with a Korean reply instruction rather than a hand-curated Korean one, the same route the other community languages took.
Fixed
- Text matching between app labels and text a person or a lab typed matched nothing at all in Korean. The app normalises text by pulling accented letters apart and dropping the accent marks, which turns Latin accents into plain letters, but a Korean syllable pulled apart into its letters was never put back together, so the normalised text no longer resembled any table the app carries. Both readers of that normalisation were affected: the check that decides whether a Coach recommendation needs a citation, and the parser that reads a printed reference range. Korean syllables are put back together now, and every other language reads exactly as before.
- No Korean analyte name could resolve to a LOINC code. The lab-name matcher kept only a to z and digits and dropped everything else, so every Korean lab name in the catalogue matched nothing, and a Korean account's cholesterol result reached a FHIR export uncoded.
- The reference-range parser now reads the tilde a Korean lab prints
(
3.5~5.0), in both the ASCII and fullwidth forms. It had the hyphen and the dashes and not the separator the reports use.
[1.37.31] — 2026-08-27
The server knew when an AI provider was failing; the operator had no way to see it.
Added
- The admin AI settings section shows a provider-health card: one row per provider type with how many users' provider chains include it, how many are currently failing, the longest unbroken run of failures, and the last success and failure times. Operator-managed providers sort first, since their failure takes the shared fallback down for everyone relying on it. The endpoint behind the card is admin-only and reports counts and timestamps, never individual users.
[1.37.30] — 2026-08-27
An operator pointing the global AI provider at a private OpenAI-compatible proxy got a network error on every call, and nothing said why.
Fixed
- The operator's global AI provider can now reach a private host, provided
that host is named in
ALLOW_LOCAL_AI_PRIVATE_HOSTS. Its base URL is typed by the operator in the admin settings, so it now consults the same allowlist as the gateway a user configures for themselves, instead of being restricted to public hosts. A private host that is not on the list is refused exactly as before, personal API keys stay restricted to public hosts, and the ChatGPT sign-in path still only talks to its built-in endpoint.
[1.37.29] — 2026-08-26
The chart range selector said points and meant days. Someone who weighs in twice a week picked "7 pts", saw two, and reasonably concluded the chart was broken.
Fixed
- The range tabs now say what they do: 7d / 30d / 90d / All, in every language. The selector has always fetched a calendar day window; only the labels claimed a point count. Anyone measuring less than daily saw fewer points than the tab promised and had no way to know the tab was the lying part.
- The "All" view no longer serves monthly means under a weekly-average caption. The server picked how coarsely to summarise from the requested window, and "All" always asks for the longest one, so every record came back in monthly buckets no matter how short its history, while the caption was worked out from the span the data really covers. A short record showed a handful of points labelled "Weekly avg". The summary level now follows the actual span and matches the caption.
- The same tab no longer means two different things depending on how the chart opened. On the dashboard's preloaded data the old code showed "the last N readings" reaching arbitrarily far back; on a directly opened chart it showed a day window. Both paths now filter to the same calendar day window, which also keeps comparison overlay data out of the visible range on sparse records.
- The mood chart windows by days like every other chart. It used to slice the last N logged days out of the whole history, so its "7" could span weeks while the weight chart's "7" meant a week.
- The aggregation chip in the chart header reads the same summary-level decision the chart and its data table use, instead of working it out separately.
- The image upgrades its OS packages at build time, so the published openssl fix for CVE-2026-14456 ships with this build instead of waiting for the next base image rebuild.
[1.37.28] — 2026-08-24
Most parts of a shared record were selectable and invisible. Ticking one and sending the link showed the recipient nothing, while the same link's PDF download carried it.
Fixed
- A share link now renders every part of the record it carries. Most of the selectable sections reached the page and were never drawn: identity, emergency data, lab results, GLP-1 therapy, logged doses, illness episodes, visits, immunizations, family history, mood and cycle. The data had been arriving all along, from the same place the PDF gets it, so someone who ticked "Lab values" had every reason to believe they had shared them and no way to find out otherwise.
- A section that carries nothing now says which kind of nothing it is. A part that was not selected stays absent entirely, because naming it would itself disclose something. A selected part with no records says so. A selected part whose module the record has switched off says that instead, so nobody reads an empty card as an empty life.
- Logged medication doses reached the FHIR download and neither the page nor the report. The one control whose effect a person could only see by opening the bundle in another program now shows the twenty most recent, with a count of what the cut left out.
- The doctor report printed the raw translation key for a visit's kind rather than its name, and so did the daily digest's upcoming-visit line. The label was built from the internal name of the kind, so a routine appointment read as "encounters.kind.ROUTINE" in a document a practice files and on a phone's lock screen.
- A lab row that diverges from its reference range now names both windows it was measured against instead of one.
Changed
GET /api/insights/derivedaccepts awindowDaysparameter, so a client can ask for a longer trend than the fixed fourteen days. The ceiling is ninety; beyond that the read would have to scan every raw reading, so a larger value is refused rather than clamped. Omitting the parameter changes nothing.- The response already carried the window asked for and the history that backed it, and the API reference now says so, so a record shorter than the window reports both.
- The wellness-score value object is modelled in the API reference. It was typed as an open record with prose describing one metric, which is why a client team read the document, built what they could see, and reported two fields as missing that were in the payload all along. The value shapes that stay undocumented are named where the gap is.
- The share link's report and FHIR downloads are published in the API reference alongside their document sibling.
[1.37.27] — 2026-08-23
Several things in this release looked fine from the outside and were not, the ECG import most of all: it failed on every watch outside a US locale and never said why.
Fixed
- Importing an Apple Health archive rejected every ECG recording exported outside a US locale, and counted each one as a failure without saying why. Run against real Apple Watch exports, not one parsed. The causes, and only one of them is about language: the file opens with a header row that has no value after it, the sample values are written with a decimal comma, the sample rate is written as a fractional figure with a comma rather than a round number, and a German watch writes the column names in German. The importer had only ever seen the US form of the file. All of these forms are accepted now.
- Two of Apple's own English verdicts were dropped as unknown. "Heart Rate Over 120" and "Heart Rate Under 50" are what the export writes, which matches neither the framework's documented names nor the wording in Apple's instructions. A verdict the app has no category for is now recorded as unrecognised, which is different from a recording that carries no verdict at all.
- The Coach could not report a single environmental correlation. Several pages built the correlation list independently with different sets of channels, so asking whether air pressure related to your sleep searched a list that never held the pair. They all read from one place now.
- A metric card's assessment received every surviving correlation in the record rather than the ones about that metric, because the step that narrows to one metric had been dropped.
- The dashboard and the analytics page looked up the same single row once per metric, dozens of times per load. It resolves once per request now.
- The check that a Coach answer backs its claims with citations passed everything outside English and German, because it looked for English and German words in text written in your language.
- The Coach refused legitimate questions in some languages and let off-topic ones through. Its topic vocabulary was English and German, so the Italian word for "series" sat on the off-topic list while nothing recognised the Italian for "measurements".
- A number the Coach wrote in prose could be matched against an unrelated
reading. Unit words were recognised in English only, so
7,4 Stundencarried no unit, and a value with no unit matched every entry it was checked against: a sleep figure could be backed by a glucose result. French dates were not parsed either, so correct prose was stripped as unverifiable. - Asking Telegram for help in French, Spanish or Polish got no reply at all.
- The mood icon picker searched only English names, so typing back the translated group header it had just displayed returned an empty grid.
Changed
- Deleting every threshold override at once asks first. Leaving the metric off the request used to erase the whole set, with no confirmation and, unlike deleting a single override, no rate limit of its own. It takes the same explicit confirmation the account deletion does. Deleting a single metric's override is unchanged.
- Removing a passkey now needs a fresh proof that you still hold it, the same check that has always applied to removing a second-factor security key. The softer check had been sitting on the primary sign-in credential. A passkey satisfies its own check, so an account with nothing else enrolled keeps control of its own credential list, and the app can still do it through the re-verification step it already uses.
- A portable backup that claims a section it does not carry is refused whole rather than restored in part, and the error names the missing section. A section the export left out on purpose and declared as omitted still restores, which is what keeps the disaster-recovery export working.
Internal
- Page loads are measured now against a large record, and the cost of a page does not grow with the size of the record.
- Every signed-in page was downloading the settings validation code. The app shell wraps all of them and borrowed one small function from a module that also carries the settings schemas, so a validation library and its modules rode along on every page load for a check that reads a URL slug. The shell has its own copy of that function now. No page grew, most got a little smaller, and the Insights overview has room under its size limit again.
- Unused code that the dead-code check had been missing since a tool upgrade is gone. Every definition still in use stayed.
[1.37.26] — 2026-08-23
The app is offered in six languages, and in several places it was reading what you wrote as though there were two.
Fixed
- Side effects you recorded on a GLP-1 medication contributed nothing to the
timeline unless you use English or German. Tapping a chip stores the
translated word, so the same symptom is
nauseaon one account andNudnościon another, and the timeline compared that against a hand-typed list of the English and German labels. The same list sat behind the Coach and behind the doctor report, where a clinician read an empty side-effect table over a record that had entries. - A lab reference range printed as
jusqu'à 5,0was read as no range at all, so the value was never flagged as normal or out of range. Ranges in all six languages parse now, and one the parser cannot read is recorded as unreadable rather than looking like a result nobody set a range for. - Lab analytes named in French, Spanish, Italian or Polish got no LOINC code
on the FHIR export, and a qualitative result of
négatiforujemnygot no SNOMED code. A lab report in one of those languages also failed the check that decides whether a document is a lab report, so it was never offered for automatic staging. - Analyte names with German umlauts never matched either, which is the older
half of the same defect and affected the language most people here use.
The matcher deleted any character outside a to z, so
Hämoglobinlost its ä and never met its own entry. It only ever worked for people who left the umlaut off.NüchternglukoseandHarnsäurewere the same. - Over the assistant connection,
Fer,Żelazo,poidsandSommeilresolved to nothing, and asking aboutPulsanswered with "Pulse-wave velocity", because a fallback matched English display names by substring. Medication names written asMetforminaorLewotyroksynamissed the class that connects a dose to a symptom.
Changed
- A route that has been removed now answers
410 Gonewith a machine-readable code, the version that removed it, and its replacement where there is one, rather than the bare404it used to. A404is indistinguishable from a broken deployment, which is why a client that met one kept retrying a route that was never coming back. The retirements are listed in the API reference and on the capability endpoint, so a client can learn about one without comparing versions. - A route can no longer disappear without a trace: it is either dropped from the API reference or listed there as retired.
[1.37.25] — 2026-08-22
Two things this release is about. The Coach could state a figure your record does not support, in exactly the situation where it was most likely to: when you asked about something you have never recorded. And a backup you cannot fully restore is not a backup, so most of what a restore used to leave behind comes back now.
Fixed
- The Coach checks its own figures against a record of what it looked up, and edits out anything that record cannot account for. That check did not run when every lookup came back empty. Ask about a metric you have never tracked, and an invented number was passed straight through, with no note that anything went unchecked, so the answer read exactly like a verified one. The check now runs on a turn where the lookups found nothing, and because removing only the digits would leave the sentence still claiming a trend that does not exist, such an answer is replaced with a statement that nothing is recorded.
- The Coach could also describe a weight history that no longer exists. Syncing a body-composition provider replaces your rows, but the monthly summaries built from the old ones stayed behind, so one screen could show a year at 95 kg beside an all-time range that never got there. A summary is an average of real readings and cannot sit outside their own extremes; when it does, that band is dropped rather than narrated.
- The Insights hero said "based on your last 90 days" whether or not there was a briefing behind it, and the pulse page offered a personalised Karvonen target band while returning population percentiles by age and sex, or a flat range when your age is unknown. Both say what they do now, in every language. The Coach's "all time" window was the same length as the option beside it called "year so far"; both labels tell the truth now.
- An account that had grouped its own mood factors could not be restored at all. The restore hit a category the file never carried, rolled the whole thing back, and handed you a database error and an empty account. The categories you create travel now, and they are written before the tags that point at them.
- Someone managing a record you gave them access to could write a titration step onto a medication and then be refused that medication's history. The read now uses the same access rule as its siblings.
- Disconnecting a Withings account left the connection state reading "connected" until something else refreshed it, because the disconnect neither logged itself nor marked the connection as gone the way every other provider does. Two more disconnects wrote no log entry at all when there was nothing connected.
- The integrations page could not tell "nothing delivered yet" from "the freshness query failed": both arrived as an empty list. The failure says so.
- On a self-hosted instance behind a reverse proxy that does not pass the host through, signing in from the iOS app failed. The redirect was built from the address the server process bound to, so it came back naming an address iOS refuses outright. Redirects to this same deployment no longer name a host at all. The same bug sat in several places and all of them are fixed.
- The Prisma CLI in the published image still resolved a dependency with a published advisory. The application itself had been updated already, but that install is a separate one with its own resolution, so the image carried the advisory while every dependency check read clean. The two are kept in step now.
- The demo instance resets its published credentials on every migration run, so the account named on the site keeps working.
- Turning the API off used to lock people out of their own tokens. The switch was checked on the read as well as the revoke, so a user could neither see nor clean up tokens that were still live. It stops tokens from working; it no longer stops you tidying them.
- A preference update read its request body with no size limit at all, where its neighbours cap at one kilobyte. Several more routes threw away the detail of why they refused a value, so a name that was empty and a name that was too long came back as the same sentence.
- Two sync routes had no rate limit where their siblings have one, and every sync route read its request body inside a catch that swallowed errors, so a typo in the body reported a successful run. A body that is absent still means an incremental sync; a body that is present and wrong is refused now.
- Two list endpoints widened their result on a typo instead of refusing it: an unknown metric type was dropped and the read came back unfiltered, and a nonsense page size reverted to the default.
- The medication ingest endpoint counted its rate limit per client address, so several bridges behind one router starved each other. It counts per token now.
Added
- The restore now returns your conversation history with the Coach: every thread, every turn in it, the record of which documents a thread was grounded in, and the facts, plans and reminders it carries between threads. Until now a restored account had a Coach that had never spoken to it and had to be told its own history again.
- It also returns the medication history: pause periods, the titration steps behind today's dose, the packs on the shelf, and the ledger of refills and consumption behind their counts. The remaining count comes back exactly as recorded rather than recalculated from the ledger, because that number is what the low-stock reminders were computed from. The per-drug reminder timings travel too, so a restored drug turns orange when it did before instead of falling back to the defaults. Archived schedules come back with their succession chain intact, and what a medication was meant to move comes back with the analyte it pointed at.
- The rest, in short: ECG strips with their trace, their classification and the reading they were filed against, where a restore into a populated account used to leave the old strips behind. Which conditions a document was filed under, and the facts extracted from it, including which of those were reviewed and committed to the record. The mood taxonomy you shaped for yourself: the groupings you created and the seeded factors you chose to hide. Personal bests, badges, and the per-day environment readings with the location periods that explain them.
- Screener history and the consent record come back in the disaster-recovery export only. Completed WHO-5, PHQ and GAD administrations include the PHQ-9 self-harm item, which is not written into a portable file that can be opened anywhere. The portable export says in its own manifest that they are absent rather than leaving you to notice.
Changed
- Per-event notification preferences are recorded as outside the backup on purpose, not as something still to do. Each one addresses a single notification channel, the channel itself is excluded for carrying secrets, and the restore deletes every channel it finds, so a carried preference would point at a row that cannot exist.
- Dependency updates: pg-boss, Playwright, Testcontainers, Prettier, and two workflow actions.
- The API reference now describes the whole API. A large part of the routes, including many the iOS app calls every day, had never been written into it, and nothing compared the document against the routes that exist. Every route is now either published or named in a list with a reason for its absence, and the reasons say which kind: the admin console cannot be reached by an API client at all, OAuth callbacks are driven by the provider, webhooks authenticate by a shared secret, and the discovery documents have their format fixed by somebody else's specification.
- The API reference also says how to authenticate, which it never did. Both schemes were defined and neither was referenced, so anyone generating a client from the document got one that sends no credentials and treats every route as open.
- The idempotency rules are in the API reference. The write operations that
honour the
Idempotency-Keyheader now declare it, together with theX-Idempotent-Replayheader they answer with, including the detail that a malformed key is ignored rather than refused.
[1.37.24] — 2026-08-16
Fixed
- The hero card on the start page no longer says last night's sleep is still missing when no sleep reaches the record at all. It used to check only whether last night had arrived, so anyone without a tracker read the note every morning about a delay that was never going to end. The note now needs a night within the last week: a flat battery or a few nights without the device keeps it, a week of silence retires it, and someone who has never recorded sleep never sees it.
- The same rule settles the day itself. Records with no sleep source used to sit on "provisional" indefinitely, which the daily push line reads too, so the notification carried the same untrue aside.
[1.37.23] — 2026-08-15
Changed
- Postponing a checkup is a button on the card now, beside "erledigt" or "jetzt messen", instead of a row inside the overflow menu. That is the shape the medication card has always had, where "genommen" and "übersprungen" sit next to each other, and it puts the action one tap away rather than three. The compact list view carries the same action as an icon button. Edit, measurements and delete stay in the menu.
Fixed
- The quick options in the postpone sheet ("+7 Tage" and its siblings) fill in the day they name even when a daylight-saving change falls inside the span. They used to add a fixed number of hours, which shifts the clock past midnight in the week a country moves its offset, so the option could write the day before or after the one on its label.
[1.37.22] — 2026-08-15
Fixed
- The doctor-report export refuses a selection that includes nothing instead of rendering an empty document. The selection panel already disabled its own button in that state, so this only ever reached the server through a hand-built request, and it spent an export slot on a file with no content in it.
- A lab value in the FHIR export no longer names UCUM as its unit system when it carries no UCUM code. That combination told a receiving system the unit was coded when it was not, which is worse than saying nothing: the unit still travels as a display string, and the coded form appears only when the analyte and unit actually resolve to one.
[1.37.21] — 2026-08-14
Fixed
- A dose that reaches the server through the batch route, the path a phone's offline queue drains through, now quiets an active snooze the same way a directly logged dose always has. Before, a take synced in later kept counting as snoozed and the reminder kept ringing.
- Marking a dose through the single-medication intake route now also closes the still-pending dose-due reminder in the web app and refreshes its badge. The other intake paths have done this since v1.18.4; this one, which is exactly the route a phone's offline replay uses, left the web reminder standing.
[1.37.20] — 2026-08-14
Added
- A checkup reminder can now be postponed or skipped. Postponing moves the next due date to a day you pick; skipping closes the current cycle and restarts the interval from today, without pretending the checkup happened. Every completion and every skip lands in a new history, kept per reminder, including whether it happened on time, so the checkups page and the API can show how a reminder has actually been handled over the years.
- The dashboard hero can show the reminder rail instead of the health score. The choice lives in the dashboard settings and does nothing until you change it. Thanks @tarantila for pushing for this one.
- The restore dialog shows what a backup contains before you confirm: per-type counts read from the stored file, with a note when the preview cannot be computed.
- A mental-health screening shows its per-item answers on the detail view, so you can see which questions carried the score, not only the total.
- Document search understands German and English medical synonyms: searching for "Blutdruck" finds a report that says "blood pressure", and the other way round.
- A deleted custom-metric entry can be brought back with one tap for a short while instead of being gone the moment you tap.
Changed
- Onboarding says plainly that Apple Health data arrives through the iPhone app, and the Apple Health card reports the backfill progress the server actually sees: accepted counts, the oldest reading, and how recently data last arrived. Reported by @HercoGC in #778.
- The measurements and mood pages keep their filters in the address bar, so a filtered view survives a reload and can be shared as a link, the way documents already worked.
- The sleep-stage tooltip labels one night's figures as main sleep, naps and total sleep instead of an averaged label that fit none of them.
- The daily digest is available offline like the rest of the app.
- Backups carry the checkup reminders and their new completion history end to end, and an appointment or vaccination that references a reminder keeps that link across a restore.
Fixed
- Apple Health ECG exports in the single-column layout import correctly. Apple writes two CSV shapes; the importer understood only the paired one and skipped every recording in the other, reporting each row as malformed. Both layouts parse now, and the declared unit is converted before validation. Reported by @HercoGC in #794.
[1.37.19] — 2026-08-14
Changed
- Medication stock understands per-slot doses everywhere now. Every schedule carries its resolved dose, the remaining-doses figure weighs each slot by what it consumes, there is a runway in days on the list and the detail, and the low-stock notification runs on the same numbers, so the card, the API and the push can no longer disagree about how long a supply lasts. Refs #767.
- A document whose summary job died no longer says "being generated" forever. After an hour it says that no summary is available and offers the manual button, and an hourly background job repairs stuck documents on its own.
- The morning reminder run gives up after bringing up an ignored Coach reminder three mornings in a row, because a reminder that nags forever is worse than none.
- Accepting an updated disclaimer is tracked by version now, so a future revision can ask again.
- A restore now tells you what it wrote, not just what it cleared, and the report covers practitioners, appointments, vaccinations and their links. The portable export names a field it could not decrypt instead of exporting it as if it were never set.
- If you move timezones, the medication compliance statistics are recomputed on your new day boundaries.
- Long histories got two fixes: the all-time summary includes readings older than five years instead of silently cutting them off, and the AI context falls back to live data when a pre-computed band is missing.
- The copy got a pass in all six languages: shorter consent buttons, a workouts empty state that no longer assumes everyone owns an iPhone, consistent toast punctuation, a medication reminder chip that no longer clips mid-sentence, and the navigation chips lost their unfortunate German name ("Navigations-Chips" instead of "Pills").
- Settings pages that had been permanently redirected are no longer built, the retired placeholder is gone, and the about page is reachable from the privacy page and the settings.
Fixed
- A measurement reminder claims its notification slot before sending, so a crash in the wrong moment cannot deliver it twice.
- Submitting the same mood entry twice at once reports "duplicate" with the entry that won instead of a confusing error.
- For anyone writing against the API, the intake-event description now documents every field the server returns, and the workout list description matches the real nullability, so generated clients decode without surprises.
[1.37.18] — 2026-08-14
Fixed
- Coach reminders tied to a moment ("remind me when I next log blood pressure", "next time I open the app") now fire. The app has offered and confirmed these since v1.22, but nothing ever evaluated them; a matching measurement or the next app open brings the reminder up, and the morning reminder run catches anything left over.
- The Apple Health import tells you what it skipped and why. Every kind of dropped sample is listed on the import card: unknown types, out-of-range values, deferred records, and cycle samples read while cycle tracking is switched off, which used to vanish without a trace. A cycle write that fails halfway also reports the failure instead of claiming zero samples.
- Deleting a measurement now reaches the pre-computed statistics. Before, a deleted reading stayed inside the day, week, month and year figures forever; those figures notice a deletion now, and a deletion reported by WHOOP recomputes the affected days.
- The sleep history repair job only deletes what it can restore. It used to clear a provider's entire sleep history and re-import just 30 days; the delete is now limited to the same window.
- The connector token settings said tokens are read-only while offering a write toggle on the same card. The text now tells the truth in all six languages.
- A lab report cannot smuggle instructions into the AI through a biomarker name anymore; names read out of documents are cleaned before they reach any prompt, and the assistant tools treat checkup labels and lab analyte names as data in the same way.
- Google Health and Withings syncs now tell the server that new data arrived, like every other integration, so analytics pick up synced data on the next read instead of waiting out the cache.
- On phones, the Medications tab no longer appears in the bottom bar when the medications module is switched off; tapping it used to open an empty page.
- The import guide links point at the documentation site instead of a page that does not exist.
- A caregiver settings toggle that has had no effect since its feature was retired is gone.
- Every create and update request from the app now carries an idempotency key, so a network retry cannot write a vaccination or an appointment twice.
- The offline page the app shows when the server is unreachable is stamped with the release version again; it had stayed two releases behind.
- The vaccination booster and suggestion endpoints are now in the API reference, as the API notes already claimed.
[1.37.17] — 2026-08-13
Changed
- The correlations page reads one figure per day and source from the daily summaries the server already keeps, instead of walking every raw reading over 180 days. For a dense record, glucose from a continuous sensor most of all, that is one row per day and source instead of every reading. Values stay the same: the daily figures are combined to match the previous per-reading averages exactly, a channel that has no daily summary yet falls back to the raw readings on its own, and sleep duration, steps and daylight time keep their raw reads because their daily figures depend on stitching a night together and removing duplicate device readings, which the summaries cannot express. Profiles far from UTC also stay on the raw path so day boundaries keep matching their timezone.
[1.37.16] — 2026-08-13
Changed
- The correlations page no longer recomputes its full discovery on every visit. The first request computes the result and keeps it on the server; repeat visits within the hour answer from that copy, and any new measurement, mood entry, illness episode or custom metric refreshes it. Accepting or dismissing a pattern still takes effect immediately.
- Switching a chart's time range keeps the previous chart visible, slightly dimmed, while the new range loads, instead of dropping to a placeholder. The dimming is on purpose: what you see during the switch is the old range, not passed off as the new one.
- The checkups and mood pages send their first page of data along with the page itself, so they open with content instead of a loading skeleton.
- Two background checks are calmer. The achievement check runs every five minutes now, less often than before, since anything you do in the app updates instantly anyway and the poll only exists for data arriving from a device. The Coach plan-proposal check stops on its own instead of polling an open conversation forever.
- On your own server, the existing
DASHBOARD_SSR_PREFETCHflag switches the preloaded first page off.
Fixed
- A checkup accepted from a Coach suggestion appears on the checkups page and the Today rail immediately. Before, it stayed invisible for up to five minutes.
- Changing a target, a threshold or the health score configuration shows on the dashboard right away, including the hero bands and score context. Before, the dashboard kept the old configuration until its next background refresh.
- Starting or ending an illness episode updates the dashboard's rest mode immediately, booking an appointment puts it on the Today rail immediately, and logging a vaccination dose updates the booster reminder on the rail immediately.
- Completing a mental-health screening refreshes the checkup list right away, and changing a source priority refreshes the affected charts right away.
- Background syncs from Fitbit, Oura, Polar, WHOOP, Strava and Nightscout now tell the server that new data arrived, so analytics and workout views pick it up on the next read instead of waiting out the cache. The same applies to illness, cycle and appointment changes, and to every other place that writes what a daily view reads.
[1.37.15] — 2026-08-13
Fixed
- Re-running the assessment no longer reports a failure while the server is still writing the result. When a refresh takes longer than the browser is willing to wait, the page says it is still working, keeps checking until the new assessment lands, then swaps it in. Only a refresh that really failed says so. Before, a slow refresh showed an error and kept the old assessment until a reload. (#786)
- Stored PDFs preview inline again. Since the sharing changes, previews load through copies the page decrypts itself, and the security policy the app sends to the browser had never been told about them, so PDF previews were blocked and the vault's image thumbnails fell back to their icons. The policy now admits the app's own decrypted documents and nothing else. (#787)
- A document uploaded while automatic AI reading is switched off no longer claims a summary is being prepared forever. The upload stopped queueing work it is not allowed to do, a stale claim heals itself on the next background pass, and documents already stuck are repaired by this update. The manual summary action is offered instead. (#788)
- When reading a document for search fails, the document now says why: a scan with no readable text, a PDF whose pages would not render (scanned fax compression formats are a known cause), a provider error, or a provider that cannot take PDF files. Before, a failed read looked identical to one that never happened. An AI answer with no text in it also no longer counts as a successful read. (#776)
- The "index all documents" confirmation shows the real number of documents queued instead of the word "true".
[1.37.14] — 2026-08-13
Fixed
- The Today rail no longer announces a weekly medication as due up to a day early. A weekly dose becomes takeable a day ahead by design, and the rail treated takeable as due, so on Friday the Saturday slot already sat there as "Medication due". The rail now keeps a dose that is not overdue off the list until its actual day in your own timezone. Taking it early stays possible on the medication card, which says which day it belongs to, and overdue doses appear regardless, as before.
- A dose logged through the quick-add button updates the home page immediately. The quick-add is reachable from every page, but it only refreshed the page you were on, so coming back to the home page could show the dose as still due for up to two minutes. Logging from anywhere now refreshes the home page the same way logging from the home page always did.
[1.37.13] — 2026-08-13
Added
- A lab report the app has read offers its values for your lab record, right on the document. When a document is classified as a lab report, its page shows each extracted value with its unit and date, and you approve or reject them one by one. Nothing is saved without your confirmation, and a value the reader was unsure about is marked for a closer look. Reading a document again by hand offers values the same way, so a report that arrived before automatic reading was switched on is no longer stuck. (#779)
- The AI settings can grant consent again after a withdrawal. Revoking consent stays a standing decision that no page visit can quietly undo; what is new is a control that lets you, and only you, opt back in.
- The Coach keeps an answer you walked away from. Leaving the page still stops the reply, but the conversation shows the interrupted turn, coming back opens that conversation rather than a fresh chat, and a retry control asks the same question again in place. (#781)
- If you host this for others, the hourly limit for reading documents with
AI is your setting now:
DOCUMENT_AI_LIMIT_PER_HOUR, matched to your provider budget. The limit message says when the next read is possible instead of "try again in a few minutes", and a read that never reached the provider no longer uses up a slot. (#776)
Fixed
- The health score could not read the two lab-based pillars. A mismatch in column names made every glycaemia and lipids read fail, so anyone with lab results saw both pillars as unreadable. They score now, and a record without lab results shows the pillar as not tracked rather than as an error. (#780)
- Importing a large Apple Health export no longer exhausts the server's memory. The archive is read in batches, so an export with millions of records imports on a default setup. An import the available memory cannot carry is refused up front with a message that says what to raise, and an import that was interrupted shows up as failed with a retry rather than spinning forever. (#775)
- Automatic document reading no longer hides the manual read action on a document that has not been read yet, so a single failed or skipped document has a way back. (#777)
- Read-everything now includes PDFs for providers that accept images; the pages are rendered to images the same way a single manual read already did, so such a setup no longer skips every PDF in the vault. (#776)
- The About-me note and the free-text allergy notes moved from Account settings into Anamnese, next to the structured allergy list they supplement.
- The temporary folder an Apple Health archive is unpacked into has an unguessable name now.
[1.37.12] — 2026-08-11
Changed
- The public privacy page matches the app again, in German and English. It describes how you delete your account from inside the app and how a second factor is handed over, what each way of running the assistant does with your text, whether that is the one this server manages, your own key, another endpoint or your own device, what happens to a document you upload and to the text read out of it, how to withdraw consent, what Apple Health sync stores here and what it does not, how long things are kept, how heart rate is represented, the App Store data categories, and that nothing here tracks you.
- The documentation site has a page for getting in touch: bug reports, usage questions, privacy and account requests, and private security reports.
[1.37.11] — 2026-08-10
Changed
- The lifestyle-context card in Anamnese saves in one action. Smoking, alcohol and shift work each had their own save button; now you set what you want across all three and one Save writes them together, with a single confirmation. If one of them cannot be saved, that entry stays unsaved and says so while the others go through, rather than a single message hiding which one failed.
- The mood chart picks up the same header as every other card, which changes nothing you do with it.
[1.37.10] — 2026-08-10
Added
- A medication can take a different dose at each time of day, all from the one supply. If you take one tablet in the morning and half a tablet at noon and at night, that is one medication with three times now, not two separate entries that split your stock in two. Each time slot carries its own dose and its own count of units consumed, so the supply counts down by the right amount at each one. An occasional as-needed dose is a slot on the same medication too, drawing from the same box, rather than a third entry. Existing medications are unchanged, and one that does not set a per-slot amount keeps counting down exactly as before.
[1.37.9] — 2026-08-10
Added
- You can record the data that matters in an emergency: blood type, an organ-donor and an advance-directive status, plus emergency contacts, implants or devices, and a free note. Enter it under Anamnese, the same place the allergies and family history live. The contacts, implants and note are encrypted at rest like the rest of your sensitive record, and every field carries through backup and restore.
- The doctor report can open with a one-page emergency summary a clinician can read at a glance: blood type, severe allergies, current medications, chronic conditions, implants, the advance-directive and organ-donor lines, contacts and the note. The page appears only when you have recorded something for it, and it is part of the report you choose to share, so it travels to the practice with the rest of the report rather than sitting somewhere separate.
[1.37.8] — 2026-08-10
Added
- The assistant answers questions about your practice visits now, both what happened and what is coming up. Ask about your visit history over a window and you get the practitioner, the reason and the outcome back, and when you have never recorded a visit it says so rather than reading an empty list as if nothing exists. Upcoming appointments arrive alongside the preventive-care due-list rather than as a separate question, so "what is coming up" has one answer. The Coach carries a small visits note as well, so it knows an appointment is near and which practice you last saw without being handed your whole visit archive.
Security
- If you host this yourself, a warning written to the server log is scrubbed of secrets on the way in now, the same as an error message or a request path already was. A warning often carries an outbound error string, and for a few integrations that string is a request address with a credential in it, so closing this entry point removes the last place a secret could reach the log unredacted.
[1.37.7] — 2026-08-09
Added
- If you run the published image, you can now set
MAXMIND_LICENSE_KEYas a runtime environment variable and the app fetches the GeoLite2 City and ASN databases itself, so login locations resolve offline without a rebuild or a manual database mount. It fetches once on worker boot when the key is set and the databases are absent, refreshes monthly, and never blocks anything: any download error leaves the previous databases in place and the online lookup keeps answering. A read-only mounted database still wins. Documented as the third way to supply GeoLite2 in the self-hosting guide. Refs #659, reported by @mathewcsims.
Security
- The MaxMind licence key no longer appears in a log line when that fetch
fails. The download address carries the key in its query string, and the
failure path used to print the whole address; it reports only the edition
and the kind of error now, and the central log redactor masks a
license_keyparameter wherever one might otherwise slip through. - A mental-wellbeing screening reminder (PHQ-9, GAD-7, WHO-5, SCI) can no longer be marked done through a hand-built request. A screening resolves only from the score a completed check-in produces, so the server turns down a request to satisfy or complete a screening reminder outright now, which is the rule the apps already followed.
Changed
- Editing a custom mood tag, a custom tag group or a custom cycle symptom now leaves an entry in the audit log, the same way deleting one already did. The two display-only actions on that screen, hiding a catalogue tag and reordering the layout, stay out of it, since those are display preferences rather than a change to your record.
[1.37.5] — 2026-08-09
Changed
- Copy across several screens reads tighter. The insight-page metric explainers open on a one-line definition now rather than a paragraph of general guidance, with the longer background staying behind the knowledge disclosure where it already lived. The measurements subtitle and the daily-briefing setup prompt each drop a redundant sentence, and the mood self-rating card takes a short title so it stops wrapping. Wording that had drifted between neighbouring screens is settled too, so the measurement delete prompts, the practitioner label on the visit and vaccination forms, and the visit-saved toasts all match the screens they ship with.
- The settings for a record you look after use the same controls as the rest of the app. They carry the shared date picker, dropdowns, text fields and switches instead of a plainer set of their own, so the birth date reads and saves in your own date order rather than depending on the browser, and each form keeps its save button at the end with any error shown just above it.
- Spacing and tap targets read more evenly across settings, the dashboard and the charts. A handful of off-scale gaps are back on the spacing scale, the document upload buttons meet the mobile tap size their neighbours already set, the revoke on a share link is a right-aligned row action rather than a full-width red bar on every row, and a few empty and loading states use the shared components now so they match their siblings.
[1.37.4] — 2026-08-09
Fixed
- A read that fails now shows a clear, recoverable error instead of an empty list. That mattered most on the share-link list, where a failed load read as "no active share links", the same as having shared nothing. It is fixed there and across the app: the API and connector token lists, the ECG detail and list, the cycle and mood insights, the dashboard preventive-care tile, the checkups list and the admin console all say when a read failed and offer a retry.
- The connector-token note in Settings claimed a token was read-only in every case, printed right above the toggle that turns write access on. It describes the real model now: read by default, write an opt-in per token, and never admin.
- The assistant consent and connector notes in Settings render as normal text rather than faint fine print, so the copy you are meant to read before turning a feature on is legible.
- The record-a-visit action on the checkups list appears only where a real practice visit is behind the reminder. A reminder you satisfy yourself, like weighing in or a mood check-in, no longer offers to file a visit it never involved.
- The practice address book searches by specialty, so you can find the dentist by field even when the name and practice hold nothing of the sort. The list groups its entries under their specialty, and the visit form's picker shows each contact's field beside the practice.
- Linking documents, lab results and illnesses to a visit is searchable instead of a flat list you had to scroll. Lab results gather under their panel and sample date so a whole day's panel attaches in one tap, documents gather by month, and the vaccination form uses the same picker.
- The respiratory-rate tile on the dashboard shows its unit in your own language instead of a fixed English label, and the personalized greeting carries its comma.
Removed
- Logging water by hand inside the app is gone. The water entry on the capture menu, the dashboard quick-add and the hydration card have all been removed. Water that syncs in from another app through Apple Health still arrives and still shows on the hydration card with its daily total, history and reference line.
[1.37.3] — 2026-08-09
Added
- A new Vaccinations module keeps a lifetime Impfpass. Switched on under Settings then Modules. Each dose sits under the disease it protects against, with its place in the series worked out on the server, so a combination shot appears under every component it covers. You add a dose by picking from a catalogue searched by disease or antigen name, or by typing it the way your pass reads; a date plus one of those two is all it takes, and the lot number, the injection site, the practice, a visit link and an encrypted note are optional. Where the catalogue knows a schedule it shows it with its source, as information rather than advice. A dose with a booster interval offers a prefilled reminder you can edit or decline, and confirming it writes an ordinary preventive-care reminder that shows on the checkups page and moves forward when you log the next dose. Scans in the document vault attach to their record, the doctor report gains an immunization section you can leave out, and the whole module follows record sharing, so a parent can keep a child's Impfpass under a record they look after. Every language the app has, including the catalogue names, and never a brand name.
Fixed
- A preventive-care reminder set to the longest interval the app allows no longer loses its next date when you log the dose meant to push it forward. It moves a full cycle ahead now instead of falling silent.
- The mood sliders show their value on its own instead of fused with the reset control, which is a small icon beside the value now.
- Date and time fields wear the same focus ring as every other input, so it hugs the field instead of painting wide or jumping left, and they no longer set off the zoom on iOS.
- The add-mood sheet keeps its scrollbar clear of the endpoint labels, so the longest wording is never clipped.
- The catalogue and practice pickers carry a name for a screen reader; a control that only showed an icon was unnamed before.
- The health-score configuration lists its topics as one flat set with dividers instead of grouping them under headings.
- In Settings, under Security, password reset sits last, after the authenticator app, security keys and passkeys.
- Inviting someone to a shared record starts with nothing preselected, so the choice between the whole record and single sections is one you make, and each scope description is one line with the detail behind an info icon.
- Sharing a record again with someone whose access had expired works. The lapsed invitation no longer blocks a fresh one with a false report that the person still has access.
[1.37.2] — 2026-08-08
Added
-
The mood record answers a second question next to your own check-in: what this day would have looked like on your own past patterns. Your self-assessment leads and is never overwritten. The comparison sits beside it with the number of days it rests on and a range rather than a single figure, and it stays silent until there is enough of your own history to stand on. It describes a connection, never a cause.
The rework grew from a concept Kai worked out in detail, from the split between how you feel and the day around it to the idea that the app should learn your own patterns instead of scoring you against fixed weights. Thanks, Kai.
-
A mood entry can hold the shape of the day around it. Four optional sections cover work, contacts, leisure and one thing that stood out, each folded away until you open it, so a quick check-in is still a face and Save. Sleep, activity and vitals come from the modules that already hold them and appear beside the entry instead of being asked for again, so a correction at the source carries through. A figure nobody recorded reads as not recorded rather than as zero, and a module you have switched off leaves its block out.
If you write against the API, a mood entry carries the five values and the day context as optional fields. An app that posts only the five-point label still writes a complete entry, because the server derives the first scale from that label.
-
Doctor visits have a place of their own. A visit records its kind, when it happened, its status, an optional practice from a contact list you keep, an encrypted note, and links to the lab results, documents and illness episodes it concerned. Those are links, not copies. A preventive-care checkup can be closed by recording the visit that fulfilled it, and a planned visit sets a reminder the server moves when you move it and clears when the visit happens. It fires once and never nags, and it carries no "done" button, because an appointment is not a task you tick off. The visit list appears in the doctor report, and you can leave it out.
-
A workout recorded before the phone learned to send its heart-rate detail can gain that curve later. When the phone re-sends a workout the server already holds, the reading-by-reading heart rate attaches to the workout that is already there, and nothing else about it changes.
Fixed
- Marking an older missed dose as taken records it at the slot's own time rather than the current moment.
- Cycle tracking leaves the Account page, since it is a module and one place is enough, and changing your password moves to Security, where the second factors and the passkeys already are.
- The dead space above each row in the Appearance list is gone, and the module descriptions that ran far longer than their neighbours are back in line.
- Research Mode is retired. The chart it gated stopped consulting it several releases ago and has been visible to everyone since, so the switch changed no screen in either position.
- The EKG page drops its lines about what the app does not do, shows the device's own reading as a tag beside the date, opens a recording on its own page with the back link above the heading, and shows the most recent handful instead of every strip ever recorded.
- A shared record shows its owner's full name where the profile has one, and the record switcher no longer gives the access level a line of its own, where it was truncated more often than it was read.
[1.37.1] — 2026-08-08
Added
- A mood entry now records five separate values instead of one. The quick check-in is unchanged: pick a face, save, done in a tap. Underneath it, a section that stays closed until you open it holds five sliders, each with wording at both ends: how the day felt, how much strain it carried, how much energy there was, how connected you were, and how able to act you felt. Anything you leave alone stays empty rather than landing in the middle. The mood page trends all five over a week, a month or three months, and the daily briefing and the assistant read them with their own age, so a value from last week is stated with its date instead of as today's.
- A lab result keeps the range its report printed. A lab report prints its own reference range next to every value, and your physician reads the value against that range, not against a general catalogue. Until now the document reading extracted the value and discarded the range beside it. It captures the printed range now, and that range decides the verdict everywhere: the API, insights, the assistant, the doctor report and its PDF, FHIR, backup and restore. The chart shows the printed window as a second band and marks where it differs from the catalogue. Values without a printed range keep the catalogue, and anything the reader cannot parse with confidence lands verbatim in a text field instead of being guessed at.
Changed
- Factor ratings are no longer offered on a new entry. A separate score for work stress and the strain slider are two answers to the same question, and only the slider is asked now. Ratings already logged still show, still edit, and still feed the breakdowns that read them.
- Sync refuses readings the app itself calls impossible now. A provider could store a pulse in the millions, and one recently did. Every sync path checks, and an implausible sample is dropped and counted, never clamped and never allowed to fail the batch. The insights baseline applies the same ranges before averaging, so an impossible stored reading no longer skews its own comparison.
- The settings pages and the admin console had drifted apart wherever nothing held them together. They share one page header, one action-row shape, one card rhythm, and a rule that a card description is one sentence, in every language the app has.
Fixed
- Mood entries survive a restore whole. The backup carried only part of an entry, so a restored one came back without its note, its tags and its own timezone. Backup files written before this restore as they did.
- The workout detail draws the whole heart-rate curve: it runs to the end of the workout instead of stopping at the moving-time mark, the min-to-max band no longer squashes the shape by dragging the axis to zero, and the peak marker renders again.
- The cycle module says "overdue" instead of "insufficient data" when a period is late, deleting a period start no longer strands the previous cycle in a state that stops all forecasting, a backdated period shows up in the calendar month it belongs to, and a period logged only as flow opens a cycle, which covers the whole Apple Health import path.
- Marking an older missed dose as taken lands on the slot it names instead of a standalone row at the current minute.
- The top card no longer says "today" about a reading that is five days old.
- A hand-entered night of sleep is stored as hours, not minutes.
- Dialogs no longer scroll sideways on a phone, and the coach launcher no longer shows a dot with an empty conversation behind it.
- If you host this for others, the admin "wipe all data" action left most tables standing. It clears everything the per-account wipe clears now, keeping only sign-in credentials, instance configuration and the grants that keep a record without a login reachable.
- The geo resolver reads mounted GeoLite2 databases first and asks the online provider only when they cannot place an address, which is what the documentation promised all along. If you mounted the databases to keep login addresses on your host, they do that now.
- SECURITY.md no longer claims the publish workflow runs on every push to main; that trigger went away in v1.4.34.
Security
- The PDF renderer behind the document vault moves past a script-execution advisory, and the image build selects that renderer by name instead of taking whichever copy the filesystem listed first.
- A dependency advisory on a small helper library reached through the framework toolchain is closed, and scheduled scans of the source tree and the published image now run alongside the blocking one.
[1.37.0] — 2026-08-07
Added
-
An invitation can name the sections it opens rather than handing over everything: readings, medications, lab results, health background, illness, mood and mind, cycle, documents. Anything you do not pick stays closed, and a section you kept back looks exactly like a record nobody shared with you.
The invitation screen says two things out loud: a section can mention the rest of your health in its notes, so sharing a section shares whatever was written in it, and the dashboard overview, the health score and the daily digest need the entire record, because a figure computed from part of one would read as a figure about the person.
Invitations you have already sent or accepted open the entire record and are untouched.
-
A third level of access. Read access looks. Write access adds. Manage access can also change and remove what is already there, including entries you made yourself, and can read the insights generated from the record. It stops at the account around the record: sign-in, second factor, connected services, tokens, where notifications go, which modules are on, and who else has access. Offering it asks for your second factor, so it happens in a browser, and it always covers the entire record, because a note in one section can be about any other. Everything a manager does is recorded under their own name.
-
A record for somebody who does not sign in. @balajiv113 asked in #360 for a way to look after a family member's record. Access between two accounts covered half of that; the other half is a person with no account at all, a child or somebody you care for. Such a record has no login and no e-mail address. You give it a name, optionally a date of birth, and the language and timezone its days and reminders are measured in. Creating one asks for your second factor and puts you on it in the same step. You can invite somebody else to look after it with you, and it can never be left with nobody: the last person on it cannot hand it back and cannot be removed. The way out is to add somebody else, or to delete the record.
Its reminders reach the people looking after it. A record with no login has no phone of its own, so its medication, measurement, safety-floor and low-stock reminders go to them, each in their own language and over the channels they had already chosen. The message names whose record it is about and carries no buttons; opening the app takes you into that record. One of you turning off their own reminders does not silence anybody else.
You can keep that record's settings too, from modules and units to thresholds and notification preferences, while your own screens stay in your own language. Connected services show a status and nothing more for now.
-
Inside a shared record the document vault opens, and "take all due" and marking a dose skipped are offered to somebody with write access.
Changed
- Write access no longer lists logging a value on a metric you track yourself, since no page a shared record opens could record one. Reading those values stays.
Fixed
- Two tabs of one browser could disagree about which record they were in. Switching records applies to your whole sign-in, so a tab left open on the record you just left went on reading and writing there until it was reloaded. Every request says which record the tab believes it is in now, and one naming a record you have left is turned down in a way the tab recovers from on its own. Switching with several tabs open could also leave every tab on a spinner for half a minute; that is fixed too. The phone app was never affected.
- Somebody with write access can no longer overturn a dose you had already recorded, on either path that reaches it. Re-sending the same decision still works.
- Closing a document could leave it named in the address bar, so a reload or a shared link re-opened the sheet you had just closed. The close checks that the browser stepped back now, and tidies the address bar when it did not.
- A control tapped before the app knew whose record was on screen no longer strands you in a form the server would turn down.
- A dose recorded from the dashboard quick-add says which record it landed in and no longer offers an Undo only the owner may use.
- The access-level options on the invitation screen are visible when reached by keyboard.
- The customise link on the mood page no longer leads someone you gave access to onto a page that tells them it was not shared.
Internal
- On a server you run yourself, migrations run on start and there is nothing to configure. Neither feature does anything until one account invites another. The account-sharing page in the self-hosting guide is rewritten for all of the above.
[1.36.1] — 2026-08-04
Added
-
Somebody you have given access to can now write into your record, not only read it. Usually the point of sharing is that another person is doing something about it: a partner logging the blood pressure they just took, someone recording that the evening dose went down. Read-only access made them watch that happen and then ask you to type it in.
A grant carries a level now. Read access is exactly what it was. Write access admits a short, closed list of things a second person may add: a reading, a lab result, a biomarker to the catalogue, an illness entry, a value on a metric you track yourself, a side effect against a drug, a medication, and marking a dose taken or skipped. Editing, deleting, restoring and importing stay with you, including on an entry the other person added a minute ago.
Every entry the other person makes is stored as yours, because it is a fact about your body and belongs in your record whoever wrote it down. Who wrote it is in the activity trail, so "who put this here" still has an answer months later. The access panel gained a feed of what has been contributed, and you are told when somebody marks one of your doses.
-
A control the record will not accept is now absent rather than present and broken. Before, someone opened a page inside your record, saw the button, filled the form, and the server said no. Nothing leaked, but it taught them the product was broken. A control that is not there says "this is not part of what you were given", which is the truth.
-
An invitation can name the day the access lapses, set when it is sent.
Fixed
- The first page someone with access landed on was the worst page in the feature. Most of the requests behind the dashboard were turned down inside somebody else's record, so the tile strip had no numbers and the top card showed an error while every page underneath worked. They answer now, except the ones that describe the person reading the screen, such as the interface language, which keep answering about you.
- The reference ranges were the wrong person's. The dashboard drew blood-pressure targets, the weight range and every shaded band on a chart from the date of birth, sex and height of whoever was looking rather than whose record was open, so a stranger's ranges were painted over their readings for the whole visit. It uses the record's own now. The BMI chart had the same problem, dividing their weights by your height; it draws nothing there now rather than a number belonging to two different people.
- Deleting a medication's dose history removed the rows outright. A mis-tap was final, and a phone that was offline at the time kept every deleted dose forever, because a row that no longer exists has nothing to tell anyone. The history is retired now the way every other deletion in the app works.
- Recording a side effect against a medication and then restoring from a backup lost it. Side effects travel with their medication now.
- Adding water by hand and undoing a deleted lab result both wrote health data and left no durable record of who did it. Both leave one now, like every neighbouring action, and the activity feed names the action when someone with access marks a dose instead of showing a blank line.
- Marking a dose and postponing a reminder went through the same request, so somebody with write access could postpone one of your medication reminders, with no upper bound and without you being told, and the activity feed called it marking a dose. Postponing a reminder is yours alone now, and so is changing a dose you already recorded.
Internal
- For anyone self-hosting, the library the server uses for outgoing requests moves past high-severity advisories published since the last release, and the notification senders were run against the new version rather than assumed to work.
[1.36.0] — 2026-08-02
Added
-
You can give another account read access to your record. @balajiv113 asked for a way to look after a family member's record in #360, and this is the shape it took: everyone keeps their own account, their own password, their own second factor. Nobody logs in as anybody.
The owner sends an invitation, and it opens nothing until the other person accepts. Either side can end it at any time, and it ends on the other person's very next request rather than at their next sign-in. An invitation can also carry a date it lapses on, so access that was meant to be temporary does not quietly become permanent because nobody remembered.
-
While you are looking at somebody else's record the whole app says so, and it opens that record and nothing else. A strip across the top names whose record you are reading, and the navigation drops what does not belong to them. Every write, and everything around the account, is turned down by the server rather than merely hidden from the screen: credentials, integrations, notification channels, share links, and handing the access on. A few reads that look harmless stay closed too, because they sit next to something that would widen access.
Two of those limits are permanent rather than pending. Someone you gave access to can never create a clinician share link, because that would be a door surviving their own revocation, and can never reach anything that would widen their own access.
-
The owner can see what the access is being used for. A panel lists who has access, when they accepted, when they last opened the record, and which days somebody else was in it. The invitation is also the consent record: who offered the access, who accepted and when, when it lapses, when it was last used, and who ended it. Revoking marks it rather than deleting it, so the history of who had access between which dates survives the access itself. The address the other person accepted from is not stored; a consent record has to say who and when, and where adds nothing.
Fixed
- A backup holding a symptom name this instance no longer recognises now restores. It used to refuse the whole file over one retired name, so a single renamed entry cost the entire account. The entry that cannot be matched is dropped, and the restore says which names it dropped and how many entries each one cost, rather than quietly losing them or refusing everything.
- The About page said the project is licensed under the GNU Affero General Public License. It has been PolyForm Noncommercial 1.0.0 since v1.15.19, as the same page's footer said correctly. It also described the GeoLite2 terms as CC BY-SA 4.0 alone; those databases ship under MaxMind's own end user licence, which incorporates CC BY-SA 4.0 and takes precedence where the two disagree. Both are named now.
- The line under the top bar and the line under the sidebar header came apart whenever a banner was showing, because the banners sat inside the content column while the sidebar is beside it. The banners span the window now, so both lines start below them.
- Chart cards on the dashboard and in Insights titled themselves one heading level below where they sit, so a screen reader walking the page heard the level jump. They sit at the right level now.
Internal
- The backup and restore runbook now says what a backup does not carry, and what that means depending on whether you restore onto the same instance or a fresh one.
[1.35.6] — 2026-08-02
Fixed
- The line under the top bar and the line under the sidebar header now meet. They were one pixel apart, because the two carried the same height but painted their border on different elements. Both take height and border from one place now, so they cannot come apart again. Still open, if you host this with an offline, demo or unmaintained-language banner showing: those banners push the top bar's line down without moving the sidebar's, which needs the page shell rearranged rather than the bars adjusted.
Internal
- A piece of code that worked out a headline verdict for the dashboard is gone. What read it was removed a while ago and nothing else was ever connected to it, so it had been computing an answer nobody read. Every signal it covered already has a live owner elsewhere.
[1.35.5] — 2026-08-02
Fixed
- Cycle reminders no longer depend on whether you happened to open the calendar. The prediction a reminder is sent from was written only while that page was being viewed, so an account that never opened it had nothing for the reminder job to read. The prediction is refreshed nightly now for every account that tracks a cycle, whether or not anyone looks at the page. A prediction that fails to save is no longer discarded in silence either.
- The sync handshake no longer marks an account as synced. Asking the server where to resume is not the same as sending anything, and treating it as a sync moved the resume point past data the app had not fetched yet. The mark is set only by the paths that actually receive data now, so it can only trail a real sync rather than run ahead of one.
- Badges are recorded whether or not you open the app. The date a badge was earned was written only when the achievements screen was loaded, so an account that earned one and did not look never had it written down, and the date is the part that cannot be worked out again later. A nightly pass records it now, with the date it was actually earned rather than the date it was noticed.
Internal
- Three reads no longer write. A read that writes widens what a read-only token can reach. One such read stays for now: opening the cycle pages creates a cycle profile for an account that has none.
[1.35.4] — 2026-08-02
Fixed
- A checkup due later today is no longer described as overdue. Two places counted the distance to a checkup in hours and rounded, which is a different question from how many calendar days away it is. On a Friday evening, a checkup on Saturday morning and one on Sunday morning both rounded to tomorrow. Both count calendar days now, in the timezone on your profile.
- The assistant and the screen beside it agree about when a checkup is due. The list the assistant reads flipped to overdue the moment the clock time passed, while the page still said today until midnight. One appointment could produce two different answers depending on which one you asked.
Internal
- The places in the server that work out who is calling are one fixed list, including the ones that identify a caller by a credential in the address rather than a login. A new one cannot be added without being listed with a reason, and the one that turns an access token into an account had been missing from it.
[1.35.3] — 2026-08-01
Added
- Apple Watch ECG recordings can be sent directly by the phone app now
instead of only arriving inside an export archive, so a strip you recorded
this morning no longer waits for your next export. For anyone writing
against the API, that is one recording per request at
POST /api/insights/ecg, behind the same module and status switches that already govern reading ECGs, so an account with ECGs switched off is unaffected. The limits are 32768 samples, a 2 MB body and 60 recordings a minute, and an unknown field in the body is rejected by name rather than ignored.
Changed
- A recording carries its own identity, so sending the same one twice ends up as the same entry rather than a duplicate or an error, and a retry after a dropped connection is safe.
Fixed
- A page no longer grows a second scrollbar for the moment between appearing and becoming usable. Every screen with a toggle briefly drew a hidden form control outside the scrolling area, so the whole window became scrollable to about twice the height of the screen until the page finished loading. On a slow connection or a busy phone the moment was long enough to flick past the end of a settings page and land on nothing. It is fixed for every page at once rather than the one it was noticed on.
Migration
- Back up before you update if your server has ECG recordings stored. Your database can hold more than one row for the same recording, because an export archive and the app name the same strip differently. A rule now prevents that, and the migration resolves what is already stored before applying it. Where two rows describe the same recording it keeps the one holding the curve over one holding only a verdict, and it carries the removed row's link to its rhythm event across so the recording keeps its place in the timeline. The rows that lose are deleted and cannot be recovered. The migration runs on boot; if it fails, the container stops with the migration named in the log and never serves traffic.
[1.35.2] — 2026-08-01
Added
- The cycle ring gets its verdict from the server: which day of the cycle you are on, when the next period is expected, and whether one is late and by how many days. The web app and the phone app each worked it out from the raw dates and did not always agree. There is one answer now, and both show it.
Changed
- Green on the Health Score starts at 70, the same threshold the readiness and sleep scores already used. A weak area still holds the band down whatever the average says, so one poor pillar cannot hide behind good ones.
- The Fitness pillar is gone. It could never produce a value, because the record cannot tell a measured fitness test from a device estimate and the pillar would not score on an estimate. It was a switch that did nothing.
- The score settings page lines up with the other settings cards: same left edge, no lines inside the card, and the save button where every other card keeps it.
- The line on the Today card saying that something had arrived, with a time but never what, is gone.
Fixed
- A saved score selection comes back exactly as you sent it. One pillar was dropped on the way to the server, so the page could never agree with what it had just saved and the save button stayed lit as if there were unsaved work.
- The cycle ring follows the timezone on your profile instead of the one your browser happens to be in, so the day of your cycle no longer shifts when you travel.
API
- The share-link fields announced for retirement are no longer served.
- A repeated request that conflicts returns its error in the same shape as every other route. One endpoint answered differently.
- The score settings route, which shipped in the previous release without being described at all, is in the API reference.
[1.35.1] — 2026-08-01
Fixed
- A weekly injection you have already taken is no longer announced again. A medication dosed on a rolling interval had its expected dose anchored on the moment you actually injected, and was then checked off against the moment it was planned for, with six hours of tolerance. Those two are allowed to sit days apart, because a late dose still counts, so the dose your own injection created could never be marked done. It stayed open, went overdue, and the app asked for it again. Anyone on a weekly medication could hit this without ever editing anything: taking a dose a few days late was enough. Push reminders were never affected, because they read the plan rather than your entries.
[1.35.0] — 2026-07-31
The Health Score becomes something you decide rather than something that happens to you.
Added
- You choose what counts toward your Health Score. A settings page of its own lists the pillars, each one on or off. It starts with everything that can be scored, so a narrower score is a decision you make rather than a default you inherit.
- Each day's score is now recorded with the recipe that produced it: the value, the method, which pillars took part, what each of them scored, and a fingerprint of the inputs it depended on. Until now nothing was kept, and every comparison was recomputed under today's rules.
- The score says when its composition was authored, so a number built from a chosen set never looks like one built from everything available.
Changed
- The module switches no longer decide what the score is made of. Turning off Sleep or Labs used to remove that pillar from the score with nothing on screen saying so. Those switches now govern what you see, and the score's own page governs what it counts. An existing account keeps exactly the composition it has today, so no number moves on upgrade.
- Changing what counts no longer reads as a change in your health. The comparison stops at the point the recipe changed and says so, rather than drawing a line across two different definitions. The per-pillar figures continue, because those stay comparable.
Fixed
- A settings change can no longer produce a message telling you your score dropped.
- The score's own coverage figure now describes the pillars it actually counts rather than every pillar that exists.
Removed
- Two refusals that could not refuse anything, and a boundary condition no input could ever reach.
Operations
- Two migrations: one for the per-account composition, one for the recorded scores. The recorded days travel through backup, restore and delete-all-data like every other table.
[1.34.5] — 2026-07-31
A release about the score explaining itself, and about sentences arriving in the language you read.
Added
- The health score sits beside the greeting again, at the top of the insights page, instead of on a separate card pinned underneath it. It has one place on the page.
- Every pillar inside the score takes its own colour. A pillar doing well was painted in the overall score's colour, so a good result inside a weak week looked like a bad one.
- A blood pressure score says how it came about. The pillar's detail now opens with which of the two values set the number and how far it sat from its boundary, what the scale means at that boundary, and that the pair it works from is a weighted average over the window rather than a reading anyone took.
Fixed
- The Coach read strip writes both of its lines in the language you use. The summary was translated and the line directly beneath it stayed English. The translations existed the whole time. The request simply never named a language, and the code behind it chose English whenever nobody did.
- The cycle insights read carried the same silence and the same English line.
- Switching language reloads those lines instead of showing the previous language out of the browser cache.
- Anything in the score that is not contributing is one quiet line instead of a block of its own. A read that failed is named once for every pillar it took down and keeps its retry. Safety guidance is shown in full. Anything not recorded yet is counted, with the names one tap away.
Internal
- A function that writes a sentence for a person has to be told the language, and none of them may fall back to English on their own.
- The space the score holds open while it loads is measured against the built panel instead of estimated. The declared minimum was smaller than the blocks inside it, so it never bound anything, and a short score was pushed further down the page than it needed to be.
- A widening of the pillar label column was taken back out. The measurement behind it did not hold: the longest label in five of the six languages overruns any column this panel can afford, so the extra width would have come off the bar.
[1.34.4] — 2026-07-31
A release about two places in the code answering one question differently.
Added
- An inferred nap is now shown as its own band in the sleep stage view instead of being blended into that night. A nap used to be counted into the night's stages, so a day with a long afternoon sleep read as a longer and shallower night than it was. Nights without a nap look exactly as they did, with no empty band and no legend entry for something that is not there.
Fixed
- A checkup that is due tomorrow says tomorrow. The dashboard card worked its due line out in rolling 24-hour steps while the checkups page used calendar days, so the same appointment could read as due tomorrow on one screen and in two days on the other. Both now read one shared rule.
- The due line and the date printed beside it are worked out on the same clock. One used the device's timezone and the other the profile's, which disagree by a day for anyone travelling or on a machine set to another zone.
- Recording something no longer takes minutes to appear on the home page. The page render and the API were each holding their own copy of the server cache, so a write emptied one and left the other serving the state from before it. They now share one.
- Returning to the dashboard no longer answers a refresh with a reading fetched before your last entry.
- The onboarding profile step says when the server could not accept a field. Valid answers are kept, rejected ones are named, and if nothing could be saved the message says which field blocked it.
- A cycle symptom's intensity survives a backup and restore. It was recorded and shown, and every export quietly dropped it. Older backup files still restore, with the intensity honestly absent rather than filled in with a zero.
Removed
- The Health-Score explainer switch in the operator settings. It promised a control that was deliberately removed two releases ago, so it had been switching nothing for months.
Internal
- The sleep composition and the single-night view now agree about which session is the night, because they ask the same function rather than each deciding.
- A dose taken before midnight stops reminding, and a genuinely missed dose still escalates.
[1.34.3] — 2026-07-31
A release about surfaces that knew something and did not say it.
Added
- The workout page now shows the day around a session instead of linking away from it: the day's pulse curve, the night before, and that day's mood, on the page. It also offers the previous session of the same sport in one tap, says how many workouts the list holds, and lets a sport narrow it.
- When a nutrient sync arrives and nothing can be stored, the nutrients page now says so and why, in plain sentences, instead of showing the same empty page someone would see who never synced at all.
Changed
- The health score appears once. It is a single card below the greeting with the breakdown behind one expander, rather than a compact copy in the greeting plus nine full-width blocks underneath. Scores with too little input leave the grid but keep their name and the reason they are not scored.
- The Coach input is one row tall until you type. It used to reserve a second row for its buttons before anything had been written.
- Card headings sit closer to their content. A card with a title and no description reserved space for a description that was never there, which affected 45 of the 48 cards in the app.
Fixed
- A profile save no longer discards everything because one field is invalid. Valid fields are saved, rejected ones are named, and if nothing can be saved the message says which field blocked it. Validator wording no longer reaches the screen.
- A failed read on the nutrients page is no longer shown as "no data yet".
Contract
- Share-link selection now publishes which leaves belong to which group and which group is the sensitive one. The server always knew; a client that could not see it had to withhold "select all" rather than risk sending psychometric scores in a single tap.
- The dashboard snapshot schema declares three fields the route has been sending all along. Because that schema is closed, a strictly generated client rejected the real response.
- Rhythm events carry their own six-value classification, separate from the three-value one on ECG rows.
- Six routes that clients consume were added to the published specification, including sixteen skip reasons enumerated from the source rather than transcribed. Three schemas were named in the specification's own prose while never being emitted; all three are now published.
Operations
- The optional worker in the bundled compose file inherited 14 of the app's 62 environment variables, missing every mail, push and notification value. It is the process that sends notifications, so a split deployment ran a worker that silently sent nothing. Both services now share one definition.
- Bounded log rotation on both containers, dropped capabilities and a memory limit on the app, with the reasoning recorded, including why the database gets neither.
- Every nutrient ingest call is now recorded, not only the ones that stored a row. The single durable trace used to be conditional on success, so a batch that was entirely rejected looked exactly like a batch that never arrived.
- The build carries its own memory ceiling, so a local run, CI and the published image all invoke it the same way.
- The self-hosting notes warn that a pinned image reference stays pinned, and name the two checks that catch a deploy which reports success while serving the previous release.
[1.34.2] — 2026-07-30
Point release. One workout that arrived more than once, and several surfaces that were reading the same record differently from one another.
Fixed
- Badminton recorded through Google Health keeps its sport instead of arriving as "Other". Reported by @tarantila.
- A workout the same source sent twice is now counted once everywhere. The workout list already collapsed the repeat, and the Coach narration, the per-sport comparison and the workout deep link did not, so one session could be folded two or three times into the averages a person is measured against. The three that missed it were fetching the workout without its duration, and the identity check needed that field.
- Insights prose renders at full contrast, and the two assessment cards share one internal rhythm again after one of them drifted away from it.
- The dashboard headline survives for anyone who has switched the score lead off.
- The service worker's fallback cache version follows the release again. It had slipped a version behind, which only shows up in a browser that cannot load the version file, and that browser then kept serving the previous release's cached pages.
Credit
Reported by @tarantila, directly rather than on the tracker: the badminton mapping, following on from the workout-type loss in v1.34.0.
[1.34.1] — 2026-07-29
Security and reliability point release.
Added
- Google Health imports now show bounded progress and a clear final result.
- Workouts keep their provider details and can be enriched with available WHOOP data without inventing pulse curves.
- Medication reminders retain overdue occurrences until their actionable window has ended and identify each occurrence uniquely.
- Intake imports report completed writes and already-recorded entries clearly.
Changed
- Dashboard scores, follow-up suggestions and hero metrics use the latest authoritative readings.
- Public pages, documents and downloads now behave consistently on desktop and mobile, with tighter spacing and clearer empty states.
- The administration and settings areas use a more consistent navigation and grouping structure.
Fixed
- Prevented duplicate medication intakes and reminders for the same occurrence.
- Fixed stale or future readings replacing newer measurements.
- Fixed cache isolation for authenticated PWA pages.
- Fixed passkey step-up, password-reset session revocation and refresh replay edge cases.
- Restricted private-network integration requests to approved origins and final sockets.
Security
- Hardened MCP read permissions, refresh handling, passkey verification and server-side request validation.
- Apple ECG data remains encrypted and source-labelled. WHOOP ECG is not inferred from pulse data.
[1.34.0] — 2026-07-28
Two things this release keeps saying to itself. A job, a sync, or a write that fails can no longer report success by falling off the end of a function. And a number the app grades you against is either yours or it says plainly that it is not.
A failure now looks like a failure
Every background job in this codebase, all 104 of them across five queues, used to signal success by returning nothing. A handler that caught an error, logged a warning, and returned was indistinguishable from one that finished its work, so a queue could fail quietly for as long as it liked. Every handler now has to construct and return the outcome of what it did, and the type system enforces it: a handler that falls off the end no longer compiles. Retry behaviour was reviewed queue by queue rather than flipped in bulk, because a rethrow changes what runs, not only what shows up on a screen.
The same fix reached the surfaces you actually watch. Six integration cards and the lab import dialog used to key their tone off whether the network call itself succeeded, so a sync that imported nothing because every reading was refused still rendered a green tick, and a re-scan where every row turned out to be a duplicate raised a success toast for zero saved readings. They now render the outcome the server actually resolved: a tick only when everything landed, a warning when part of it did not, an error when none of it did.
Grading you against your own numbers
The Health Score graded your weight against a target nobody set, silently computed from your height and never shown anywhere. If you had entered your own weight target on the targets page, the score ignored it. Both the targets page and the score now read the same target you actually set, and an account with no target of its own gets a plain trend reading instead of a number graded against a hidden goal. The score's weight row now says outright which of the two it used.
The Health Score itself is rebuilt on the same rules about coverage and where a value came from that the rest of the app's derived metrics already use. Every pillar now says what went into it and what did not, weights redistribute openly when a pillar has nothing to grade instead of quietly changing what the number means, and the card explains itself the way the other Insights cards do. Because both of these changes move the same number, they are explained once: the score carries a version marker, and the first delta you see after updating is suppressed rather than reported as a real day-to-day change, with a note on the card saying why.
Steps, active energy, distance, and flights now compare today's running total against what is typical for you at this same hour, not just against your latest reading, which never meant anything for a metric that only makes sense as a full day. It needs at least two weeks of your own history before it says anything, and until then it says so honestly instead of guessing from three days of data.
What was asked for, and built
An account-defined custom metric can now feed the same correlation engine your built-in metrics already run through, so a value nothing else in the app tracks can be checked against your own outcomes rather than sitting in its own silo. Caffeine and nicotine join alcohol as taggable factors in that same engine. A correlation or crosstab finding can be dismissed once it has been looked at, and stays dismissed instead of recomputing itself back into view on every run.
Three facts about you, smoking status, alcohol pattern, and shift or rotating-schedule work, can now be recorded as dated, correctable entries rather than buried in free text, with their own switch for whether they reach the Coach's prompt at all.
The "worth a look" rail on the dashboard can now be trimmed to the kinds of item you actually want to see there. Turning a tile off there only hides it; it does not touch the medication reminder itself, which keeps firing on its own channel regardless of what the hero card shows.
Two defects, found by people who read the code and the numbers
A workout synced from Google Health with a sport type the app's mapping table did not recognise lost that type outright and rendered as "Other," identical to a workout that genuinely had no category. The original label is now kept on the row, the same way three other providers already keep theirs; a full re-sync repairs workouts already written under the old behaviour.
A same-time baseline band for a metric that cannot go negative, such as step count, could report a lower edge below zero. One call site is now required to name which metric it is drawing a band for, so the floor comes from that metric's own declared range rather than being patched in by hand at each caller.
Reminders for a multi-dose schedule stopped matching the dose they were meant for. The reminder tick computed one window length for a whole schedule and applied it to every time slot in it, so a twice-daily schedule gave its first dose a ten-hour grace period nobody configured while a single-dose schedule with a narrow window escalated correctly but then had almost no room to register a dose logged late. Each slot now resolves its own window: an explicit per-dose override first, then the schedule's own grace setting, then the same default every other unconfigured dose gets, bounded so two doses in one day can never share a window.
A Fitbit workout kept only the category this app sorted it into and dropped the raw activity name and type id it arrived with, unlike every other provider this app syncs from. An unrecognised Fitbit activity had nothing left to diagnose it by. Both are now kept on the row, and carried through backup and restore the same way.
The backup
Your custom metrics and their readings, and the encrypted health-profile facts above, are now carried by export and restore. Neither was before; a restore silently rebuilt an account without either and reported success. The hourly shape behind the same-time baseline is also carried, because past a short grace window it is the only copy of that curve anywhere. And the summary shown after uploading a backup now counts every record class it restores, including the nutrient day totals it was quietly leaving out of its own total.
For operators
Four migrations. 0283 adds the intraday cumulative profile table, 0284
adds the effective-dated health-profile fact table and its inclusion enum,
0285 adds correlation-pattern identity, 0286 seeds the caffeine and
nicotine mood tags. None deletes or rewrites a measurement.
Credit
Reported by @lutzkind (#613, #614, #616, #617, #649, and a long run of earlier reports this release also carries forward). Reported by @tarantila, directly rather than on the tracker: the Google workout-type loss, and a below-zero baseline reading that turned out to affect other metrics once someone went looking. Built by @mathewcsims (Refs #662): the per-slot reminder window fix.
Dedication
This release is dedicated to the people who reported something, asked for something, or noticed something and said so: @balajiv113, @ChaosExAnima, @doenke, @DrGithubble, @Flieger37, @k-nacion, @kevinpapst, @livelylinux, @lutzkind, @Manschk3rl, @mathewcsims, @muhdusama, @Nazza01, @nhemnt, @StefanIndustries, @TalesFromTheStack, @tarantila, @Uvenstedt, and @zet1200. None of you owe this project anything, and all of you gave it time anyway.
@lutzkind gets a line of his own. Twenty-four issues so far, several of them arriving with the broken code path already traced, and a habit of pushing back on a design decision until it actually gets better. More than one feature in this release started as his idea, not mine, and I did not expect that from a project I built to track my own blood pressure.
@StefanIndustries was the first person to send this project money. What was sent stays between us; the amount was never the point. Deciding this was worth paying for, and saying so out loud, was not something I expected from a self-hosted app nobody asked me to charge for.
This release carries a good part of what came out of that list, among other things a workout category that two providers were silently dropping, a below-zero reading nobody else had caught, and a reminder window that ignored half its own schedule. It does not carry all of it. What is still open stays open because it is waiting its turn, not because it got set aside.
More is welcome, from anyone: an idea, a rough thought about how something ought to work, a screenshot of a number that looks wrong. Especially the number that looks wrong.
[1.33.2] — 2026-07-28
The cycle-symptom fix in v1.33.1 did not work
v1.33.1 said it had fixed custom cycle symptoms in backups. It had not. The fix was real in three places and missing from the one that decides what ends up in the file. The code that reads your cycle data returned the symptoms you created yourself. The file format declared a field to hold them. The restore knew how to put them back. But the step that assembles the file copies its contents across one field at a time, and that list was never extended, so every backup written since carried an empty list of custom symptoms.
The same release also replaced a silent drop with an error. Before it, a backup referencing a symptom the restoring instance did not have lost that one link and finished. After it, the restore stops and reports a failure. Together with the empty list, that took an account with a symptom of its own from losing a single link to being unable to restore at all. That is the group the fix was written for.
Nothing in your stored data was damaged. But a backup written by v1.33.1 or earlier does not hold the symptom definitions you created, and restoring one of those files on this version will fail at the first day-log referencing one. Take a fresh backup after updating and keep it alongside the older files.
The assembly step no longer lists fields by hand. It copies each section whole, so a field added to a section reaches the file because of the way the code is written rather than because somebody remembered. Both ends of this were covered by tests that passed for the whole time it was broken, and nothing covered the join between them, so there is now a test that writes a real backup for an account with a symptom of its own, removes that symptom from the database, restores the file, and checks the row and its day-log link came back.
[1.33.1] — 2026-07-27
A patch release about the same thing as the one before it: places where the app knew something and said something else.
Two that change what you receive
Medication reminders arrived every fifteen minutes. The rule that decides whether a reminder has already been sent was keyed on a ledger that could not tell one dose slot from another, so an overdue dose kept re-announcing itself for as long as it stayed overdue. One account was getting roughly fifty mails a day. A reminder now goes out once per dose, per escalation step, per local day, and two doses due at the same time no longer replace each other on the lock screen.
"Handled by the app" now silences only the app. The setting that lets the iOS client take over medication reminders was suppressing every channel, so switching it on quietly turned off Telegram, ntfy, webhooks, email and web push as well. That was never what it said it did. If you have been using it as a general mute, those channels will start delivering again, and the setting does what its name says.
Three that were reported
Someone self-hosting could not use an OpenAI-compatible gateway. LiteLLM, OpenRouter and vLLM all speak the OpenAI wire, but the provider list had no entry that honoured a base URL of your own, and an earlier answer from us said otherwise. There is now a provider for exactly this, with its own key, model and endpoint. The plain OpenAI entry stays pinned to OpenAI, so a key meant for one never travels to the other.
Timestamps followed the browser rather than the profile in four places. The worst of them: a shared report downloaded as a PDF could carry a different date than the page it was downloaded from, for the same reading. Charts also drew their week and month boundaries in one fixed timezone, so an evening reading landed in the wrong bar for anyone living elsewhere.
Some dashboard tiles could be seen and not switched off. The mobile app
draws a handful of widgets the web has no render path for, and it writes
them into the same stored layout, so an account ended up holding rows it
never set and could not change from anywhere. bmi was one of them. They
now appear in the dashboard settings in their own labelled group, with the
same show and hide controls as everything else and no reordering, because
placement stays the mobile app's. The group only shows up for widgets your
layout actually holds, so an account with no phone client sees nothing new.
The backup
Three things it carried and could not restore, or could not carry at all.
A cycle symptom you created yourself was never included, so on restore its key resolved to nothing and the link was dropped without a word. Three lines away in the same function, an unknown cycle reference threw an error. Same kind of missing reference, two different answers.
A mood factor you created yourself had the opposite ending: the restore looked only for the built-in ones, found your own missing, and stopped. The file was complete and unusable.
Nutrient day totals have been written into every backup since v1.29 and were discarded on restore, silently, while the restore reported success.
What a backup contains is now declared in one place, with a written reason for every record type left out. Around thirty types are still carried by neither the file nor the restore, and they are named there rather than left to be discovered. The export card no longer promises "every record you own", because it does not.
For operators
Two migrations. 0280 adds the gateway provider's columns, 0281 turns the
integration ledger's single failing-part slot into a set. Neither deletes or
rewrites a measurement.
A provider that syncs several things separately can now remember every part that is failing at once. Until now the last one to report overwrote the rest, which is the limit named in the v1.33.0 notes and not fixed then.
Self-hosters can point GEOLITE2_DIR at their own GeoLite2 databases. The
variable was read by the code and missing from the compose whitelist, so it
never reached the container. The message in the admin area used to tell
self-hosters to set a secret in this repository, which is not something they
can do.
Credit
Reported by @k-nacion (the OpenAI-compatible gateway and the timezone rendering), @mathewcsims (the unreachable GeoLite2 setting, found by reading the strings inside a running container), @Nazza01 (the dose-history import), @doenke (the table under the chart, and for narrowing his own request until it fit one release) and @lutzkind (the dashboard layout).
[1.33.0] — 2026-07-26
Eight defects, and every one of them had the same shape: something failed reliably, the code recorded it precisely, and no person ever found out.
"Delete All Data" deleted 13 tables out of 92 and reported success. What survived included mental-health questionnaires, lab results, the menstrual record, allergies, family history, the whole document vault, every AI conversation, and clinician share links that kept working. On the account row itself, 11 of 119 columns were cleared, so name, insurer, insurance number, home coordinates, profile photo and every integration credential stayed. The wipe now covers everything the account owns except what is needed to sign in again, the list is derived from the schema so a newly added table cannot quietly survive, and the confirmation says what will go before you agree rather than after.
A device sync failed 1149 times without anyone being told. The escalation had no rung above "temporary", so a connection could fail hourly for weeks while the card read "connected". Worse, providers that sync several things separately had one healthy part erase the error recorded by a broken one, twice an hour, including a connection that had already been parked for a dead credential. A failing connection now shows how long it has been failing, escalates after a day, and stops retrying after two with a reconnect button.
One limit worth naming rather than leaving to be discovered: the ledger remembers a single failing part per provider. If two parts fail at overlapping times, the recovery of the later one still clears the error of the earlier. That is narrower than the case this fixes, and it is not closed.
The Environment module had never once saved a row, for anyone. One declaration in 49 was missing the line that maps it to the database, so every write referred to a type that does not exist. The job failed every night and recorded that failure faithfully, and no code anywhere reads a failed job. Both halves are fixed: the module writes, and a failing background job now names itself on the admin page and on the affected screen. Past days can be fetched from the weather source; the nightly job fills the last week by itself and the backfill button covers the rest.
Sleep was counted twice. A device that re-exports a night it already sent left two overlapping in-bed periods, and both were added. One night read ten hours in the headline and twenty in the legend. The same fault inflated the time-asleep figure itself. Every stage total is now the measure of its own period rather than the sum of overlapping ones.
Twenty-two controls could destroy something without asking. Among them a single tap that cleared the allergy list along with a text note, and a switch labelled "Custom range" that deleted the stored range when switched off. Every destructive control is now registered, and a new one cannot ship without being classified.
Medication stock was written off that had not expired. The thirty-day clock that applies to an opened injection pen was being applied to a blister of tablets, so pressing out the first tablet started a countdown and a month later the rest were declared unusable. The container kind was recorded and never read. Pens and ampoules keep the clock; blisters, bottles and inhalers do not. A repair returns stock that was written off, and the dates that drive it can now be seen and corrected on the item itself.
Insights scrolled sideways on every phone. The app scrolls inside an inner area that absorbed the overflow, which is why it went unnoticed for so long. It is gone now.
A third value for gender was accepted by the app and missing from the published API reference in five places, and dropped or coerced in nine more. A fitness calculation scored anyone without a recorded gender using the male coefficients, and the doctor report labelled any unrecognised value as non-binary. Every place the field is read, written, documented or displayed was gone through.
Four additions
Medication history from a widely used iOS export tool can now be read directly, as the file actually is, rather than converted by hand. The scheduled time and the time taken stay two separate facts, which is what a hand-mapped file destroys. Every column is either used, reported, or named as not read, and you see which before you import. Seven status values are ruled on individually: only "taken" and "skipped" become doses, and a snoozed reminder or an unanswered notification is counted and named rather than turned into a decision you never made.
The Coach can tell "you never recorded this" apart from "you recorded it, but outside the range I looked at". Someone with 1,597 imported glucose readings from April 2024 was told there was no glucose data at all, and the Coach then changed the subject, because one reason code covered both situations and the instruction to move on fired on both. It now says what is actually true and offers to look further back.
A mood tag can now be paired against the next morning's resting heart rate and heart-rate variability. Until now a tag such as alcohol had three channels to land in, and the only one that looked at the following day needed a wearable recovery score that plenty of accounts never record, so the question of what a tagged evening does overnight had no path at all. The difference is printed without a colour verdict, because a rise reads as worse on one row and better on the next, and an association cannot carry that judgement.
Every chart on the metric pages now carries the points it drew as a table underneath, collapsed by default with the count in the button. It is handed the same points the chart draws and the same formatter the tooltip uses, so a cell and a tooltip cannot print two different numbers for one reading. The caption names the grain, so a weekly row says it is a weekly average, and units sit in the header rather than in every cell, which is what you want when the rows go into a spreadsheet.
For operators
Four migrations. 0276 adds retention indexes, 0277 adds failure-streak
tracking, 0278 scopes the intake import key per account, 0279 repairs
inventory that was wrongly expired. None deletes or rewrites a measurement.
Credit
Reported by @lutzkind (sleep double-counting, the gender field, the Environment enum, the Coach retrieval window, the next-day pairing for mood tags in #616, and a run of earlier reports), @Nazza01 (the intake import, plus the export file and the format documentation that made reading it possible), @doenke (#636, the table under the chart) and @DrGithubble (the unit and precision work that led here).
Two of these defects were found because someone outside this project read the code and said what was wrong. The Environment module had been broken for every user since it shipped and nobody inside noticed.
[1.32.39] — 2026-07-26
The doctor report now carries what you chose, and nothing else.
Ten of the twenty switches did nothing. The panel offered controls for oxygen saturation, body fat, body composition, resting heart rate, heart-rate variability, VO2 max, steps and distance. Switching them off changed nothing: the value was folded away before it reached the report. Two more claimed less than they did, and the medication switch left the dose-by-dose record in place while appearing to remove the drug list.
Allergies and family history went out regardless. They were read straight from the database in the export route, so a report could exclude them and the structured file in the same download would still contain them.
The mental-health questionnaires went out without being asked for. The depression, anxiety, wellbeing and sleep scores reached the structured export, the automation interface and a shared link, gated only by a module that had been switched on by default some releases ago. They now have their own entry and are off unless you pick them.
The cycle export shipped ten other things. The card that promised to export only cycle data sent an empty request, and an empty request fell back to the defaults, so it also carried weight, blood pressure, pulse, oxygen saturation, body fat, the medication list, adherence, lab results, allergies and family history. The comment above that code said every other section was off.
What replaces all of it. One picker, on both surfaces that choose a scope. Twelve groups, each releasable or excludable as a whole and openable down to the individual value. All 77 measurement types and 13 record sections are in it, each in exactly one group. Sensitive material sits below a divider with no group control, so no single click can turn on more than one of those at a time.
Nothing is preselected. A new report starts empty and one button fills in what a general visit usually needs, which you can then change. Generating is not possible while nothing is chosen.
A metric added to the app in future cannot appear in a scope you saved earlier. That is not a promise, it is a compile error: a new measurement type without a group breaks the build, and every path that produces a report has to name where its scope came from.
Share links. A link can now be fetched as a PDF or as a structured file behind the same passphrase that already protects the view, which is what a practice actually does with one. The old promise of an interface that never existed is gone. Links created before this release carry no explicit choice and are therefore closed; each one can be reissued in a click with its label, period and expiry carried over.
The written summary has left the report. It could not be checked against what you chose, and prose whose scope cannot be shown has no place on a document whose value is exactly that.
Structured export corrections. Someone who records their gender as something other than female or male was exported as female. FHIR has a code for it and it is now used, and an unrecognised value is left out rather than guessed. The capability document described a resource twice and omitted another that was actually served; both corrected.
Also in this release. Roughly one page load in three showed raw text keys for a moment instead of words, because the language file was racing the application bundle and losing. It is now loaded before the first render.
Converted values were shown with more precision than they have. A weight in pounds could read 207.896 where the conversion declares one decimal place. The rounding was defined for every conversion and read by exactly one screen out of all the ones that display a converted value, so everywhere else the raw result of the multiplication went straight through. It now happens where the conversion happens, so a surface cannot render an unrounded value even if it forgets to ask. Metric values are deliberately untouched, because the metric render is meant to be exactly what is stored, and the entry path keeps full precision so a saved value cannot drift a little further each time.
Thanks to @DrGithubble, who reported this on #627 after confirming the earlier unit fix. It turned out to be the second half of the same defect they had found the first time.
For operators. Migration 0275 stores the new scope and removes the old
preferences. Existing share links are closed by it and need reissuing.
[1.32.38] — 2026-07-25
Blood-glucose readings from anything other than the manual entry form were never saved.
A rule added years ago required every blood-glucose reading to carry a context: fasting, after a meal, random, or bedtime. That is a fair thing to ask of someone typing a reading in by hand. It is impossible for a sensor, which measures every few minutes and cannot know what you were doing.
So every automated path failed. A CSV import reported the rows as skipped and still showed a green tick. Nightscout counted them as failed rows and carried on. Apple Health has no context field at all, so not one of its readings could ever have been stored. The same for readings written through the automation interface, through Telegram, and through the JSON importer. None of it was visible: no error reached anyone, and the affected charts and panels simply stayed empty as though nothing had been measured.
A reading with no context is now stored with no context, which is what it is. Where a context is given it is still checked. Manual entry still asks for one, because the form offers the choices and nothing is lost by asking.
Absence is now shown as absence: a reading without a context is left out of the per-context breakdowns rather than being counted as one of them.
Imports no longer claim to have worked when they did not. A run that wrote nothing is shown as a failure. A run that wrote some rows and skipped others is shown as a partial result. Only a run that actually landed something shows as done. Skipped rows are grouped by reason with a count instead of listing the first fifty identical lines, and the per-row detail is still there behind a disclosure. This covers the CSV, JSON and Apple Health cards alike.
Reported by @lutzkind, who traced it to the parser, ran it in isolation to rule out the timestamp and unit handling, and checked the database afterwards to prove nothing had been written. Files that were rejected can simply be imported again.
For operators. Migration 0274 relaxes the constraint. It adds no column
and rewrites no row. Readings that were refused in the past were never stored,
so they need a fresh import from the source.
[1.32.37] — 2026-07-25
Two ways a record could quietly lose data, both closed.
An identifier that a client sends along with a reading is meant to say "this is the same reading you already have". One client was sending a value that changed every time the app restarted, so nothing ever matched, and every sync created a fresh copy. On one instance that produced 23 medications nobody had ever taken, none of them with a dose or a single logged intake, because the doses were looking for a medication under an identifier that no longer existed.
The server now refuses an identifier that cannot possibly be stable, with a 422 that says what is wrong instead of quietly making duplicates. A batch rejects only the offending entry and still accepts the rest, so one bad row cannot stop a sync. Twelve entry points check it. The identifier shapes that real apps and devices actually send all still go through, so the new rule cannot get greedy and start dropping valid data.
The second one was worse and had no symptom at all. The repeat-protection key on a medication intake was unique across the whole database rather than per account. Two people generating the same key independently meant the second one could not save their dose at all. The database refused it, and the intake was gone. The key is now unique per account, and every lookup asks who it belongs to. Recording an intake through the automation interface without sending a key had a related problem: the first one saved and every one after it was discarded as a repeat. Also fixed.
The migration does not touch any existing intake. It relaxes a rule rather than tightening one, so nothing that was allowed before becomes disallowed now.
[1.32.36] — 2026-07-25
Nothing here changes what you see. This release is about the checks that run before a release goes out.
A recurring shape of mistake in this project is a half-built feature: something is stored, or sent to the app, and the part that was supposed to use it never gets written. Each end works on its own, so nothing complains. Three checks now run on every build and refuse that. One walks the fields the app receives about your account and insists each one is actually read somewhere. One compares the measurement table against what a backup writes out, so a new column cannot go missing from your backup file. The third fixes a check that had gone quiet: it was meant to find translation entries no screen uses any more, but one line of it had been waving through several thousand entries at once for a long time without anyone being able to tell.
With that check honest again, 149 leftover translations turned out to belong to screens that were rebuilt or removed some time ago. They are gone from all six languages. Nothing that is on screen today was touched.
[1.32.35] — 2026-07-25
The doctor report stops asking for your practice name every time.
You type a clinic or practice name onto the cover of a health record export, and until now that was all it did. The field emptied itself again the moment you left the panel, so next month's report started from a blank line. The name you used last time is now kept and filled in for you. Clearing the line works the same way and is remembered too, so a cover without a practice line stays without one. The name is saved only once the report has actually been produced, which means an export that fails partway leaves the previous value alone.
A backup now carries the origin of your daily totals. Steps, distance and similar figures that arrive from Apple Health as one number per day are stored with a note of where that number came from, so a rough estimate read out of an archive can never overwrite a total the phone measured itself. That note was missing from the backup file, so a restore brought the figures back without it and the protection went quiet. It rides the backup now. Restoring an older backup written before this fix marks the affected daily totals as unclassified instead of leaving them ambiguous, which is the state a later sync knows how to repair.
Migration 0272 drops two columns that were written and never read. They held a contributor count and a source fingerprint for archive derived daily totals, and no screen, export or repair path has ever looked at either. The figures themselves and the origin note above are untouched.
[1.32.34] — 2026-07-25
Housekeeping on the interface. Most of it you will not see.
The sections on the insights overview had each been given their own card frame by hand, and the copies had drifted apart on the one thing nobody thought to write down: how much room to leave between the blocks inside them. Some used 8 pixels, some 12, one 16, all sitting one under the other in the same column. They now share a single frame with a single rhythm, so a few of those cards open or tighten very slightly and all of them pick up the same faint shadow their neighbours already had.
When a metric page cannot load, the message now reads in the normal text colour with a warning glyph beside it, instead of the grey reserved for timestamps and units. A failure is what that surface has to say at that moment, so it should not look like a footnote.
The rest is invisible from the outside. The sleep composition card uses the same title row as every other card, a handful of one-off text sizes and paddings went back onto the scale the rest of the app uses, and the spacing rule that was already enforced in the build now has nothing left to catch.
[1.32.33] — 2026-07-25
The moodLog integration is gone. It bridged mood entries to and from a separate self-hosted mood app back when HealthLog had no real mood module of its own. That stopped being true a long time ago: mood is tracked natively here now, with tags, rated factors and notes, and the bridge had nothing left to add. Its settings card was withdrawn a while back and the rest of it kept running in the background against an upstream nobody was pointing at any more. This finishes the job properly.
- Your mood entries are untouched. Every entry stays exactly where it is, including the ones that originally arrived through moodLog. They keep their source label, they still show up in the mood list, the filters, the charts, the correlations and the doctor report. Nothing was deleted and nothing was relabelled. Removing the pipe does not remove what came through it.
- What was removed. The hourly pull, the inbound webhook, the outbound
push, the per-account credentials and webhook secret, the admin toggle, the
entry on the integrations status feed, and the message strings behind the old
card. Migration
0271_drop_moodlog_integrationdrops the six columns those surfaces used and clears the retired sync schedule. It names no mood table. - One less admin switch that did more than it said. The instance-wide toggle was described as controlling mood-log reminders. It actually shut off the whole integration, quietly, with no other visible effect. It is gone along with the thing it gated.
If you were still relying on the bridge, export your mood data from the other app and import it once; the entries land in the same place they always did.
[1.32.31] — 2026-07-25
The integrations page could tell you a connection was failing. It could not tell you a connection had stopped trying. Those look the same from the outside: the page keeps showing whatever error was recorded last, and it keeps showing it for as long as nobody looks, because nothing anywhere read how old the last attempt actually was. This release adds that reading, and every status on the page now comes from it.
- A sync that has stopped running is labelled as stopped. When a connection has not even attempted to sync for a day, it says so, with how long it has been. Previously it kept displaying the last error it managed to record, which reads like something that is still retrying.
- Per data type, when a value last arrived. Each connected card now opens a short list: every data type that connection has ever delivered, and how long ago the newest one came in. A type that has gone quiet for two weeks while the rest of the connection is healthy is called out, which is the shape a single broken pipe takes: one revoked permission, one endpoint that started returning nothing, while the sync itself keeps reporting success. Types the connection has never delivered are simply not listed. Nothing is invented.
- Apple Health has a staleness threshold. Any timestamp at all used to paint the green "data received" badge, so a phone that stopped delivering three weeks ago looked exactly like one that delivered this morning. After a week of silence the card now says so and points you at the app's Apple Health permissions. It also lists the same per data type detail as the others.
- A configured connection with no card of its own now appears. If your account has a connection set up that the page has no card for, it gets a plain status row instead of being left off entirely. A connection can no longer be running, failing and invisible at the same time.
- The status colours mean one consistent thing. Red is now reserved for the cases your own action fixes, which is reconnecting or resuming. An upstream outage or a rate limit carries the amber tone instead, because clicking reconnect against a server error has never helped anyone. Every card uses the same mapping. Three of them used to disagree with each other on the same page.
- Nightscout reads the same status source as everything else. It was the last card fetching its own status separately, which is why it could show a different colour for the same condition.
The assistant tools report the new verdict too, so asking why your data looks stale gets the real answer instead of "connected".
[1.32.30] — 2026-07-25
Height was the last value still asked for in centimetres regardless of the setting. Both previous releases closed with a note saying so. It sat outside the fix because height is not a measurement at all: it is a field on your profile, so the conversion the reading surfaces go through never reached it.
- Height is entered in the unit you use. With imperial selected, the profile card and the onboarding step ask for feet and inches in two boxes, the way a person actually states a height. With metric selected the single centimetre box is exactly what it was.
- The value you type comes back. Feet and inches are kept to the nearest whole inch, which converts to centimetres without a remainder, so saving 5 ft 11 in and reopening the page shows 5 ft 11 in again. The limits the fields offer are the imperial equivalents of the ones the server enforces, rounded inward, so a height you are allowed to type is never refused on save.
- Storage is unchanged. Height is still kept in centimetres, the API still sends and receives centimetres, and BMI and the weight bands still work from that same value. A metric account's stored height is untouched by this release.
- Switching the unit keeps what you were typing. The unit selector sits on the same card as the height field, so flipping it re-expresses whatever is in the field rather than clearing it.
With this the preference reported in #627 reaches every value the app shows you or asks you for.
[1.32.29] — 2026-07-25
Fitbit re-scores a night hours after you wake up: blocks get split, merged or relabelled. HealthLog identified each block by its position in the list, so a re-score that renumbered the blocks looked like a set of brand new ones. The previous version stayed on file, nothing removed it, and from then on the night counted twice. It was quiet: the sync kept reporting success, because the sync was in fact working.
- A re-scored night no longer double counts. Each sleep block is now identified by the moment it starts, which does not move when Fitbit rescores the night. A block that changes from light to deep updates in place instead of leaving the old copy behind, and anything the new scoring dropped is cleared from that night's window before the new blocks are written.
- Nights already affected repair themselves. A one off pass runs once per
connection after the update and rereads the past year of sleep, so the
duplicates collapse without you doing anything. It covers 365 days, the same
horizon the Fitbit history import uses. Nights older than that keep whatever
they already hold; they cannot gain new duplicates, since Fitbit does not
rescore year old nights. Migration
0270_fitbit_sleep_repair_markerrecords which connections have been through the pass. - Rate limits are handled instead of ignored. When Fitbit asks for a short wait, the request now waits and tries again once. A long wait is refused outright and left to the next scheduled run, so one throttled account cannot hold up everybody else's.
- Fewer pointless writes. Fitbit rereads the last 24 hours every hour. Rows that came back unchanged were rewritten anyway, which showed up as needless traffic for the iOS app. Unchanged rows are now left alone.
[1.32.28] — 2026-07-25
Oura used to look back exactly one week and no further. If the sync had been broken for longer than that, or if the ring sat unused and the older nights only reached Oura's cloud later, those days fell outside the window and were never imported. Nothing said so, because a sync that finds nothing still counts as a sync that worked.
- Oura catches up after a gap. The window is now measured from the newest Oura reading actually on file rather than from a fixed week. A ten day gap pulls eleven days on the next run and closes itself; a month is the ceiling. When data is arriving normally the window stays at a week and nothing about the hourly run changes.
- A fresh Oura connection imports a month instead of a week. Deeper history is a separate question and is not part of this release.
- Sync now for Oura, Polar, Strava and Nightscout. These four had no way to ask for a sync; they only ever ran on their scheduled pass. Each connected card gets the same button WHOOP and Fitbit already had, limited to a few runs per minute.
- Connecting starts the first sync straight away. Oura, Polar and Nightscout used to sit empty for up to an hour after connecting, waiting for the next scheduled pass. The first pull now runs on connect, and the schedule stays as the fallback.
[1.32.27] — 2026-07-25
Target and threshold panels now follow the metric and imperial preference. The previous release listed this as the one place still reading in kilograms and Celsius: with imperial selected a weight chart read in pounds while the target reference right below it read in kilograms.
- Target ranges read in your unit. On the weight insight page the target band, the range bar, the status pill, and the 30 day average all read in pounds when imperial is selected. Body water and bone mass follow the same rule in the threshold settings.
- Editing a target works in the unit you see. The target editor and the threshold settings seed their fields from the stored value converted to your unit, check what you type against limits expressed in that same unit, and convert back before saving. A value typed at the displayed limit is accepted instead of being rejected against a kilogram bound you never saw. Saving and reopening shows the number you typed.
- Storage is unchanged. Targets are still kept in SI units, and a metric account's saved values are untouched by this release.
- Known remaining. Height is still entered in centimetres.
This finishes the job @DrGithubble started in #627.
[1.32.26] — 2026-07-25
The metric and imperial display preference is now applied everywhere your weight, body composition, waist, and temperature readings appear. Until now the setting was saved but almost nothing read it, so the numbers stayed in kilograms and Celsius regardless of the choice. Thanks to @DrGithubble, who reported this in #627 and traced it himself, down to the hardcoded kilograms in the entry form and the missing imperial branch in the display transform. His follow-up was the more useful half: a reading genuinely tagged in another unit was being relabelled by any surface that printed the type's standard unit. That is what turned a one-screen fix into a proper one.
- Weight and body composition follow the preference. With imperial selected, weight, muscle mass, body water, bone mass, fat mass, and grip strength read in pounds across the dashboard tiles, the charts, the record list, the insight pages, and the coach read strip. Body temperature, skin and wrist temperature read in degrees Fahrenheit, and waist reads in inches. Storage is unchanged: every reading is still kept in SI units, and a metric user sees exactly the same numbers as before.
- Manual entry converts at the point of entry. When you log or edit a value with imperial selected, the field is labelled in your unit and the number you type is converted back to SI on save, so a pound reading is stored as the correct kilogram value rather than a mislabelled one.
- A logging tool can no longer store a foreign unit. The measurement logging tool used by connected assistants now converts a recognised unit hint (such as pounds or Fahrenheit) to SI, and refuses anything it does not recognise, so a reading is never saved under the wrong unit.
- Known remaining. Height is entered in centimetres for now. Target and threshold panels also still read in kilograms and Celsius, so with imperial selected a weight chart reads in pounds while its target reference below it reads in kilograms. The stored values are correct either way; both are separate follow-ups.
[1.32.25] — 2026-07-24
Mirrored medications are now marked as such, and the server limits how many medications one external source can mirror so a client bug can no longer fill the list with duplicates.
- Mirrored medications are marked on the list. The medications list now reports whether each entry was mirrored from an external source (today Apple Health) or created in HealthLog directly. A synced row is no longer indistinguishable from one you added yourself.
- One external source can no longer flood the list. The server refuses to mirror more than 30 medications from a single external source per account. A client that keeps re-sending the same medication under a changing identifier can no longer create a fresh row on every sync. A repeat of a medication already mirrored still resolves to the existing row, and medications you add yourself are never affected.
[1.32.24] — 2026-07-24
Polar sync is more resilient when one data type is unavailable. A single collection that a device or account cannot serve no longer blanks the rest of the Polar data on every hourly check.
- One unavailable Polar data type no longer blocks the others. SpO2 on a watch without the sensor, or a data type an account is not entitled to, used to fail the whole Polar sync and leave recharge, sleep, and activity empty. Each collection now imports on its own, so the ones that are available still come in and only the one that failed is held back.
- A failing Polar collection is named on the integration status. When a collection cannot be read, the sync is recorded as failed with the failing collection named, instead of showing a healthy state over missing data.
- Unexpected Polar response shapes are logged, not read as empty. If Polar returns a body that does not match the expected shape, it is now recorded for diagnosis rather than silently treated as "no data", which previously could mark a sync healthy while importing nothing.
[1.32.23] — 2026-07-24
Sync status now tells the truth for the connected wearables and CGM sources. A few paths could report a sync as healthy when it had actually failed, which reset the failure streak, hid the reconnect prompt, and let a dead grant get retried on every hourly tick. This release closes those gaps so a failed sync reads as failed, an interrupted history import is retried instead of marked complete, and a couple of sources start pulling data sooner.
- A dead WHOOP token no longer reports a successful sync. When the stored grant is revoked, the connection stays parked with its reconnect prompt and the last-synced time stays put, instead of flipping back to connected and hammering the revoked token every hour. This holds on both the manual sync and the hourly background path.
- An interrupted WHOOP history import is retried, not marked done. A full-history backfill that stops partway is no longer stamped complete, so the gap is retried rather than frozen in place. To pull deep history after connecting, run a full sync from Settings. It walks back to 2020.
- Withings ECG now syncs on its own schedule. A watch-only account with no scale previously never had its ECG recordings pulled. An hourly check now brings them in without waiting on a webhook.
- Withings activity and sleep write failures now surface. A failed write no longer reads as a clean sync; it shows up on the integration's status instead of leaving a green state over missing data.
- Oura, Polar, and Nightscout sync errors that previously vanished now surface. A write that fails partway through is recorded on the integration status rather than being silently dropped, and each error is classified and redacted by the source that owns it.
- Strava history import starts at connect time. Deep history begins importing right after you connect instead of waiting for the next server restart.
- WHOOP cycle data uses a wider catch-up window. A day's strain and energy figures that are still being revised are re-fetched for a week, matching the other WHOOP collections, so a late revision is not missed.
[1.32.22] — 2026-07-24
The concurrent-edit protection that reached the layout surfaces now covers the rest of the preference records too. Notification preferences, module toggles, coach preferences, and the mood-tag layout all get the same protection, so a save based on an older read stops and reloads instead of quietly reverting a change another writer just made. Two more races are closed on the server side: a stock correction can no longer overwrite a dose consumption that ran at the same moment, and a privacy-mode change is now respected by an insight generation that was already running. Older clients that do not send the new marker keep working exactly as before.
- Notification preferences no longer clobber a concurrent change. The server merges each category into one stored record. A save based on an older read now reloads instead of writing the whole record back without the change another device just made. This is what closes the case where a web save could silently switch off the app-managed medication reminder flag and bring back a duplicate reminder.
- Module toggles, coach preferences, and the mood-tag layout get the same protection. Each is one stored record that more than one surface can write; a stale save is now stopped and reloaded rather than reverting a newer one. The coach-preferences save covers both the tuning options and the excluded-metric list together, so a conflict leaves both untouched.
- A stock correction can no longer race a dose consumption. Correcting the remaining count on a container now runs under the same per-medication lock a logged dose takes, so the two serialize instead of one overwriting the other.
- A privacy-mode change is respected by an in-flight insight generation. A briefing that was already generating when you switch the privacy mode no longer writes its old-scope output over the cache the switch cleared. It stops short of the write and the next run regenerates under the new mode.
- Older clients are unaffected. A request that does not carry the new base marker keeps the previous save behavior, so nothing needs updating to keep working. The new marker is documented in the published API spec.
[1.32.21] — 2026-07-24
Insights and medications layout saves, and the coach about-me note, are now protected against a concurrent edit overwriting a newer one. This is the same protection the dashboard layout already had. If you save a layout in one place while something else saved a change to the same record a moment earlier, your save now stops and reloads the current state instead of quietly reverting that other change. Older clients that do not send the new marker keep working exactly as before.
- Insights layout saves no longer clobber a concurrent edit. Saving the overview arrangement and saving the pill order are two separate surfaces that write the same record. If one lands while the other was based on an older read, the later save now reloads and asks you to redo it rather than reverting the first.
- Medications layout saves get the same protection. The card/table view toggle and the manual order both write one record, so a stale toggle can no longer resurrect an order you just saved, and the reverse holds too.
- The coach about-me note is protected as well. A save based on an older read is stopped and reloaded instead of overwriting a newer version of the note.
- Older clients are unaffected. A request that does not carry the new base marker keeps the previous save behavior unchanged, so nothing needs updating to keep working.
- The dashboard layout save is now documented in the published API spec. It was missing from the OpenAPI file even though it was the first surface to get this protection.
[1.32.20] — 2026-07-24
The medication-adherence insight now passes through the same safety screen as every other insight card. It was the one card of eight that showed the model's text without running that screen first. It already held back a reply that came back broken or cut off, but a dose-change instruction or an invented figure could still reach it. It now runs the same check the other seven cards run, and anything that trips the check falls back to the plain, data-grounded line the card shows when no model is set up.
- The medication-adherence card is screened like the rest. A reply that reads as a dose change, or that cites a made-up risk figure, is held back and the card shows its plain fallback. A normal grounded summary still shows as before.
- Two background notification lines can no longer print a raw model reply. The "just in" reaction line and the proactive coach nudge now catch a reply that arrived as structured data or got cut off partway, and use their fixed wording instead of printing it as is.
- Smaller cleanups in the same area. A stray marker left behind by a cut-off coach reply is now stripped from the text, and an unreachable branch in the adherence figure was removed.
[1.32.19] — 2026-07-24
After you logged a dose, the Today view could keep showing it as still due until you reloaded the page. The Today hero and the dashboard read from a cache that refreshes on a timer, and a write made from another screen marked that cache stale without actually re-reading it. The change reached the server, but the screen you came back to still showed the old state for up to two minutes, or until a hard refresh.
- A dose you take now clears from Today at once. This covers the "Take all due" button on the medications page, editing or deleting a logged dose, and importing intake history. The Today view and the dashboard now re-read the moment the write lands instead of waiting for the timer.
- A preventive-care reminder marked done leaves the Today rail immediately. Marking one off, editing it, or deleting it updates the Today view straight away rather than after a refresh.
- A lab reading you add updates its cards right away. Adding a reading refreshes that marker's assessment on the same page and the "what changed since your last panel" card, instead of leaving them on their earlier text.
- A briefing you regenerate shows up on Today without a reload. The new briefing now reaches the Today hero and the dashboard as soon as it is ready.
- Saving a dashboard layout keeps its safeguard against a conflicting edit. A visit to the home page could leave the layout editor without the token that protects a save, which quietly turned off the protection added in v1.32.16 against two edits overwriting each other. The editor now always loads its own copy, so the protection stays on.
Thanks to @lutzkind, whose run of reports surfaced the stale Today view.
Refs #581
[1.32.18] — 2026-07-24
When a Fitbit token was revoked or expired, the hourly sync noticed the failure but then stamped the same cycle as a success. The connection flipped back to connected, the sync position moved forward, and after you reconnected the catch-up window could no longer reach the days that were missed. So the data was skipped for good, and in the meantime the failing token was retried every hour.
- A revoked Fitbit token no longer reports a successful sync. A failed token refresh now fails the whole cycle. The connection stays parked until you reconnect, the sync position holds instead of advancing past real data, and the once-per-failure alert stops repeating every hour.
- Data after a reconnect is no longer skipped. Because the position is held back on a failed cycle, the next successful sync still covers the days that were missed rather than starting after them.
- A write failure during a sync holds its place too. If rows were fetched but could not be saved, the cycle keeps its position so the next run fetches them again instead of letting them fall outside the catch-up window.
- An interrupted history backfill no longer marks itself complete. A partial first-time import is retried until a full pass finishes, so the one-year history walk fills in rather than freezing halfway.
If your Fitbit history already has a gap from before this release: reconnect Fitbit if prompted, then open Settings, Integrations, Fitbit and run a full sync. It re-walks the last year and fills the gap in place. Gaps older than a year are outside the backfill window and cannot be recovered.
[1.32.17] — 2026-07-24
The mood correlations on the Insights page could line up a day off. Your mood is dated by the calendar day you logged it in your own timezone, but the metrics it was matched against were still grouped by the UTC day. If you live away from UTC, an evening reading slid onto the next UTC day, so a mood and a reading from the same evening were paired as if a day apart. A regression from v1.32.12, when the mood side moved to local days on its own.
- Mood now pairs with metrics from the same local day. The blood pressure, weight, and resting pulse series that mood is correlated against are grouped by the day you logged them in your own timezone, so both sides of the pairing share one calendar. An evening entry no longer drifts onto the next day.
- Non-UTC users get the correct same-day figures. For anyone west of UTC an evening reading used to pair with the wrong day's mood, turning a same-day relationship into a one-day-apart one. Those correlations and their scatter charts now reflect the day the data was actually recorded. UTC users see no change.
[1.32.16] — 2026-07-24
Changing which tiles and charts appear on your dashboard could get undone if you also toggled a hero score ring at about the same moment. Both actions save the whole layout, and the ring change was built from an older copy, so whichever reached the server last won. Thanks to @lutzkind for the clear report.
- A saved layout now stays saved. Your dashboard save is treated as the authoritative one. If another change arrives after it but was based on an older version of the layout, the server declines that write rather than letting it overwrite your work. The ring toggle also carries only its own fields, so it can no longer pull stale tiles along with it.
- The two saves take turns instead of racing. While a layout save is in flight the score ring controls are held, and while a ring change is saving the Save button waits, so the two writes can no longer overlap on the server.
- A change underneath you reloads instead of getting lost. In the rare case where another tab saved first, the layout refetches from the server and a small note asks you to make your change again against the fresh copy.
Refs #581.
[1.32.15] — 2026-07-24
Your Polar training sessions now sync into HealthLog, alongside the recovery, sleep, and daily activity that Polar already brought in. Thanks to @StefanIndustries, whose report asked for this after the vitals sync landed.
- Polar exercises arrive as workouts. Every run, ride, swim, or gym session you upload to Polar Flow shows up in your workout history with its sport, start time, duration, distance, calories, and heart rate. The raw Polar sport label and training load ride along in the background so nothing is lost. The sync reads your last 30 days of uploads once an hour, so a session synced late from your watch is still caught.
- A Polar session does not double up with your other sources. If the same workout also reaches HealthLog through Apple Health or another connection, the two are matched and only one is shown. A Polar watch counts as a direct heart-rate recording, so it ranks above a Strava re-upload of the same run but below your primary on-wrist wearables. You can change that order under Settings, Sources.
- The two Polar halves stay independent. A hiccup fetching your training sessions no longer holds back the recovery and sleep sync, and the reverse holds too, so a problem on one side never masks the other.
[1.32.14] — 2026-07-24
When the Coach removes a number it could not check against your data, the reply now reads clearly and tells you what happened.
- A withheld figure reads as a quiet omission. The Coach checks every number
in its answer against the figures it was actually shown, and holds back any it
cannot verify so a drifted value never reaches you as if it were real. That
held-back number used to show up as an internal
[unverified]marker in the middle of the sentence. It now appears as a short editorial mark, and a small muted line under the message tells you that some figures could not be checked and were left out. The line is worded for each of the six languages and stays out of the way until there is something to say. - Coach action cards and the tool trace survive a reload. A suggested reminder, a confirm-to-create action card, and the "what I looked at" tool trace were saved with the message but dropped when the conversation was reopened, so they quietly vanished on reload. They are now restored with the rest of the message, so reopening a thread shows the same cards and trace it showed the first time.
[1.32.13] — 2026-07-24
An Insights assessment card no longer shows the raw model output when a response comes back incomplete. If the underlying text was cut off, the card now falls back to its computed summary instead of printing the unfinished data.
- A cut-off assessment stays hidden. The per-metric assessment cards, like the one for resting pulse, ask the model for a small structured reply and then read the summary out of it. When that reply arrived truncated, the reader could see the whole raw reply on the card, including the property name, the braces, the quotes, and the escaped line breaks. The card now recognises an incomplete or malformed reply and serves the deterministic summary it already falls back to when no model text is available, so the raw output never reaches you.
- Plain-text assessments are untouched. A model that answers in an ordinary sentence rather than the structured shape is still shown as written, so this change only affects broken replies and leaves normal ones exactly as before.
- The other cards behave the same way. Medication compliance and the derived-score assessments read their summary the same way, so they now hold back a broken reply too rather than persisting it as the day's text.
Thanks to @lutzkind for reporting this one.
[1.32.12] — 2026-07-24
A mood you log late in the evening now lands on the day you logged it, in your own timezone, both on the dashboard and in the Coach's mood view.
- Your late-night mood stays on the right day. The pre-computed mood history used to file each entry by its UTC date, so a mood logged after dark in a timezone that runs ahead of or behind UTC could jump to the neighbouring day once the nightly rebuild ran. Every mood now files under the calendar day it was logged in your own timezone, matching the raw entry list.
- The dashboard and the Coach agree. The daily mood series behind the dashboard tile and the Coach's mood view now read the same day for an entry as the entry list does, so a value no longer shifts by a day when the faster pre-computed path takes over from the live one.
- The correction applies itself. On upgrade the pre-computed mood history is cleared and rebuilt under the corrected keying. While it rebuilds, mood reads fall back to the live calculation, which was already correct, so there is no window where the wrong day is shown.
[1.32.11] — 2026-07-24
Signing in to the iOS app on a self-hosted domain now runs through your own web login, so saved passwords and passkeys behave the way they should.
- Sign in through your own domain. On a self-hosted instance the app now opens your instance's real web login page inside a secure in-app browser. Because the login happens on your own origin, your password manager files the credentials under the right site and passkeys can run at all, which the older native form could not do on a custom domain.
- A single-use handoff, not a token in a link. Once you have logged in, the server hands the app a one-time code that lives for about ninety seconds and can be redeemed only once. The access and refresh tokens never ride a URL. The whole exchange mirrors the existing native single sign-on handoff and reuses its protections, including the per-request PKCE binding.
- The login has to happen inside the flow. The app receives a code only when you actually authenticate during that browser session. A login left open in the background from before cannot complete the handoff by itself, so a stray link can never quietly sign the app in as you.
- Your second factor still applies. An account with an authenticator app or a security key completes that step on the web page before any code is issued, exactly as it does everywhere else.
[1.32.9] — 2026-07-23
The Coach's numeric verifier now remembers the numbers you were actually shown across a whole conversation, so figures you legitimately carry forward stop getting marked unverified while an invented one still gets caught.
- A figure from earlier in the chat is no longer flagged when you bring it back up. It now keeps a running record of every number the Coach was given this turn and in earlier turns, plus your goal figures, your medication doses, and the reference bands it was shown. A value it can trace to one of those is left alone even a few turns later. A value the Coach only wrote in an earlier reply is never trusted on that basis, so a number that once slipped through cannot quietly become authoritative later.
- An educational sentence keeps its numbers. A line that states a general guideline, like the note that adults usually need seven to nine hours of sleep, is recognised as a reference and left intact instead of having its numbers stripped. This works in every supported language.
- A dose you are actually on reads as a restatement, a different one does not. When the Coach says to keep taking a dose, the reminder is allowed only when the amount matches a dose on your schedule. A wrong maintenance amount, or any wording that nudges the dose up or down, is still held back.
- Fabricated risk numbers are caught in more languages. The screen that blocks an invented clinical risk figure or a named risk-engine result now reads French, Spanish, Italian, and Polish to the same depth it already read English and German, including a percentage written out in words.
[1.32.8] — 2026-07-23
Health records written through the app now carry where they came from, and a HealthKit sync can say what set it off.
- A dose logged from the app is recorded as an app entry. Every intake used to be stored as a web entry no matter how it arrived. The server now reads the transport it authenticated the call with and tags the dose to match, so a dose logged in the native app reads as an app entry while one logged in the browser stays a web entry. A reminder confirmation, an import, and an Apple Health mirror keep their own labels. The value is derived on the server and can never be set from the request body.
- The dose history shows how each dose was recorded. The per-dose history returns a source label on every intake, so a client can show at a glance whether a dose came from the web, the app, a reminder, an import, or an Apple Health mirror. Rows written before the label existed report as unknown rather than guessing.
- A HealthKit sync can report what woke it. The batch upload accepts an optional trigger tag reading foreground, background, or push. It is recorded for diagnostics only and never changes which readings are stored or how they are deduplicated, so sending it or leaving it off produces the same result.
[1.32.7] — 2026-07-23
The Coach's numeric verifier now reads numbers the way you actually write them, so a correct figure stops getting marked unverified while an invented one still gets caught.
- A number you reformatted is no longer flagged. It recognises a rounded restatement, a delta narrated as a drop, an adherence rate written as a percent, minutes read back as hours, a reformatted date, and a thousands-separated step count, each checked against the figure the server actually gave the Coach. A drifted number, like an average of 128 quoted back as 138, is still caught.
- A blood-pressure claim can no longer borrow a weight number. Every figure is matched inside its own unit family, and a raw reading never earns the looser rounding reserved for a headline average, so a fabricated average cannot slip through on a nearby sample.
- A correct clinical-risk answer streams through untouched. Naming ASCVD or a ten-year risk to explain that a clinician computes it now passes. What still blocks is an asserted figure or a categorical verdict, whether it carries digits ("your risk is about 14%"), spelled-out words ("roughly twelve percent"), or no number at all ("SCORE2 would put you in the high-risk band").
- A dose you already take reads as support. "Keep taking your 7.5 mg as prescribed" passes, while anything that moves the dose stays blocked.
- A figure removed as unverifiable leaves the sentence intact. The strip touches only the exact token, so a year like 2023 is never clipped by a flagged 23.
Refs #587, #591.
[1.32.6] — 2026-07-23
Hardening and data-integrity fixes that were reviewed earlier and are now folded into the trunk.
- Outbound Web Push dials through the protected network path. A push endpoint now goes through the same safeFetch wrapper the rest of the app uses, so an operator-configured endpoint cannot be pointed at an internal address.
- A bulk mood edit saves all at once. The mood rows and their tag links are written in one transaction, so a failure part way through no longer leaves a partial save.
- Nutrient entries are bounded on write and included in the backup. Values are range-checked when saved, and the per-day nutrient records now travel with the full backup and restore.
- Every response path carries the baseline security headers, including the early returns that used to skip them.
- Provider re-sync tolerates an empty token-refresh body. Oura, Polar, and Fitbit treat an empty body as transient rather than a hard failure, so a reconnect is not lost to a blank response.
- The Coach reply stream aborts cleanly when you navigate away, and the daily metric series carries its own min and max.
- Also folded in: the WebAuthn relying-party origin gating (localhost only in development), the medication-compliance point-window bound, and a request-time future-date bound on the medication-intake import.
No migrations. No breaking changes.
[1.32.5] — 2026-07-23
- Browser SSO sign-in no longer fails on a duplicated callback. With the service worker's navigation preload enabled, a single-use OIDC callback could be requested twice. The first request redeemed the authorization code and set the session. The second hit the identity provider with the now-consumed code and failed, so every successful sign-in landed on an error page. The service worker now settles one-shot auth navigations itself, so the callback reaches the server exactly once. The callback is also idempotent: a duplicate that already holds a database-valid session is sent into the app instead of erroring, while a forged or session-less state still fails closed and never redeems a code.
No migrations. No breaking changes.
Thanks to @doenke (#600), whose report pinned the cause (service worker navigation preload plus the single-use OIDC code) down to the exact mechanism, so the fix went in as reported.
[1.32.4] — 2026-07-23
- The Coach's numeric verifier stops flagging figures it was actually given.
A restated difference, average, or percent change computed from two of the
numbers the model was shown, a sample count restated from the data inventory
that rides every turn, a calendar-date ordinal ("July 21st"), and a
numbered-list marker no longer trip the inline
[unverified]rewrite that was appearing in place of correct values. A genuinely fabricated figure, one with no grounding at all, is still caught and stripped.
No migrations. No breaking changes.
[1.32.3] — 2026-07-23
Security release. It ships on its own so the change to how a second factor is proven can be watched in isolation.
- A native client can manage its own second factor. The iOS app can now turn two-factor authentication on or off, add or remove a security key, and regenerate recovery codes over its API session. Those actions were reachable only from a browser before. It does so through a short-lived, single-use elevation the app earns by re-proving a factor, never by presenting its token alone.
- The re-proved factor decides what the elevation reaches. Re-proving with a password reaches exactly what a plain browser session reaches. The actions a browser gates behind a completed second factor now require the same over the API: a re-proved TOTP code or security key, never the password alone. Administrative endpoints remain reachable only from a browser session, unchanged.
One migration (0267), additive. Addresses the native 2FA-management gap (healthlog-iOS #57).
[1.32.2] — 2026-07-23
Security release. Ships on its own and ahead of everything queued, because it closes a set of freshly disclosed framework vulnerabilities that affect the running instance today.
- Next.js updated to 16.2.11, closing four high-severity advisories in the App Router: a middleware/proxy bypass, two server-side request-forgery paths (rewrites and Server Actions), and a denial of service. The middleware/proxy advisory is the most relevant here, since the request edge enforces the public-path allowlist and the authentication redirect. No application code changed; this is the dependency patch alone.
Carries the fixes already merged for 1.32.1 (honest health-score absence, Coach score questions, Apple Health import and Pulse fidelity, three silent data-loss races), since those reached the trunk first. No migrations beyond what 1.32.1 declared (none). No breaking changes.
[1.32.1] — 2026-07-23
- The Personal Health Score reads "not enough data yet" instead of zero. An account with records that do not yet meet any score pillar's minimum (for example a single mood entry, no paired blood pressure, one weight reading) showed a red 0 rather than an honest absence. The score is now withheld until at least one pillar is computable; a genuinely computed 0 is still shown.
- The Coach stops refusing ordinary questions about your own scores. A question about a Sleep Score or Health Score no longer trips the block on a fabricated clinical risk; it now matches the specific risk calculator it was meant to catch, not the plain word "score", and still blocks a genuinely invented risk figure.
- The Apple Health import no longer stalls at "unpacking". A large export is now unpacked as a stream rather than decompressed whole into memory on the shared event loop, and a job left mid-unpack by a restart is picked up by a periodic reconcile instead of sitting stuck with no error. A byte ceiling still guards against a decompression bomb.
- The Pulse insights page shows pulse, not resting heart rate. It no longer silently swaps its main chart to resting heart rate when any resting data exists; resting heart rate appears as a clearly labelled second series when present, and the resting-calibrated target band no longer paints behind raw pulse readings.
- Dashboard layout changes can no longer be reverted by a concurrent score-ring save. The instant score-ring toggle now sends only the ring fields instead of resending a cached snapshot of the whole layout, closing a race where an in-flight ring update could land after a tile or chart Save and silently restore the older layout.
- Measurement-reminder cadence edits recompute against the cadence that was actually saved. Clearing an interval or a recurrence rule to switch a reminder's schedule no longer leaves the next-due date computed from the just-replaced value for one cycle.
- ntfy notification settings keep a saved auth token across unrelated edits. Toggling the channel, or editing the server URL or topic, no longer silently clears a previously configured token.
No migrations. No breaking changes.
Thanks to @lutzkind (health-score zero-state, the Coach refusing score questions, the dashboard layout race, the Pulse page) and @Nazza01 (the Apple Health import stall). These fixes came straight from their reports.
[1.32.0] — 2026-07-22
-
Provider ingestion and daily reactions now preserve the correct identity. Arrival reactions are bound to the exact marker revision, workout selection matches field donors to the correct surviving session, long-range buckets honor the user's timezone, MCP pages stay stable, and one Withings subscription repair can no longer block fallback polling for other users.
-
Large reads and writes are bounded end to end. Measurement exports stream in pages without splitting sleep sessions, oversized projections cannot evict the shared cache, dense reports and metric series use database aggregation, upload reads have byte, time, and concurrency limits, full-history backfills share admission control, and medication imports use durable chunks with resumable, bounded final rollups.
-
Capture and history flows are complete on mobile and desktop. Workout history loads beyond the first page, metric pages open preselected capture forms and return to their origin, failed water submissions retain their draft, and confirmed discards clear it.
-
Interactive forms share one safe dismissal rule. Dirty forms require confirmation across tracked Back and Forward traversals, Apple Health setup exposes successful native syncs, document selection is consumed on Coach dismissal but preserved across full-page Coach navigation, conversation titles can be renamed with rollback, and hidden tabs refresh meaningful server state.
-
Accessibility coverage now exercises representative authenticated routes, themes, dialogs, and sheets. Small text keeps compliant contrast, OCR uses a visible labeled control, workout sections use real headings, and compact actions retain 44-pixel touch targets.
-
Client and API boundaries are explicit. Localized stable error codes cover measurement, mood, lab, and onboarding failures; client fetches and query keys are lint-enforced; AI overrides are documented; and large Coach, report, target, and provider modules now use one-way focused builders.
-
Production image processing uses the patched Sharp 0.35 runtime. This closes the current high-severity libvips advisory chain inherited through Next.js while retaining verified production-build compatibility.
Migrations 0264_whoop_owner_identity, 0265_withings_subscription_state,
and 0266_medication_intake_import_jobs. No breaking changes.
[1.31.8] — 2026-07-21
- MCP search cursors keep lab results stable across pages. Distinct analyte
names are ordered before offset pagination, preventing repeated or skipped
lab entries when a client follows
nextCursor. - Opening the medication wizard no longer risks clearing entered fields. The one-shot URL flag is removed without starting a second page navigation, so slower browsers keep the active wizard instance and its form state.
No migrations. No breaking changes.
[1.31.7] — 2026-07-20
- Current OpenAI reasoning models use the request fields they support.
Official GPT-5, o1, o3, and o4 models receive
max_completion_tokenswithout unsupportedtemperatureorseedfields; compatible custom gateways retain the legacy parameters they advertise. - Apple Health XML cumulative totals no longer add overlapping sources. Imports aggregate each source/day independently and select the highest source subtotal, while recording where each figure came from prevents XML estimates or late legacy samples from overwriting authoritative native HealthKit statistics. Existing legacy aggregates can be repaired by re-uploading their archive.
- Every currently actionable medication reaches Today. Equal-time doses no longer collapse to one row, overdue doses win deterministic ties, and weekly or custom intake windows use the scheduling engine's real availability boundary. The shared digest fixes both the web start page and iOS Home.
Migration 0263_apple_health_aggregate_authority. No breaking changes.
[1.31.6] — 2026-07-21
- The hardened runtime image can run database migrations again. The
isolated Prisma CLI now exposes its pinned
dotenvandprisma/configdependencies to the copied production configuration while package managers remain absent from the final image.
No migrations. No breaking changes.
[1.31.5] — 2026-07-21
- The production image no longer ships build-time package managers. pnpm 11 verifies the lockfile under explicit build-script policy, npm and Corepack are removed after migrations are prepared, and linked worktrees stay out of local Docker build contexts.
No migrations. No breaking changes.
[1.31.4] — 2026-07-21
- Web and background processes now start and recover predictably. Invalid encryption-key configuration fails before the service accepts traffic, worker-only processes report their real health, connection limits are divided safely across replicas, and provider backfills keep their bounded queues during reconnects.
- Published images are reproducible and verified before promotion. The release pipeline builds one multi-architecture image, validates its digest, startup, migrations, health, and reported version, and only then promotes that exact artifact to production.
- CI now uses isolated, deterministic test environments. Integration tests receive their own database and cryptographic settings before application code loads, worktrees install hooks consistently, and browser tests use the intended application process on every platform.
- Concurrent iOS registrations no longer fail intermittently. Device and APNs notification-channel reconciliation now share one database lock and transaction, so simultaneous first registration remains idempotent.
No migrations. No breaking changes.
[1.31.3] — 2026-07-20
- Document summaries now stay in the language you chose. The same account language is used whether a summary is created in the background or requested from the document sheet. A saved summary remains visible and can be replaced with Generate again after you switch languages.
- The Coach composer has more room on phones. The writing field now gets its own line, with document and conversation actions below it and Send on the right. The unreliable browser dictation button and its dead-end permission states are gone.
No migrations. No breaking changes.
[1.31.2] — 2026-07-20
- Provider imports now commit identity and progress atomically. Withings, WHOOP, Oura, Polar, Strava, and Apple Health reconcile external and natural measurement identities under database locks, preserve tombstones, emit arrivals only for genuine inserts, and advance cursors only after durable writes. OAuth refresh-token rotation is serialized per user and provider.
- Webhook and import success now means the source data is durable. moodLog writes before acknowledging delivery, while Withings ECG callbacks enter a durable queue with bounded retry and replay-safe processing. WHOOP backfills commit bounded chunks without advancing failed resource cursors, and legacy mood imports use deterministic, collision-free retry identities. Operational JSON import failures are reported as failures instead of misleading duplicate skips.
- Recovery is complete, exact, and owner-safe. Canonical backups round-trip every supported record class, including encrypted document content, measurement reconciliation identities, and tombstones. Restore rejects account mismatches before writing, validates backup enums at the boundary, processes stable measurement IDs in bounded batches, and preserves recovery integrity across both uploaded and off-host payloads.
- Device and mood edits no longer lose related state. Same-user APNs registrations merge transactionally without crossing account boundaries, refresh-token bindings follow the surviving device, and mood edits preserve rated factors unless the caller explicitly replaces them.
- Polar sync follows the current AccessLink collection API. Collection requests no longer include the obsolete user ID path segment, and current activity, Nightly Recharge, cardio-load, sleep, and SpO2 response shapes are normalized without losing stable measurement identity.
- The July dependency bundle is included. PDF.js 6 now releases its loading task correctly; Canvas 1.0, Radix UI 1.6.2, Tailwind CSS 4.3.3, the Tailwind Prettier plugin 0.8.1, and setup-node 7 are integrated.
No migrations. No breaking changes.
[1.31.1] — 2026-07-19
- Arrival events now follow committed inserts across every writer. Provider re-sync updates stay silent, partial Apple Health imports retain arrivals from earlier committed batches, all newly inserted workouts emit, and sleep refreshes distinguish new stages from updates. Web-only processes now retry their bounded send-only queue connection instead of permanently losing arrival jobs after one transient startup failure.
- Reaction and workout generation are single-spend operations. Durable database claims close worker races and provider-retry gaps, daily token reservations are atomic and reconciled on every terminal path, and both generation and display honor the user's Insights and Workouts module choices plus server-managed consent.
- AI context is narrower and more accurate. Arrival reactions use the record that actually landed, completed sleep uses the reconstructed night rather than one stage segment, user-authored lab fields are fenced as data, workout comparisons use one agreed session and date, and every metric and derived-score coach entry carries its exact supported scope.
- Dashboard state survives old and partial clients. Saved comparison range points and newly supported metric widgets are preserved, while Today freshness remains aligned across local-day boundaries, DST, tab focus, and split web/worker deployments.
Three migrations (0260, 0261, 0262). No breaking changes.
[1.31.0] — "The record that reacts" — 2026-07-19
A milestone release: the record now visibly responds when something lands, instead of waiting for a daily rollup to notice.
- Today shows what just came in. A completed night's sleep, a workout, a first weigh-in of the day — the home screen now carries a calm "just in" note for a few hours after each one, and at most one written line a day per kind, grounded in your own numbers. Nothing changes if you run without an AI provider: the sentence is the garnish, not the feature — the arrival itself still appears, including on an otherwise-empty record, and an open dashboard refreshes it as soon as the tab becomes visible again. The underlying change that makes this possible without new polling or a flood of background work: every write path that can produce a genuinely new reading now emits a single typed event, day-scoped and de-duplicated, that a mass import or a provider re-sync produces none of — a ten-year import or a month of catch-up sync stays silent, by construction, not by luck.
- A finished workout gets its own paragraph. Opening a recent session now shows a short, grounded read of that one activity — duration, effort, where it sits against your own recent sessions for the sport — with no training advice and no target zones. It is generated once, the first time you open a fresh session, never on re-sync and never for older workouts already in your record.
- "Ask why" reaches every read-only card. Every per-metric status card, every derived-score page, and a finished workout now carries a way straight into the coach, pre-loaded with just that card's own data so the conversation starts already grounded — not with a blank prompt.
- The lab-marker card and the offline metric texts speak with the same voice as everything else — meaning first, in your own language across all six locales, checked by an automated tone pass that now runs on every change to these surfaces so this kind of drift gets caught before it ships again.
- The dashboard layout stops discarding a comparison choice on save, and four clinical widgets stop disappearing from a saved layout — both from a save made by a client that does not know those particular fields yet.
- Mood and BMI reach the app's home screen, and a medication pen can now carry the maker and printed strength that show up in the native pen list.
- Every new AI call this release can cause is metered and bounded: a daily cap per kind, a duplicate-content check that makes a re-post free, and a token reservation that is always reconciled, including on every failure path — the same discipline the rest of the AI surface already holds to.
Two migrations this release (0257, 0259); an additional dashboard-widget migration carried over from the prior line. No breaking changes.
[1.30.35] — 2026-07-19
- The metric texts you see without an AI provider now speak your language. They shipped in German and English only; French, Spanish, Italian and Polish fell through to English. These are what nine metric cards render whenever no provider is reachable — no key configured, an outage, a spent budget — so for those readers this text was the app's voice, in the wrong language. Written per language rather than translated: where a sentence could not be formed naturally, that language got its own sentence.
- The lab-marker card leads with what a value means, like every other card already did. It was the last one still instructed to state the number first.
- The response length for a metric assessment is set in one place again, instead of one card carrying its own inline pair.
- The fallback texts open on something honest rather than on an instruction, and the lab-marker card no longer shows the multi-metric overview tip on a single value.
No migrations. No breaking changes.
[1.30.34] — 2026-07-19
- Rearranging your dashboard no longer wipes the comparison baseline. A layout save from a client that does not know about that setting — the app never sends it — silently reset it to "none". Every field of the layout now has to declare whether an absent value means "clear it" or "leave it alone", so the next field added cannot repeat this.
- Four clinical widgets stopped disappearing from a saved layout. Pain, grip strength, waist and waist-to-height were dropped by the save as unknown, so a placed widget was lost every time.
- Mood and BMI reach the app's home screen. Both existed in the record but were absent from the summary the app reads, so those two tiles could not be shown while every other one worked.
- A pen carries its manufacturer and printed strength. Without them, containers entered on the web could not be shown in the app's pen list.
- Destructive controls ask first. Signing every other device out, revoking a session, and revoking a trusted device fired on a single tap. Two more had neither a confirmation nor an undo: a Coach reminder and a travel entry. Deleting a document still does not ask — it can be undone, which covers the mis-tap.
- Delete confirmations stop overclaiming. Four of them promised the deletion could not be undone while the same screen offered an undo. Warnings that are wrong make the ones that are right easier to ignore.
- The sleep-debt figure is described correctly again. The calculation changed to a running balance that credits surplus in v1.19.0; the label still said "cumulative shortfall". The wrong description had spread to the Coach, which was reasoning about the number on the old definition, and to the published API description.
- The glucose reference band can be chosen on the web. The setting already worked and already fed the Coach — nothing on the web set it. It selects a reference range; it is not a diagnosis and is never inferred from a reading.
One migration (0256), additive. No breaking changes.
[1.30.33] — 2026-07-19
Security release. It ships on its own because it changes how a browser session is recognised.
- The session cookie is a purpose-generated secret. It used to be the session record's own database key — an identifier built to avoid collisions, not to be unguessable, and one that appears wherever that record is referenced. The cookie now carries 32 bytes from the system's random source, and only its hash is stored, so the value in your browser exists nowhere on the server.
- You are not signed out by this. Sessions from before the change keep working and are simply not renewed, so they run out their normal 30 days and disappear. Every sign-in from this release onward is on the new scheme.
- The device list no longer hands out working credentials. It identified each device by that same record key — which, for a session predating this release, is that device's cookie. It now shows an opaque handle that is useless as a login, and the same handle is what signs a device out.
- A refresh token cannot be rotated from a different device, and an expired one is refused. The test that was supposed to prove the expiry passed a token that had never been issued, so it proved nothing: with the expiry check deleted, the suite still went green. It is a real test now.
- A Nightscout address is checked when you save it, not only when it happens to be probed first. The check itself already refused private, loopback and cloud-metadata addresses.
Operators: rotating API_TOKEN_HMAC_KEY now also signs out every browser
session, not just API tokens. One migration (0255), additive.
[1.30.32] — 2026-07-19
- A withdrawn AI consent stays withdrawn. Opening the AI settings granted consent again if you had taken it back, and recorded it as though you had done it yourself. A withdrawal is now a standing decision that only an explicit act can lift.
- The web can withdraw AI consent at all. Until now only the app could; on the web, consent could be given and never taken back. The AI settings show what currently stands and offer the control that changes it. Withdrawing takes effect immediately and deletes nothing you have recorded.
- Documents get read after you switch automatic reading on. Reading was only ever started at upload, so documents already in your vault stayed unread no matter what the setting said. Turning it on now works through what is already there, in batches, asking the same permission as any other read.
- A quoted passage in a document review is the document's own text. The quote shown for a finding was the wording that came back from the read, not the line it was taken from — so it could be a paraphrase of your own record presented as a verbatim quote. The passage is now looked up in the document itself, and a finding whose source cannot be located says so instead of showing a quote you cannot check.
- Pages that arrive with your data pre-loaded now say so on the wire. They were never cacheable in practice, but nothing stated it. Anything behind the sign-in wall is now marked private and uncacheable at the edge, so no proxy or future change can hold one person's record and hand it to someone else.
- Deleting your last reading of a kind no longer leaves stale weekly, monthly and yearly figures. The daily figure went; the longer spans kept the old aggregate indefinitely.
- The documented API matches what the endpoints return. Twenty corrections, including a total that the measurements list reports in a different place than the specification claimed, four medication figures named wrongly, a missing intake source, and eleven error codes that were never written down.
- A retry after an unusable AI answer asks for the right shape. The correction prompt demanded fields that do not exist and named none of the ones actually required, so the one corrective attempt steered away from the right shape instead of towards it.
No migrations. No breaking changes.
[1.30.31] — 2026-07-19
- Today shows a dose before it is late. The card only surfaced a medication once it was already overdue, so it could report nothing needing attention while a dose was due. A due dose now reads as a calm note, an overdue one as a warning, and a day with everything taken still shows the all-clear. Reminders are unchanged — they still only fire once a dose is late.
- The daily briefing stops disappearing. The check that verifies every figure against your own data was looking in fewer places than the briefing draws from, so a briefing quoting your medication adherence or a change since last month was thrown away as unverifiable. It now checks against everything the briefing is built from. And when a figure genuinely cannot be verified, yesterday's briefing stays up instead of the section going blank.
- The "read the full briefing" link is gone from Today. Tapping the health-score ring already opens the same page.
- The close button on a document sits flush with the edge again.
- A document no longer says its summary is being generated forever. Summaries are only started when a document is uploaded while automatic reading is on — there is no catch-up for documents you already had, so the view promised a summary that was never coming, and hid the button that would have made one. It now says what actually happened — not attempted, unavailable, or held back — and offers to generate one in every case. A summary you generate that way is kept.
- Removed the "read by AI" note at the bottom of a document.
One migration (0254). No breaking changes.
[1.30.30] — 2026-07-19
Four languages get their own words; a failed read stops claiming you have no data.
- Reminder notifications, the assistant's reply language, chart and calendar labels, and durations were English or German for French, Spanish, Italian and Polish. The translations existed; the code that picked between them only knew two languages. It now uses the account's own.
- Polish counts correctly. Polish has a third plural form that a two-form scheme cannot express, so counted phrases read wrong. The other five languages resolve exactly as before.
- A screening history that failed to load said "not taken yet". On a mental-health surface that is a false statement about your own record — you could reasonably conclude you never completed something you did complete. That surface, and a nutrient card with the same flaw, now say the reading failed and offer to try again. Nothing retries silently: a failure you cannot see is worse than one you can.
- Three sleep-rhythm cards that share one reading each printed the same failure notice; the page now shows it once.
[1.30.29] — 2026-07-19
- A connector token that may only read is now refused at the write itself, not only by never being offered the write. The protection was real but rested entirely on how the connection is assembled, so a future change to that assembly could have opened writing with no test noticing. Any write added later inherits the check.
- Names and text read out of your records now carry an explicit boundary when handed to a connected assistant, matching what the app already does for its own assistant.
- Reading a medication list from a photo measured its cost against the server-wide limit even when you use your own key. It now uses your own limit. The older way of counting that caused this — the third surface to have it — is removed entirely rather than fixed in place, so nothing can reach for it again.
- Removed a module that read as a check on generated recommendations but had not run for some time. It could not simply be switched on: it required fields the current format does not have, so enabling it would have rejected nearly every generation. The measurement it fed is unaffected and still runs.
No breaking changes.
[1.30.28] — 2026-07-19
- Daily briefings count against the AI budget. They were the one remaining kind of generated text that did not, so a morning's briefings could run past the limit you set. On your own key they count against your limit, not the server's.
[1.30.27] — 2026-07-19
- The resting heart-rate chart no longer stops partway. Older readings get condensed after a while, and the chart treated the condensed days as the whole story — so recent days went missing. It now shows both, and says when a day is an estimate.
- Summaries recompute for the whole day or month. Recomputing from a point in the middle replaced the full period with a partial figure. Affected summaries correct themselves the next time that day is written to, or on the next nightly pass.
- A repeated daily total in one upload keeps the newer value. It kept the older one.
No breaking changes.
[1.30.26] — 2026-07-19
- Text read out of your documents is treated as data. Lab field names were passed to the assistant unchecked, so a document whose labels were written to look like instructions could influence later answers. Those names are cleaned now, and document text is marked as yours rather than as something addressed to the assistant.
- A reminder the assistant picks up from a chat is proposed, not active. It waits for you to confirm it, like its other suggestions. Reminders from before this release keep working unchanged.
No breaking changes.
[1.30.25] — 2026-07-19
- Generated cards count against the AI budget. Metric cards, biomarker cards, scores and the period narrative were not counted at all. On your own key they count against your limit, not the server's.
- Switching language no longer regenerates everything twice. The same card in six languages counted as six separate jobs.
- Long assistant conversations get cheaper again. Past a certain length the full context was re-sent on every turn instead of once.
No breaking changes.
[1.30.24] — 2026-07-19
- The safety check on generated text covers every card, in every language. It ran on the assistant's replies but not on the metric cards, biomarker cards or document summaries — so a sentence the assistant would refuse to send could still appear on a card. It also only knew English and German wording, leaving four languages unprotected. Both are fixed.
- A document transcription is deliberately never filtered: it reproduces your own document, and a letter from your prescriber is a record, not advice from us.
No breaking changes.
[1.30.23] — 2026-07-19
- Switching a module off now also applies to connected assistants. With a module off, a connected assistant could still read that data — including the full doctor summary — and so could a shared clinician link. Both now honour the switch.
No breaking changes.
[1.30.22] — 2026-07-19
- Background jobs stop piling up on themselves. Duplicate jobs were meant to be collapsed but were not, so a large sync could queue thousands of identical recomputations and a restart could re-queue a running history import. Existing installations are updated on the next start.
No breaking changes.
[1.30.21] — 2026-07-19
- Dose reminders land on the right hour across a clock change. On the day the clocks change, a reminder could resolve an hour off, and the same dose could end up recorded twice. Nothing stored changed.
- The next-dose day label was wrong when your phone's timezone differed from the one in your profile.
No breaking changes.
[1.30.20] — 2026-07-19
- A restore refuses when the backup file names a different account than the backup you selected.
No breaking changes.
[1.30.19] — 2026-07-19
- Switching a module off now holds everywhere. The sync feed still sent cycle data, the FHIR export still returned the full record, and the dashboard summary still built glucose and sleep cards regardless of the switch. Nothing is deleted — turning a module back on finds the history complete.
- Two assistant features sent data out without checking your consent first. Both now check, and fall back to their plain text when consent is absent.
No breaking changes. The native app may see a 403 from the FHIR endpoints where a module is off.
[1.30.18] — 2026-07-19
- Health data no longer reaches the logs or the backups. Cached responses are encrypted at rest — they echo what was just saved, so cycle notes and mood text sat unencrypted in every backup for a day. Diagnostic log entries are now scrubbed where they are written, and a failed AI request no longer records what came back with it.
Log lines and backups written before this release may still contain those values. No breaking changes.
[1.30.17] — 2026-07-19
- API tokens enforce their scope. A token issued for medication intake reached every endpoint, including the full backup export. Routes that accept a narrow token now have to say so.
- Revoke and re-issue any token you gave to a third party. Treat it as having had that reach for its whole lifetime.
- The generic "create API token" button is gone. The token it made never worked for the medication intake it advertised, while reaching everything else. Tokens are issued per medication from that medication's own toggle. Existing tokens stay listed and revocable.
No breaking changes to the web app, the native client or the MCP connector.
[1.30.16] — 2026-07-19
- Resting heart rate stops being lost. Older heart-rate readings are condensed on a schedule, and a resting value is derived before the raw readings go. A faulty check stopped that derivation after the first run while the raw readings were still cleared, so each run lost another day's resting figure for good. Days already affected cannot be recovered; from here nothing further is lost.
No breaking changes.
[1.30.15] — 2026-07-18
- Assessments come in the language you set. French, Spanish, Italian and Polish accounts got German or English text instead of their own. Insight cards, the per-metric assessments, scores, the biomarker card and the period narrative now follow your language. German and English are unchanged. Stored texts in the four languages refresh on their next nightly run.
No breaking changes.
[1.30.14] — 2026-07-18
The workouts page joins the pages that paint on first load.
- The workouts list opens with the sessions already there. It was the last high-traffic surface still fetching its rows after the page mounted, so it showed a loading skeleton first and filled in a moment later. The list is now read on the server and handed to the page populated, the same way the dashboard, insights, medications, and coach pages already work. The list logic moved into a shared read so the page and the API endpoint go through one cached, deduplicated pass instead of two.
- API docs: the heart-rate bucket example matches what the server
accepts. The aggregated heart-rate
externalIdwas documented with an hourly example from its first version; the server has taken 10-minute buckets since v1.30.7. The description, the example timestamp, and the min/max field notes now describe what the server actually accepts.
No breaking changes.
[1.30.13] — 2026-07-18
More pages that paint on first load.
- The insights, medications, and coach pages open with their content already there, the same way the dashboard now does — their above-the-fold data is read on the server and handed to the page populated, instead of a shell that fills in after a second round of requests. Nothing that would trigger AI work is pre-run: the daily briefing and the coach's own reads stay on-demand as before.
No breaking changes.
[1.30.12] — 2026-07-18
Faster medication reads.
- The dashboard's medication section stays fast as your log grows. Its reads now use dedicated indexes instead of scanning the full intake history on every load, so a long history no longer slows down the daily view.
No breaking changes.
[1.30.11] — 2026-07-18
Native single sign-on.
- The iOS app can sign in through your identity provider (OIDC SSO). Until now, single sign-on only worked in the browser — the native app couldn't complete it, which locked native users out entirely on an SSO-only instance. The server now supports a native sign-in handoff (an in-app authorization flow that returns the app's normal token, never a cookie), so the app can offer SSO once it adopts the flow. Existing password and passkey sign-in are unchanged. Multi-factor sign-in still applies, and an SSO session never satisfies a step-up prompt — those still require your own second factor.
No breaking changes.
[1.30.10] — 2026-07-18
Localized insight text, and a richer workout view.
- Correlation insights now read in your language. The "this tends to go with that — a pattern worth watching, not a cause" lines under a metric were always written in English; they now render in your interface language (German, Spanish, French, Italian, Polish), with the association-not-causation framing preserved in each.
- A workout you open shows more of what was recorded. The detail view now draws the session's heart-rate curve over time, its route as a clean line, the time spent in each effort zone, per-kilometre splits, and how it compares to your own average for that sport — all from data already synced, with no new connection. Anything a given workout didn't record simply isn't shown.
No migration. No breaking changes.
[1.30.9] — 2026-07-18
Faster dashboard first load.
- The top card and the charts paint with their data on first load instead of flashing empty and then filling in. The daily summary and the charts' 31-day series are now read on the server and handed to the page already populated, so there's no wait for a second round of requests after the page appears. The welcome/summary card, likely the slowest thing to show before, now lands with the first paint.
No migration. No breaking changes.
[1.30.8] — 2026-07-18
Hardening for the new heart-rate upload validation.
- A malformed bucket timestamp is rejected, not silently misfiled. A bucket whose time rolled over (a 24:00, an impossible day) is now refused rather than stored under a non-standard key, where it would have failed to merge with a re-post and could land on the wrong day.
- The per-window low/high is sanity-checked before it counts. A window's low and high are kept only when they are in a plausible range and sit either side of the average; a glitched reading no longer skews the day's high/low band. The average is always kept.
No migration. No breaking changes.
[1.30.7] — 2026-07-18
Groundwork for a tidier heart-rate history.
- The server accepts a 10-minute aggregated heart-rate upload. A watch records heart rate every few seconds, and uploading one row per raw sample fills the measurements list with the same value repeated many times a minute. The server now takes one row per 10-minute window — its average, with the low and high of the window — and the within-day pulse curve reads it directly, so the detailed 10-minute chart and the elevated-at-rest note are preserved. The de-duplication reaches the list once the mobile app sends the aggregated shape; existing per-sample history is unaffected and reconciles itself over time.
- The daily high/low reflects the true spread. A day built from 10-minute windows now reports the real intra-window extremes, not the range of the averages.
No migration. No breaking changes.
[1.30.6] — 2026-07-18
Accessibility pass.
- Heatmaps read to a screen reader. The mood and medication-compliance grids now expose each recorded day — its date and value — through a hidden day list, so the per-day figures are no longer reachable by pointer only. The chart looks exactly the same.
- Small labels meet the minimum size. Muted meta text across settings, admin, the coach panel, and the insights cards is lifted off the sub-
text-xssizes; tabular figures and chart chrome are unchanged. - A couple of section headings. Two metric-page sections became proper headings, so heading navigation lands where it should.
No migration. No breaking changes.
[1.30.5] — 2026-07-18
Making a shipped feature findable, and a sync-status fix.
- ECG has a home you can reach. Recordings now have their own page in Insights, a pill in the Heart group that appears once you have a recording, and links to it from the resting-pulse and heart-rate-variability pages. Before this they were reachable from a single spot, and a deep link into the overview could land before the section had drawn. The page is unchanged in substance — the same non-diagnostic, device-attributed view.
- The "last synced" time stops lagging. A sync driven by an incoming update now records the connection's last-success time straight away, instead of leaving it frozen until the next full poll — so the Settings status and the assistant's integration read reflect a sync that already happened.
No migration. No breaking changes.
[1.30.4] — 2026-07-18
The read-only assistant connector now reaches the full record.
- More of your data is readable. The connector gained direct reads for nutrition and vitamins, the intraday pulse curve, and stored ECG recordings, plus discovery for a set of device metrics that were collected but not advertised — wrist temperature, cardio recovery and load, day and workout strain, sleep score, breathing disturbances, fall count, the six-minute-walk distance, stair speeds, and energy expenditure. They can now be compared and baselined by name.
- Correctness. Heart-rate variability resolves whether the source records the SDNN or the RMSSD flavour, so a ring- or strap-only account is no longer reported as having none. A workout logged by two devices is counted once, matching the workouts list — this also corrects the figure the coach sees.
- Exposure stays deliberate. The mental-health screeners and the environmental signals remain off the connector by construction; a metric is only readable once it has been named for it.
No migration. No breaking changes.
[1.30.3] — 2026-07-18
Follow-up fixes from a full quality re-check of the app.
- Correctness reaches every surface. The timezone, window, and grain fixes now also cover the coach's correlations, the per-metric cards, and the period narrative (they had kept an older copy). Daily insight cards roll over at your own midnight, not a fixed one. Personal records that a second device had inflated are re-derived on the next start, so a genuine record can be set again.
- The metrics catalog shows a loading state and a retry on error, instead of briefly reading as "not tracked".
- Small. One shared info popover, larger tap targets on the view toggles, a notch-safe height fix on the coach history page, and a few wording and translation gaps closed (a body-composition caveat, study citations, the backup scope note).
No breaking changes.
[1.30.2] — 2026-07-18
- Coach history is fully reachable. The conversation list pages through your whole history and search runs on the server (by title), instead of only the first ~20 loaded chats.
- Navigation and flows. A document links to the lab values it produced; the two "Recovery" pages are named distinctly and cross-linked; mood, mental wellbeing, and mood insights link to each other; the nutrients card moved out of "Source priority"; the onboarding checklist and tour no longer point at dead ends; asking the coach from a metric or a plan carries that context in.
- Correctness. Correlations, the intraday chart, achievements, and the nutrients overview now bucket days by your own timezone and lead with the most recent data; a multi-source day is no longer counted twice.
- Performance. The workouts list caches its de-duplication per filter, so paging through a long history is quick.
No migration. No breaking changes.
[1.30.1] — 2026-07-18
Polish and small fixes.
- Mobile. Coach and document text wraps instead of overflowing; the intake history is a card list on small screens instead of a sideways-scrolling table; tap targets meet the minimum size; two dialogs became bottom sheets; the workout detail header stacks cleanly.
- Feedback. Loading skeletons match the content that replaces them (no more layout jump), and the Today priority-card actions show a pending state and can’t fire twice on a double-tap.
- Remembered choices. A chart’s time range and the measurement type you last logged are remembered; a coach chat deletes with an Undo; a backdated dose can be undone; lab panels get “Save & add another”; pull-to-refresh works on more pages (and a scroll-position bug that made it fire anywhere is fixed).
- Consistency, accessibility, wording. Spacing and contrast tidied, more empty states use the shared component, headings and focus rings improved, a few German phrasings corrected, the daily line reads more calmly, and several explanations gained their guideline citation.
No migration. No breaking changes.
[1.30.0] — 2026-07-18 — Quality pass
A broad correctness, performance, and polish release, plus two new features. Nothing new is required of you; existing data is untouched.
Added
- Document summaries. An uploaded document can get a short plain-language summary, generated once at upload and stored encrypted. It runs only when the external-AI document reading you already opted into is on, and never touches older documents.
- Efficacy chart, readable. The medication efficacy chart scales its axis to the data (a real trend no longer looks flat), gains a “% since start” view, and the history tab reaches your full intake history, not just the last 90 days.
- A metric catalog. A quiet “All metrics” view lists everything HealthLog can track and which device supplies each — so a metric you don’t have data for stays out of your daily views but is still discoverable in one place.
Fixed — correctness
- The number shown matches reality. The workouts and measurements lists now lead with the most recent entries (a long history no longer hid recent days), and a metric recorded by two devices on the same day is counted once, matching the dashboard — instead of being summed twice.
- Strava workouts carry their real sport (cycling, running, …) — like WHOOP already did — with a history backfill, and back-to-back rides no longer collapse into one.
- Personal records and correlations no longer double-count multi-source days or mix per-reading and daily-total values.
Fixed — medical safety
- The hypoglycemia alert now uses your own glucose unit; the doctor-report PDF states your own timezone; the crisis card offers Austrian and Swiss lines, not only German ones; and resting-pulse labels drop clinical-verdict wording for calmer, neutral framing.
Fixed — the daily view
- The Today view is faster (its heaviest reads are now cached instead of recomputed on every refresh) and updates immediately after you log a measurement, mood, or water — not only medication.
- The intraday heart-rate chart no longer draws a continuous shape across gaps in sparse data, and shows how many readings it’s based on. A failed load shows a retry instead of an empty state.
Fixed — onboarding, dates, consistency, accessibility
- Connecting a device no longer dead-ends on a fresh account, and the setup steps (including the callback URL) are shown up front.
- A date from a previous year now shows the year, everywhere it’s displayed.
- Contrast, chart colours in the light theme, and a handful of duplicated components were tidied; keyboard access and headings improved on several surfaces; the insights and settings menus are grouped more sensibly.
Changed
- A fuller backup. The full backup now also includes lab results, illness episodes, allergies, family history, workouts, and a documents list. What isn’t included (raw document files, workout GPS/heart-rate detail) is stated plainly.
Performance
- The Today digest and the insights cards read through the pre-aggregated tier instead of scanning raw rows; batch imports write in one pass.
Two migrations (0250, 0251). No breaking changes.
[1.29.5] — 2026-07-17
Security
- Hardened the same-origin redirect check. A crafted relative post-login
nextpath (e.g./..//evil.com) passed the same-origin check but left a protocol-relative pathname that could re-resolve to a foreign origin when re-used in a redirect or client navigation. The check now re-verifies the reconstructed path resolves on-origin and rejects it otherwise. Covers the OIDC callback and the password/passkey login redirect.
Fixed
- OIDC login redirects use the operator app URL, not the request URL.
Behind a reverse proxy that does not forward the original host, the login
and callback routes built their browser redirects from the address the app
was reached at inside the container, sending users to an unreachable
internal URL after login. Every OIDC redirect is now built from the
operator-set
NEXT_PUBLIC_APP_URL, matching the convention the other integrations already use.
[1.29.4] — 2026-07-17
- The workouts list shows the most recent first. The list was ordered oldest-first, so on a longer history the newest sessions never made the first page. It now leads with the latest workout.
- Past-year dates show the year. A workout from a previous year now carries its year in the list, so a December date is never mistaken for this year's.
[1.29.3] — 2026-07-17
-
Hydration and micronutrients now have a home. Your fluid intake and the vitamins and minerals your device records get a surface. A water tile on the dashboard shows the day's total with quick-add amounts; a new Nutrients page under Insights shows hydration over the last month, caffeine when you record it, and the vitamins and minerals you actually have data for — each against its reference daily intake where that applies, shown as context, never as a verdict. Manual water entries and device-synced totals now coexist instead of overwriting each other. Sparse logging is never read as a deficiency.
The nutrients tracking stays opt-in — enable it in Settings, or with the one-tap prompt on the page. Device-synced micronutrients flow once your client sends them.
One migration (0249). No breaking changes.
[1.29.2] — 2026-07-17
- WHOOP workouts carry their real sport. WHOOP was the only source that didn't translate its activity into the app's own sport names, so WHOOP-tracked rides and runs landed unlabelled instead of as cycling or running. New and existing WHOOP workouts are now mapped, so your bike rides show up as cycling — history included. When the same session comes in from two sources, the surviving row now keeps the richest reading (heart rate, energy, distance) instead of dropping it.
- Scroll back through the day's heart rate. The intraday heart-rate view gains a day navigator to step through previous days. The fine-grained history now stays for 90 days (was 14); older days show the coarser hourly shape.
- Clear a card from Today. A milestone, a new ECG, or a tension note can be dismissed from the Today rail once you've seen it; the section tightens when the last one is gone. The cards that need an action stay until you act.
- Vitals context moves into an info tap. The vitals tiles carry an (i) with the context — your reading against your personal range, and against your age where that applies — instead of a permanent caption. The VO₂ max heading no longer gets cut off.
- Oura cycle phases feed the cycle tracker. Where your Oura connection provides cycle-phase data, it now fills the cycle tracker as a background source; your own entries always win.
Two migrations (0247, 0248). No breaking changes.
[1.29.1] — 2026-07-17
A round of fixes and small refinements from living with the daily view.
- Today refreshes right after a dose. Logging or undoing a medication now updates the day's read immediately — the medication line no longer lingers on a stale count until the next reload.
- A calmer Today hero. The extra score rings are gone; the hero keeps the one health-score ring and a tighter layout.
- The Coach names the document you attached. Asking about a document now shows its title in the chat instead of a plain "1 document" count, and the attach menu stays within its popover on long labels.
- A fresh install starts with tracking on. New installs now enable the tracking modules by default — you can still switch any of them off. The external-AI options and the daily push stay off until you turn them on.
- Insights vitals lose the explainer line. The "your personal normal range is the median…" caption is removed from the baseline tiles.
- Illness journal: set and edit the end. You can mark an episode's end date, clear it back to ongoing, and backdate the start. When nothing strayed from your personal range, the course section now reads a short settled line instead of appearing empty.
- The morning refresh covers every sleep source. The refresh that folds last night's sleep into the day now triggers from Google Health, Fitbit, Oura, and Polar as well, not only Withings, WHOOP, and Apple Health. Each skipped refresh is now recorded so a stale morning read can be traced to its cause.
No new data is collected and there is no migration.
[1.29.0] — 2026-07-17 — A daily health companion
This release draws a line under a run of work that turns HealthLog from a place you record your health into one that meets you each day — and it completes that picture with the pieces that make it feel whole.
Over the last releases the dashboard gained a Today view that leads with the day's read, a health score that refreshes the moment last night's sleep syncs, a coach check-in when a plan is due for a look-back, and an optional morning note. Documents came closer to hand — the Coach opens beside them, you can bring several into a conversation, and lab values move between a document and your labs. This release adds the layer that ties it together:
- Your heart's rhythm, in context. ECG recordings are no longer a page on their own — they're referenced where your resting heart rate and rhythm are discussed, always as the reading your device produced, never a reading of the app's own. A new recording surfaces gently in Today.
- The shape of your day. Heart rate is now drawn across the day, not just as a daily number — so you can see it settle and climb. When it stays elevated while you're at rest and not moving, Today notes it plainly as possible tension, carefully and never as a verdict.
- Quiet milestones. Reaching a steady stretch in your target range, or a new personal best, is marked with a calm moment — states you've reached, not streaks to keep, so there's nothing to break and nothing to nag.
- One piece throughout. The Today view brings back the health rings you choose, each surface carries its metric's own calm colour, and the whole thing reads as one design rather than a stack of features.
Nothing new generates when you open the app — every daily surface reads the insight prepared overnight. No new data is collected; this makes more of what's already yours.
Four changes bring documents and your record closer together:
- Ask the Coach opens beside your document. Asking about a document now opens the Coach in a side panel rather than jumping to a full page — expand it to full screen whenever you like, and it keeps its place.
- Bring documents into a Coach chat. From the composer's "+", attach one or more documents — pick from your vault or upload a new one, at the start or mid-conversation — and the Coach answers grounded in them. A conversation with a document runs in its fenced mode: grounded in those documents only, with no access to your health data or tools, so nothing in an uploaded file can reach anything it shouldn't.
- Lab values from a document, proposed for your labs. When a document you've had read looks like lab work, its values are detected and proposed — one tap to add them to your labs; they're never added without your say-so.
- A lab scan is also kept. Scanning a lab report into your labs now also files the document in your vault (encrypted, like every upload) and links the two, so the reading and its source stay together.
Migration 0245 (attaching multiple documents to a conversation) accompanies this release; existing document chats carry over unchanged.
Three pieces turn the Today view into a daily loop:
- A coach check-in. When a coach plan is due for a look-back, it surfaces as a calm card in Today with keep, adjust, or let-go — one tap. Letting go retires the plan quietly; silence never changes it.
- A morning that reflects last night. The score and read are prepared overnight, but last night's sleep usually hasn't synced by then. Now, the moment it arrives, the sleep-dependent parts refresh on their own — so by the time you look, the day is up to date. Until sleep is in, Today says so plainly instead of showing a stale score.
- An optional daily nudge. A once-a-day morning notification with the day's read, off by default and opt-in per channel under Notifications. It carries the refreshed digest when sleep is in, never repeats within a day, and is a calm note — never an alert.
All three read the digest prepared overnight; nothing is generated when you open the app.
With the Today view now leading the dashboard, the older opt-in hero card it replaced is retired, so the two never stack. The greeting stays, the tiles and charts are unchanged, and a layout stored with the old hero switched on simply ignores that setting — nothing to redo.
[1.28.53] — 2026-07-16 — A "Today" overview at the top of your dashboard
The dashboard opens with a Today view: your health score, the day's lead read, and a short "worth a look" list — a dose window that's open, an integration to reconnect, a check-up coming due — each a single tap to the right place. The familiar tiles and charts sit just below, unchanged.
It reads the insight that's already prepared overnight, so it loads instantly and never generates anything on open. When last night's sleep hasn't synced yet, it says so plainly and fills in the sleep-dependent parts once the data arrives, rather than showing a stale or empty score. This is the first piece of a single daily view that later surfaces — a morning refresh, an optional daily note — will all build on.
The dashboard gains tiles for readings that were synced and charted but never had a place on the home strip: heart-rate variability, blood oxygen, breathing rate, wrist temperature, and — for anyone with a body-composition scale — muscle mass, body water, and bone mass.
Each tile appears only once you have that reading, so nothing new shows up on an account that doesn't track it, and every tile can be toggled or reordered under Settings → Dashboard like the rest. Tapping one opens its detail view. Nothing changed in how the readings are stored or synced — this only surfaces what was already there.
[1.28.51] — 2026-07-16 — Chatting about a document lives in the Coach now
Asking the Coach about a document used to open a separate, half-finished chat panel that never showed up in your Coach history. It now opens the real Coach: the conversation appears in your Coach list like any other, marked with a small document badge and a "Chatting about: