1.1 MiB
Changelog
[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.
Since v1.30.17 there was no way to push a reading in. That release made API tokens enforce their scope, which closed a real hole (a token issued for medication intake had been reaching every other endpoint, the full backup export included) and removed the generic "create API token" button along with it. What went unnoticed was that the button had been the only self-service route to a token that could write a measurement. A scale or a watch bridge piped through Home Assistant had no door left, and the OAuth integrations do not reach local sensors.
There is now a token for exactly that job, and for nothing else.
Added
- A measurement ingest token, minted from Settings → API & Tokens.
Contributed by @Antiheld86 in #881, from their proposal in #878. It posts
readings to
/api/measurementsand/api/measurements/batch, on your own record. That is the whole list. It cannot read a reading back, cannot change or delete one, cannot reach the export, and cannot mint another token, so a credential sitting in a container you did not write has a worst case of junk in your own record, not your health history leaving the building. Shown once, expires after a year, and revoked from the same page as every other token. - A guide for wiring one up, with a worked Home Assistant
rest_commandand automation, the measurement types a scale or a watch bridge tends to report, and what each error response means. - The API endpoint list on that settings page now shows the two ingest endpoints alongside the medication one.
- A reference-range bar beside every lab reading, contributed by @TimonBed in #860 and #880. The lab list, its mobile cards and the history table on an analyte's page carry a compact bar: the reference window in green, the reading as a marker on it, blue below the window, amber above it. A range with only a minimum or only a maximum gets a bar too, with the one stated bound labelled. A reading far outside its window (vitamin D at 7 against a floor of 30, say) widens the bar's scale so the marker stays visible instead of sitting pinned to the edge. In the list the status badge, the bar and the trend sparkline line up in columns across rows, and every point on the sparkline takes the colour the bar gives it. A qualitative reading, or one without a range, shows no bar.
Changed
- Readings pushed with this token are recorded as manually entered, which means you can correct one in the app if a sensor sends something wrong. A push that claims to come from Apple Health is refused rather than quietly relabelled: that label is half of how the iOS app recognises its own rows, and a bridge borrowing it corrupts the phone's sync instead of merely mislabelling a reading.
- Such a push no longer moves the phone's sync checkpoint. It never should have: the checkpoint records that your phone delivered what it was holding, and a scale advancing it could make the phone skip a window of its own samples.
- The below-range and above-range badges on a lab reading carry a light tint (blue below, amber above) instead of the neutral grey they shared; in range stays neutral. Part of #860.
- Routine dependency updates: the Prisma Postgres adapter to 7.10.0 (see Fixed), and the Argon2 binding, the WebAuthn server library, Radix, Lucide, the form resolvers and the dev tooling by a patch or minor each.
Fixed
- Settings, Integrations: the warning that the configured OAuth callback
origin differs from the address you are browsing on, added in v1.38.0,
never appeared in the published image, and the copyable callback address in
each setup guide showed only a path. Both read the app address inside the
browser bundle, where it is fixed at build time and the published image is
built without it. Both values now come from the server at request time and
reflect the
NEXT_PUBLIC_APP_URLor*_REDIRECT_URIyou actually set. A structural test refuses a client module that reads that variable again. - Lab OCR with a GPT-5 model on the OpenAI provider path (your own key or
the operator's admin key), fixed by @TimonBed in #861. The list of models
the app trusts to read an image stopped at
gpt-4o,gpt-4.1,gpt-4-turboand the full o-series, so a GPT-5 model was judged unable to read a photo and never sent one; the Codex path already knew better. The extraction call's completion budget doubles from 4000 to 8000 tokens, because a GPT-5 model spends part of it on reasoning and a dense report's JSON was being cut off before the last row. And when the provider itself fails, the error now says so ("The configured AI provider could not process this report") and the server log carries the upstream status, model and error excerpt, where it used to blame the photo and ask for a clearer one. - A lab report imported as a document could leave a reference window whose
floor sits above its ceiling on the record. The bounds come from the
extraction: when a report prints no range text, the two numbers the model
reported are the whole of the window, and a transposed pair (100 as the
low bound, 30 as the high) went through unchecked. Manual entry, the OCR
path and the biomarker endpoint all refuse such a pair; the document path
did not, and when the analyte was new to the record the pair was also
written to its freshly minted catalog marker, so every later reading of
that analyte was judged against a window that cannot be true. The pair is
now dropped rather than swapped, because a transposed range says the
transcription is wrong and not which of the two numbers is; the verbatim
range text survives, so nothing the report actually printed is lost. The
extraction drops it, editing a staged fact on the review screen refuses
it with the same message the lab forms use, and confirming a fact drops
it as the last gate, which is what covers facts staged before this rule
existed. Counted as
labs.referenceRange.transposedin the wide events. The trend sparkline had read the same pair as a minimum-only range and painted a high value green; it now shows a neutral point, as the range bar next to it already showed nothing (#882). - Bumping the Prisma Postgres adapter to 7.10.0 broke the image build on
both architectures, before any release carried it, with
Cannot find module '/app/node_modules/pg/lib/index.js'.pghad sat in devDependencies, and the assertion that the standalone image can resolve it (pg-boss loads it by plain Node resolution) had passed only because our pin and the adapter's happened to agree on a version; the bump moved the adapter's copy to 8.23.0 while ours stayed at 8.22.0, the file tracer packed only the adapter's copy, and the top-level link pointed at the other.pgis a runtime dependency now and the lockfile carries one copy, so the two cannot drift apart on the next adapter release (#872).
Security
mysql2is pinned past GHSA-3f6p-5ww8-9rcr (an auth-plugin downgrade that leaks the credential) in both the application tree and the separate Prisma CLI install inside the image. It reaches the tree only through Prisma's MySQL driver, which this app never opens.- The token is confined to your own record. Pointing it at a record somebody has shared with you is refused, and refused before the sharing grant is even consulted, so no future change to what sharing permits can widen what one of these tokens reaches.
- Minting one requires being signed in on the web. No API token can mint one of these, whatever it is allowed to do: the sign-in a phone holds lasts a day and what it could mint here lasts a year, so allowing it would let a credential that leaked for an afternoon leave behind one that outlives cancelling it.
- Closed a narrower gap found while building this. A repeated request
carrying an
Idempotency-Keycould be answered from the cache after a check that asked only whether a sharing grant was live, and not which credential was being presented. Nothing could be written that way, but a cached response about a shared record could be returned to a token that the endpoint itself would have turned away.
[1.38.0] — 2026-08-30
The health score works with the record a person actually keeps.
Until now it demanded three separate areas of health plus one measured value before it would grade at all. Someone who tracks blood pressure and sleep, carefully, for months, got the same blank card as someone who had just signed up. The refusal was the loudest thing on the dashboard and it never said what was missing in a way you could act on.
Three areas is now a recommendation. A score is worked out for one area or two the same way it is for five, and the card says what it rests on.
Added
- The score names its own basis. Below full breadth the card carries one line, "Based on {n} of {recommended} areas of health", and the number stays at full size in its band colour at every tier. A narrower score is a smaller claim, not a worse one, and it is never dressed up as a full one. The areas themselves are one tap away in the composition list that was already there.
- The score says when a pillar stops counting. If a window rolls past its floor or a source goes quiet, the composition changes and the card says which area left and why, in the same words the pillar uses when it is short of data. Dismissible, and it only comes back if the composition moves again. Deliberately not a notification: this is something to notice, not something to be woken for.
- Every score response carries an optional
scoreBasisblock with the area count, the recommendation, the tier and whether a measured physiological value is among them. Additive on all three carriers, so an older client keeps working unchanged. - Tailscale is a documented way to serve HealthLog, from a self-hoster's suggestion in #847 (see v1.37.33).
Changed
- Refusal now means one thing only: nothing in the record can be scored yet. Every other case gets a number.
- A pillar selection in settings saves from one pillar upward. The page still recommends three, because a one-area score is honest but thin.
scoreVersionmoves from 3 to 4. Stored days keep the version they were written under; deltas suppress across the change as they always have.- The demo seeds every area, so a fresh demo shows the score at full breadth instead of decaying to a cardiometabolic-only view after a few weeks.
Fixed
- Warning on the integration cards when the configured OAuth callback address does not match the address the app is served from, contributed by @TimonBed in #855. Self-hosted OAuth otherwise fails with nothing on screen to explain it.
- The coverage line no longer says the score "needs" three areas, which stopped being true with this release.
- The panel-geometry check had never actually measured anything in CI. It needs a browser, and the job that runs it never installed one, so it caught the launch failure and reported four passing assertions. It now installs the browser, fails loudly in CI when one is missing, and skips with a stated reason locally.
Security
- A structural guard pins the rule that keeps invented targets out of the score: every scored band must cite a clinical threshold or a published guideline, and nothing may be synthesised from a person's height or age beyond the guideline bands that are documented as such. The BMI-22 default that shipped before v1.34 fails this guard.
[1.37.33] — 2026-08-29
A community day: a contributor's first code fix, a self-hoster's suggestion turned into documentation, and the housekeeping a code-scanning tab is for.
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 deliberate 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.
- The admin notice about login locations resolving through the online geo lookup repeated on every worker boot, because the only-once latch was a per-process boolean. The anchor now persists in the notification ledger, survives restarts and retention pruning, and resets only when the configuration state actually 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
0o600.TRUST_PROXY_HOPSjoined the composeenvironment:whitelist so a.envvalue reaches the container without editing the compose file. - The HTML stripper the notification senders share is hardened against multi-character bypasses: one helper, applied to a fixpoint, replaces five per-sender copies of a single-pass regex.
- The operator docs went through a truth audit against the current code: stale claims corrected, invented features removed, and the OpenAPI 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 pinned to an exact version; the code-scanning backlog is triaged to zero untreated findings, with false positives dismissed in writing.
[1.37.32] — 2026-08-28
Korean joins as a seventh interface language, contributed by @aucun6352, and the three things that had to be fixed before it could actually work.
Added
- Korean (
ko) ships as a seventh interface language, AI-initial and community-maintained alongside French, Spanish, Italian and Polish. The whole surface 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 an honest note. The Coach and the daily briefing take the reviewed English body with a Korean reply directive rather than a hand-curated Korean one, the same route the other community locales took.
Fixed
- A matcher that reads app text against text a person or a lab typed matched nothing at all in Korean. The shared fold decomposes with NFD and strips the combining marks, which turns Latin accents into plain letters, but Hangul decomposes into jamo, and jamo are not combining marks, so they survived and the folded text no longer resembled any table the app carries. Both readers of that fold were affected: the normative-claim check that grades whether a recommendation needs a citation, and the parser that reads a printed reference range. The fold recomposes now, and every Latin, Polish and German fold is byte-identical to before.
- No Korean analyte name could resolve to a LOINC code. The key normaliser kept
[a-z0-9]and dropped everything else, so every Korean lab name in the catalogue normalised to the empty string and the derived index held none of them, 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 actually use.
[1.37.31] — 2026-08-27
The retry ledger 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' chains touch it, how many are currently failing, the worst uninterrupted failure run, 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 backing endpoint 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 refusal 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 user-configured gateway instead of being pinned to public hosts. A private host that is not allowlisted is refused exactly as before, personal API keys keep the pinned public posture, and the ChatGPT sign-in path stays fully pinned.
[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 locale. 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 the aggregation tier from the requested window, and "All" always requests ten years, so every record came back in monthly buckets no matter how short its history, while the chart captioned the series from the real span. A four-month record showed four points labelled "Weekly avg". The tier now follows the span the data actually covers, and a test pins the server tier against the caption ladder for every span up to ten years so the two cannot drift apart again.
- The same tab no longer means two different things depending on how the chart mounted. 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 shadow data from leaking into 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 grain decision the chart and its data table use, instead of re-deriving it separately.
- The runtime 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
Eleven 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. Eleven of the seventeen 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 aggregator the PDF uses, 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 account has switched off says that instead, since the recipient would otherwise read 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. Three surfaces built the label by interpolating an enum member into a key space whose entries are lower case, so a routine appointment read as
encounters.kind.ROUTINEin 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, which is the last day before the read leaves the bucketed rollup tier for an unbounded scan; beyond it the request is refused rather than quietly clamped. Omitting the parameter changes nothing.- The response already carried the two numbers that tell a caller what they actually got, and now says so in the contract: one field for the window that was asked for, another for the history that backed it. A three-week record answering a thirty-day request reports both.
- The wellness-score value object is modelled in the published API document. It was typed as an open record with prose describing one metric, which is why a client team read the contract, built what they could see, and reported two fields as missing that were in the payload all along. The value shapes that stay opaque are named where the gap is, rather than left to be rediscovered.
- The share link's report and FHIR downloads are published in the API document alongside their document sibling.
[1.37.27] — 2026-08-23
Five things this release fixes have the same shape: a check that was green because it was not checking.
Fixed
- Importing an Apple Health archive rejected every ECG recording exported outside a US locale, and counted each one as a failure without recording why. Four separate causes, none about the recording: the file opens with a header row that has no value after it, waveform samples are written with a decimal comma, the sample rate reads
511,422 hertzrather than512 Hz, and a German watch writes the column names in German. The parser had been written against a test fixture and the fixture was US-style throughout. All four forms are accepted now, and both dialects are pinned side by side so a later edit cannot narrow it back to one. - Three of Apple's own English verdicts were dropped as unknown.
Heart Rate Over 120andHeart Rate Under 50are what the export actually writes, which matches neither the framework's documented case names nor the wording in Apple's instructions. A verdict the parser cannot represent is now told apart from one it has never heard of. - The Coach could not report a single environmental correlation. Four surfaces assembled the correlation matrix independently and three carried different channel sets, so asking whether air pressure related to your sleep searched a matrix that never held the pair. All four assemble from one place now. The Coach's file header had claimed for months that it ran the same scan as the insights page, which is why nobody looked.
- A metric card's assessment received every surviving correlation in the record rather than the ones about that metric. The filter that narrows to one metric had been replaced by the dismissal filter instead of chained with it, so the name, the docstring and the early-return guard all said "one metric" while the return said otherwise. Its test passed because the fixture held one pair.
- Reading a personal record issued the same single-row lookup once per metric. On a three-year record the dashboard did it 25 times and the analytics page 17. The comment above the helper had warned about exactly this since it was written. It resolves once per request now.
- Citation coverage reported full coverage for four of the six languages, because it measured against an English and German word bank while the text it measured was written in the reader's 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 kind, and a value with no kind clears the gate against every entry in the ledger: a sleep figure could ground itself on 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. The endpoint erased the whole set when the metric parameter was omitted, with no confirmation and, unlike its sibling, 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 of possession, the same gate that has always guarded removal of a second-factor security key. The softer gate had been sitting on the primary sign-in credential. A passkey satisfies its own gate, so an account with nothing else enrolled keeps control of its own credential list, and the app keeps the capability through the step-up it already uses.
- A portable backup that claims a section it does not carry is refused whole rather than restored in part, and the refusal names the missing section. A section the export deliberately left out and declared as omitted still restores, which is what keeps the disaster-recovery export working.
Internal
- The read surfaces have measured budgets for the first time. Against a three-year record (29,565 measurements, 1,095 mood entries, 5,475 medication intakes, 20,688 rollup rows) the heaviest surface issues 103 statements and 77 KB, and page load does not depend on record size at all. The budgets pin query counts, payload sizes and which rollup arm was taken rather than wall-clock, because counts were identical across runs while wall-clock moved up to sevenfold on the same machine. One ratio budget asserts that counts do not grow between a 30-day record and a three-year one.
- Every authenticated page was downloading the settings validation layer. The app shell wraps all of them and read one small function from a barrel that also re-exports the managed settings-patch schemas, so Zod, the module registry and three validation modules rode along on every page load for a check that reads a URL slug. The shell reads that function from its own module now. No page grew, a hundred of them lost 5 to 8 KB, and the Insights overview has room under its budget again after three releases pressed against it.
- The dead-code gate had been blind to every re-export since knip 6.16. The tool changed at 6.28 and the old silence turned out to be an artefact of one setting rather than a clean tree: with that setting off, the old and new versions agree exactly. Two dead constants and 204 unimported re-exports are gone; every definition still in use stayed.
[1.37.26] — 2026-08-23
The app is offered in six languages, and in seven 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 genuinely cannot read is recorded as unreadable instead of quietly 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 written with German umlauts never matched either, which is the older half of the same defect and affected the language most people here use. The key normaliser deleted any character outside a to z, so
Hämoglobinbecamehmoglobinand never met the alias written ashamoglobin. 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 pass 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 same list is published in the API document and on the capability endpoint, so a client can learn about a retirement without comparing versions. - Removing a route now has two legal answers rather than one, and a test enforces the choice: drop it from the published contract, or register it as retired. Doing neither, which is how a client finds out by rendering an error, no longer passes.
[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 the register of what a restore left behind went from twenty-seven tables to two.
Fixed
- The Coach checks its own numbers against a record of what it looked up, and rewrites any figure 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 plain statement that nothing is recorded.
- The Coach could also narrate a weight history that no longer exists. Syncing a body-composition provider replaces your rows, but the monthly summaries folded from the old ones stayed behind, so the same block could show a year at 95 kg beside an all-time range that peaked at 80. A summary is an average of real readings and cannot fall outside their own extremes; when it does, the history band is dropped and the Coach is told it was withheld rather than shown a number nobody can account for.
- The Insights page promised "based on your last 90 days" whether or not there was a briefing behind it, and the pulse page promised a personalised Karvonen target band while computing population percentiles by age and sex, or a flat 60 to 100 when your age is unknown. Both now say what they do, in every language. The Coach's "all time" window was 365 days, the same 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. Not partially: the restore hit a foreign key on a custom category the file never carried, rolled the whole transaction back, and answered with a constraint name and an empty account. The categories a person creates travel now, and they are written before the tags that point at them.
- Signing in from the iOS app failed behind a reverse proxy that does not pass the host through. The redirect was built from the address the server process bound to, so it came back as
https://0.0.0.0:3000/..., which iOS refuses outright. Redirects to this same deployment no longer name a host at all. Seven places had the same bug; all seven are fixed, and a test fails if the pattern comes back. - The Prisma CLI in the published image still resolved the vulnerable deepmerge-ts. The application tree had been pinned already, but that install is a separate one with its own resolution, so the image carried the advisory while every dependency check read clean. A test holds the two pins 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 operator 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 two neighbours cap at one kilobyte. Four 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.
- Someone managing a shared record could write a titration step onto a medication and then be refused that medication's history. The read is fenced at the same level as its siblings now.
- Disconnecting a Withings account left the connection state reading "connected" until something else refreshed it, because the teardown neither wrote its audit row nor parked the ledger the way every sibling does. Two more teardowns wrote no audit trail at all when there was nothing connected.
- Two sync routes had no rate limit where their siblings carry two each, and every sync route read its request body inside a swallowing catch, 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.
- The integrations page could not tell "nothing delivered yet" from "the freshness query failed": both arrived as an empty list. The failure says so.
- 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. Per-drug reminder phase timings travel too, so a restored drug turns orange when it did before instead of falling back to the defaults. Archived schedule eras 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 waveform, 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 an account shaped for itself: the groupings it created and the seeded factors it 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
The published API document listed 262 paths. The application answers on 410.
The drift check compares the registry against the committed document and fails when they disagree. It has never compared the routes on disk against the registry, so a route that was never registered produced no drift and no failure: the document stayed internally consistent while going quietly incomplete. Forty-three of the missing paths are ones the iOS app calls every day.
Every route is now either published or named in a list with a reason for its absence. Seventy-three are deliberately outside, 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 three discovery documents have their format fixed by somebody else's specification.
The document also says how to authenticate, which it never did. Both schemes were defined and neither was referenced, so anyone generating a client from the contract got one that sends no credentials and treats the whole surface as open.
Two tests keep it that way. One walks the route tree and fails on a verb that is neither published nor excused. The other fails on an operation that names no scheme.
- The idempotency contract is in the published API document. Thirty-four write operations declare the
Idempotency-Keyheader they honour and theX-Idempotent-Replayheader they answer with, including the detail that a malformed key is ignored rather than refused. - Per-event notification preferences are recorded as deliberately outside the backup rather than as an unpaid debt. Each one addresses a single notification channel by a foreign key, 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.
[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.
- The restore dialog shows what a backup contains before you confirm: per-type counts read from the stored file, with an honest 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.
- 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 shell.
- 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.
[1.37.19] — 2026-08-14
Changed
- Medication stock understands per-slot doses everywhere now. The remaining-doses figure weighs each scheduled slot by its own dose, the API carries the resolved dose per slot plus a runway in days, and the low-stock notification runs on the same arithmetic, so the app, the API and the push can no longer disagree about how long a supply lasts.
- A document whose summary job died no longer says "being generated" forever. After an hour the detail view says honestly that no summary is available and offers the manual action, and an hourly sweep repairs stuck documents in the background.
- The daily coach reminder sweep counts how often it has surfaced an ignored reminder and quietly dismisses it after three mornings instead of nagging indefinitely.
- Accepting an updated disclaimer is now tracked by version, so a future revision can actually ask again.
- The restore report lists practitioners, appointments, vaccinations and their links, and states what was written, not only what was cleared. The portable export names any field that could not be decrypted instead of exporting it as if it were never set.
- Changing your timezone re-folds the medication compliance statistics so the day boundaries match your new zone.
- Long health histories are honest again in two places: the all-time summary includes readings older than five years instead of silently truncating, and the AI context falls back to live data when a pre-computed band is missing.
- A cleaner copy voice across all six languages: shorter consent buttons, a source-neutral workouts empty state, consistent toast punctuation, "Navigations-Chips" instead of "Pills" in German, and a medication reminder chip that no longer clips mid-sentence.
- 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 mid-send can no longer deliver the same reminder twice.
- A simultaneous double-submit of the same mood entry reports "duplicate" with the winning entry instead of a confusing error.
- The intake-event API description now documents every field the server actually 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 actually fire. The app has offered these since v1.22 and confirmed them, but nothing ever evaluated them; a matching measurement or the next app open now surfaces the reminder, and the daily sweep backs it up.
- The Apple Health import tells you what it skipped and why. Every dropped sample class is now listed on the import card: unknown types, out-of-range values, deferred records, and cycle samples that were read while cycle tracking is switched off, which previously vanished 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 aggregates. Before, a deleted reading stayed inside day, week, month and year statistics forever; the freshness probe can now see tombstones, and the WHOOP deletion webhook 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 bounded to the same window.
- The connector token settings said tokens are read-only while offering a write toggle on the same card. The copy now tells the truth in all six languages.
- A lab report cannot smuggle instructions into the AI layer through a biomarker name anymore; document-derived names are cleaned before they reach any prompt, and the assistant tools fence checkup labels and lab analyte names the same way.
- Google Health and Withings syncs now refresh the server-side caches like every other integration, so analytics pick up synced data on the next read instead of waiting out the cache lifetime.
- The vaccination booster and suggestion endpoints are published in the API description, as the contract notes already claimed.
- 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 service worker's offline fallback version is anchored to the release version again; it had silently stayed behind for two releases.
[1.37.17] — 2026-08-13
Changed
- The correlations discovery reads its measurement series from the pre-aggregated daily tier instead of walking every raw reading over 180 days. For a dense record, glucose from a continuous sensor most of all, this turns tens of thousands of rows into at most one row per day and source. Values stay the same: the daily figures are composed to match the previous per-reading averages exactly, a channel without aggregate coverage falls back to the raw path on its own, and sleep duration, steps and daylight time keep their raw reads because their daily figures depend on session reconstruction and device de-duplication that the aggregates 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 180-day discovery on every visit. The first request computes and caches the result server-side; repeat visits within the hour answer from the cache, 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 deliberate: what you see during the switch is marked as 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 render with content instead of a loading skeleton. Self-hosters can switch this off with the existing
DASHBOARD_SSR_PREFETCHflag. - Two background checks are calmer now. The achievement check runs every five minutes instead of every two, 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.
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 is reflected on the dashboard right away, including the hero bands and score context. Before, the dashboard kept showing 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-side caches that new data arrived, so analytics and workout views pick the data up on the next read instead of waiting out the cache lifetime. The same applies to illness, cycle and appointment changes.
- A structural test now enforces that every place that writes data a daily view reads also triggers that view's refresh, so this class of stale display cannot quietly return.
[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 now keeps watching quietly until the new assessment lands, then swaps it in and confirms. Only a refresh that provably failed says so. Before, a slow refresh showed an error toast and the page held on to the old assessment until the next day.
- Document previews show again. The security policy the app sends to the browser did not cover the way previews load since the sharing fence, so PDF previews were blocked outright and image thumbnails quietly fell back to their icons. The policy now admits exactly the app's own decrypted documents and nothing else.
- A document uploaded while automatic AI reading is switched off no longer claims a summary is being generated forever. The upload stopped queueing summary work it is not allowed to do, a stale claim heals itself on the next pass, and documents already stuck in that state are repaired by the update. The manual summary action is offered instead, as it should have been.
- When reading a document for search fails, the document now says why, in plain words: 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.
- 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 non-overdue dose 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. 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 reads of the page you were on, so coming back to the home page showed the dose as still due for up to two minutes. Logging from anywhere now refreshes the home page's daily reads the same way logging from the home page always did.
[1.37.13] — 2026-08-13
Added
- A read lab document offers its values for your lab record, right on the document. When a document is classified as a lab report, the detail view shows a review step with each extracted value, its unit and its date, and you approve or reject them one by one before anything is written. Nothing is committed without your confirmation, and a value the reader was unsure about is marked for a closer look first. Reading a document again by hand now stages values the same way the automatic path does, so a report that arrived before automatic reading was switched on is no longer stuck.
- The hourly limit for reading documents with AI is an operator setting now, not a fixed number. Self-hosters set
DOCUMENT_AI_LIMIT_PER_HOURto match their provider budget. The limit message tells you when the next read is actually possible instead of a vague "try again in a few minutes", and a read that never reached the provider no longer uses up one of your slots. - The web AI settings can grant consent again after a withdrawal. Revoking AI consent stays a standing decision that no page visit can quietly undo; what is new is an explicit control that lets you, and only you, opt back in when you want the AI features again.
- The Coach keeps an answer you walked away from. Navigating off the page still stops the reply, deliberately, but the conversation now shows the interrupted turn instead of losing it, coming back opens that conversation rather than a fresh chat, and a retry control asks the same question again in place.
Fixed
- The health score could not read lab-based pillars. A column-name mismatch made every glycaemia and lipids read fail, so the score treated accounts with lab results as unreadable and the two pillars never scored. The reader uses the real columns now, and an account without lab results shows the pillar as not tracked rather than as an error.
- Reading a large Apple Health export no longer exhausts the server's memory. The archive streams through the parser in bounded batches instead of accumulating in memory, so a multi-hundred-megabyte export with millions of records imports on a default self-host setup. An import the available memory genuinely cannot carry is refused up front with a message that says what to raise, and an import that was interrupted mid-run shows up as failed with a retry, rather than spinning forever.
- Automatic document reading no longer hides the manual read action on a document that has not actually been read yet, so a single failed or skipped document has a way back.
- Read-everything now includes PDFs for providers that accept images. The pages are rendered to images for those providers, the same way a single manual read already did, so a Codex-style setup no longer skips every PDF in the vault.
- The About-me note and the free-text allergy notes moved from Account settings into Anamnese. The free-text notes sit directly under the structured allergy list they supplement, labelled as the place for what the structured list cannot hold, and the account page keeps to account matters.
[1.37.12] — 2026-08-11
Changed
- The public privacy policy now matches the current app and server data flow in German and English. It explains the in-app account-deletion route and MFA handoff, optional server-managed and bring-your-own assistant providers, on-device processing, document and OCR use, consent revocation, Apple Health sync versus HealthLog storage, retention rules, heart-rate representation, the full App Store data categories, and that HealthLog does not track you.
[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.
[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 phantom 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 only appears when you have recorded something for it, and it is part of the report you choose to share, so it travels with the report to the practice rather than sitting somewhere separate.
[1.37.8] — 2026-08-10
Added
- The assistant can answer questions about your practice visits now, both what happened and what is coming up. A new reading over the assistant wire returns your visit history for a window you ask about, with the practitioner, the reason and the outcome, and it says plainly when you have never recorded a visit 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
- 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 URL 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
- A self-hoster running the published image can now set MAXMIND_LICENSE_KEY as 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.
Security
- The MaxMind licence key no longer appears in a log line when the runtime GeoLite2 fetch fails. The download URL carries the key in its query string, and the failure path used to print the full URL; it now reports only the edition and the kind of error, and the central log redactor also masks a license_key parameter wherever one might otherwise slip through.
Security
- A mental-wellbeing screening reminder (PHQ-9, GAD-7, WHO-5, SCI) can no longer be marked done through a crafted request. A screening resolves only from the score a completed check-in produces, so the satisfy and complete routes now refuse a screening reminder outright rather than trusting that the app never offers that action for one.
Changed
- Editing a custom mood tag, a custom tag group or a custom cycle symptom now records an audit entry, the same way deleting one already did. Removing a custom tag or symptom together with its history leaves a trace of the change. Hiding a catalogue tag and reordering the layout stay out of the audit log by design, 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 now open on a one-line definition 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 and stops reusing a word from the assessment beside it. 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 managed profile now use the same controls as the rest of the app. The forms for a delegated profile 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 now use the shared components so they match their siblings.
[1.37.4] — 2026-08-09
Fixed
- The connector-token note in Settings no longer claims a token is read-only in every case, which the write toggle right below it disproved. It now states the real model: read-only by default, write access an opt-in you turn on per token, and no token ever reaches admin functions.
- A failed load of your share links now says so, with a way to retry, instead of quietly showing "no active share links" as if you had never shared anything. The same held for the API and connector token lists on the security screens.
- A read that fails now shows a clear, recoverable error across the app instead of an empty list or a silent gap. The sharing and token screens, 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, rather than reading as "nothing here".
- The AI consent and connector notes in Settings render as normal text now, not 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 now appears only where there is a real practice visit behind the reminder. A reminder you satisfy yourself in the app, like weighing in or a mood check-in, no longer offers to file a visit it never involved.
- The practices and doctors address book searches by specialty now, so typing "Zahnmedizin" finds the dentist even when the name and practice hold nothing of the sort. The list groups its entries under their specialty, with the ones you left unspecified gathered at the end, 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 now instead of a flat list you had to scroll. Each link block shows what you have picked as removable chips over an add button that opens a searchable sheet; lab results gather under their panel and sample date so you can attach a whole day's panel in one tap, and documents gather by month. The vaccination form uses the same picker for its document link.
- 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
- An immunization record. A new Vaccinations module, switched on under Settings then Modules, keeps a lifetime Impfpass. 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 of around thirty entries, searched by disease or antigen name, or by typing it the way your pass reads; a date plus one of those two is all that is required, and the lot number, the injection site, the practice, a link to a visit and an encrypted note are each optional. Where the catalogue knows a schedule it shows it with its source, as information rather than advice. A dose that carries a booster interval offers a prefilled reminder you can edit or decline, and confirming it writes an ordinary preventive-care reminder that appears on the checkups page and moves itself forward when you log the next dose. A vaccination scan in the document vault can attach to its record, the record can link documents it already holds, and the doctor report gains an immunization section you can leave out. The module rides the record-sharing model, so a parent can keep a child's Impfpass under a managed profile. Six languages throughout, including the catalogue names, and never a brand name.
Fixed
- The mood sliders no longer read their value fused with the reset control, so a slider set to 4 shows the number on its own instead of "4 leeren". The reset is a small icon beside the value now.
- Date and time fields wear the same focus highlight as every other input. The ring hugs the field instead of painting wider than it or jumping to the left when you click in, and the fields 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 by the scroll track.
- 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 now sits last, after the authenticator app, security keys and passkeys.
- Sharing a record again with someone whose access had expired works. The old lapsed grant no longer blocks a fresh invitation with a false report that the person still has access.
- The scope options when inviting someone start with nothing preselected, so the choice between the whole record and single sections is one you make rather than one already made for you.
- The write and manage descriptions on the invite form are one line each now, with the full detail behind the info icon instead of a wall of text.
- A preventive-care reminder set to the longest interval the engine allows no longer loses its next date when a dose is logged against it. The reminder now moves a full cycle forward instead of falling silent.
- The catalogue and practice pickers now carry a name for a screen reader; a control that only showed an icon was unnamed before.
[1.37.2] — 2026-08-08
Added
-
Doctor visits and the practices behind them have a home now. A visit records what kind it was, 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 — linked, never copied, so a correction in the source shows through. Visits and their checkups live under a segmented view; a preventive-care checkup can be closed by recording the visit that fulfilled it, and a due checkup can carry a planned appointment. A planned visit sets a reminder that the server re-anchors when you move it and clears when the visit happens or is cancelled; it fires once and never nags, and it carries no "done" button, because the appointment is not a task you tick off. The whole thing rides the record-sharing model as its own section, and it shows up in the doctor report as a visit list you can leave out.
-
The mood module can now say what a day would have looked like on your own past patterns, and how far the day you recorded sits from that. It is a second, separate reading: your own rating stays the leading one, it is stored exactly as you gave it, and nothing overwrites it. The comparison appears only once there are enough days to say anything honest, it always carries the number of days it was built from and a range rather than a single figure, and it names which of the things you recorded weighed most on that day. It describes a connection between things you wrote down. It never claims a cause.
The rework this reading sits on grew from a concept Kai worked out in detail. Thanks, Kai.
Below fifteen days there is nothing at all. Between fifteen and thirty the page says how far along it is instead of guessing. Between thirty and sixty the comparison is there and is labelled as provisional. Where the days are there but nothing in them is steady enough to compare against, it says that too, rather than producing a number to fill the space. The comparison is rebuilt overnight from your entries, and if you delete enough of them it goes away again instead of standing there with a fresh date on it.
-
A mood entry can now carry the shape of the day around it. Four sections sit below the sliders — work, contacts, leisure, and one notable event — and every one of them is closed until you open it, so a quick check-in is still a face and Save. What you fill in is counted on the closed section's header, so nothing you wrote is out of sight without a hint that it is there. The work section asks how the day went and how long it ran, the contacts section asks who and how it felt rather than how many people, the leisure section asks what the free time went into, and the event section holds one thing that happened with a note beside it. That note is encrypted at rest like every other free-text field.
-
Sleep, activity and vitals are not asked for a second time. They are read from the modules that already hold them and shown read-only beside the entry, with the source of each figure named and a way in to correct it there. Correct a sleep session in the sleep module and the mood entry shows the corrected figure the next time you open it, because nothing was copied. A figure nobody recorded reads as not recorded rather than as zero, and a module you have switched off leaves its block out entirely instead of showing empty rows: sleep answers to the sleep module, resting heart rate and heart-rate variability to recovery, the body line to the illness journal. Steps and active energy have no module of their own and are always shown, which is how the rest of the app already treats them. Body symptoms stay in the illness journal, linked rather than duplicated.
Where two devices reported the same day, the figures follow the source order you set rather than adding both together. A phone and a watch both counting your steps is the ordinary case, and summing the pair would have shown a day you did not walk.
-
Where the app compares the day's context to the day's mood, it names how many days each side of the comparison rests on, right beside the sentence, and it describes a connection rather than a cause. The comparison runs the same statistics the tag board already uses, including the correction that keeps a wide sweep from turning one-in-twenty noise into a finding.
-
The iOS contract now carries both the five mood values and the day context. Everything is optional and nothing an existing build sends has changed meaning: an app that posts only a five-point label still writes a complete entry, because the server derives the pleasantness value from that label.
-
Workouts recorded before the phone learned to send heart-rate detail can pick that detail up afterwards. When the app sends a session the server already holds and the payload carries the reading-by-reading heart rate this time, the curve is attached to the workout that is already there and shows up on its detail page. The session itself is untouched: duration, calories, the average and maximum heart rate, the times, all stay as they were first recorded. A workout that already has its curve is left alone, so the phone can walk back through years of history as often as it likes without anything being written twice. If one session's readings arrive unusable, that session alone is reported back and the rest of the batch still lands.
Fixed
-
Marking an older missed dose as taken from the intake history now records the dose at the slot's own date and time. Marking Tuesday 09:00 from a Thursday evening used to land on the right slot but stamp the intake with the current moment; the recorded time now matches the claim being made. The optimistic paint had promised this all along, and the skipped-to-taken flip in the same view already behaved this way.
-
The Appearance list in Settings had a strip of dead space above every row that the first row did not have — 24 px on a desktop, 16 px on a phone, unclickable, sitting between one module and the next. The rows read a step apart from the top of their own list. They now sit the same distance from the line above them, and the geometry sweep measures it so it cannot drift back.
-
The Account settings page is the profile again. Cycle tracking is a module and its switch lives in the Modules hub, so the second on-ramp here is gone along with the "manage in Account" link beside that switch, which pointed at an anchor nothing rendered. Changing the password moved to Security, where the second factors and the passkeys already are, and it leads that page.
-
Five module rows ran two to three times longer than their thirteen neighbours: environment, external assistant access, documents, mental wellbeing and nutrient intake. Every one of them ended in "off by default", restating what the switch beside the row already shows. They are one sentence now, like the rest. The mental-wellbeing row also named two screeners where the module ships four, and what happens to your answers is now said on the mental-wellbeing page itself, where you are about to give them.
-
Research Mode is retired. It was an opt-in for the estimated GLP-1 level curve, but that chart stopped consulting the flag several releases ago and has been visible to everyone since — so the switch on the Advanced page changed no screen in either position. The curve is simply part of the medication page, and the switch, its acknowledgment dialog, and the stored acknowledgment are gone. Advanced is now what it reads as: reset the data, delete the account.
-
The ECG page opened with three lines about what HealthLog does not do before showing a single recording, two of them saying the same thing. They are gone; the recording's own page still states that the result shown is the device's. The device's verdict now rides the timestamp as a tag instead of taking a line of its own. The overview teaser shows the five most recent strips rather than every one ever recorded, while the ECG page itself still lists them all, so nothing is hidden where you go to look. A recording has its own address now, with the back link above the heading, so a strip can be linked to and the browser's own Back works.
-
An entry in the record switcher no longer carries the access level on a line of its own. In a menu that narrow it was truncated more often than it was read, and the banner states it in full once you are inside the record.
-
The record owner's name reads in full on the switcher and the banner when they have set one, ahead of the greeting name — so a delegate entering somebody's record sees whose it is by who they are.
[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, and its answer maps onto a new pleasantness scale so nothing about that entry is second class. Underneath it, a section that stays closed until you open it holds five sliders running 0 to 10, 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, and clearing a slider takes the answer back. Entries you have already logged carry the mapped value for the first scale and nothing invented for the other four.
-
The mood page trends all five, over seven, thirty or ninety days, each drawn the way you answered it. A scale you have never filled in is named as not recorded rather than drawn flat through the middle. The daily briefing and the assistant read all five as well, each with its own age, so a value from last week is stated with its date instead of as today's.
-
Factor ratings are no longer offered on a new entry. A one-to-five score for work stress and a nought-to-ten stress scale are two answers to the same question, and only the second is asked now. Nothing was removed: ratings already logged still show, still edit, and still feed the breakdowns that read them.
-
A lab result now keeps the reference range printed on its report. The document reading used to extract the value and discard the range beside it, and for any analyte already tracked, the general catalog band was stamped over what the report said, although the physician evaluates against the printed range. Extraction now captures that range, through a parser that understands the common German report forms: two-sided windows with dash or "bis", one-sided bounds in a dozen spellings, thousands separators in both conventions, and text verdicts like "negativ". Anything it cannot parse with confidence lands verbatim in a text field instead of being guessed at. For a value that carries a source range, 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 source window as a second band and marks a divergence from the catalog. Values without a printed range keep the catalog as their net.
-
Three scheduled security workflows join the pipeline, all additive. CodeQL analyses the TypeScript tree on pull requests and weekly. Trivy scans both the filesystem, with development dependencies included, and the published container image on a schedule; the dev-dependency flag exists because the recent PDF-renderer advisory was invisible to a production-only audit. Scorecard publishes to the OpenSSF API weekly. The blocking pre-publish image scan keeps its job; these add what it structurally cannot see.
Changed
- The settings pages and the admin console are one design now. They had drifted apart wherever no primitive held them: three techniques for the header-to-body gap, body indents alternating between an icon column and the card edge, buttons in four placements and five variants, and descriptions that had grown into paragraphs. One page header serves both shells, one action-row shape, one connect-button variant, and a card description is one sentence, enforced by a check that fails on the next paragraph; 36 descriptions were shortened or moved into the body across all six languages. The geometry is measured rather than asserted by eye, across 41 routes, two locales and two viewports, and the measurement found what reading could not: one card variant sat a step tighter than its neighbors on every desktop width, and the layout child pages never mounted their way back to the settings hub.
Fixed
-
Mood entries now survive a restore whole. The disaster-recovery backup was carrying only the rated factors of an entry, so every tag ticked present or absent was missing from the file, and it was dropping the note, the entry's own timezone and the sync counter as well. A restored entry came back without its text, read its day boundaries under the old Berlin assumption whatever zone it had been logged in, and could lose the next sync round to a paired phone still holding a higher counter. The backup carries every column now, both halves of the tag taxonomy come back, and a structural check fails the build if a future column goes missing the same way. Backup files written before this restore exactly as they did.
-
The workout detail draws the whole heart-rate profile. The curve used to stop at the moving-time mark, so a ride with twenty minutes of stops lost twenty minutes of curve with no gap and no marker; it now runs to the session end. The min-to-max band no longer drags the y-axis to zero, which had squashed the effort shape into the top quarter of the chart on exactly the dense sessions with the most shape to show. The peak marker renders again instead of being discarded whenever the peak sat above the bucket means. And a session with heart-rate figures but no profile behind them is named as such, distinct from one where nothing was measured.
-
Marking an older missed dose as taken no longer lands on the wrong slot. The route resolved the target only by time window around "now", so a backfilled dose fell through to a standalone row at the current minute, the named slot stayed missed, and the refetch wiped the optimistic tick. The route now honours the named slot for backfills, and the history read binds each row to its slot.
-
The insights baseline no longer compares an impossible stored reading against a band built from the same reading. Values the app itself refuses at every write surface, such as a pulse in the millions, still sat in the table from older syncs and skewed both ends of the comparison. The read side now applies the app's own declared plausibility ranges before averaging or banding.
-
Sync writers now refuse readings the app itself calls impossible. Every person-driven write path enforces the plausibility bands, but a provider sync could still store a pulse in the millions, and one recently did. The gate now sits at the shared reconciler and at the five writers that hold the database directly, ahead of the insert-or-overwrite decision, because the overwrite branch was the dangerous one: an impossible reading could replace a good stored value. An implausible sample is dropped and counted, never clamped and never allowed to fail the batch, and a read-only diagnostic lists any stored rows outside the declared bands.
-
The hero card and the day's signals no longer say "today" about a reading that is five days old. Deviations carry their age now, today-claims are bounded to one day, and a stale value stays visible as what it is: a last reading from N days ago.
-
A hand-entered night of sleep is no longer stored as minutes when the field asks for hours; 7.5 had become seven and a half minutes and nothing objected. The submit path converts at the boundary and the field accepts a decimal comma.
-
An overdue period can say so now. The phase builder ended the open cycle at the predicted next start, so from that day on the verdict read "insufficient data" instead of "overdue". The open cycle runs through today, with the ovulation anchor pinned to the predicted length so a growing window cannot walk the estimate.
-
Deleting a period start no longer leaves the previous cycle as a closed torso that stops all forecasting. Neighbour boundaries are re-derived from the rows that remain, on both delete paths, and the sheet's delete now actually removes the cycle rather than only the day log.
-
A backdated period saved fine on the server and painted nothing in the calendar, because the view read a fixed ninety-day window. The month grid now loads its own month, and every save reports success or names its error.
-
A period logged only as flow now opens a cycle, under a conservative rule: no spotting, no intermenstrual bleeding, nothing within the minimum cycle distance of an existing start. Until now such a period never created a cycle row, so charts and verdicts saw no first period; this covered the whole Apple Health import path. The cold-start prompt also counts real periods now instead of asking for a first period beside three logged ones, and the day-of-cycle number no longer depends on how wide the read window happened to be.
-
Dialogs no longer scroll sideways on a phone. Two primitives conspired: the date and time fields wrap a text input whose intrinsic size demands more width than a phone dialog has, and the dialog scroll body promoted the horizontal axis to auto, turning every intentional focus ring into sideways panning. Both fixed once in the primitives, and sixty width-by-dialog combinations measure clean.
-
The coach launcher no longer shows a dot with an empty conversation behind it. A reminder sweep flipped reminders to due without writing anything into the thread, and the flag that should mark them surfaced had no writer anywhere, so opening the coach could not clear it. The sweep now writes the reminder into the conversation and marks it surfaced in one step; the dot means a message you can read.
-
Smaller surface fixes in the same pass: the empty delivery-status card names the integrations page instead of pointing "below", in all six languages; the lab-comparison card ages out on the data instead of asking to be dismissed, with the comparison window now a year for web and the native client alike; the mental-wellbeing intro is shorter; the lifestyle entries in the anamnesis settings get a proper action row; the coach composer text sits centred in its pill.
-
The admin "wipe all data" action wiped nine tables of 122. The list lived inline in the route, a snapshot of the schema from the day it was written, and its result line read identically however much was cleared. The route now derives from the same wipe plan as the per-account wipe, in one transaction: 88 tables cleared, and every survivor is either a sign-in credential, instance configuration, or the single deliberate exemption of account grants. A build-time rule fails if the route ever names a table inline again.
-
The geo resolver read the mounted GeoLite2 databases only after asking the online provider, while every operator-facing document promised the reverse. A self-hoster who mounted the databases specifically to keep login addresses on the host kept none of them there, silently. The order is now offline first, online only when the local databases cannot place the address.
-
SECURITY.md claimed the publish workflow runs on every push to main; that trigger was removed in v1.4.34. The sentence now matches the workflow.
Security
-
The PDF renderer behind the document vault moves to a release that closes a script-execution flaw. A crafted PDF could get JavaScript to run inside the renderer, and the vault feeds it every PDF that is uploaded. Two things limited what that reached, and neither is a substitute for the fix: the renderer runs server-side only, because a PDF is displayed through the browser's own viewer rather than this library, and it already ran with font evaluation turned off. A second copy of the same library, pulled in by a development dependency, is pinned past the flaw as well, since the runtime image ships every copy it finds rather than only the one that gets used.
-
The container image now runs the version of the PDF renderer the project pins. The build had been taking whichever copy the filesystem listed first, and more than one copy reaches the image, so the pinned version was not reliably the one that ran. It is selected by name now, and a copy that never arrived fails the build instead of quietly leaving another one in its place.
-
A dependency advisory names nanoid below 3.3.17, reached through the framework toolchain; a bounded override floats it past the flaw, and the audit is clean again.
[1.37.0] — 2026-08-07
Added
-
Sharing a record no longer has to mean sharing the whole of it. An invitation can name the parts it opens: readings, medications, lab results, health background, illness, mood and mind, cycle, documents. Everything you do not pick stays closed, and a section you kept back is refused the same way a record nobody shared with you is, so what you held back is not visible from the outside.
Two things the invitation screen says out loud rather than leaving you to work them out. A section can mention the rest of your health in its notes and its names, so sharing a section shares whatever was written in it. And the dashboard overview, the health score and the daily digest appear only when the entire record is shared, because a figure computed from part of a record would read as a figure about the person.
Every grant written before this release opens the entire record and is untouched. Nobody has to agree to anything again.
-
A third level of access. Read access looks at the record. Write access adds to it. 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. The line it stops at is the account around the record: your login, your second factor, your connected services, your API tokens, where your notifications go, which modules and thresholds are on, and who else has access.
Offering manage access asks for your second factor, so it can only be done in a browser. Accepting it can be done anywhere. It always covers the entire record. There is no way to hand somebody everything about part of you, because a note in one section can be about any other.
Everything a manager does is recorded under their own name, with a verb that says what it was rather than "made a change".
-
A health record for somebody who does not sign in. @balajiv113 asked in #360 for a way to look after a family member, and a grant between two accounts only ever covered half of that. The other half is the person who has no account to begin with: a child, or somebody you care for.
A managed profile has no login and no e-mail address. You give it a name, optionally a real date of birth, and the language and timezone its own days and reminders are measured in. Nothing is invented from a year you did not give. Creating one asks for your second factor, and you become its first guardian in the same transaction, so there is no moment where the record exists and nobody is looking after it.
You can invite a second guardian, who accepts the invitation the way any other is accepted. The record can never be left with nobody: the last guardian cannot hand it back and cannot be removed, and the screen says so where the refusal happens. The way out is to add somebody else, or to delete the profile.
-
The reminders a managed profile produces reach the people looking after it. A record with no login has no phone and no chat of its own, so medication reminders, measurement reminders, safety-floor alerts and low-stock alerts go to its guardians, each in their own language and over the channels they had already chosen. The message names whose record it is about. It carries no buttons: opening the app takes you through the ordinary switch into that record rather than acting on it from a lock screen.
One guardian turning off their own reminders on their own phone does not silence the others.
-
A guardian can keep a managed profile's settings. Its modules, its units, its language and timezone, its thresholds and its notification preferences, read and written as that record's settings rather than as yours. Your own screens stay in your own language while you do it. Connected services show a status and nothing else for now: connecting, syncing and disconnecting a wearable on somebody else's behalf is not part of this release.
-
The document vault opens inside a shared record.
/documentswas offered in the navigation of somebody else's record and then refused every read behind it, so the page rendered an error card and the illness view rendered another one beside it. A clinical letter is health data belonging to the record, and reading it is much of the reason to hand somebody the record in the first place. The list, a document and its file, the preview tiles and the filter bar all answer now. Uploading, retyping, deleting, linking a document to an illness and every AI action stay with the owner. -
"Take all due" is offered to somebody with write access. Marking a dose is one of the things that access admits, and the sweep records exactly the doses the individual cards would. It was withheld on a note about a different endpoint that it does not use. A helper with five morning tablets to confirm taps once.
-
Marking a dose skipped is offered wherever marking it taken is. The dose history offered a delegate one of the pair and not the other, though both are the same admitted verb and the medication card already offered both.
Changed
- Write access no longer covers logging a value on a metric you track yourself. The capability shipped named on the consent screen with nothing behind it: the only form that records one sits on a page a shared record does not open, so nobody could have used it. The consent copy now lists what the level actually admits. Reading those values stays.
Fixed
-
Switching records with more than one tab open could leave every tab on a spinner for half a minute. While a switch is in flight the other tabs hold, and they are released by the answer the server gives afterwards. A tab that reloaded during the switch was mistaken for somebody else starting a second one, which quietly detached the hold from the switch it belonged to, so no answer could release it and the tabs waited out the timeout instead. The release now stays attached to the switch that opened it.
-
Closing a document could leave it named in the address bar. Opening a document from a card adds a step to the browser's history so that Back closes it again, and closing it asks the browser to consume that step. The browser can decline, and when it did the sheet closed while the address bar went on naming the document, so a reload or a shared link re-opened the one you had just closed. The close now checks that the step was consumed and tidies the address bar itself when it was not.
-
Two tabs of one browser could disagree about which record they were in. Switching records moves a session rather than a tab, so a tab left open on the record you just left went on reading and writing there until somebody reloaded it. Every request now carries the record the tab believes it is in, and one naming the record it has left is refused with an answer the tab recovers from by re-reading who it is. A session that has never opened somebody else's record is unaffected, and so is the phone app, which carries the record on the request itself and never held a selector to go stale.
-
Somebody with write access could still overturn a dose you had already recorded. The rule that keeps that with the owner was on one request and the medication card reaches the same entry through another, where an explicit "skipped" was applied rather than refused, and the stock the dose had used was credited back on the way. Both now refuse a decision that disagrees with the one already there. Re-sending the same decision still works, because a second tap, a phone catching up after no signal and a retry after a partial sweep all look like that, and somebody standing over a pill box hits all three.
-
A control could be tapped in the moment before the app knew whose record was on screen, leaving somebody stranded in a form the server would refuse. The capture sheet and the dashboard's quick-entry sheets now withdraw the form when the answer arrives rather than standing on it, which is what the measurements page and the medication wizard already did.
-
Recording a dose from the dashboard quick-add says which record it landed in and no longer offers an Undo that only the owner may use.
-
The access-level options on the invitation screen are visible when reached by keyboard. The radio itself is hidden by design and its label carried no focus ring, so tabbing through the one screen where write access is granted moved an invisible selection.
-
The customise link on the mood page no longer leads a delegate to a page that tells them it is not part of what was shared.
[1.36.1] — 2026-08-04
Added
-
Somebody you have given access to can now write into your record, not only read it. The point of sharing a record is usually 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 now carries a level. 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. It admits nothing else. Editing, deleting, restoring and importing stay with you, including on an entry the other person added a minute ago.
Every entry a delegate 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 trail rather than in the row, so the record reads the same either way and the question "who put this here" still has an answer months later.
The access panel gained a feed of what has actually been contributed, and you are told when somebody marks one of your doses.
-
A control the record refuses is now simply absent rather than present and broken. Before this, a delegate opened a page inside somebody else's record, saw the button, filled the form, and was refused by the server. Nothing leaked, because the refusal was right. It still taught them the product was broken. A greyed-out button says "this exists and does not work for you"; 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 a delegate landed on was the worst page in the feature. Ten requests behind the dashboard refused, so the tile strip had no numbers and the hero rendered an error card, while every feature page underneath worked perfectly. Each of those ten has been decided rather than waved through: eight read the record, and the two that describe the person reading the screen — the interface language and the deployment's own switches — keep answering about them.
-
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. Inside somebody else's record that meant a stranger's reference ranges painted over their readings, and because the page could not load the record's own bands it stayed that way for the whole visit.
-
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 about. The history is now retired the way every other deletion in the app already works.
-
Recording a side effect against a medication and then restoring from a backup lost it. The rows travel with their medication now, and a round trip through the real backup and the real restore proves it rather than a list asserting it.
-
Adding water by hand and undoing a deleted lab result both wrote health data and left no durable record of who did it. Both write one now, like every neighbouring action.
-
The activity feed showed a blank line when a delegate marked a dose, because the sentence was keyed on a name the route does not use.
-
Marking a dose and deferring a reminder went through the same request, so somebody with write access could postpone one of your medication reminders rather than only record a dose. There was no upper bound on how far, you were not told, and the activity feed described it as marking a dose. Deferring a reminder is yours alone now, and so is changing a dose you already recorded.
-
Inside somebody else's record the BMI chart divided their weights by the height of whoever was looking. It draws nothing there now rather than a number belonging to two different people.
Internal
-
Three high-severity advisories on the outbound HTTP path, published after the last release, are closed. One of them is in the client that backs the application's single documented egress boundary, so the dispatcher's own suite was re-run against the new version rather than assumed.
-
A journey test clicked a menu after waiting for the network to go quiet, which is not the same as waiting for the page to become interactive. The click landed on markup the browser had not wired up yet and was lost, and the test then spent thirty seconds waiting for a menu that was never going to open. It waits for the control to report itself open now.
[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. Sharing is a grant from one account to another, and it is only ever a grant. Nobody logs in as anybody.
The owner sends an invitation and it confers nothing until the other person accepts. Either side can end it at any time, and it ends on the delegate's very next request rather than at their next login. An invitation can also carry a date it lapses on, checked live, so access that was meant to be temporary does not quietly become permanent because nobody remembered.
While you are inside somebody else's record the whole application says so. There is a strip across the top naming whose record you are reading, the navigation drops what does not belong to them, and everything that would let a delegate widen their own reach is simply not there: no credentials, no integrations, no notification channels, no ability to hand the access on. Read access is read access, and the server refuses a write under one regardless of what the screen offers.
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 a day by day view of which days somebody else was in it.
-
The grant row is also the consent record. One row carries who offered the access, who accepted and when, when it lapses, when it was last used, and who ended it. Revoking stamps the row rather than deleting it, so the history of who had access and between which dates survives the access itself.
Fixed
-
A backup with a symptom key this instance no longer recognises now restores. It used to fail the whole file over one retired key, so a single renamed entry cost the entire account. The unresolvable link is dropped, and the restore says exactly which keys 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, which the same page's own 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 now named.
Internal
-
The line under the top bar and the line under the sidebar header come apart whenever a banner is showing. They are meant to form one continuous seam across the window; a stack of banners pushed one of them down by as much as 184 pixels, because the banners sat inside the content column while the sidebar is beside it. The banners now span the window, so both bands start below them.
-
Chart cards on the dashboard and in Insights titled themselves one heading level below where they sit. Their neighbours in the same grid already titled themselves correctly, and nothing contained the difference, so a screen reader walking the page heard the level jump. For an account with no daily summary the page went straight from its title to a chart card with nothing in between.
-
Several accessibility checks looked at pages that had not finished arriving and reported problems the app does not have. One of them waited for a heading and then checked that the heading exists, which is a check that could never fail. They wait for real content now, and separately, the dashboard's chart cards were outside the accessibility tree until they finished revealing, so nothing had been checking them at all.
-
Fourteen checks that compare the source tree against a frozen list were walking it with a pattern that never matches a directory beginning with a dot. Three live routes under
.well-known, including both discovery documents the machine interface publishes, sat outside all fourteen while every one reported success. They now share one walker, and that walker refuses to run if it finds implausibly few files, because a sweep that finds nothing agrees with a list that contains nothing. -
The address a delegate accepted an invitation from is no longer stored. A consent record has to say who and when, and this one does. Where adds nothing and would have been kept for as long as both accounts exist.
-
Local development worktrees were excluded from git, the linter and the test runner under a directory name that is no longer the one in use, so all three were walking eleven copies of the source tree. Linting took ten minutes and reported findings from unrelated commits.
-
The backup and restore runbook now states what a backup deliberately does not carry, and what that means depending on whether you restore onto the same instance or a fresh one.
What a delegate can and cannot reach
Forty-four read routes serve a shared record. Every one of them was reviewed against five questions before it was admitted, and a route that could not answer all five stayed out. Of a hundred and twenty candidates, seventy-six did. Four that look harmless are among the refusals: one returns two numbers and no secret but shares a file with the endpoint that mints an ingest token, one differs from the list beside it by a single conditional paragraph about the owner, one reads as pure vocabulary and quietly pulls a layout preference, and three return a timestamp that doubles as the write credential for the same path.
Nothing else. Every write in those same files, and every route that was not admitted, refuses outright while somebody is inside another record. Credentials, integrations, notification channels, share links and grant management are not merely hidden from the screen; the server will not serve them.
Two of the refusals are worth naming because they are permanent rather than pending. A delegate cannot mint a clinician share link, because that would be a door surviving their own revocation. And a delegate cannot reach anything that would widen their own access, which is the property the whole feature rests on.
[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 now take height and border from one place, so they cannot come apart again without a check failing.
Internal
- A resolver that worked out a headline verdict for the dashboard is gone. Its consumer was removed a while ago and it was never wired to anything else, so it had been computing an answer nobody read. Every signal it covered already has a live owner elsewhere.
- Two waits in the browser-test helper could never time out, which turned a stalled asset into a confusing error at the following step instead of a clear one at the stall. They are bounded now.
- A browser check waited for a marker that the loading placeholders also carry, so it could inspect a page that had not finished arriving and report a problem that was not there. It waits for real content now.
[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 now refreshed nightly for every account that tracks a cycle, whether or not anyone looks at the page.
- A failed prediction write is no longer silent. It used to be discarded, so nothing anywhere recorded that it had not happened.
- 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 now set by the three paths that actually receive data, so it can only trail a real sync rather than run ahead of one.
- Badges are awarded whether or not you open the app. The date a badge was earned was recorded 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 reconstructed later. A nightly pass now records it, using the date it was actually earned rather than the date it was noticed.
Internal
- Three read endpoints no longer write. A read that writes widens what a read-only credential can reach, which the code says plainly at the place the rule is enforced, and it had drifted at three routes. A check now watches those three.
[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, something due Saturday morning is fourteen hours away and something due Sunday morning is thirty-eight, and rounding turned both into tomorrow. Both now count calendar days in the timezone on your profile.
- The assistant and the screen beside it now 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 set of places that can work out who is calling is now frozen by a check. It covers surfaces that read a session and surfaces that authenticate by a credential in the address rather than a session, which no earlier check watched at all. Adding one now fails the check until it is listed with a reason.
- One place that turns an access token into an account was outside the previous check's reach, because the check looked for a phrase the file happens to write across two lines. It matched nothing and reported success. It now tolerates the formatting and fails when it finds nothing to match, so an empty result can no longer read as a pass.
- Test fixtures that stated the score version by hand now read it from the same place the application does, so a future change to it cannot leave them behind.
[1.35.3] — 2026-08-01
Added
- Apple Watch ECG recordings can now be sent directly by the iOS app, through a
new endpoint at
POST /api/insights/ecg, instead of only arriving inside an export archive. One recording travels per request, and the endpoint is gated by the same module and status switches that already govern reading ECGs, so an account with the surface switched off is unaffected.
Changed
- A recording carries its own identity, so sending the same one again resolves to the same record. A strip that arrives once inside an export archive and again from the app ends up as a single entry rather than a duplicate or an error.
Fixed
- A page no longer grows a second scrollbar for the moment between appearing and becoming interactive. Every screen with a toggle briefly rendered a hidden form control that sat outside the scrolling area, so the whole document became scrollable to roughly twice the height of the screen until the page finished loading. On a slow connection or a busy device the window was long enough to flick past the end of a settings page and land on nothing. The scrolling area now contains its own children, which closes the whole class rather than the one page it was noticed on.
Migration
- This release removes duplicate ECG recordings. An existing database can hold more than one row for the same recording, because the export archive and a live client name the same strip differently. A uniqueness 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 actual waveform over one holding only a verdict, and it carries a removed row's link to its rhythm event onto the row that is kept, so resolving a duplicate never costs the recording its place in the timeline. Rows that lose are deleted, and they cannot be recovered. Back up the database before deploying if that matters for your instance.
[1.35.2] — 2026-08-01
Added
- The cycle ring now reads its verdict from the server: which day of the cycle you are on, how many days until the next one is expected, and whether a period is late, with the number of days. Both clients read the same answer instead of each working one out.
Changed
- The Health Score turns green at 70, the same threshold the readiness and sleep scores already used. A weak pillar still holds the band down no matter how good the average is, so a single poor area cannot be averaged out of sight.
- The score settings page was rebuilt to match the other settings cards: one reading edge, no separator lines, the save button where the other cards keep it.
- The Fitness pillar is gone. It could never produce a value, because the record cannot tell a measured fitness test from a device estimate, so it was offering a choice that did nothing.
- The arrival line under the greeting is gone. It never said what had arrived.
Fixed
- A saved score selection now comes back exactly as it was sent. One pillar was quietly stripped on the way to the server, so the page could never agree with what it had saved and the save button stayed lit for good.
- The cycle wheel follows the timezone on your profile rather than the one your browser happens to be in.
Contract
- Two share-link fields that were agreed for retirement are no longer served.
- A conflict on a repeated request now returns its error the way every other route does. One place in the whole codebase answered with a different shape.
- Five gaps in the published specification are closed, including a route that shipped with the score settings and was never described at all, and one that had been promised for months.
[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. A guard reads the types rather than the parameter names, so renaming the parameter or moving the default onto the destructuring pattern does not get past it.
- 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 test that was meant to prove a dose taken before midnight stops reminding could not fail: the stand-in for the database ignored the time window it was asked for. It now honours it, and both directions are pinned, so 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 coverage-and-provenance contract 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 its canonical category 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, which a test enforces, 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. Two guards were watching for exactly that and both measured the document, while the app scrolls inside an inner area that absorbed the overflow. They now measure what actually moves.
A third value for gender was accepted by the app and missing from the published contract 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 contract, 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 are pinned by tests, so a future 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. That is a structural fix: 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 canonical SI, 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 canonical 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 canonical SI, 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 canonical 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 canonical, 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 guard, 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 guard. 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 contract 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 guard. 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 guarded 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 contract is now 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 guards 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 guard 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 envelope 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 same guard covers the other cards. 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 output guard 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. The guard 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 output guard 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. The guard 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 refusal 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 guarded 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, a request-time future-date bound on the medication-intake import, and pinned regression tests for the snapshot-cache tenant key, the GlitchTip path-secret redaction, and the Withings credentials delete.
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 fabricated-clinical-risk refusal; the guard 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 contract. 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 their supported request contract.
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 explicit provenance 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 canonical sessions and dates, 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. A sweep found two more with 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 contract 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 contract has been 10-minute buckets since v1.30.7. The description, the example timestamp, and the min/max field notes now describe the shipped contract.
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
More audit-backlog fixes.
- 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 guard. 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 guard 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 canonical 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: