# Changelog
## [1.39.2] — 2026-09-26
Documents can be brought over from Paperless-ngx and Papra, conditions
get a body site with a view of everything filed at one place on the body,
open check-ups stay due after their reminder, and backups of very large
accounts work in the standard 1 GB container.
### Added
- **Document import from Paperless-ngx and Papra.** Settings, API &
Tokens has a Document tokens card. A document token (scope
`documents:write`, 365 days, at most ten live per person) can upload to
your own vault and do nothing else; its upload answers only
`{ id, duplicate }`. `scripts/import-documents.mjs` copies every
document with a chosen tag from Paperless-ngx 2.16 or later or from
Papra, with title and date, and supports `--since`, a dry run and
`--ai-read`. A Paperless-ngx 3.0 workflow can post new documents
directly. Each document remembers its source system and id, shown on
the document as "Imported from … · ID …". Re-sending a known source id
answers `200` as a duplicate; a source id whose document you deleted
answers `200` with `deleted: true` and stores nothing, also after the
document is purged. A file you already had is recognised by its
content, and up to 20 extra source ids are remembered per document
(`409 documents.inbound.sourceAliasLimit` past that).
`GET /api/documents/inbound/source` answers whether a source id is
known without sending the file; it and a source id in the upload's
query string share an allowance of 5,000 an hour. Uploads by a document
token have their own hourly limit, `DOCUMENT_UPLOAD_LIMIT_PER_HOUR`
(default 120, 1 to 1000), so an import never holds up your own uploads.
Thanks to @AntonPalmqvist for #1038 and to the self-hoster behind #938.
- **Hold AI reading back for imports.** An upload with `aiRead=defer` gets
a preview and a local text index but is not summarised, not staged for
lab import and not sent to a provider by any automatic path, including
the summary catch-up after switching auto-read on and Index all for
search. Read with AI or Generate summary on the document ends the hold.
The import script defers by default.
- **Body site on conditions.** A condition takes a body site in your own
words, stored encrypted, and an optional side (left, right, both), with
suggestions from sites already used. Its page links the site to the new
Body sites tab.
- **Body sites tab in Checkups.** Lists every body site across visits and
conditions, grouped regardless of spelling case. Picking a site, and
optionally a side, lists the visits and conditions there with their
linked documents, lab results and visits. Left or right includes both
sides. Conditions appear only with the illness module on and, for
someone you share with, only with Illness in their access; links they
cannot open show as placeholders. `GET /api/body-sites` serves it.
Thanks to @Cnote43 for #1025.
- **Body site in the doctor report, share page, Coach and MCP.** The
conditions table gets a Body site column when at least one condition
has a site; the clinician share page, the MCP doctor-visit summary,
`get_illness_recovery` and the Coach's illness context carry it too.
### Changed
- **Stored backups are kept in encrypted pieces.** The weekly and manual
backup is stored as sealed pieces of about 1 MiB, each checked before a
restore, preview or download releases anything, so the size of a backup
no longer depends on the app's memory. The limit derived from the heap
(about 105 MB in a 1 GB container) is gone; `BACKUP_MAX_STORED_MB`
(default 2048) is the only one left. On a test account of 2.6 million
readings in a 1 GB container: backup 87 s, preview 20 s, download of the
1.4 GB file 25 s, restore about 4.5 minutes, every reading matching.
Thanks to @mills1975 for #1031.
- **A backup is not replaced while it is being restored.** The weekly and
manual backup skip an account whose restore is queued or running; the
next run catches up. Two backups of the same account at once run one
after the other. A backup replaced while it is read answers `409
backup_changed` ("replaced by a newer one while it was being read")
instead of an error that looked like tampering.
- **Key rotation converts older backups.** Backups stored as one value
before this release, uploaded ones included, are converted into pieces
under the new key, one at a time and with little memory, keeping their
date. A backup that fails its check is left unchanged and counted as an
error.
- SimpleWebAuthn 14 and React 19.3. The passkey algorithm list is pinned,
so existing passkeys keep working.
### Fixed
- **A reminder moved an open check-up to its next date.** Sending a
reminder advanced `nextDueAt` by a full cycle without the check-up being
done, so a questionnaire every 14 days read as due in two weeks and a
yearly check-up as due next year, and neither showed on the start page.
A reminder that repeats less often than weekly, once only, or has no
occurrence left now stays due until a qualifying reading, done, skip or
snooze, and is resent at most once a week while open (new
`lastNotifiedAt`). Weekly or more frequent reminders roll on as before.
Migration `0355` returns reminders moved this way in the last 90 days to
the day they were reminded, only where the notification record shows it
unambiguously.
- **Editing a check-up rescheduled it.** Saving any change recomputed the
due date from now, and the web form resent its first date shifted to the
browser's midnight. Only a change to the interval, rule or first date
(compared by calendar day) reschedules now; a notify-hour change keeps
the day, and switching a check-up off and on keeps an open slot.
- **The start page missed check-ups and visits due today.** Check-ups due
today show by name (up to three, then "+N") from the morning of the due
day; every visit booked today stays for the whole day, with the next day
within 48 hours as a second item; both always keep a place among the
three items. Layouts saved before visits were a start page item get
them switched on once.
- "Overdue by 1 days" reads "Overdue since yesterday".
- **A restore changed Apple Health daily averages.** Restoring a backup
marked daily averages from the nightly clean-up as coming from an
unknown older source; they now come back exactly as saved.
- **Key rotation counted every weekly backup as an error.** Since 1.38.6
rotation could not re-encrypt backups written as one stream, so it never
finished clean.
### Upgrade notes
- Take a database backup before upgrading.
- Migrations: `0352` stores backups in pieces, `0353` adds the source of
imported documents, the deferred-reading marker and the tables that keep
deleted imports deleted, `0354` adds body site and side to conditions,
`0355` adds `last_notified_at`, repairs check-ups a reminder had moved
on and switches visits on for older start page layouts. None deletes
anything.
- Going back to 1.39.1 is not supported: it cannot read backups stored in
pieces. Restore the database backup from before the upgrade instead.
- 1 GB is the minimum memory for the container that runs background jobs;
in 512 MB no backup can run.
- If a key rotation reported errors for `data_backups` before, run it
again after updating, before retiring the old key.
- The API changes are additive, and iPhone app 1.0.3 and 1.0.4 keep
working. A condition edit that leaves out `bodySite` or `laterality`
keeps the stored values.
## [1.39.1] — 2026-09-25
Medication history stops counting misses from before a medication existed,
and large accounts can run their nightly jobs, export and restore again. A
restore now runs in the background with progress. New: a procedure
history, medications kept as a record without tracking intake, and
readings from home-automation bridges marked as coming from an external
device.
### Added
- **Keep a medication as a record.** Every medication has a new Track
intake switch at the top of its Schedule tab and on the last page of the
medication form. With it off, the medication keeps its dose, start and
end dates and schedule, and you can still read and change them, but
nothing is due: no reminder on any channel, no dose on the doses card or
in the today list, no Taken and Skip buttons. It is left out of
adherence on the medication card, the dashboard, the doctor report and
in the Coach, and the list marks it "Record only". Doses you logged
before stay in its history. Switched back on, it is tracked from that
moment and the days it was off never count as missed. Every medication
starts with tracking on. Thanks to @Cnote43 for #1033.
- **A history of procedures and surgeries.** A visit can be of the new
kind "Procedure or surgery", with a body site in your own words and an
optional side (left, right or both). The body site is stored encrypted.
Checkups has a third tab, Procedures, listing every procedure newest
first, with a search by body site that also looks at the reason, so an
operation recorded earlier as a general visit still turns up. An
existing visit can be switched to the new kind without typing anything
again. The doctor report has a new Surgical history section covering
your whole history; the standard selection includes it, while a
selection you saved earlier keeps what you chose. A share link shows it
when the link includes it. Thanks to @Cnote43 for #1025.
- **Sleep stages can be sent by name.** A bridge such as Tasker or Home
Assistant can post one stage to `POST /api/measurements` with
`type: "SLEEP_DURATION"`, a `sleepStage` of `IN_BED`, `ASLEEP`,
`AWAKE`, `CORE`, `DEEP` or `REM`, and either `startDate` and `endDate`
or `measuredAt` (the end) and `value` (minutes). It is stored and
deduplicated like the stages the iPhone app sends, and a repeat answers
`200` with `status: duplicate`. Sent with the ingest token, the stage is
stored with the source `EXTERNAL`. Thanks to @ocurero for #972.
- **Long background jobs show up in the app log.** A job that runs for
more than a minute writes a line naming its queue when it finishes, and
one every two minutes while it runs, with how far it has got and how
much memory the app uses. A job past its time limit writes a warning,
and after a restart the log names every job the restart cut off.
- **Out-of-range upload entries say which limit applied.** An entry in
`/api/measurements/batch` skipped as `value_out_of_range` now carries
`convertedValue` and `range` (`min`, `max`, `unit`), so a value sent as
a fraction where a percentage was expected is easy to spot.
- **A rejected measurement upload names its reason.** The validation
`422` from `/api/measurements/batch` carries
`meta.errorCode: "measurement.batch.invalid"`, so a client can tell the
batch will never be accepted as sent.
- **Units per dose like 1½.** Units per dose used to take a whole number
or one of ¼, ⅓, ½, ⅔ and ¾, so one and a half tablets could not be
saved. Any value above 0 and up to 100 with up to four decimal places
now works, for the medication, for a single dose time and for the
packaging settings on the supply tab, which before could not be saved
for a medication taken as half a tablet. The form keeps its buttons and
adds Other, which reads 1.5, 1,5, 1 1/2 or 1½ and shows what will be
saved. The card shows 1½. Thanks to @gesus14 for asking in #1034.
### Changed
- **Readings sent with the ingest token are marked as coming from an
external device.** They used to be stored as manually entered, so later
nobody could tell a reading from the scale from one typed in by hand.
They now carry the source `EXTERNAL`, show an "External device" badge
and can be picked out with the source filter in the measurements list.
You can still correct them. A request that names any `source` at all is
refused with `422`; the server decides the source from the token, and
the Home Assistant example sends none. Readings pushed before this
update stay manual, because nothing tells them apart. On a day with both
a typed and a bridged reading, the daily totals, charts, health score
and doctor report count the typed one; before, both counted and could
inflate a running total. A re-push of entries sent before the update can
create duplicates once. Thanks to @Antiheld86 for #892.
- **A restore runs in the background.** Restoring a large account takes a
few minutes, and a reverse proxy that closes a request after 60 seconds
(nginx does by default) showed an error while the restore finished on
the server. After you confirm a restore, the backups page shows the
current step and how many readings have been written, and the progress
survives leaving or reloading the page. The account shows its current
data until the restored data is written in one go. Only one restore per
account runs at a time. A failure says why and what to do next, and the
backup stays on the list so you can start again without uploading the
file. A restore interrupted by a server restart before its data is
written starts again once by itself; a second interruption is left for
you to restart. A failure after the data is written, while charts are
rebuilt, is never run again: the data is restored and the charts catch
up by the next night.
- **A job that runs out of time says more on the admin page.** It names
how long the job had and that it was either too slow for that limit or
the app restarted while it ran, instead of only "job timed out".
- **Backups of several hundred megabytes can be uploaded.** The admin page
compresses the file before sending it, `.json.gz` files are accepted as
they are, and the limit is 512 MB. The off-host restore script can write
a compressed file for this.
### Fixed
- **All-time averages and counts no longer count some days twice.** Since
1.37.19 the all-time figures combined the stored daily summaries with the
raw readings older than five years, and a day that had both was counted
in each. Long histories were affected most, and one more day every day,
as days aged past the five-year mark. The all-time mean and count on a
metric's insights page, the device and resilience tiles, the glucose
reading count and the dashboard summaries now count every reading once,
so some of these figures change after the update. Nothing needs to be
repaired in the stored data.
- **Medication history before a medication was added.** If you added a
medication at 4 pm and then logged the 2 pm dose, "Last 90 days" showed
it as off-schedule and due at the evening slot, and listed a morning slot
from before the medication existed. The dose now counts for its own slot,
and slots from before you added the medication appear only if you logged
something for them. The adherence rate, the calendar, the doctor report
and the Coach follow the same rule; the doctor report and the Coach
used to count those earlier slots as missed. The morning slot no longer
turns into a forgotten dose a day later and breaks your miss-free
streak, and migration `0350` puts back the ones already marked that way.
Thanks to @sreeramachandramurthy for the report and the screenshots
(#1028).
- **A portable restore keeps when a medication was created.** It used to
date every medication to the day of the restore, and missed doses in the
restored history dropped out of the adherence rate. Backups exported
from 1.39.1 on carry the date. For older ones the restore uses the
earliest date the backup records for that medication, such as its first
dose or the start of its first schedule, and never a date after the
restore.
- **The adherence calendar no longer counts a skipped or missed entry
that belongs to no scheduled time as a dose taken.**
- **A dose attributed to a slot by hand reads the same as one logged
there.** A dose taken shortly before the slot's window counted as on
time on its own but as late once attributed. The confirmation now says
what attributing does.
- **Nightly jobs finish on large accounts.** On an account with more than
a million readings, for example after a multi-year Apple Health import,
the nightly clean-up of heart-rate, HRV, blood-oxygen, step and energy
samples and the backup could fail with "job timed out": the clean-up
loaded a type's whole history into memory and the backup built its whole
copy in memory, and the app ran out of memory and restarted. The
clean-up now works one day at a time with about a third of the memory,
and when it runs short of time it stops and continues where it left off.
The backup saves its copy piece by piece and keeps the previous one if a
run fails. On an account of 1.25 million readings in the standard 1 GB
container both finish in a few minutes while the app keeps answering.
Thanks to @mills1975 for the report (#1031).
- **Large accounts can export and restore.** Downloading the full export
of such an account, with or without a passphrase, ran the app out of
memory, and its weekly backup could not be restored, opened or
downloaded; the admin page wrongly said it "could not be decrypted".
Exports now stream out as they are written and backups are read in small
parts. On the same account the export takes about 25 seconds and a
restore about two and a half minutes, and the restored readings match
the backup.
- **An Apple Health export import needs much less memory.** An import of
300,000 readings held 12 MB instead of 53 MB and ran in 35 seconds
instead of 55. After an import, chart summaries are rebuilt only for the
period the import covered.
- **Smaller memory fixes for large accounts.** The half-hourly check for
personal records no longer loads a type's whole history. Rotating the
encryption key no longer reads every measurement at once, and its report
counts only rows that hold encrypted data. The one-time repair of step,
energy and distance records can no longer lose them when interrupted.
A retried off-host backup continues with the accounts it has not reached
yet.
- **The cycle calendar shows intercourse and other logged entries.** A day
with intercourse carries a small diamond, explained in the legend. A
pregnancy, progesterone or ovulation result, contraception, spotting, a
temperature, a mucus or cervix reading or a note shows as a small ring,
so such a day no longer looks empty. Screen readers name each entry.
Someone who sees your cycle through a shared account sees these marks
only if they could already open the day. Thanks to @Nazza01 for the
report and the screenshot (#1032).
- **Editing a vaccination dose no longer removes its documents.** Ever
since documents could be linked to doses (1.37.3), a dose opened for
editing did not know its linked documents: it showed "Link a document"
with nothing under it, and saving any change, such as a batch number,
quietly unlinked every document. That was our bug. The dose now shows
its linked pages and saving leaves them in place.
- **Document links are easier to tell apart and harder to remove by
accident.** On a document, each linked dose or visit shows its date, so
three COVID-19 doses no longer read as three identical chips. Clicking a
chip opens that dose, visit, condition or document; only the small x
removes a link, and you can undo right after. Thanks to @Cnote43 for
writing up every step (#1024).
- **`/api/user/profile` reports the time zone the server uses.** When the
stored zone is unusable it shows the instance default, as `/api/auth/me`
already did.
- **`upsert_failed` in the nutrient and cycle uploads means "send it
again".** The API description said to drop such an entry. A retry with
the same `Idempotency-Key` now writes again instead of getting the
earlier failure back.
### Upgrade notes
- Take a database backup before upgrading.
- Migrations: `0346` adds the `EXTERNAL` measurement source, `0347` adds
the table that tracks restore jobs, `0349` adds body site and side to
visits, `0350` returns misses wrongly stamped on slots before a
medication's creation to pending (nothing is deleted), and `0351` adds
the Track intake switch, on for every medication.
- `POST /api/admin/backups/{id}/restore` now answers `202` with a `jobId`
and a `statusUrl`. `GET /api/admin/backups/restores/{jobId}` returns the
job's status and result, `GET /api/admin/backups/restores` lists recent
restores, and a second restore of the same account answers `409` with
`meta.errorCode: backup.restore.active`.
- Everything else in the API is additive, and iPhone app 1.0.3 and 1.0.4
keep working. For a record-only medication the shipped app stops
reminding, but its card still shows Taken and Skip, and a schedule
change made on the phone is not applied. Change that schedule on the
web for now.
## [1.39.0] — 2026-09-25
AI is optional everywhere now: your data loads whatever the AI settings
say, and each AI switch stops the work it names instead of hiding it. A
module you switch off leaves the whole app, and there are fixes for
heart-rate streams, charts, the cycle calendar and labs.
### Added
- **The weight tile judges a change against your own target.** Below your
target band a gain counts as progress, above it a loss does, and inside
the band staying steady is progress while a small move either way is
shown neutrally. The weight note, the daily briefing, the overview
analysis and the Coach know the target too, and changing the target
refreshes the weight note without a new weigh-in. Without a target the
tile works as before. Thanks to @COBO5671 for #1006 and the screenshots.
- **A document can be linked to every visit and vaccination it covers.**
The document's own view in the vault lists the visits and vaccination
doses it belongs to and lets you add or remove them, so a childhood
vaccination record can be filed against every dose it lists in a few
taps. Links made from either side are the same links. Thanks to @Cnote43
for #1024 and @CritLoren for #1005.
- **The vaccination page shows the booster you planned.** Next to each
vaccine's heading it says when the next booster is due, marked once it
is due within a month and again once it is overdue. The app shows the
boosters you planned and does not work out dates of its own (#1005).
- **A new "Reading documents" switch for people who run a server.** It
sits under Admin, Coach and covers AI reading of stored documents,
lab report scans and turning a typed medication description into a
schedule. It is on by default.
- **Reading documents has its own consent.** Sending a document, a lab
scan or a medication description to an AI service outside your server
now needs consent for reading documents, including with your own OpenAI
or Anthropic key or your own ChatGPT account. A self-hosted model needs
none. The app asks at the place you start the read and grants exactly
that; the consent has its own withdraw button in Settings, AI. It does
not open the Coach or AI analysis, and consent for AI analysis alone
does not cover documents. A full AI consent given before this release
still covers both.
- **What the Coach stored stays yours with the Coach off.** Your
conversations, the facts it remembered, your plans and the reminders you
asked it to keep can be read and deleted in Settings whether the Coach
is switched off on the server, hidden by you, or has no provider.
Confirming, finishing or moving a plan or reminder waits until the Coach
is back.
- **One short hint instead of a message on every card.** While no AI
provider is set up and you could set one up, Insights and Settings, AI
show a single note that AI is optional, with a way to set it up and a
"Not now" that hides it on both pages. With AI switched off on the
server, setup and the getting-started list do not bring AI up.
### Changed
- **Your data never depends on AI.** Readings, charts, scores,
correlations, ECG recordings, heart rhythm events, the breathing and
lab-change cards, intraday pulse and the baseline strip on each metric
page load the same with AI on or off, with or without a provider, and
with or without your consent. Apple Watch ECG uploads are
stored in every case. Before, several of these reads sat behind AI
switches, and the Insights overview could fail with "Failed to load
insights". Thanks to @CritLoren for the log in #975.
- **AI parts are left out when AI is off for you.** The daily briefing,
the sentence under your greeting and under the trend charts, the notes
on metric pages, lab markers and workouts, the line after a new reading,
the Coach button and menu entry, and the "describe it in words" button
in the medication wizard only appear while they can work. Text a model
wrote earlier is hidden from the next page load and returns when AI is
back; a switch or a missing provider deletes nothing. A summary the app
composed from your numbers alone is not AI text and stays.
- **Nothing is written by a model at night unless AI is on for you.** The
nightly briefing, status notes, period summaries, arrival lines, workout
notes and Coach memory upkeep now check the server's switches, your AI
analysis switch, your provider and your consent for each person as the
run reaches them. Switching AI off during a nightly run stops the rest
of that run, and a reply that arrives after you withdrew consent is
thrown away. Week and month summaries still appear, written from your
numbers, and Coach nudges arrive in their plain form when only consent
is missing.
- **The assistant switches stop AI work instead of hiding it.** When a
switch in Admin, Coach is off, nothing it covers is sent to a model,
and that now includes document reading, lab scans and medication text,
which the switches used to leave running. A read queued before the
switch was turned off stops when it runs. "Status cards" is renamed
"Status notes". If the server cannot read the switches for a moment,
every AI feature stays off for that request.
- **The Coach has one switch for people who run a server.** It is the
Coach switch under Admin, Coach. The Coach row under module
availability now follows it and links to it; an operator who had turned
the Coach module off there has the Coach switch turned off.
- **The "Insights" module is now "AI analysis".** It decides whether a
model writes the briefing, status notes and workout notes for you. With
it off, the Insights tab stays in the menu and the bottom bar, and the
Today summary and the readiness, recovery and sleep score rings stay on
the dashboard. Breathing screening now follows the Sleep module and
"What changed since your last panel" follows the Labs module.
- **Hiding the Coach hides only the Coach.** It used to stop the nightly
briefing and status notes as well, which the setting never said. If you
had the Coach hidden, AI analysis is switched off for you in this
release so nothing new is written; switch it back on under Settings,
Modules if you want the notes.
- **The morning briefing notification keeps coming with AI analysis
off.** It carries your health score or the day's reminders without the
first sentence a model would have written.
- **Someone managing your record sees the stored AI text you would see.**
Your briefing, notes and summaries show for them whenever they show for
you, but opening your record never starts AI work on it.
- **Coach reminders wait while the Coach is unavailable.** They were
surfaced into a conversation nobody could open and dismissed after a few
unanswered rounds. They now wait and appear once the Coach is back.
- **Withdrawing AI consent deletes the notes written under it.** When no
consent for AI analysis is left, the status notes, the daily briefing,
the model-written period summaries, the arrival lines and the workout
notes are deleted in the same step. Your Coach conversations, the facts
it remembered, your plans and reminders, and your document summaries are
your records and stay. The confirmation says which of these applies to
you. When the consent card cannot read your consent, it says so with a
retry instead of showing it as withdrawn.
- **Switching a module off removes it everywhere.** Mood, Sleep, Labs and
the other modules now leave the Insights tabs, the trends row on the
overview, the add menu (including the measurement types they own, such
as sleep and glucose), the dashboard tiles, the overview layout settings
and the score rings. The trends row moves on to the next chart so it
still shows three.
- **Correlations only use what you track.** The pattern finder, and the
patterns the Coach, the status notes and the weekly and monthly review
look at, leave out mood, sleep, glucose, medication adherence, symptoms,
weather or lab results once that module is off. Turning it back on
brings them back, and patterns you had dismissed stay dismissed. Because
fewer pairs are checked, a weak pattern between two things you still
track can appear or disappear when you switch a module.
- **A link to a switched-off page says why.** A bookmark to a page whose
module is off shows a short notice with a button to Settings, Modules
when the switch is yours. When the page is outside what someone shared
with you or the server turned the module off for everyone, it says so
and leaves the button out. The Nutrients page uses the same notice and
its own "turn on" button is gone.
- **Clearer messages when AI is refused.** The Coach, document reading,
lab scans and the medication reader tell a missing consent, AI switched
off on the server, a record someone shared with you and a missing
provider apart. A spent daily budget in the medication reader no longer
shows "too many requests". Settings, AI names what the server has
switched off, and "Test connection" says AI is off on this server
instead of calling the model or showing an error code.
- **Status notes live in their own encrypted storage.** They used to be
kept as plain text in the audit log. They are now encrypted like the
rest of your health data, covered by key rotation, left out of backups
because they are rebuilt each night, and removed by "Delete All Data".
- **A year of per-minute heart rate reads in about half the time** when a
chart asks for it.
- **If you write against the API**, `GET /api/auth/me` gains
`ai: { capabilities, provider }`, which says per feature and for the
record in use whether AI is available and why not. Routes that serve
data no longer answer `403` for an AI reason; routes that mix data with
model text answer `200` with the model text as `null` and an `ai` object
giving the reason. This covers `GET /api/insights/comprehensive`,
`/api/insights/derived`, `/api/insights/narrative`, the status note
routes, `/api/workouts/{id}`, `/api/daily/digest` and
`/api/dashboard/snapshot`, among others. AI refusals carry
`meta.errorCode` with `meta.capability`, `meta.reason` and, where it
applies, `meta.module`; the new codes are `ai.record.notPermitted`,
`ai.provider.none` and `ai.unavailable`. Document reading, lab scans and
medication text refuse with `assistant.disabled.documentAi`, and
`/api/insights/generate` with `assistant.disabled.briefing` instead of
`assistant.disabled.coach`. The consent routes accept a new kind,
`ai_extraction`. `aiAvailable` on `GET /api/user/ai-provider` is false
while the master switch is off, and every card from
`GET /api/insights/cards` says `provider: "rules"`.
- **More for API clients.** New additive fields are
`tiles.weightTrend` on `GET /api/dashboard/snapshot` (`direction` is
`up-good`, `up-bad` or the new `hold`), `renewals` on
`GET /api/vaccinations`, `vaccinationLinks` on
`GET /api/documents/inbound/{id}` and `vaccinationIds` on its `PATCH`,
and `cycleDay` and `periodEndable` on each day of
`GET /api/cycle/calendar`. `GET /api/medications/{id}/dose-history`
reports `from` as the requested start rather than the medication's
creation time. `GET /api/auth/me` reports the server's default time
zone when the stored one cannot be used.
### Removed
- **The "Correlations" assistant switch.** It only ever turned off
statistics, which no model writes, so correlations now always load.
### Fixed
- **The worker no longer runs out of memory on a watch that streams heart
rate.** On an account with heart rate every minute or faster, the worker
grew until it crashed with "JavaScript heap out of memory" within 15 to
40 minutes, because the heart-rate status card read three months of
readings one by one. The card and the other background summaries,
including the Coach's cycle summary, now read one total per day. If you
raised `NODE_OPTIONS=--max-old-space-size` to keep the worker up, you
can leave it or remove it. Thanks to @ckaotik for the report, the logs
and the patient testing in #1023.
- **Daily totals stay on the same day across chart ranges.** Steps,
active energy, flights climbed and walking distance could move to the
neighbouring day between the 7-day and 30-day ranges, because the longer
ranges cut days at midnight UTC. Every range of single days, up to a
year, now cuts them at your own midnight; longer ranges still group
weeks, months and years by UTC days. Thanks to @mills1975 for #1026 and
the screenshots.
- **The 7-day chart shows the whole week on a streaming source.** It read
individual readings up to a limit that per-minute heart rate reaches
after about three and a half days. Every range now reads one value per
day, and the 7-day and 30-day ranges agree (#1026).
- **Charts on the medication page, in the Coach and in recovery use your
time zone.** They cut days in Berlin for everyone. When a stored time
zone cannot be used, the server and the app both fall back to the
server's default. A bare offset such as `+05:30` is no longer accepted
as a time zone, because the database would cut days on the wrong side of
UTC; pick a named zone such as Asia/Kolkata (#1026).
- **Recent doses appear under "Last 90 days" in a medication's history.**
A dose logged for a time before the medication was added, a dose taken
shortly before its scheduled time, and a dose added while the tab was
open were missing until later or until a reload. Thanks to
@sreeramachandramurthy for #1028 and for spotting the time pattern.
- **Past dates in the cycle calendar show their own cycle day.** The log
sheet labelled any date with today's cycle day, and "My period ended
this day" only appeared while today was a period day. It now shows the
day of the cycle the date belongs to and offers the period end on the
first days of any logged cycle. Thanks to @sreeramachandramurthy for
#1004 and the step-by-step notes.
- **Save in the cycle log needs something to save.** An empty Save stored
an empty entry the calendar could not show, which looked like a failed
save. It now asks you to pick a flow, a symptom or another sign (#1004).
- **Cycle history entered out of order lands on the same cycles.**
Logging the second day of a period before the first no longer opens a
separate one-day cycle; the earlier day becomes the start, and days
logged before a back-filled start move to the right cycle. If you
already have such a one-day cycle, open the real first day and tap "My
period started this day" again. A start tapped by mistake a few days
early can be removed again, and the start it replaced comes back
(#1004).
- **Selecting a cycle symptom no longer moves the list.** Symptoms are one
per line with the optional 1 to 4 intensity beside them. Thanks to
@sreeramachandramurthy for #1001.
- **Labs "Save and add another" keeps your biomarker.** The second save
said "Pick a biomarker first" although one was shown. The form now keeps
the biomarker, the date and the visit and clears only the value and its
note. Thanks to @sreeramachandramurthy for #1029.
- **Manual entry has the rest of the scale's readings.** Visceral fat, fat
mass, fat-free mass, lean body mass and muscle mass can be entered by
hand, in kilograms or pounds as you set. French, Italian and Polish
labels that gave two of these one name are distinct. Thanks to @ocurero
for #1013.
- **The "Link a document" picker offers your whole vault.** It said "No
documents yet." on vaccinations and visits even with a full vault,
because it asked for more documents per page than the server accepts.
Documents dated within a week come first, the rest are listed by month
and searchable, and a failed load shows a retry. The condition picker on
visits had the same problem (#1024, #1005).
- **The Insights overview no longer fails as a whole.** Each section loads
on its own and a failed one offers a retry. "Today's signal", the heart
rhythm events and the mood patterns card say when they could not load
instead of hiding or spinning, a score page no longer says "not enough
data" when it could not read the score, and with the cycle module on but
no cycle in progress the scores row no longer leaves a gap (#975).
- **Restoring instance settings from a backup made before this release
keeps the Coach off** when the backup had the Coach module switched off.
### Upgrade notes
- **Take a database backup before upgrading.** Three migrations run.
`0343` adds the "Reading documents" switch, moves the old Coach module
setting into the Coach switch, switches AI analysis off for everyone who
had the Coach hidden and removes the Correlations switch. It cannot be
undone automatically; going back to an earlier version means restoring
that backup. `0344` creates the encrypted status note storage and
deletes the old status note rows from the audit log without copying
them, so the next night writes every note again, about one normal night
of extra model use. `0345` adds one column to cycle starts and changes
nothing else.
- **The Correlations switch is gone** from Admin, Coach. Correlations
always load now.
- **`GET /api/feature-flags` is deprecated.** It still answers, with a
`Deprecation` header pointing to `GET /api/auth/me`, and the web app no
longer reads it. The iPhone app and other clients should read `ai` on
`GET /api/auth/me` instead, expect `200` with `null` model text on the
mixed reads listed under Changed, and handle the three new error codes.
The route is removed in the first release after the next iPhone app.
## [1.38.25] — 2026-09-20
Three readings that never arrived, and one import that turned away a
perfectly good export.
### Fixed
- **Oxygen saturation and body fat from Apple Health reach the server
again.** The app sent both as a percentage while the server scaled them
a second time, so a reading of 97 % arrived as 9700, failed the
plausibility check, and was dropped. Worse, the phone treated that as
final and moved past the reading, so it never tried again. The importer
now takes either spelling, which means current app versions stop losing
those readings without waiting for an app update. Thanks to
@mbreitkreuz for tracing it across both sides.
- **A cycle symptom can be logged without an intensity again.** Picking a
symptom and saving without tapping the 1 to 4 selector was refused with
"Could not save. Try again.", and removing the symptoms made the save
work, which pointed at the wrong thing entirely. An unrated symptom has
always been a valid entry; only the check at the entrance disagreed.
Thanks to @sreeramachandramurthy for the report and the log.
- **The Apple Health import accepts your export whatever iOS named it.**
The importer looked for `export.xml` in lowercase only. iOS writes that
name in the language of the phone and some versions capitalise it, so
ordinary exports were refused with a message claiming the archive was
invalid. It now matches in any capitalisation, takes a translated name
when it is unambiguous, and when it really cannot find the file it lists
what the archive does contain. Thanks to @mills1975 for the report.
## [1.38.24] — 2026-09-16
Insights loads when the assistant is switched off, and the bundled
database no longer ages behind its own security fixes.
### Fixed
- **Insights loads with the assistant switched off.** An operator who turned
the assistant off on the server got an error in place of the whole Insights
overview. Its main read and several tiles were tied to the assistant switch
even though none of them uses a model, so the derived tiles, the ECG list and
the device-flagged rhythm events stayed dark, and a watch ECG sent from the
phone was refused. These reads follow the Insights module now, and the parts
a model writes still disappear when the assistant is off. Thanks to
@CritLoren for the log in #975 that showed which requests failed.
- **The trends row leaves mood out when the mood module is off.** Switching
the mood module off already hid the Mood tab on Insights, but the overview
still drew a mood chart in its trends row. It skips mood now, the same way
the tab strip does.
### Changed
- **The bundled database moves to PostgreSQL 16.15, and stays current from
here.** The compose file pinned 16.14, and the pin is deliberate: a database
change should be something you can read before it happens. What was missing
was anything watching it, so it sat five weeks behind a release that carried
security and data-corruption fixes. New PostgreSQL minor releases now arrive
as an update we review and ship. Upgrading within PostgreSQL 16 needs no dump
or restore. If you run your own PostgreSQL instead of the bundled one,
nothing here affects you. Thanks to @tarantila for #987.
- **Supply chain.** The container scanner's installer is fetched from a commit
and checked against its hash before it runs, PostgreSQL major versions no
longer arrive as automatic update proposals, and the app moves to nodemailer
10 along with updates to sax, sonner, jose, the canvas renderer and the
virtual-list helper. Nothing changes in how the app behaves.
## [1.38.23] — 2026-09-15
Connecting an AI assistant through MCP works in Chrome and Edge again, and
lab-report scans can be raised past six an hour.
### Fixed
- **Connecting an AI assistant through MCP works in Chrome and Edge again.**
After you pressed Allow on the consent screen, the grant went through but
the browser stayed on the page and never returned to the assistant, so
claude.ai and other clients waited forever. The consent page carried a
security policy that only allowed its own address as a form target, and
Chromium-based browsers apply that to the redirect back to the client as
well. The consent page now allows exactly the return address it has
checked against the connecting client, and nothing else. If you added a
reverse-proxy rule to work around this, you can remove it. Thanks to
@Mark-Milkis for tracking it down in #983.
- **A lab scan that never reached the AI no longer uses up one of your
scans.** A file that was too large, not an image or PDF, a PDF that could
not be rendered, or a scan the daily AI budget turned away still counted
against the hourly limit. Only scans that are actually sent to the provider
count now.
### Added
- **If you host this yourself, you can raise the lab-scan limit.** Scanning a
stack of reports after a doctor's visit ran into a fixed six scans an hour.
Set `LABS_OCR_LIMIT_PER_HOUR` to allow more (default 6, between 1 and 1000),
the same way `DOCUMENT_AI_LIMIT_PER_HOUR` works for documents. The daily AI
budget still bounds the cost. Thanks to @rkerian for #985.
### Changed
- **If you write against the API**, the redirect after the MCP consent
decision is now `303 See Other` instead of `302`. The 429 from
`POST /api/labs/ocr/extract` now also carries `meta.retryAt`.
- Prisma moves to 7.10 for the app, its client and the migration tool in the
image, together with updates to Playwright, lucide-react, pdfjs and the CI
actions. Nothing changes in how the app behaves.
## [1.38.22] — 2026-09-13
A channel test no longer runs on settings you have not saved.
### Fixed
- **The Test button waits until you save.** On the webhook, ntfy, email and
Telegram cards, Test always sends through the settings that are saved,
not what is in the form. After you changed a field, the card still showed
"Saved" from the last save, and Test quietly used the old settings. That
is how a webhook switched to the Gotify format kept failing with the old
message body. Now the "Saved" note goes away as soon as you change
something, and Test stays off with a short "Save your changes first"
until you save. Thanks to @sreeramachandramurthy for the screenshot in
#947 that showed it.
## [1.38.21] — 2026-09-13
A channel test says why it failed, a Gotify server gets the message it
expects, and Settings is back inside a profile you look after.
### Added
- **The webhook can send in Gotify's format.** Gotify expects the priority
as a number and refused HealthLog's messages, which carry it as a word.
Choose Gotify under Payload format on the webhook card and the message
arrives the way Gotify wants it: routine events with sound, medication
reminders as a banner on Gotify's Android app, urgent events at the top
of the scale. Existing webhooks keep sending exactly what they sent
before. The notifications guide now shows both message formats and how
to set up Gotify, with the URL ending in `/message` and the token in the
`X-Gotify-Key` header. Thanks to @sreeramachandramurthy for #947.
### Fixed
- **Pressing Test on the webhook, ntfy or email card now tells you what
went wrong.** When the other end refused the message, the card said
"Test failed" and nothing else, although the server knew the answer; one
self-hoster had to dig through the container log to learn that their
relay had answered 400. The card now names the cause, such as a refused
token, a wrong address, a relay that rejected the message or a mail
server that did not answer in time, with the status code. For webhook
and ntfy it also quotes the first part of the relay's own error message,
unless it looks like a token or contains one of your saved secrets. Real
deliveries were never affected; only the test button hid the reason.
The notification test in the admin area shows the same cause for each
channel, and a relay refusing a test no longer counts as a server error
in the operator's log. Thanks to @sreeramachandramurthy for #947.
- **The hint under the webhook address no longer says it must be public.**
A target on your own network works when the server operator lists it in
`NOTIFICATION_PRIVATE_ORIGINS`, which has been possible since 1.38.17.
- **Settings is back in the menu while you are inside a profile you look
after.** Switching into a managed profile removed Settings from the menu
entirely, on the desktop and on the phone, even though the profile's own
settings page existed and worked: which modules it tracks, its targets and
where its notifications go. The only way there was typing the address.
The menu now offers Settings inside that profile and opens the profile's
own page. If someone gave you manage access to their own account, Settings
opens their medical history, the one settings page that access covers.
Notifications stays out of the menu while you are inside someone else's
record. Thanks to @sreeramachandramurthy for asking in #939.
- **The note on a manage invitation says when a second factor is needed.**
It said offering manage access always asks for your second factor. It
asks only if you have one set up; that it works only in a browser is true
either way.
- **A different app on the address where HealthLog used to run now loads
normally.** If you stopped HealthLog and started something else on the
same address and port, HealthLog's offline worker in your browser kept
answering: the other app got HealthLog's cached files and your changes to
it never showed up, until you removed the worker by hand in the browser's
developer tools. The worker now checks whether the page still comes from
HealthLog, and when it does not, it clears its cache and removes itself.
When HealthLog is only down, or a reverse proxy in front of it answers
with an error, nothing changes and the offline page still appears. If
you had push notifications switched on in that browser, the worker stays
registered and keeps them, and only stops serving its cached files. Thanks
to @el-abcd for describing this in #847.
- **If you ship logs to Loki, a Loki on your own network works now, and a
failed push says so.** Only public addresses were allowed, so a Loki on
the same Docker network or on your LAN was refused without a word, and
events were dropped. `LOKI_ENDPOINT` comes from your server environment,
so the address you set there is now trusted as it is; link-local and
cloud metadata addresses stay refused. It also takes either the base URL
or the full push URL ending in `/loki/api/v1/push`; pasting the full URL
used to break shipping just as quietly. When a push fails, the container
log gets one line with the Loki address, the reason and how many events
were lost, at most every five minutes per reason, and never the password.
Thanks to @sreeramachandramurthy for the empty Grafana in #947.
### Changed
- **If you write against the API, a failed channel test answers 502 with
the cause.** `POST /api/settings/webhook/test`, `/ntfy/test` and
`/email/test` used to answer 500 for every refusal from the other end.
They answer 502 now, with `meta.errorCode` (one of
`credentials_rejected`, `endpoint_not_found`, `rate_limited`,
`upstream_error`, `upstream_rejected`, `redirected`, `timeout`,
`connection_failed`), `meta.upstreamStatus`, and for webhook and ntfy
`meta.upstreamBody` with at most 200 characters of the relay's reply;
email carries `meta.smtpCode` instead. The 422 for a private address
that is not allowed is unchanged, and 500 is left for failures the
server cannot name. `GET /api/settings/webhook` gains `format`
(`generic` or `gotify`); a `PUT` without it keeps the stored value.
## [1.38.20] — 2026-09-11
Signing in no longer tells anybody which addresses have an account here.
### Security
- **Signing in now takes the same time whether or not the account exists.**
A wrong password and an address nobody has registered here have always
given you the same answer on screen, but the server used to reach that
answer much faster when there was no account to check, and the same when
the account signs in with a passkey and has no password at all. Someone
probing the sign-in page could read that difference and learn which
addresses are registered, and which of those have a password. Every
refusal now does the same work before it answers.
- **Changing your email address is limited to ten changes an hour.** The
server tells you when the address you typed already belongs to another
account, which is the only sensible thing to say to someone who typed one.
Nothing stopped a signed-in caller from asking that question over and over
about addresses that were not theirs. Ten an hour per account is far above
any real use of the field, and a save that leaves your address as it is
does not count against it. If you hit the limit while saving other things
on the settings page, those are saved and only the address is held back,
with a note under the field. If you write against the API, a request that
asked for nothing but the address gets a 429 on `PUT /api/auth/profile`
and `PATCH /api/user/profile` with `meta.errorCode` set to
`profile.update.emailRateLimited` and a `Retry-After` header saying how
long to wait; a request that carried other fields gets 200 with `email`
listed in `rejectedFields` under the code `rate_limited`.
### Removed
- **The endpoint that answered whether an address has an account here is
gone.** `POST /api/auth/check-user` took an email address or a username
and said, to anyone who asked and without any sign-in, whether an account
existed and whether it had a passkey or a password. It was built to decide
which field a sign-in screen shows first, which is not worth telling the
world who has an account here. The path now answers 410 and says so in the
API reference rather than disappearing, so anything still calling it gets
a clear "this is gone, stop retrying" instead of a 404 that looks like a
broken server. If you wrote something against it, the sign-in request
itself is the replacement: send the credential and read the answer.
## [1.38.19] — 2026-09-11
The first run tells you what is actually true about your account, an
invitation link behaves, and the AI you ask a question to has budget left
when you ask it.
### Fixed
- **An invitation link opened while you are already signed in no longer
offers to register.** It showed a registration form, and completing it
quietly swapped the session you were in for a brand new account. The link
now says you are signed in and what you can do about it, and the server
refuses the registration outright rather than relying on the screen to
hide the button.
- **The setup step that connects a device says what is true about it.**
Every provider was described as "connected, first sync running", including
ones that had never synced and ones nobody had connected. A provider with
no history of its own says so now, and the step offers to connect it.
- **The last screen of the setup no longer claims the AI works when nobody
has asked it anything.** The offer appeared, and said insights were ready,
as soon as an instance had a provider configured. It appears only when
that provider has actually answered, and a freshly installed instance says
its provider has not been tried yet instead of announcing a failure.
- **A page whose module is switched off says which switch it is** instead of
showing an empty page, and the getting-started checklist no longer asks
for things that are already done.
- **Background insights can no longer use up the day before you ask a
question.** The daily AI limit counted every request against one number,
whoever was paying for the model behind it, so a day of automatic insights
could leave your own question refused. It is counted per payer now, and a
fifth of it is held back for what you do by hand: on a plan you bring
yourself the automatic work runs up to 1 600 000 tokens a day and your
questions keep 400 000. Nothing changes for an instance whose AI is only
used by hand.
### Security
- **The demo account can no longer be taken over through the setup step.**
The step that asks for date of birth, height and sex went through a route
that accepts eleven fields, including the contact address of the account
everyone shares. It writes those three and nothing else now, refused on
the server rather than left out of the form.
### Changed
- **`GET /api/integrations/status` can answer `unknown`** for a provider
with no sync history, next to the existing values; that case used to be
reported as `connected`. The native client does not read this field.
- **`GET /api/user/ai-provider` gains three fields** describing the
instance's shared AI: whether it is healthy, untried or failing, whether
it may be offered, and whether the person already agreed to it.
- **When the AI limit stops a request, the answer says which ceiling was
reached** and whose budget it was.
- **Two migrations run on start** (`0340_coach_usage_operator_tokens`,
`0341_arrival_reaction_cost_owner`), both adding one column with a
default.
## [1.38.18] — 2026-09-10
The first run asks what you need and switches on exactly that; the old
step-by-step wizard and the columns it wrote are gone.
### Added
- **A needs-based setup replaces the wizard.** A few questions (who the
record is for, the areas to watch, medication on a schedule, where
readings come from, an upcoming visit), one about units only when an area
needs it and the account does not hold it yet, then a confirm screen that
shows what the answers switch on, one first task with its form right there
(connect a source, add the first medication with a reminder, or log one
reading), and a done screen with the checklist open. Every answer is saved
as it is given, so leaving and returning resumes at the same step; every
question but the first can be skipped, and "Skip for now" on the welcome
screen leaves the app usable with nothing switched on. "Set up again" and
"Show the checklist again" live under Settings → Account. The server works
out the modules from the answers once, the way v1.38.16 introduced; a
module the person switched on by hand, or one that already holds data, is
never turned off by an answer.
- **Answers given for someone you look after go to their record.** With
"someone I look after" the questions run in your own record, their record
is created on the confirm screen, and the modules go to that record while
yours stay as they were. Finishing without creating it changes nothing on
your record either. The checklist gets a row for that record instead of
reading those answers as yours. If you write against the API:
`POST /api/onboarding/complete` takes `managedRecordId`; a record the
caller does not manage is a 404, as on the managed-profile routes.
- Module pages say which switch is off. A page whose module is off shows an
empty state that names the switch instead of a blank page, and the two
tour stops that had nothing to point at (integrations, the health record)
have their anchors.
- The setup is available in every language the app has.
### Changed
- **The dashboard order is arranged from the answers the first time,** and
only while it has not been arranged by hand, so an order somebody set
themselves is never overwritten.
### Removed
- **`POST /api/onboarding/step`, the goal slugs, and the
`users.onboarding_step` and `users.onboarding_goals` columns** (migration
`0339_retire_wizard_columns`). The iOS app never called the route or read
the columns; `POST /api/onboarding/disclaimer`,
`POST /api/onboarding/tour` and the disclaimer and tour fields on
`GET /api/auth/me` are unchanged.
### Upgrade note
- **Take a backup before deploying, and expect no image rollback.** The
migration drops the two columns the v1.38.16 and v1.38.17 images read on
every sign-in, so an older image against a migrated database fails at
login, not only in the wizard. Rolling the image back means restoring the
database, or re-adding the columns with their defaults, which loses
nothing because nothing at this version writes them:
`ALTER TABLE users ADD COLUMN onboarding_step integer NOT NULL DEFAULT 0, ADD COLUMN onboarding_goals text[] NOT NULL DEFAULT '{}';`
## [1.38.17] — 2026-09-10
The second of two close doses can be logged from the card again, and a
webhook or ntfy target on your own network can be reached again with the
operator's say-so.
### Fixed
- **The second of two close doses can be logged from the card again.** On
a schedule whose neighbouring doses sit closer than twelve hours (08:00
and 12:00, say), taking the first dose made the card offer tomorrow's
first dose, and a skip pressed next landed on tomorrow. A taken, skipped
or auto-missed dose counted as settling every scheduled time within a
window sized for the twice-daily gap, so the first dose swallowed its
neighbour and the card walked past it. A logged dose now settles exactly
one scheduled time, the nearest one within that window, and the
adherence ring on the card follows the same rule, so the due slot and
the ring cannot disagree. The window still covers a dose logged a little
off its scheduled time, or across a clock change.
- **You can allow a webhook or ntfy target on a private network (#947).**
The senders look the target up at send time and refuse a private,
loopback, link-local or CGNAT address; that is the DNS-rebinding
protection from #217, and it stopped a Gotify behind a LAN reverse proxy
exactly as designed, with no way to allow it.
`NOTIFICATION_PRIVATE_ORIGINS` is that way now: a comma-separated list
of exact origins such as
`https://gotify.example.com,http://ntfy.lan:8080`, read from the server
environment only. A listed origin is still looked up, the address it
resolved to is the one the connection uses, and redirects are forbidden.
Link-local, the metadata range and the unspecified address stay refused
even when listed; loopback can be listed for a relay on host networking.
A malformed entry is logged once, without its query string or
credentials, and allows nothing. Listing a name does not cover its
sub-hosts or other ports, because a private name cannot be told from a
public one at save time. The grammar is the same as
`NIGHTSCOUT_PRIVATE_ORIGINS`, which now shares the parser and the
address rules; existing Nightscout entries, loopback ones on host
networking included, behave exactly as before. Reported by
@sreeramachandramurthy.
- The test button says why a private target was refused. Saving a private
address that is not listed, or testing it, now names the variable to set
instead of an internal error that sent the operator to the log. Saving,
testing and the scheduled delivery all take the same decision, and an
allowed send is marked in the request log with its channel and origin. A
link-local, metadata or unspecified target says that no setting can open
it, since telling the user to ask the operator would only send them in a
circle. The refused save or test is a 422 with
`meta.errorCode = private_origin_not_approved`, or
`private_origin_not_grantable` for a target no setting can open.
## [1.38.16] — 2026-09-10
Mostly plumbing. Two things you may notice yourself: someone you gave
cycle-only access to sees less, and the admin page tells you whether last
night's off-site backup worked for every account. The rest is for people
who host the app or write against its API.
### Added
- **If you host this for others, Admin → Backups now shows which accounts
have a recent off-site backup.** The nightly job used to report one
number, so an account that kept failing was invisible. There is a row per
account now with the time of its last copy and whether that copy is on
schedule, due, overdue or has never happened. On the first day after this
update every row says "No record yet"; the next night fills them in.
- **The server side of the new first-run setup.** Your record can store its
setup answers (who it is for, which areas to watch, medication on a
schedule, where readings come from, an upcoming visit, units), and the app
derives which modules to switch on from them. It never switches off a
module you turned on yourself or one that already holds data. The screens
that ask the questions come in a later release; the first run looks the
same for now. For API clients: the answers appear as `onboarding` on
`GET /api/auth/me`; `PATCH /api/onboarding/answers` saves one answer,
`POST /api/onboarding/complete` finishes, `POST /api/onboarding/restart`
starts over.
### Fixed
- **Someone with cycle-only access no longer sees the rest of the record.**
The cycle insights page compared luteal and follicular averages for
weight, resting heart rate, HRV, sleep, steps, glucose and mood, and
showed them to anyone whose access covered the cycle, whether or not it
covered those areas. It now shows only what the access covers.
- **The app no longer offers buttons that would have failed for someone with
limited access.** Add, edit and delete on the vaccination pass for
read-only access, an editable visit sheet, capture forms and the quick
entry on the start page suggesting kinds outside the access, and a handful
of pages that opened for anyone with any access at all. Each now checks
its own permission. A booster reminder is also no longer created for a
record the access does not cover, and a wipe, an account deletion and an
encrypted export state in the API reference that they refuse while you are
looking at someone else's record, which they always did.
- **The error-reporting card on the admin page says what it actually does.**
It showed a green "Configured" as soon as an address was set, even with
sending off, and never said where reports go. It is green only when a
report left within the last fourteen days, amber with the reason
otherwise, and it names the target host. There is no default target:
errors never leave your server unless you point them somewhere, and
`docs/ops/deploy.md` says how.
### Changed
- **If you write against the API, every validation error now lists the
fields it rejected** in `details.issues`. Some routes used to answer with
a status code and one sentence. The message and the status did not change,
so nothing that reads them today breaks.
- **A request body that is not JSON at all answers 400 on every route.**
Seven preference routes said 422; the `meta.errorCode` they carried is the
same, and the native app already handles both. A body that parses but
fails validation is still a 422. `PUT /api/auth/me/report-selection` names
the ids it does not know in `meta.unknownLeaves` instead of failing with a
bare code.
- **Routes that create or edit a single row share one write limit** of 300
writes a minute per account. A 429 from it carries `Retry-After`, the
`X-RateLimit-*` headers and `meta.errorCode = record_write.rate_limited`,
so a client can tell it from a route's own limit. Batch endpoints keep
their own.
- **The GLP-1 log and the custom cycle symptom accept `Idempotency-Key`**,
so a retried request returns the first answer with
`X-Idempotent-Replay: true` instead of creating a second row.
- **Every error code the server can send is listed in the API reference by
area.** A new code cannot be added without appearing there, and no
existing code changed.
## [1.38.15] — 2026-09-10
The rest of #939 for records you keep for someone else, one adherence
figure everywhere, and better API manners toward a client that retries.
### Added
- **You can edit a record you look after now, and record its sex (#939).**
Name, date of birth, language, timezone and sex can be changed after
creation, and the creation form asks for sex so cycle tracking can
follow from it. Both need the fresh second factor that create and delete
already ask for, and edits are capped at ten an hour.
`PATCH /api/managed-profiles/{id}` answers 404 for a record you do not
manage and 422 with the field list for a bad value. Requested by
@sreeramachandramurthy.
- **Modules are switched per record.** The Modules card and the navigation
follow the record you are looking at, so while you are looking after
someone else's record you see what that record has open. Someone you
gave access to only certain sections no longer learns whether you track
your cycle, screeners, illness episodes or supplements. On the API side,
`GET` and `PATCH /api/record-settings/modules` read and write the
modules of the record you are acting on, and `modules` and
`cycleTrackingEnabled` on `GET /api/auth/me` describe that record; the
payload says which fields describe the caller and which the record.
- **The module-off notice says why.** It names the reason and offers the
switch only when it is the record's own choice. `GET /api/auth/me`
carries a `moduleAccess` map beside `modules`, saying per module
`enabled`, `disabled` (the record's choice), `not_granted` (outside the
access you were given) or `unavailable` (switched off for the instance);
`modules[key]` is `moduleAccess[key] === "enabled"`, so the booleans
keep their meaning.
### Fixed
- **A skipped dose is a pause, not a miss, on the dashboard too.** The
medication card, the dose history and the doctor report have left a
skip you chose out of the count since v1.15.9; the dashboard tile
counted it as missed, so one day could show two rates. They agree now.
- Saving the Modules card no longer freezes a record's cycle switch
against a sex recorded later. Every save wrote the cycle flag over the
one that follows from sex; it now goes only when it changed.
- A refused second-factor action no longer hides its reason behind the
dialog. Regenerating recovery codes or turning off the authenticator
kept its dialog open when the server said no (usually an expired
second-factor window); both close now.
- The edit form for a record you look after keeps what you typed when the
list refreshes, and a timezone-only edit no longer writes your language
into a record that had none.
- The chip saying your phone owns the medication reminders can appear now.
It could never render before. `GET /api/auth/me` publishes
`notificationPrefs`, including
`notificationPrefs.medication.clientManaged`.
- **A restore from the admin backups page no longer rewrites instance-wide
settings unless you tick the box.** It used to whenever the snapshot
carried them; the checkbox sits next to the typed confirmation and is
off by default (`restoreInstanceSettings` for API clients). The runbook
says what a restore replaces (rows added after the snapshot are removed)
and describes the console path.
- A stored copy that will not open is refused with a clear message, not an
internal error, whether it was tampered with or written under a key the
instance no longer holds, and whether you restore, download or preview
it. A client sees a 422 with
`meta.errorCode = backup.payload.undecryptable`.
- The admin status rows stay readable when the worker is absent or
crashed. The text used to switch to a colour that fell below the
contrast floor in exactly the state an operator most needs to read.
- The admin reminder check can name one account.
`POST /api/admin/notifications/reminder-check` takes an optional
`userId`; without it the check stays instance-wide as before.
- **If you write against the API, every 429 now says when to come back.**
Most rate-limited routes, the batch endpoints, `/api/sync/changes` and
`/api/auth/refresh` among them, sent no rate-limit headers. Every 429
from the limiter carries `Retry-After` in whole seconds plus
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset`; a
429 relayed from an AI provider keeps its own shape.
- Pages of a history pull never overlap or skip. Lists ordered by a
timestamp that can tie (measurements, labs, the measurement drill-down,
custom-metric entries, medication intake, mental-health assessments)
break the tie on `id`.
- The API reference allows what the server sends. The documented error
response allows `details.issues`, which the multi-field 422 always sent,
and the sync response declares `cycleDays` and `cycles`.
- Generic authentication errors carry `meta.errorCode`
(`auth.session.missing`, `auth.mfa.code_invalid`, ...), documented on
the 401 and 403 responses; a route's own credential check may still
answer with prose alone.
### Changed
- New end-to-end checks run before each release. Medication adherence from
setup through take and skip to the card, the history and the dashboard
tile; the doctor report with its text read back; the second factor from
enrolment through step-up, a wrong code, a replayed code and a recovery
code that works once; backup and restore through the settings pages; and
notification preferences through to what was delivered. Several fixes
above were found while writing them.
## [1.38.14] — 2026-09-09
Two bugs reported by @mbreitkreuz, both fixed at the root (#943, #944), a
first-run step that used to swallow an error, and quieter checks before a
release.
### Fixed
- **Walking distance from an Apple Health `export.zip` import was stored a
thousand times too small (#944).** Apple writes every record in the
archive in the unit the phone displays, kilometres or miles, and the
import read the number without reading the unit, so a 2.5 km day landed
as 2.5 metres. The import now converts units the way the workout import
already did, which also caught a workout archived with its energy in
kilojoules. Importing the archive again repairs the stored rows.
- **Blood glucose readings without a meal-time tag now count everywhere
(#943).** A meter synced through Apple Health does not say whether a
reading was before or after a meal, so an account could hold hundreds of
readings and see no Blood Glucose tile, no glucose target and an empty
glucose section in the doctor report, with the module on and no hint
anywhere. Untagged readings are a group of their own now: they light the
tile with a value and a trend, get a target card judged against the
random-reading range, and appear in the report and in the FHIR export.
Tagged readings keep their per-meal breakdown.
- **The first-run profile step no longer throws away what the server told
it.** A height outside the accepted range, or a date the server would
not take, was dropped on save while setup moved on and marked itself
finished; the value was gone and nothing said so. A refused field now
says why underneath the input, in the interface language, and the step
waits. The profile card in Settings behaves the same way. The iOS app
has the same gap and is tracked there.
- **If you host this yourself and someone no longer has their Apple Health
archive,** `scripts/repair-apple-health-distance.ts` repairs exactly the
rows the import wrote, once per account; it only reports what it would
change unless told to write. Runbook at
`docs/ops/apple-health-distance-repair.md`.
- **A retried medication write no longer lands twice.** Adding a
medication, logging a side effect and registering a supply container now
honour `Idempotency-Key`, so a replay after a lost response, which is
what the iOS app's offline queue sends, returns the first answer marked
`X-Idempotent-Replay: true` instead of creating a second row. Documented
per route.
### Changed
- `scripts/synthetic-journey.mjs` walks through an instance after a
deploy: it warms the instance, checks that `/api/version` matches the
tag just published, signs in, writes a reading, reads it back and
deletes it; the `synthetic-journey` workflow runs it by hand against an
instance of your choice, and `docs/ops/deploy.md` says what a failed
step means.
- The off-site backup variables `BACKUP_S3_REGION` and
`BACKUP_RETENTION_DAYS` are in `.env.production.example`, and the backup
runbook no longer claims the worker deletes old copies: it never does,
the bucket's lifecycle rule does.
- `POST /api/nightscout/connect` is in the API reference, where it was
missing.
- The checks that run before a release had been failing for reasons
unrelated to the app: browser tests that recognised the wrong element or
raced the server, bundle limits with no headroom, a dependency check
that failed pull requests over advisories that changed nothing. Those
are fixed, and new browser checks cover blood glucose in both units and
an Apple Health import end to end.
## [1.38.13] — 2026-09-09
While looking after someone else's record you get the add, edit and delete
controls the server has accepted since v1.37, decided per area and
published by the server. Reported by @sreeramachandramurthy in #939.
### Fixed
- **Looking after someone else's record, the controls the server accepts
are shown again.** While looking after a record for a child, or a record
shared at the manage level, the pages for mood, wellbeing check-ins,
visits, allergies, family history and labs showed no way to add
anything, although the server had accepted those entries since v1.37.
The app had hidden every such control until the server side was
confirmed, and that hold was never lifted. Lifting it with one switch
would have been wrong too: documents cannot be handed to anyone else at
any level, so the documents page would have offered upload and delete
and refused each one. The app now asks per area, so the controls appear
for measurements, medications, labs, profile and illness from the write
level up, and for mood and cycle at the manage level; the documents page
says in one sentence that only the record owner can add documents. On
the API side, every entry in the account access list on
`GET /api/auth/me` carries `writableDomains` and `manageableDomains`,
naming the parts of the record that access may write to and may manage;
both are additions. Switching modules per record, editing a record after
creation and asking for sex at creation are still open in #939 and
follow as one piece.
## [1.38.12] — 2026-09-09
A dependency release: the image and the lockfile stop carrying the
advisories a container scanner reported, and `next build` learns to
type-check the application without the test tree.
### Security
- **Scanner advisories cleared, none in this code.** next moves to 16.3.4
(two critical advisories, and it pulled a vulnerable sharp), nodemailer
to 9.1.1 (one high, three medium), js-yaml to 4.3.2 (high), hono under
the MCP SDK's node server to 4.13.5 (three medium), vitest to 4.1.11 and
baseline-browser-mapping to 2.11.0 (medium). The packages HealthLog does
not depend on directly (sharp, js-yaml, hono and baseline-browser-mapping)
move through pnpm overrides limited to the vulnerable ranges, so exactly
the copies the scanner named change and nothing else does.
### Changed
- **`next build` type-checks the application, not the test tree.** Next
16.3 checks the whole TypeScript project during the build, which reaches
the unit tests and their fixtures under `tests/`; the image build leaves
that folder out on purpose, so it failed with a missing fixture module.
The build now reads `tsconfig.build.json`, which extends the main config
without the test tree, and `pnpm typecheck` keeps covering everything.
If you build your own image there is nothing to do; if you run
`next build` from a checkout without `tests/`, it now works.
## [1.38.11] — 2026-09-08
"Sign out everywhere" now means everywhere, a renewal cannot outrun a
sign-out, and a retried request can no longer be answered from a cached
response that a different credential earned.
### Fixed
- **"Sign out everywhere" signs out the other phones, not the one pressing
it.** A phone login is two credentials, a refresh token and the access
token paired with it, and the server checks only the second on each
request. The button revoked the refresh tokens, so every other device
kept working until its access token ran out, up to a day. And a phone
pressing the button described itself to the server as a browser, so the
exception meant to spare the caller matched nothing: the phone revoked
its own refresh token and was the one device signed out, at its next
renewal. The button now revokes the paired access tokens of every other
device in the same step, and a phone is recognised by its access token,
so its own login is the one kept. Long-lived API tokens from the
settings page are untouched, as before. The iOS app's confirmation text
will go back to saying "everywhere else" once a build against this
release is out.
- **A renewal that overlaps a sign-out loses.** Renewal read the presented
refresh token as live, issued a new pair, and only then marked the old
one used, with a check that asked "not yet used" but not "not yet
revoked". A sign-out-everywhere or a credential rotation landing in that
window was overtaken: the pair issued a moment after the sign-out was
the one live login left. The check now includes revocation and the pair
issued inside the window is retired. The client hears `revoked`, which
it answers with a fresh sign-in, rather than `already_used`, which it
would retry.
- **A retried request can no longer be answered from a cached response
that a different credential earned.** When a client retries with the
same idempotency key, the saved answer is returned before the route's
own permission check runs, so what the cache may answer has to match
what the route would allow. Two gaps: someone acting on a record shared
with them, whose access had since been narrowed or edited, still got the
answer cached under the wider access, because the replay checked only
that some live access existed; and a narrow-scoped API token could read
an answer cached by a full-access credential of the same account on a
route outside its scope. Cached answers for access to someone else's
record are now tied to that access, its level and its scope, and a
narrow token's answers to the token, so any change of authority sends a
retry back to the route to decide afresh. Browser logins and full-access
tokens acting on their own record are keyed exactly as before; no client
changes.
### Changed
- The README's record list still said immunization records were out of
scope; the vaccination record has shipped since v1.37.3.
## [1.38.10] — 2026-09-06
A busy phone is no longer signed out at the moment it renews its
credentials, and the nightly off-site backup streams and fits a small
container.
### Fixed
- **A busy phone could be signed out at the very moment it renewed its
credentials.** The app renews its access token about once a day, and the
old token used to be revoked the instant the renewal went through. A
phone with several requests in flight had sent some with the old token a
moment earlier; those came back as revoked, the app read that as "sign
in again", renewed again with the refresh token it had just spent, and
that looks exactly like a stolen token being replayed, so every
credential for that device was withdrawn and the person had to log in
again. The outgoing token now stays valid for fifteen seconds, or until
its own expiry if that is sooner: long enough for requests in flight to
finish, short enough to gain nothing worth having. Replay protection is
unchanged; a refresh token presented twice still withdraws the device's
credentials on the spot, and so does signing out. The iOS app tolerates
the race on its side too, so the two halves close it together.
- **The nightly off-site backup no longer takes the instance down with
it.** The first time it was pointed at a real destination it ran out of
memory within seconds and restarted the container for everyone on it,
because the job runs inside the app process. The weekly backup had the
same problem in v1.38.6, and that fix had only half reached this job:
the upload still built the whole backup as one piece, compressed it
whole, encrypted it whole and sent it whole, four full copies of the
record at once. Now the record goes out table by table straight into
compression, then encryption, then an upload in parts, so no complete
copy ever exists and what the process holds no longer depends on how
much you have recorded. Measured under the real memory ceiling with a
large record, the old way dies while still building the copy and the
new way finishes with room to spare; the stored copy for such a record
is about 9 MB. If you set `BACKUP_S3_ENDPOINT` and its companions on an
instance with a large record, you can leave them set after this.
- **A nightly run that uploaded nothing for anybody now reports failure.**
It used to say it was fine. Bad credentials, a missing bucket or a
refused connection upload nothing, leave no half-finished parts behind,
keep the server up, and fail the run with the storage provider's own
reason. A run where some accounts got a copy still succeeds, with the
rest counted, because retrying everyone over one object would re-upload
everyone's. The runbook lists what the bucket grant needs, including
permission to abort a multipart upload.
### Changed
- Off-site copies carry their authentication tag at the end. The tag can
only be produced once the last block is in, so a leading one meant the
whole object had to exist before its first byte could be sent, which is
the thing that could not be streamed. The encryption itself is
unchanged; the tag still covers every byte and is checked before any
plaintext comes back. Copies written by earlier versions still restore
without a flag: an old and a new copy of the same record came back with
identical row counts, deleted rows included.
## [1.38.9] — 2026-09-05
The date order you choose now reaches every date on screen, a gateway that
refuses now says why, and a plain-HTTP self-host can write again. Two of
the reports came from @foxbcx (#922) and @arisalthaus (#492); thank you.
### Fixed
- **The date order you pick under Settings only reached the entry form.**
@foxbcx switched away from MM/DD/YYYY, watched the date picker follow,
and found the chart axes and the measurements list still spelling every
date the old way. The setting had reached the form and nothing else; the
rest fell back to the language default without saying so. It now reaches
the chart axes and their tooltips, the measurements list, the medication
charts, the doctor-report PDF, and the page a practice opens through a
link you gave them, which spells dates the way you read them rather than
the way the practice does. That page also picked up your 12- or 24-hour
preference, which it had been ignoring while the PDF used it. Month
names stay in the interface language: the setting decides the order of
the numbers, not the language of the words, because the alternative is a
German month name in an English report.
- **A gateway that refused the call reported nothing but "empty content".**
With OpenRouter as the OpenAI-compatible gateway, a refused request
showed up as "empty content" with no reason, because OpenRouter answers
with success and puts the error inside the body. The reason now comes
through in the gateway's own words, a missing balance as a missing
balance and a data-policy block as a block, and the provider health card
and the fallback chain treat each as they would the same answer from any
other provider. The local provider had the same blind spot, including
for an error that arrives before the first streamed token, and is closed
too.
- The import guide links under Settings → Import went the long way round.
Both the CSV card and the JSON card now link straight to the guide
instead of through a redirect.
- **On a self-host served over plain HTTP, nothing could be saved.**
@arisalthaus, on a LAN, found that uploads did nothing and worked out
that the transport was the reason. Nothing in a save needs HTTPS, but
since v1.37.18 the app labelled every upload and every save with an id
from a browser call that is only offered on a secure page. On an http://
address the save failed before a request left the page, and nothing said
why: recording a measurement, editing a medication, restoring a backup,
all of it. The id now comes from a source plain HTTP allows, so writes
work over HTTP on a LAN, a VPN or Tailscale as they do over HTTPS. What
a browser still withholds from a page without HTTPS is passkey sign-in,
push notifications and installing the app to the home screen; no
application can hand those back, and the self-hosting docs now say so.
## [1.38.8] — 2026-09-03
The heart-rate-variability tile now works for accounts whose HRV comes from
a ring or a strap, and an account can no longer pile up measurement tokens
without limit. Both from @Antiheld86.
### Changed
- **You can hold up to ten measurement tokens at a time.** Past that,
creating another is refused until you revoke one, so a script stuck in a
retry loop meets a wall instead of leaving you hundreds of live
credentials to clear out by hand. Only live tokens count: revoking one
frees a slot at once, an expired one stops counting on its own, and the
tokens your browser or phone uses to sign in are counted separately. A
household with a scale, a watch bridge and a couple of scripts sits well
under the limit; it is there to catch a runaway, not to ration.
### Fixed
- **The heart-rate-variability tile now appears on the dashboard for
people whose HRV comes from a ring or a strap.** Turning it on under
Settings did nothing and said nothing, while the same readings charted
fine on the HRV page, which made it look as though the layout was
broken. HRV is stored two ways that happen to share a unit: Apple
Health, Fitbit and Google Health record SDNN, while Oura, Polar, WHOOP
and most watch bridges record nightly RMSSD. Every other place HRV
appears already accepted either one; the tile looked only for SDNN. It
now uses whichever series you have, titled "HRV (RMSSD)" when that is
the one on screen so a reading of 40 is not mistaken for a collapsed
SDNN figure. An account with both keeps seeing SDNN, and the two are
never blended. The tile's freshness caption was also read off SDNN while
RMSSD was displayed, which would have marked a current tile as stale; it
reads the series on screen now.
- Turning the Recovery module off now takes RMSSD off the dashboard in
every case. The dashboard has two ways of loading its tiles, and the one
used when the snapshot is switched off did not drop the series, so a
disabled module still showed up there.
## [1.38.7] — 2026-09-03
The weekly backup produces a backup again. v1.38.6 stopped it taking the
instance down when it failed, and also stopped it succeeding.
### Fixed
- **The weekly backup refused every account because the server had been up
a while.** The check added in v1.38.6 watched how much memory the whole
server process was using, garbage included, and gave up above a line
that a long-running server sits above on uptime alone. So every account
was refused on the first chunk it wrote, the run finished in seconds, and
it reported success. Size had nothing to do with it: an account whose
entire stored backup is about a megabyte got the same message as one
holding hundreds of thousands of readings, each told its record was too
large and that the operator should buy memory. An instance in that state
can go a month and a half without a usable copy. That check is gone
rather than tuned. The backup is measured against the one thing that
really grows with a record, the encrypted copy it produces, against a
fifth of the server's memory limit. An account whose backup really does
not fit still fails as one job rather than taking the instance down, and
the message says what was measured and what it crossed. Measured with a
large record under a cap well below the default container, the backup
finishes with room to spare; built in one piece, the old way, the same
record still dies at that cap.
- **A backup run that wrote nothing for anybody reported success.**
Accounts present and none backed up came back as a completed job, so the
failing-jobs panel stayed empty and the backups page listed the copies
it already had. A run that protected nobody is now a failed run, and the
backups page shows it as one. A run that writes some accounts and not
others still succeeds, with the failures counted per account, because
failing the whole run over one record would redo everyone's backup on
every retry. Still open: on an instance with several people, an account
that fails every week while the others succeed is invisible, because the
age shown is the newest copy across all accounts. That needs a
per-account view and text in every language, so it waits for a later
release.
## [1.38.6] — 2026-09-03
Blood glucose can be shown in mmol/L, which it never could before, and the
weekly backup finally sits inside key rotation. Text the server writes now
arrives in the language you actually read the app in.
### Changed
- The rotation tool says what it did not look at. A zero that means "nothing
left" and a zero that means "never looked" read the same, and an operator
acts on that zero by dropping a key. Each run now ends with how many
columns it walked out of how many exist, names any it skipped, and exits
with an error when the two disagree. The runbook's retire-the-key step is
rewritten around that count, and it tells an operator who already rotated
on an older release how to get their backups back.
- `greetingHour` is no longer part of the dashboard snapshot response. A
client that decoded it as required needs the field made optional or
dropped; the `timezone` beside it is unchanged and is what the greeting
should come from.
### Fixed
- **Blood glucose can be shown in mmol/L. It never could before.** The
dashboard tiles, the glucose page, the targets panel, the CSV and FHIR
exports, the doctor report, the Coach's notes and the low-reading alert
all read the setting, and nothing set it. The unit is now a dropdown in
Settings under your profile, beside the metric/imperial one and not folded
into it, because metric countries are split on which unit they read
glucose in. Readings are stored in mg/dL and stay that way; the unit only
picks how they are shown. The entry form, the measurements list and the
edit sheet ask in your unit and convert on the way in, so a 5.3 typed by
an mmol/L reader is filed correctly instead of as a number that passes
every check and reads back on every screen as a severe hypo. Editing a
value entered in mg/dL does not convert at all, so correcting a typo
cannot rewrite a reading into the other unit.
- The same reading converted two different ways. Import and display used
slightly different factors, so a value imported in mmol/L did not come
back out as the number it went in as. Both ends take the same factor now.
- **Text the server writes arrives in the language you read the app in.**
The interface takes its language from a cookie; the nightly briefing, the
Coach nudge and the reminders take it from your account. A single write on
page load kept the two in step, and when it was lost (a paint before
sign-in, an offline moment, a tab closed in flight) nothing retried, so
one English paragraph could sit on an otherwise German page for the life
of the account. That is how it was reported: the sentence above the health
score came back in English on an account read entirely in German. The
server keeps the two in step itself now, correcting the account once when
a request arrives in a language it does not agree with. It cannot be
dropped in transit, it repairs accounts that have been out of step for
months on their next page load, and only a language the app was actually
being read in can reach the account.
- The phone app's dashboard greeting no longer falls back to English on a
German instance. That request carries no language cookie, so an account
that never opened the language picker got English instead of the
instance's language. It now follows the same order every background writer
uses: your preference, then the operator's default, then English.
- The greeting no longer contradicts the rest of the page. Separate pieces
of code answered "what hour is it" for the salutation, and one of them
fell back to the device's clock whenever the configured timezone would not
resolve, so the header could say good morning while the page said good
evening and the dates belonged to a third zone. Both greetings read one
helper now, which stays on the configured zone and falls back to the app's
default rather than to the device. The server value is gone: it was a
clock reading served from a cache for up to an hour, long enough to name a
salutation the clock had already left behind.
- An Apple Health import records which export it came from. The import
status had always promised the instant the Health app stamped on the
archive, and the parser had always skipped it. It reads it now and writes
it the moment it goes past, so a run that later fails still says which
export it was working from.
- **The weekly backup no longer takes the instance down with it.** Last
release's compression was measured on a machine with a multi-gigabyte
heap; the container it runs in is capped at 1 GB, and there the same pass
still died, and because the job shares the application process every
signed-in user on the host went with it. The document is now written piece
by piece: the tables that grow without bound (readings, doses, mood
entries) are read a page at a time straight into the compressor and the
cipher, every other section is released as soon as its own JSON exists,
and nothing is held whole. A record doubled in size now finishes well
inside that heap, and the writer stops itself before the limit, so an
account too large for its host is one failed backup in the run's summary,
not a restart for everybody. Backups written under either older shape
still restore.
- **The provider health card says what a failing AI provider sent back.**
The card could report a provider as failing without saying whose problem
it was; the status of every failure had been recorded since v1.11 and
nothing read it. It is shown beside the failure now, which tells a dead
key from a provider having a bad afternoon, and blank where there was no
status to record.
- **Backups were outside key rotation.** The weekly snapshots and uploaded
packs are encrypted like everything else, but under a column called
`data`, and the rotation tool recognised encrypted columns by their name.
So it never read a backup, reported zero rows remaining, and the runbook
told you that zero meant the old key was safe to retire; following it made
every stored backup permanently unreadable. Rotation covers it now, in
bounded batches, and re-seals the ciphertext without reading the
plaintext, so both stored forms come back byte-identical. The
idempotent-replay response cache was outside rotation for the same reason
and is rotated too.
- Rotation crashed part-way through and said nothing. Running the tool
against a seeded record showed it dying on the mood day-context note, a
table keyed on the entry it belongs to rather than on an `id`. Everything
after it, the backups included, was never reached. The walk now takes the
table's actual key.
- The document index's verbatim text was rotating but never appeared in the
summary, so the new coverage count read it as skipped. It has its own line
now.
- **A misspelled field in a request is no longer accepted and thrown away.**
Send `disableCoachh` where the server expects `disableCoach` and the
answer used to be 200 with the key dropped. The settings toggles, the
restore and bulk-delete endpoints, message feedback, the admin user
update, the diabetes and Coach switches, the onboarding acknowledgements,
the provider chain, the chart overlay preferences and the two client-side
reporters now refuse a field they do not recognise, and the 422 names the
key. A key name is bounded and stripped of anything that is not an
ordinary identifier before it is echoed.
- Sync, layout, import and the read filters keep accepting a field they do
not know, on purpose. The phone app ships optional fields ahead of the
servers that will read them, and a self-hosted install runs whichever
release its operator last pulled, so a server ignoring a field it has
never heard of is what keeps an updated app working against an older
install. On a read filter a stray parameter discards nothing, the OAuth
endpoints are required by their standards to ignore unknown parameters,
and an import file comes from another version or another tool by
definition.
### Internal
- A column holding ciphertext is now recognised by what the code writes into
it, not by what it is called, so a column like the backup blob cannot sit
outside rotation again.
- The check that asks whether every column has both a reader and a writer
had been wrong in both directions: every column it reported was a lookup
key, and a column no code touched was the one thing it could not see. It
works now, and its first real run found the glucose unit and the Apple
Health export date (read and never written, both fixed above), the
provider-health status (written and never read, now shown), and the
known-device first-seen date, which no page or export asks for yet.
## [1.38.5] — 2026-09-03
The weekly backup costs a fraction of the memory it did, the dashboard stops
scanning the two largest tables, and several settings now do what they say.
### Security
- Two dependency minimums raised. A URI parser that arrives under several
dependencies, the connector SDK among them, was below the fix for
advisories about host confusion and request forgery; a query-string parser
was below the fix for an array-limit bypass and a denial of service. Both
are raised past their fixes.
### Fixed
- **The weekly backup uses far less memory.** On a real instance it had not
produced a copy since mid-July: the whole record sat in memory several
times over and the process died within seconds. The stored copy is now
compressed before it is encrypted, and each stage is released before the
next is built. A record of a few hundred thousand readings, on a generous
heap, finishes in under half a minute. The pass has a two-hour window of
its own now, so a slow disk cannot be mistaken for a broken run. The
nightly off-site copy is built the same way, and older backups still
restore.
- **A failed backup is visible now.** It went unnoticed for five weeks
because on the backups page an old copy looks exactly like Sunday's. The
page now says how old the newest scheduled copy is once it is past due,
and how the last run ended when it ended badly, with the reason the queue
recorded.
- **The dashboard opens faster on accounts with years of readings.** Two
reads behind the metric tiles scanned every row instead of using an index.
Both ask per metric now, and neither grows with the archive; the tiles
show the same numbers.
- Mood correlations no longer call a handful of days a finding. The mood
page painted "Strong" or "Moderate" on a card after a few paired days. It
now needs the twenty paired days and the significance test the rest of the
app does. Fewer cards show a coefficient, and a card without one says
whether you are still collecting days or nothing stood out.
- A medication with no dose time no longer reports perfect adherence. It
showed 100 percent in the insights list, the blood-pressure read and the
compliance summary, and lifted every average. It is left out until it has
a schedule, as the doctor report always did.
- A day you never opened the app is no longer a skipped dose. The
blood-pressure medication chart counted every day without an intake record
as a zero, so a week away read as a week off your medication. Only days
with an intake record count now; a dose you marked as skipped still does.
- A lab note this instance can no longer decrypt survives an export. Such a
note or biomarker context used to come out empty, and a restore finished
the job. Both export as a marker saying the text was encrypted under a key
this instance no longer holds, and the marker survives a restore. The text
itself is still unrecoverable without the old key; you can now see
something was lost.
- A deleted mood entry or custom-metric reading counts as deleted in the AI
features. The correlation channel built from a custom metric kept using
deleted readings, and the check that decides whether an AI assessment is
still current could not see a deletion, so yesterday's text was re-dated
and served as today's. Both skip deleted rows now.
- A dismissed correlation finding comes back when its evidence has really
moved. The check compared two numbers instead of the stored fingerprint,
so a dismissal restored from a backup that carried the fingerprint but not
the numbers could never be released. It reads the fingerprint now, and a
dismissal still survives ordinary sampling noise.
- **If you host this yourself, settings you could set but not change.** The
`environment:` block in `docker-compose.yml` is a whitelist, so a value in
`.env` looked configured and never reached the app. Now forwarded, each
with a line in `.env.production.example`: `DEMO_MODE`; `LOG_LEVEL`,
`LOG_SAMPLE_RATE`, `LOG_SLOW_THRESHOLD_MS` and `LOG_INCLUDE_STACK`;
`LOKI_ENDPOINT`, `LOKI_USERNAME` and `LOKI_PASSWORD`;
`DATABASE_STATEMENT_TIMEOUT_MS`; `COACH_MESSAGE_RETENTION_DAYS`,
`HOST_METRIC_RETENTION_DAYS`, `DENSE_INTRADAY_RETENTION_ENABLED` and
`INTEGRATION_FAILURE_ALERT_THRESHOLD`; `TELEGRAM_WEBHOOK_SECRET`;
`NIGHTSCOUT_PRIVATE_ORIGINS`; `WITHINGS_REDIRECT_URI`,
`FITBIT_REDIRECT_URI` and `GOOGLE_HEALTH_REDIRECT_URI`;
`OPENMETEO_BASE_URL` and `OPENMETEO_GEOCODING_URL`;
`ADMIN_AI_BASE_URL_ALLOWLIST`, `CODEX_MODEL`, `CODEX_MODEL_FALLBACK_CHAIN`
and `COACH_EXPERIMENT_VERDICT`; `APNS_KEY`, `APNS_KEY_FILE` and
`APNS_PRODUCTION`; and `DASHBOARD_SSR_PREFETCH`. The Data & Privacy page
now quotes the Coach retention window you actually set, and a Codex slug
rotation is something you can ride out by setting a model rather than
waiting for a release. A variable the server reads can no longer be left
off that list without a written reason.
- `APNS_KEY` and `APNS_KEY_FILE` work where the documentation always said
they did. Only `APNS_KEY_B64` ever reached the container, so choosing
either of the other two got a silently disabled APNs and no explanation.
- The Web Push key loader no longer advertises names it cannot read. The
`WEB_PUSH_*` aliases and `NEXT_PUBLIC_VAPID_PUBLIC_KEY` were never on the
compose whitelist or in any example file, so a value under those names
could not arrive. They are gone, and the self-hosting guide no longer
mentions them. The example file also said the real `VAPID_*` names were
absent from the whitelist; they have been on it since v1.17.1.
- The AI switches save when they are sent to the general admin settings
endpoint. That endpoint accepted them, answered as if saved and stored
nothing, so a script had no way to tell a written switch from a dropped
one. They are written now and echoed back. The admin console's own
endpoint was never affected.
### Security
- A notification channel the operator switched off stops delivering. Turning
Telegram, ntfy or Web Push off instance-wide only hid the setup card while
every account that had configured the channel kept receiving on it. A
channel that is off delivers nothing now, the skip shows in the
notification diagnostics with its reason, and switching it back on
restores delivery without anyone re-enabling anything. Turning such a
channel on from the user side is refused with a message that says why.
- Every module's API routes are now declared with the module itself, so a
module that is switched off cannot leave a route reachable by being
forgotten. Nothing was reachable this way before, and nothing changes for
you today.
- Every webhook verb that checks the shared secret is rate-limited now. The
Withings, Telegram and Coolify hooks limited their POST but not the GET
and HEAD a webhook UI sends to confirm a URL: a free way to guess a
callback token. Each verb runs the same limit before it looks at the
secret; the real services send one or two probes.
## [1.38.4] — 2026-09-02
The shared AI key reaches the provider its address names, and the image
carries no flagged dependency versions.
### Fixed
- **The shared AI key reaches the provider its address names.** If you
filled the server key with an Anthropic key and
`https://api.anthropic.com/v1`, every AI call failed and nothing in the
error said why: the request was always built for OpenAI's API and posted
at Anthropic's, which speaks a different shape, and the address field
accepted the value happily. The base URL now picks the client. Anthropic's
host, and any subdomain of it, is called on Anthropic's own Messages API
and defaults to a Claude model; every other host keeps the
OpenAI-compatible path, which is also the right one for a local server or
a gateway. A base URL that is not a URL at all behaves as it did. The
operator card says which providers the field accepts. Found in a fork by
@sweidinger, who worked around it there; this fixes it at the source.
### Security
- **The published image no longer ships flagged dependency versions.** Some
minimum versions had gone stale: the sanitiser behind the doctor-report
PDF sat below the fix for GHSA-55q2-fjhq-7xh7, the CSS processor the build
runs on sat below the fix for CVE-2026-69153, and a schema library that
arrives through two unrelated parents had no minimum at all
(CVE-2026-59952). All are raised inside their existing major, so nothing
changes shape for anyone.
- **The Prisma command line inside the image gets the same versions as the
app.** The image carries a separate install of it, and that one does not
read the project's version overrides. It was quietly resolving an HTTP
server (GHSA-frvp-7c67-39w9, CVE-2026-39406) and the same schema library
(CVE-2026-59952) at the flagged versions while the application itself was
already safe. Both are set to the safe versions on that side too. The
comparison between the two files had been skipping every package whose
name starts with an at sign, so a version set in one place and not the
other went unseen; it sees them now.
- The automatic dependency merge runs with less privilege. That workflow
handed a write token to every job in the run; the default is read-only now
and the write scopes sit on the one job that spends them. Which updates
merge automatically has not changed.
## [1.38.3] — 2026-09-02
An account with two-factor can be deleted from the phone, and every data
source is called by its name.
### Fixed
- **An account with two-factor can be deleted from the phone.** Deleting the
account, and erasing the record while keeping the account, both ask for a
fresh proof of your second factor once you have one enrolled. That proof
lived on the browser sign-in, which the app does not have, so anyone with
an authenticator app or a security key was refused from the app no matter
what they sent, and a browser was the only way through. The app now
re-proves the factor the same way it already does when you manage
two-factor. Nothing about the web path changed, and a mistyped
confirmation does not use up the proof. One thing to know when erasing the
record from the app: API tokens are part of what that request deletes, so
the app signs itself out and signs in again.
- **Every data source is called by its name.** In Settings, Sources, some
entries in the priority ladder showed their internal name, `GOOGLE_HEALTH`
instead of Google Health, and Oura, Polar and Strava the same way. The
label list was written when there were far fewer sources, and every
provider added since fell through to the raw value. The same thing
happened in the measurement list's source filter and column, and in the
medication intake history, where a dose synced from Apple Health read "via
APPLE_HEALTH". Each is complete in every language now, and a new provider
cannot ship without its name.
- If you write against the API, the deletion and erasure routes accept a
single-use elevation from `POST /api/auth/step-up` (a code, security key
or passkey) sent back in the `X-Step-Up` header. A token on its own still
gets nowhere, a password-proved elevation is refused here as a password
login never satisfied this check on the web, and the proof is spent only
when the erasure is about to run.
## [1.38.2] — 2026-09-02
Anthropic keys work on current Claude models again, and the daily briefing
stays in the language you read the app in.
### Fixed
- **Anthropic keys work again on current Claude models.** Everything that
asks the model for JSON (the daily briefing, the status cards, lab and
document extraction) had been failing on the Claude 4.6 family upward, on
Opus 5 and Sonnet 5, and on the Fable models. The app steered the answer
by starting the model's reply with an opening brace and gluing it back on
afterwards; those models reject that outright, so anyone with a current
model got a provider failure on each of those calls while plain chat kept
working. The trick is gone on every model. The request asks for a single
JSON object and nothing else, and an answer wrapped in backticks or
prefixed with text still parses. Reported independently by @sweidinger,
whose fork retried without the prefix.
- **Lab and document OCR no longer refuse a current Claude model.** The
image check knew the Claude 3 and 4 families by name, so a key set to a
newer model was told the provider cannot read images even though it can,
and a lab photo was never sent. Any Claude from the 4 family upward, and
every Fable model, now counts. The model presets in Settings also offer
`claude-sonnet-5` and `claude-opus-5`.
- **The daily briefing stays in the language you read the app in.** Someone
using the app in German could find the dashboard lead and the Today
briefing in English, and no amount of reloading changed it. The nightly
writer resolved an account that had never picked a language to English
rather than to the instance's language, and the briefing cache kept no
record of the language it was written in, so every reader got whatever the
last writer left there. The refresh only replaced it once the cache was a
day old and the provider answered, which with a provider at its usage
limit is never. The cache now records its language: if you read in another
language you get the "preparing" state and a fresh briefing in your own,
and briefings written before this release are served as before until the
next write.
- On a server you run yourself, every background writer (the nightly
briefing, the status cards, period narratives, reaction lines, the morning
refresh, the briefing push, coach nudges, reminders, and the document and
workout summaries) now resolves a missing account language to your
instance's default first and only then to English.
## [1.38.1] — 2026-09-01
You can push readings in from your own hardware again, and a lab reading
shows where it sits against its reference range at a glance. @Antiheld86
built the measurement ingest token (#881, from their proposal in #878);
@TimonBed built the reference-range bar and the lab-list alignment (#860,
#880) and fixed lab OCR for GPT-5 models (#861).
### Added
- **A token for pushing readings in.** Since v1.30.17 API tokens keep to
their scope and the generic token button is gone, so a scale or a watch
bridge through Home Assistant had no way in. Settings → API & Tokens now
mints a token that adds readings to your own record and nothing else; it
cannot read, change, delete, export or mint another token. Shown once,
valid for a year, revoked like any other. Contributed by @Antiheld86 in
#881, from their proposal in #878.
- The docs site has a guide for wiring one up, with a worked Home Assistant
example, the measurement types a scale or a watch bridge tends to report,
and what each error means.
- **A range bar beside every lab reading**, from @TimonBed in #860 and #880.
The lab list, its mobile cards and each analyte's history carry a compact
bar: the window in green, the reading as a marker, blue below, amber
above. A one-sided range gets a bar with its one bound labelled, and a
reading far outside its window widens the scale so the marker stays
visible. Badge, bar and sparkline line up in columns and share the
colours. A qualitative reading, or one without a range, shows no bar.
- The token posts to `/api/measurements` and to `/api/measurements/batch`,
and the settings page lists both beside the medication endpoint.
### Changed
- Readings pushed with the token count as manually entered, so you can
correct one in the app if a sensor sends something wrong.
- The below-range and above-range badges on a lab reading carry the same
light tint as the bar, blue below and amber above; in range stays neutral.
Part of #860.
- Routine dependency updates ride along: the Prisma Postgres adapter (see
Fixed), the Argon2 binding, the WebAuthn server library, Radix, Lucide,
the form resolvers and the dev tooling.
- A push that claims to come from Apple Health is refused rather than
relabelled, because that label is half of how the iOS app recognises its
own rows, and such a push no longer moves the phone's sync checkpoint.
### Fixed
- Lab OCR works with a GPT-5 model on the OpenAI path, fixed by @TimonBed in
#861. The list of models trusted to read an image stopped at the GPT-4
generation, so a GPT-5 model was never sent the photo. The extraction
budget doubles because these models spend part of it on reasoning, and a
provider failure now says so instead of blaming the photo.
- A lab report imported as a document could leave a reference range whose
low bound sits above its high one, and for a new analyte that pair went
into its catalog entry too. The pair is now dropped rather than swapped,
the range text as printed survives, editing or confirming a staged fact
applies the same rule, and the sparkline no longer paints such a reading
green (#882).
- The OAuth callback warning added in v1.38.0 never appeared in the
published image, and the copyable callback address showed only a path;
both were baked in at build time. They now come from the server and
reflect the `NEXT_PUBLIC_APP_URL` or `*_REDIRECT_URI` you actually set.
- A Prisma adapter update had broken the image build before any release
carried it, because two copies of `pg` drifted apart. It is a runtime
dependency now with one copy in the lockfile (#872).
### Security
- `mysql2` is updated past GHSA-3f6p-5ww8-9rcr in both the application tree
and the Prisma CLI install inside the image. It reaches the tree only
through Prisma's MySQL driver, which this app never opens.
- The ingest token is confined to its owner's record; pointing it at a
record someone shared with you is refused before the sharing grant is
consulted.
- Only a web sign-in can mint one. No API token can, because a phone's
sign-in lasts a day and this token lasts a year.
- A repeated request with an idempotency key could be answered from the
cache after a check of the sharing grant alone, not of the credential
presented. Nothing could be written that way, but a cached answer about a
shared record could be returned to a token the endpoint itself would have
turned away.
## [1.38.0] — 2026-08-30
The health score works with the record you actually keep.
Until now it needed three separate areas of health plus one measured value
before it would grade at all. Someone who had tracked blood pressure and
sleep for months got the same blank card as someone who had just signed up,
and the card never said what was missing. Three areas is now a
recommendation: one area scores, the arithmetic is the same at every width,
and the card says what the number rests on.
### Added
- **Below full breadth the card says what the number rests on.** One line,
"Based on {n} of {recommended} areas of health", and the number stays full
size in its band colour. A narrower score is a smaller claim, not a worse
one. The areas themselves are one tap away in the composition list.
- **When an area stops counting, the card says which one and why.** If a
window runs out of recent data or a source goes quiet, the card names the
area that left, in the same words that area uses when it is short of data.
You can dismiss it, and it only comes back if the composition moves again.
It is not a notification: worth noticing, not worth being woken for.
- Tailscale is a documented way to serve HealthLog, from a self-hoster's
suggestion in #847.
- For anyone writing against the API, every score response carries an
optional `scoreBasis` block with the area count, the recommendation, the
tier and whether a measured physiological value is among them. It is
additive, so an older client keeps working.
### Changed
- A blank card now means one thing: nothing in your record can be scored
yet. Everything else gets a number.
- The pillar selection in settings saves from one pillar upward. The page
still recommends three, because a one-area score is thin.
- The demo seeds every area, so a fresh demo shows the score at full breadth
instead of narrowing to a cardiometabolic view after a few weeks.
- For API clients, `scoreVersion` moves from 3 to 4. Stored days keep the
version they were written under, and deltas are suppressed across the
change as before.
### Fixed
- The coverage line no longer says the score "needs" three areas.
- The integration cards warn when the configured OAuth callback address does
not match the address the app is served from, from @TimonBed in #855.
Self-hosted OAuth otherwise fails with nothing on screen to explain it.
### Security
- Every scored band cites a clinical threshold or a published guideline.
Nothing is made up from your height or your age beyond guideline bands
documented as such; the BMI-22 default that shipped before v1.34 is
exactly what that rule keeps out.
## [1.37.33] — 2026-08-29
A community day: a contributor's first code fix, a self-hoster's suggestion
turned into documentation, and housekeeping.
### Added
- Tailscale is now a documented way to serve HealthLog, suggested by
@el-abcd in #847. For a single Docker box, `tailscale serve` stands 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 funnel` is covered as the explicit
public opt-in. The guide lives in the self-hosting docs and on
docs.healthlog.dev, next to the reverse-proxy guide it complements.
### Fixed
- A password shorter than the minimum showed its "too short" label in the
strength palette's colour instead of reading as invalid. Fixed by
@TimonBed in #848, their first contribution here: the label now uses the
destructive colour while the bars stay muted.
- If you host this for others, the admin notice about login locations
resolving through the online geo lookup repeated on every worker restart,
because the marker that should have shown it only once lived in the
process's memory. The marker now lives in the notification history,
survives restarts and retention pruning, and resets only when the
configuration state changes, so the notice fires once when the condition
appears and once more only if it goes away and comes back (#851). A host
that has disabled IP lookups outright is no longer notified at all; there
is no egress to warn about.
- Restored backups briefly wrote decrypted health data with default file
permissions; the restore script now creates its output owner-only.
`TRUST_PROXY_HOPS` joined the compose `environment:` whitelist so a `.env`
value reaches the container without editing the compose file.
- The HTML stripper the notification senders share now withstands
multi-character bypasses: one helper, applied until nothing changes,
replaces the per-sender copies of a single-pass pattern.
- The operator docs were checked line by line against the current code:
stale claims corrected, invented features removed, and the API reference
brought in line, here and on docs.healthlog.dev.
### Security
- Dependency overrides lift `flatted`, `js-yaml` and `minimatch` past their
published advisories; the Prisma CLI install in the production image is
fixed to an exact version; every open code-scanning alert was either fixed
or dismissed with a written reason.
## [1.37.32] — 2026-08-28
Korean joins as a seventh interface language, contributed by @aucun6352 in
#839, together with the fixes that had to land before it could work.
### Added
- Korean (`ko`) ships as a seventh interface language, community-maintained
alongside French, Spanish, Italian and Polish. The whole app speaks it:
dashboard, settings, share views, the clinician report, notifications and
the AI prompts. The PHQ-9, GAD-7 and WHO-5 questionnaires carry their
officially published Korean wording so a score stays comparable to the
Korean literature; the sleep questionnaire stays in its English-validated
form with a note saying so. The Coach and the daily briefing take the
reviewed English body with a Korean reply instruction rather than a
hand-curated Korean one, the same route the other community languages
took.
### Fixed
- Text matching between app labels and text a person or a lab typed matched
nothing at all in Korean. The app normalises text by pulling accented
letters apart and dropping the accent marks, which turns Latin accents
into plain letters, but a Korean syllable pulled apart into its letters
was never put back together, so the normalised text no longer resembled
any table the app carries. Both readers of that normalisation were
affected: the check that decides whether a Coach recommendation needs a
citation, and the parser that reads a printed reference range. Korean
syllables are put back together now, and every other language reads
exactly as before.
- No Korean analyte name could resolve to a LOINC code. The lab-name matcher
kept only a to z and digits and dropped everything else, so every Korean
lab name in the catalogue matched nothing, and a Korean account's
cholesterol result reached a FHIR export uncoded.
- The reference-range parser now reads the tilde a Korean lab prints
(`3.5~5.0`), in both the ASCII and fullwidth forms. It had the hyphen and
the dashes and not the separator the reports use.
## [1.37.31] — 2026-08-27
The server knew when an AI provider was failing; the operator had no way to
see it.
### Added
- The admin AI settings section shows a provider-health card: one row per
provider type with how many users' provider chains include it, how many
are currently failing, the longest unbroken run of failures, and the last
success and failure times. Operator-managed providers sort first, since
their failure takes the shared fallback down for everyone relying on it.
The endpoint behind the card is admin-only and reports counts and
timestamps, never individual users.
## [1.37.30] — 2026-08-27
An operator pointing the global AI provider at a private OpenAI-compatible
proxy got a network error on every call, and nothing said why.
### Fixed
- The operator's global AI provider can now reach a private host, provided
that host is named in `ALLOW_LOCAL_AI_PRIVATE_HOSTS`. Its base URL is
typed by the operator in the admin settings, so it now consults the same
allowlist as the gateway a user configures for themselves, instead of
being restricted to public hosts. A private host that is not on the list
is refused exactly as before, personal API keys stay restricted to public
hosts, and the ChatGPT sign-in path still only talks to its built-in
endpoint.
## [1.37.29] — 2026-08-26
The chart range selector said points and meant days. Someone who weighs in
twice a week picked "7 pts", saw two, and reasonably concluded the chart was
broken.
### Fixed
- The range tabs now say what they do: 7d / 30d / 90d / All, in every
language. The selector has always fetched a calendar day window; only the
labels claimed a point count. Anyone measuring less than daily saw fewer
points than the tab promised and had no way to know the tab was the lying
part.
- The "All" view no longer serves monthly means under a weekly-average
caption. The server picked how coarsely to summarise from the requested
window, and "All" always asks for the longest one, so every record came
back in monthly buckets no matter how short its history, while the caption
was worked out from the span the data really covers. A short record showed
a handful of points labelled "Weekly avg". The summary level now follows
the actual span and matches the caption.
- The same tab no longer means two different things depending on how the
chart opened. On the dashboard's preloaded data the old code showed "the
last N readings" reaching arbitrarily far back; on a directly opened chart
it showed a day window. Both paths now filter to the same calendar day
window, which also keeps comparison overlay data out of the visible range
on sparse records.
- The mood chart windows by days like every other chart. It used to slice
the last N logged days out of the whole history, so its "7" could span
weeks while the weight chart's "7" meant a week.
- The aggregation chip in the chart header reads the same summary-level
decision the chart and its data table use, instead of working it out
separately.
- The image upgrades its OS packages at build time, so the published openssl
fix for CVE-2026-14456 ships with this build instead of waiting for the
next base image rebuild.
## [1.37.28] — 2026-08-24
Most parts of a shared record were selectable and invisible. Ticking one and
sending the link showed the recipient nothing, while the same link's PDF
download carried it.
### Fixed
- A share link now renders every part of the record it carries. Most of the
selectable sections reached the page and were never drawn: identity,
emergency data, lab results, GLP-1 therapy, logged doses, illness
episodes, visits, immunizations, family history, mood and cycle. The data
had been arriving all along, from the same place the PDF gets it, so
someone who ticked "Lab values" had every reason to believe they had
shared them and no way to find out otherwise.
- A section that carries nothing now says which kind of nothing it is. A
part that was not selected stays absent entirely, because naming it would
itself disclose something. A selected part with no records says so. A
selected part whose module the record has switched off says that instead,
so nobody reads an empty card as an empty life.
- Logged medication doses reached the FHIR download and neither the page nor
the report. The one control whose effect a person could only see by
opening the bundle in another program now shows the twenty most recent,
with a count of what the cut left out.
- The doctor report printed the raw translation key for a visit's kind
rather than its name, and so did the daily digest's upcoming-visit line.
The label was built from the internal name of the kind, so a routine
appointment read as "encounters.kind.ROUTINE" in a document a practice
files and on a phone's lock screen.
- A lab row that diverges from its reference range now names both windows it
was measured against instead of one.
### Changed
- `GET /api/insights/derived` accepts a `windowDays` parameter, so a client
can ask for a longer trend than the fixed fourteen days. The ceiling is
ninety; beyond that the read would have to scan every raw reading, so a
larger value is refused rather than clamped. Omitting the parameter
changes nothing.
- The response already carried the window asked for and the history that
backed it, and the API reference now says so, so a record shorter than the
window reports both.
- The wellness-score value object is modelled in the API reference. It was
typed as an open record with prose describing one metric, which is why a
client team read the document, built what they could see, and reported two
fields as missing that were in the payload all along. The value shapes
that stay undocumented are named where the gap is.
- The share link's report and FHIR downloads are published in the API
reference alongside their document sibling.
## [1.37.27] — 2026-08-23
Several things in this release looked fine from the outside and were not,
the ECG import most of all: it failed on every watch outside a US locale and
never said why.
### Fixed
- Importing an Apple Health archive rejected every ECG recording exported
outside a US locale, and counted each one as a failure without saying why.
Run against real Apple Watch exports, not one parsed. The causes, and only
one of them is about language: the file opens with a header row that has
no value after it, the sample values are written with a decimal comma, the
sample rate is written as a fractional figure with a comma rather than a
round number, and a German watch writes the column names in German. The
importer had only ever seen the US form of the file. All of these forms
are accepted now.
- Two of Apple's own English verdicts were dropped as unknown. "Heart Rate
Over 120" and "Heart Rate Under 50" are what the export writes, which
matches neither the framework's documented names nor the wording in
Apple's instructions. A verdict the app has no category for is now
recorded as unrecognised, which is different from a recording that carries
no verdict at all.
- The Coach could not report a single environmental correlation. Several
pages built the correlation list independently with different sets of
channels, so asking whether air pressure related to your sleep searched a
list that never held the pair. They all read from one place now.
- A metric card's assessment received every surviving correlation in the
record rather than the ones about that metric, because the step that
narrows to one metric had been dropped.
- The dashboard and the analytics page looked up the same single row once
per metric, dozens of times per load. It resolves once per request now.
- The check that a Coach answer backs its claims with citations passed
everything outside English and German, because it looked for English and
German words in text written in your language.
- The Coach refused legitimate questions in some languages and let off-topic
ones through. Its topic vocabulary was English and German, so the Italian
word for "series" sat on the off-topic list while nothing recognised the
Italian for "measurements".
- A number the Coach wrote in prose could be matched against an unrelated
reading. Unit words were recognised in English only, so `7,4 Stunden`
carried no unit, and a value with no unit matched every entry it was
checked against: a sleep figure could be backed by a glucose result.
French dates were not parsed either, so correct prose was stripped as
unverifiable.
- Asking Telegram for help in French, Spanish or Polish got no reply at all.
- The mood icon picker searched only English names, so typing back the
translated group header it had just displayed returned an empty grid.
### Changed
- Deleting every threshold override at once asks first. Leaving the metric
off the request used to erase the whole set, with no confirmation and,
unlike deleting a single override, no rate limit of its own. It takes the
same explicit confirmation the account deletion does. Deleting a single
metric's override is unchanged.
- Removing a passkey now needs a fresh proof that you still hold it, the
same check that has always applied to removing a second-factor security
key. The softer check had been sitting on the primary sign-in credential.
A passkey satisfies its own check, so an account with nothing else
enrolled keeps control of its own credential list, and the app can still
do it through the re-verification step it already uses.
- A portable backup that claims a section it does not carry is refused whole
rather than restored in part, and the error names the missing section. A
section the export left out on purpose and declared as omitted still
restores, which is what keeps the disaster-recovery export working.
### Internal
- Page loads are measured now against a large record, and the cost of a page
does not grow with the size of the record.
- Every signed-in page was downloading the settings validation code. The app
shell wraps all of them and borrowed one small function from a module that
also carries the settings schemas, so a validation library and its modules
rode along on every page load for a check that reads a URL slug. The shell
has its own copy of that function now. No page grew, most got a little
smaller, and the Insights overview has room under its size limit again.
- Unused code that the dead-code check had been missing since a tool upgrade
is gone. Every definition still in use stayed.
## [1.37.26] — 2026-08-23
The app is offered in six languages, and in several places it was reading
what you wrote as though there were two.
### Fixed
- Side effects you recorded on a GLP-1 medication contributed nothing to the
timeline unless you use English or German. Tapping a chip stores the
translated word, so the same symptom is `nausea` on one account and
`Nudności` on 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,0` was read as no range at
all, so the value was never flagged as normal or out of range. Ranges in
all six languages parse now, and one the parser cannot read is recorded as
unreadable rather than looking like a result nobody set a range for.
- Lab analytes named in French, Spanish, Italian or Polish got no LOINC code
on the FHIR export, and a qualitative result of `négatif` or `ujemny` got
no SNOMED code. A lab report in one of those languages also failed the
check that decides whether a document is a lab report, so it was never
offered for automatic staging.
- Analyte names with German umlauts never matched either, which is the older
half of the same defect and affected the language most people here use.
The matcher deleted any character outside a to z, so `Hämoglobin` lost its
ä and never met its own entry. It only ever worked for people who left the
umlaut off. `Nüchternglukose` and `Harnsäure` were the same.
- Over the assistant connection, `Fer`, `Żelazo`, `poids` and `Sommeil`
resolved to nothing, and asking about `Puls` answered with "Pulse-wave
velocity", because a fallback matched English display names by substring.
Medication names written as `Metformina` or `Lewotyroksyna` missed the
class that connects a dose to a symptom.
### Changed
- A route that has been removed now answers `410 Gone` with a
machine-readable code, the version that removed it, and its replacement
where there is one, rather than the bare `404` it used to. A `404` is
indistinguishable from a broken deployment, which is why a client that met
one kept retrying a route that was never coming back. The retirements are
listed in the API reference and on the capability endpoint, so a client
can learn about one without comparing versions.
- A route can no longer disappear without a trace: it is either dropped from
the API reference or listed there as retired.
## [1.37.25] — 2026-08-22
Two things this release is about. The Coach could state a figure your record
does not support, in exactly the situation where it was most likely to: when
you asked about something you have never recorded. And a backup you cannot
fully restore is not a backup, so most of what a restore used to leave
behind comes back now.
### Fixed
- The Coach checks its own figures against a record of what it looked up,
and edits out anything that record cannot account for. That check did not
run when every lookup came back empty. Ask about a metric you have never
tracked, and an invented number was passed straight through, with no note
that anything went unchecked, so the answer read exactly like a verified
one. The check now runs on a turn where the lookups found nothing, and
because removing only the digits would leave the sentence still claiming a
trend that does not exist, such an answer is replaced with a statement
that nothing is recorded.
- The Coach could also describe a weight history that no longer exists.
Syncing a body-composition provider replaces your rows, but the monthly
summaries built from the old ones stayed behind, so one screen could show
a year at 95 kg beside an all-time range that never got there. A summary
is an average of real readings and cannot sit outside their own extremes;
when it does, that band is dropped rather than narrated.
- The Insights hero said "based on your last 90 days" whether or not there
was a briefing behind it, and the pulse page offered a personalised
Karvonen target band while returning population percentiles by age and
sex, or a flat range when your age is unknown. Both say what they do now,
in every language. The Coach's "all time" window was the same length as
the option beside it called "year so far"; both labels tell the truth now.
- An account that had grouped its own mood factors could not be restored at
all. The restore hit a category the file never carried, rolled the whole
thing back, and handed you a database error and an empty account. The
categories you create travel now, and they are written before the tags
that point at them.
- Someone managing a record you gave them access to could write a titration
step onto a medication and then be refused that medication's history. The
read now uses the same access rule as its siblings.
- Disconnecting a Withings account left the connection state reading
"connected" until something else refreshed it, because the disconnect
neither logged itself nor marked the connection as gone the way every
other provider does. Two more disconnects wrote no log entry at all when
there was nothing connected.
- The integrations page could not tell "nothing delivered yet" from "the
freshness query failed": both arrived as an empty list. The failure says
so.
- On a self-hosted instance behind a reverse proxy that does not pass the
host through, signing in from the iOS app failed. The redirect was built
from the address the server process bound to, so it came back naming an
address iOS refuses outright. Redirects to this same deployment no longer
name a host at all. The same bug sat in several places and all of them are
fixed.
- The Prisma CLI in the published image still resolved a dependency with a
published advisory. The application itself had been updated already, but
that install is a separate one with its own resolution, so the image
carried the advisory while every dependency check read clean. The two are
kept in step now.
- The demo instance resets its published credentials on every migration run,
so the account named on the site keeps working.
- Turning the API off used to lock people out of their own tokens. The
switch was checked on the read as well as the revoke, so a user could
neither see nor clean up tokens that were still live. It stops tokens from
working; it no longer stops you tidying them.
- A preference update read its request body with no size limit at all, where
its neighbours cap at one kilobyte. Several more routes threw away the
detail of why they refused a value, so a name that was empty and a name
that was too long came back as the same sentence.
- Two sync routes had no rate limit where their siblings have one, and every
sync route read its request body inside a catch that swallowed errors, so
a typo in the body reported a successful run. A body that is absent still
means an incremental sync; a body that is present and wrong is refused
now.
- Two list endpoints widened their result on a typo instead of refusing it:
an unknown metric type was dropped and the read came back unfiltered, and
a nonsense page size reverted to the default.
- The medication ingest endpoint counted its rate limit per client address,
so several bridges behind one router starved each other. It counts per
token now.
### Added
- The restore now returns your conversation history with the Coach: every
thread, every turn in it, the record of which documents a thread was
grounded in, and the facts, plans and reminders it carries between
threads. Until now a restored account had a Coach that had never spoken to
it and had to be told its own history again.
- It also returns the medication history: pause periods, the titration steps
behind today's dose, the packs on the shelf, and the ledger of refills and
consumption behind their counts. The remaining count comes back exactly as
recorded rather than recalculated from the ledger, because that number is
what the low-stock reminders were computed from. The per-drug reminder
timings travel too, so a restored drug turns orange when it did before
instead of falling back to the defaults. Archived schedules come back with
their succession chain intact, and what a medication was meant to move
comes back with the analyte it pointed at.
- The rest, in short: ECG strips with their trace, their classification and
the reading they were filed against, where a restore into a populated
account used to leave the old strips behind. Which conditions a document
was filed under, and the facts extracted from it, including which of those
were reviewed and committed to the record. The mood taxonomy you shaped
for yourself: the groupings you created and the seeded factors you chose
to hide. Personal bests, badges, and the per-day environment readings with
the location periods that explain them.
- Screener history and the consent record come back in the disaster-recovery
export only. Completed WHO-5, PHQ and GAD administrations include the
PHQ-9 self-harm item, which is not written into a portable file that can
be opened anywhere. The portable export says in its own manifest that they
are absent rather than leaving you to notice.
### Changed
- Per-event notification preferences are recorded as outside the backup on
purpose, not as something still to do. Each one addresses a single
notification channel, the channel itself is excluded for carrying secrets,
and the restore deletes every channel it finds, so a carried preference
would point at a row that cannot exist.
- Dependency updates: pg-boss, Playwright, Testcontainers, Prettier, and two
workflow actions.
- The API reference now describes the whole API. A large part of the routes,
including many the iOS app calls every day, had never been written into
it, and nothing compared the document against the routes that exist. Every
route is now either published or named in a list with a reason for its
absence, and the reasons say which kind: the admin console cannot be
reached by an API client at all, OAuth callbacks are driven by the
provider, webhooks authenticate by a shared secret, and the discovery
documents have their format fixed by somebody else's specification.
- The API reference also says how to authenticate, which it never did. Both
schemes were defined and neither was referenced, so anyone generating a
client from the document got one that sends no credentials and treats
every route as open.
- The idempotency rules are in the API reference. The write operations that
honour the `Idempotency-Key` header now declare it, together with the
`X-Idempotent-Replay` header they answer with, including the detail that a
malformed key is ignored rather than refused.
## [1.37.24] — 2026-08-16
### Fixed
- The hero card on the start page no longer says last night's sleep is still
missing when no sleep reaches the record at all. It used to check only
whether last night had arrived, so anyone without a tracker read the note
every morning about a delay that was never going to end. The note now
needs a night within the last week: a flat battery or a few nights without
the device keeps it, a week of silence retires it, and someone who has
never recorded sleep never sees it.
- The same rule settles the day itself. Records with no sleep source used to
sit on "provisional" indefinitely, which the daily push line reads too, so
the notification carried the same untrue aside.
## [1.37.23] — 2026-08-15
### Changed
- Postponing a checkup is a button on the card now, beside "erledigt" or
"jetzt messen", instead of a row inside the overflow menu. That is the
shape the medication card has always had, where "genommen" and
"übersprungen" sit next to each other, and it puts the action one tap away
rather than three. The compact list view carries the same action as an
icon button. Edit, measurements and delete stay in the menu.
### Fixed
- The quick options in the postpone sheet ("+7 Tage" and its siblings) fill
in the day they name even when a daylight-saving change falls inside the
span. They used to add a fixed number of hours, which shifts the clock
past midnight in the week a country moves its offset, so the option could
write the day before or after the one on its label.
## [1.37.22] — 2026-08-15
### Fixed
- The doctor-report export refuses a selection that includes nothing instead
of rendering an empty document. The selection panel already disabled its
own button in that state, so this only ever reached the server through a
hand-built request, and it spent an export slot on a file with no content
in it.
- A lab value in the FHIR export no longer names UCUM as its unit system
when it carries no UCUM code. That combination told a receiving system the
unit was coded when it was not, which is worse than saying nothing: the
unit still travels as a display string, and the coded form appears only
when the analyte and unit actually resolve to one.
## [1.37.21] — 2026-08-14
### Fixed
- A dose that reaches the server through the batch route, the path a phone's
offline queue drains through, now quiets an active snooze the same way a
directly logged dose always has. Before, a take synced in later kept
counting as snoozed and the reminder kept ringing.
- Marking a dose through the single-medication intake route now also closes
the still-pending dose-due reminder in the web app and refreshes its
badge. The other intake paths have done this since v1.18.4; this one,
which is exactly the route a phone's offline replay uses, left the web
reminder standing.
## [1.37.20] — 2026-08-14
### Added
- A checkup reminder can now be postponed or skipped. Postponing moves the
next due date to a day you pick; skipping closes the current cycle and
restarts the interval from today, without pretending the checkup happened.
Every completion and every skip lands in a new history, kept per reminder,
including whether it happened on time, so the checkups page and the API
can show how a reminder has actually been handled over the years.
- The dashboard hero can show the reminder rail instead of the health score.
The choice lives in the dashboard settings and does nothing until you
change it. Thanks @tarantila for pushing for this one.
- The restore dialog shows what a backup contains before you confirm:
per-type counts read from the stored file, with a note when the preview
cannot be computed.
- A mental-health screening shows its per-item answers on the detail view,
so you can see which questions carried the score, not only the total.
- Document search understands German and English medical synonyms: searching
for "Blutdruck" finds a report that says "blood pressure", and the other
way round.
- A deleted custom-metric entry can be brought back with one tap for a short
while instead of being gone the moment you tap.
### Changed
- Onboarding says plainly that Apple Health data arrives through the iPhone
app, and the Apple Health card reports the backfill progress the server
actually sees: accepted counts, the oldest reading, and how recently data
last arrived. Reported by @HercoGC in #778.
- The measurements and mood pages keep their filters in the address bar, so
a filtered view survives a reload and can be shared as a link, the way
documents already worked.
- The sleep-stage tooltip labels one night's figures as main sleep, naps and
total sleep instead of an averaged label that fit none of them.
- The daily digest is available offline like the rest of the app.
- Backups carry the checkup reminders and their new completion history end
to end, and an appointment or vaccination that references a reminder keeps
that link across a restore.
### Fixed
- Apple Health ECG exports in the single-column layout import correctly.
Apple writes two CSV shapes; the importer understood only the paired one
and skipped every recording in the other, reporting each row as malformed.
Both layouts parse now, and the declared unit is converted before
validation. Reported by @HercoGC in #794.
## [1.37.19] — 2026-08-14
### Changed
- Medication stock understands per-slot doses everywhere now. Every schedule
carries its resolved dose, the remaining-doses figure weighs each slot by
what it consumes, there is a runway in days on the list and the detail,
and the low-stock notification runs on the same numbers, so the card, the
API and the push can no longer disagree about how long a supply lasts.
Refs #767.
- A document whose summary job died no longer says "being generated"
forever. After an hour it says that no summary is available and offers the
manual button, and an hourly background job repairs stuck documents on its
own.
- The morning reminder run gives up after bringing up an ignored Coach
reminder three mornings in a row, because a reminder that nags forever is
worse than none.
- Accepting an updated disclaimer is tracked by version now, so a future
revision can ask again.
- A restore now tells you what it wrote, not just what it cleared, and the
report covers practitioners, appointments, vaccinations and their links.
The portable export names a field it could not decrypt instead of
exporting it as if it were never set.
- If you move timezones, the medication compliance statistics are recomputed
on your new day boundaries.
- Long histories got two fixes: the all-time summary includes readings older
than five years instead of silently cutting them off, and the AI context
falls back to live data when a pre-computed band is missing.
- The copy got a pass in all six languages: shorter consent buttons, a
workouts empty state that no longer assumes everyone owns an iPhone,
consistent toast punctuation, a medication reminder chip that no longer
clips mid-sentence, and the navigation chips lost their unfortunate German
name ("Navigations-Chips" instead of "Pills").
- Settings pages that had been permanently redirected are no longer built,
the retired placeholder is gone, and the about page is reachable from the
privacy page and the settings.
### Fixed
- A measurement reminder claims its notification slot before sending, so a
crash in the wrong moment cannot deliver it twice.
- Submitting the same mood entry twice at once reports "duplicate" with the
entry that won instead of a confusing error.
- For anyone writing against the API, the intake-event description now
documents every field the server returns, and the workout list description
matches the real nullability, so generated clients decode without
surprises.
## [1.37.18] — 2026-08-14
### Fixed
- Coach reminders tied to a moment ("remind me when I next log blood
pressure", "next time I open the app") now fire. The app has offered and
confirmed these since v1.22, but nothing ever evaluated them; a matching
measurement or the next app open brings the reminder up, and the morning
reminder run catches anything left over.
- The Apple Health import tells you what it skipped and why. Every kind of
dropped sample is listed on the import card: unknown types, out-of-range
values, deferred records, and cycle samples read while cycle tracking is
switched off, which used to vanish without a trace. A cycle write that
fails halfway also reports the failure instead of claiming zero samples.
- Deleting a measurement now reaches the pre-computed statistics. Before, a
deleted reading stayed inside the day, week, month and year figures
forever; those figures notice a deletion now, and a deletion reported by
WHOOP recomputes the affected days.
- The sleep history repair job only deletes what it can restore. It used to
clear a provider's entire sleep history and re-import just 30 days; the
delete is now limited to the same window.
- The connector token settings said tokens are read-only while offering a
write toggle on the same card. The text now tells the truth in all six
languages.
- A lab report cannot smuggle instructions into the AI through a biomarker
name anymore; names read out of documents are cleaned before they reach
any prompt, and the assistant tools treat checkup labels and lab analyte
names as data in the same way.
- Google Health and Withings syncs now tell the server that new data
arrived, like every other integration, so analytics pick up synced data on
the next read instead of waiting out the cache.
- On phones, the Medications tab no longer appears in the bottom bar when
the medications module is switched off; tapping it used to open an empty
page.
- The import guide links point at the documentation site instead of a page
that does not exist.
- A caregiver settings toggle that has had no effect since its feature was
retired is gone.
- Every create and update request from the app now carries an idempotency
key, so a network retry cannot write a vaccination or an appointment
twice.
- The offline page the app shows when the server is unreachable is stamped
with the release version again; it had stayed two releases behind.
- The vaccination booster and suggestion endpoints are now in the API
reference, as the API notes already claimed.
## [1.37.17] — 2026-08-13
### Changed
- The correlations page reads one figure per day and source from the daily
summaries the server already keeps, instead of walking every raw reading
over 180 days. For a dense record, glucose from a continuous sensor most
of all, that is one row per day and source instead of every reading.
Values stay the same: the daily figures are combined to match the previous
per-reading averages exactly, a channel that has no daily summary yet
falls back to the raw readings on its own, and sleep duration, steps and
daylight time keep their raw reads because their daily figures depend on
stitching a night together and removing duplicate device readings, which
the summaries cannot express. Profiles far from UTC also stay on the raw
path so day boundaries keep matching their timezone.
## [1.37.16] — 2026-08-13
### Changed
- The correlations page no longer recomputes its full discovery on every
visit. The first request computes the result and keeps it on the server;
repeat visits within the hour answer from that copy, and any new
measurement, mood entry, illness episode or custom metric refreshes it.
Accepting or dismissing a pattern still takes effect immediately.
- Switching a chart's time range keeps the previous chart visible, slightly
dimmed, while the new range loads, instead of dropping to a placeholder.
The dimming is on purpose: what you see during the switch is the old
range, not passed off as the new one.
- The checkups and mood pages send their first page of data along with the
page itself, so they open with content instead of a loading skeleton.
- Two background checks are calmer. The achievement check runs every five
minutes now, less often than before, since anything you do in the app
updates instantly anyway and the poll only exists for data arriving from a
device. The Coach plan-proposal check stops on its own instead of polling
an open conversation forever.
- On your own server, the existing `DASHBOARD_SSR_PREFETCH` flag switches
the preloaded first page off.
### Fixed
- A checkup accepted from a Coach suggestion appears on the checkups page
and the Today rail immediately. Before, it stayed invisible for up to five
minutes.
- Changing a target, a threshold or the health score configuration shows on
the dashboard right away, including the hero bands and score context.
Before, the dashboard kept the old configuration until its next background
refresh.
- Starting or ending an illness episode updates the dashboard's rest mode
immediately, booking an appointment puts it on the Today rail immediately,
and logging a vaccination dose updates the booster reminder on the rail
immediately.
- Completing a mental-health screening refreshes the checkup list right
away, and changing a source priority refreshes the affected charts right
away.
- Background syncs from Fitbit, Oura, Polar, WHOOP, Strava and Nightscout
now tell the server that new data arrived, so analytics and workout views
pick it up on the next read instead of waiting out the cache. The same
applies to illness, cycle and appointment changes, and to every other
place that writes what a daily view reads.
## [1.37.15] — 2026-08-13
### Fixed
- Re-running the assessment no longer reports a failure while the server is
still writing the result. When a refresh takes longer than the browser is
willing to wait, the page says it is still working, keeps checking until
the new assessment lands, then swaps it in. Only a refresh that really
failed says so. Before, a slow refresh showed an error and kept the old
assessment until a reload. (#786)
- Stored PDFs preview inline again. Since the sharing changes, previews load
through copies the page decrypts itself, and the security policy the app
sends to the browser had never been told about them, so PDF previews were
blocked and the vault's image thumbnails fell back to their icons. The
policy now admits the app's own decrypted documents and nothing else.
(#787)
- A document uploaded while automatic AI reading is switched off no longer
claims a summary is being prepared forever. The upload stopped queueing
work it is not allowed to do, a stale claim heals itself on the next
background pass, and documents already stuck are repaired by this update.
The manual summary action is offered instead. (#788)
- When reading a document for search fails, the document now says why: a
scan with no readable text, a PDF whose pages would not render (scanned
fax compression formats are a known cause), a provider error, or a
provider that cannot take PDF files. Before, a failed read looked
identical to one that never happened. An AI answer with no text in it also
no longer counts as a successful read. (#776)
- The "index all documents" confirmation shows the real number of documents
queued instead of the word "true".
## [1.37.14] — 2026-08-13
### Fixed
- The Today rail no longer announces a weekly medication as due up to a day
early. A weekly dose becomes takeable a day ahead by design, and the rail
treated takeable as due, so on Friday the Saturday slot already sat there
as "Medication due". The rail now keeps a dose that is not overdue off the
list until its actual day in your own timezone. Taking it early stays
possible on the medication card, which says which day it belongs to, and
overdue doses appear regardless, as before.
- A dose logged through the quick-add button updates the home page
immediately. The quick-add is reachable from every page, but it only
refreshed the page you were on, so coming back to the home page could show
the dose as still due for up to two minutes. Logging from anywhere now
refreshes the home page the same way logging from the home page always
did.
## [1.37.13] — 2026-08-13
### Added
- A lab report the app has read offers its values for your lab record, right
on the document. When a document is classified as a lab report, its page
shows each extracted value with its unit and date, and you approve or
reject them one by one. Nothing is saved without your confirmation, and a
value the reader was unsure about is marked for a closer look. Reading a
document again by hand offers values the same way, so a report that
arrived before automatic reading was switched on is no longer stuck.
(#779)
- The AI settings can grant consent again after a withdrawal. Revoking
consent stays a standing decision that no page visit can quietly undo;
what is new is a control that lets you, and only you, opt back in.
- The Coach keeps an answer you walked away from. Leaving the page still
stops the reply, but the conversation shows the interrupted turn, coming
back opens that conversation rather than a fresh chat, and a retry control
asks the same question again in place. (#781)
- If you host this for others, the hourly limit for reading documents with
AI is your setting now: `DOCUMENT_AI_LIMIT_PER_HOUR`, matched to your
provider budget. The limit message says when the next read is possible
instead of "try again in a few minutes", and a read that never reached the
provider no longer uses up a slot. (#776)
### Fixed
- The health score could not read the two lab-based pillars. A mismatch in
column names made every glycaemia and lipids read fail, so anyone with lab
results saw both pillars as unreadable. They score now, and a record
without lab results shows the pillar as not tracked rather than as an
error. (#780)
- Importing a large Apple Health export no longer exhausts the server's
memory. The archive is read in batches, so an export with millions of
records imports on a default setup. An import the available memory cannot
carry is refused up front with a message that says what to raise, and an
import that was interrupted shows up as failed with a retry rather than
spinning forever. (#775)
- Automatic document reading no longer hides the manual read action on a
document that has not been read yet, so a single failed or skipped
document has a way back. (#777)
- Read-everything now includes PDFs for providers that accept images; the
pages are rendered to images the same way a single manual read already
did, so such a setup no longer skips every PDF in the vault. (#776)
- The About-me note and the free-text allergy notes moved from Account
settings into Anamnese, next to the structured allergy list they
supplement.
- The temporary folder an Apple Health archive is unpacked into has an
unguessable name now.
## [1.37.12] — 2026-08-11
### Changed
- The public privacy page matches the app again, in German and English. It
describes how you delete your account from inside the app and how a second
factor is handed over, what each way of running the assistant does with
your text, whether that is the one this server manages, your own key,
another endpoint or your own device, what happens to a document you upload
and to the text read out of it, how to withdraw consent, what Apple Health
sync stores here and what it does not, how long things are kept, how heart
rate is represented, the App Store data categories, and that nothing here
tracks you.
- The documentation site has a page for getting in touch: bug reports, usage
questions, privacy and account requests, and private security reports.
## [1.37.11] — 2026-08-10
### Changed
- The lifestyle-context card in Anamnese saves in one action. Smoking,
alcohol and shift work each had their own save button; now you set what
you want across all three and one Save writes them together, with a single
confirmation. If one of them cannot be saved, that entry stays unsaved and
says so while the others go through, rather than a single message hiding
which one failed.
- The mood chart picks up the same header as every other card, which changes
nothing you do with it.
## [1.37.10] — 2026-08-10
### Added
- A medication can take a different dose at each time of day, all from the
one supply. If you take one tablet in the morning and half a tablet at
noon and at night, that is one medication with three times now, not two
separate entries that split your stock in two. Each time slot carries its
own dose and its own count of units consumed, so the supply counts down by
the right amount at each one. An occasional as-needed dose is a slot on
the same medication too, drawing from the same box, rather than a third
entry. Existing medications are unchanged, and one that does not set a
per-slot amount keeps counting down exactly as before.
## [1.37.9] — 2026-08-10
### Added
- **You can record the data that matters in an emergency:** blood type, an
organ-donor and an advance-directive status, plus emergency contacts,
implants or devices, and a free note. Enter it under Anamnese, the same
place the allergies and family history live. The contacts, implants and
note are encrypted at rest like the rest of your sensitive record, and
every field carries through backup and restore.
- The doctor report can open with a one-page emergency summary a clinician
can read at a glance: blood type, severe allergies, current medications,
chronic conditions, implants, the advance-directive and organ-donor lines,
contacts and the note. The page appears only when you have recorded
something for it, and it is part of the report you choose to share, so it
travels to the practice with the rest of the report rather than sitting
somewhere separate.
## [1.37.8] — 2026-08-10
### Added
- **The assistant answers questions about your practice visits now,** both
what happened and what is coming up. Ask about your visit history over a
window and you get the practitioner, the reason and the outcome back, and
when you have never recorded a visit it says so rather than reading an
empty list as if nothing exists. Upcoming appointments arrive alongside
the preventive-care due-list rather than as a separate question, so "what
is coming up" has one answer. The Coach carries a small visits note as
well, so it knows an appointment is near and which practice you last saw
without being handed your whole visit archive.
### Security
- If you host this yourself, a warning written to the server log is scrubbed
of secrets on the way in now, the same as an error message or a request
path already was. A warning often carries an outbound error string, and
for a few integrations that string is a request address with a credential
in it, so closing this entry point removes the last place a secret could
reach the log unredacted.
## [1.37.7] — 2026-08-09
### Added
- **If you run the published image, you can now set `MAXMIND_LICENSE_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, reported by @mathewcsims.
### Security
- The MaxMind licence key no longer appears in a log line when that fetch
fails. The download address carries the key in its query string, and the
failure path used to print the whole address; it reports only the edition
and the kind of error now, and the central log redactor masks a
`license_key` parameter wherever one might otherwise slip through.
- A mental-wellbeing screening reminder (PHQ-9, GAD-7, WHO-5, SCI) can no
longer be marked done through a hand-built request. A screening resolves
only from the score a completed check-in produces, so the server turns
down a request to satisfy or complete a screening reminder outright now,
which is the rule the apps already followed.
### Changed
- Editing a custom mood tag, a custom tag group or a custom cycle symptom
now leaves an entry in the audit log, the same way deleting one already
did. The two display-only actions on that screen, hiding a catalogue tag
and reordering the layout, stay out of it, since those are display
preferences rather than a change to your record.
## [1.37.5] — 2026-08-09
### Changed
- **Copy across several screens reads tighter.** The insight-page metric
explainers open on a one-line definition now rather than a paragraph of
general guidance, with the longer background staying behind the knowledge
disclosure where it already lived. The measurements subtitle and the
daily-briefing setup prompt each drop a redundant sentence, and the mood
self-rating card takes a short title so it stops wrapping. Wording that
had drifted between neighbouring screens is settled too, so the
measurement delete prompts, the practitioner label on the visit and
vaccination forms, and the visit-saved toasts all match the screens they
ship with.
- The settings for a record you look after use the same controls as the
rest of the app. They carry the shared date picker, dropdowns, text fields
and switches instead of a plainer set of their own, so the birth date
reads and saves in your own date order rather than depending on the
browser, and each form keeps its save button at the end with any error
shown just above it.
- Spacing and tap targets read more evenly across settings, the dashboard
and the charts. A handful of off-scale gaps are back on the spacing scale,
the document upload buttons meet the mobile tap size their neighbours
already set, the revoke on a share link is a right-aligned row action
rather than a full-width red bar on every row, and a few empty and loading
states use the shared components now so they match their siblings.
## [1.37.4] — 2026-08-09
### Fixed
- **A read that fails now shows a clear, recoverable error instead of an
empty list.** That mattered most on the share-link list, where a failed
load read as "no active share links", the same as having shared nothing.
It is fixed there and across the app: the API and connector token lists,
the ECG detail and list, the cycle and mood insights, the dashboard
preventive-care tile, the checkups list and the admin console all say when
a read failed and offer a retry.
- The connector-token note in Settings claimed a token was read-only in
every case, printed right above the toggle that turns write access on. It
describes the real model now: read by default, write an opt-in per token,
and never admin.
- The assistant consent and connector notes in Settings render as normal
text rather than faint fine print, so the copy you are meant to read
before turning a feature on is legible.
- The record-a-visit action on the checkups list appears only where a real
practice visit is behind the reminder. A reminder you satisfy yourself,
like weighing in or a mood check-in, no longer offers to file a visit it
never involved.
- The practice address book searches by specialty, so you can find the
dentist by field even when the name and practice hold nothing of the sort.
The list groups its entries under their specialty, and the visit form's
picker shows each contact's field beside the practice.
- Linking documents, lab results and illnesses to a visit is searchable
instead of a flat list you had to scroll. Lab results gather under their
panel and sample date so a whole day's panel attaches in one tap,
documents gather by month, and the vaccination form uses the same picker.
- The respiratory-rate tile on the dashboard shows its unit in your own
language instead of a fixed English label, and the personalized greeting
carries its comma.
### Removed
- **Logging water by hand inside the app is gone.** The water entry on the
capture menu, the dashboard quick-add and the hydration card have all been
removed. Water that syncs in from another app through Apple Health still
arrives and still shows on the hydration card with its daily total,
history and reference line.
## [1.37.3] — 2026-08-09
### Added
- **A new Vaccinations module keeps a lifetime Impfpass.** Switched on under
Settings then Modules. Each dose sits under the disease it protects
against, with its place in the series worked out on the server, so a
combination shot appears under every component it covers. You add a dose
by picking from a catalogue searched by disease or antigen name, or by
typing it the way your pass reads; a date plus one of those two is all it
takes, and the lot number, the injection site, the practice, a visit link
and an encrypted note are optional. Where the catalogue knows a schedule
it shows it with its source, as information rather than advice. A dose
with a booster interval offers a prefilled reminder you can edit or
decline, and confirming it writes an ordinary preventive-care reminder
that shows on the checkups page and moves forward when you log the next
dose. Scans in the document vault attach to their record, the doctor
report gains an immunization section you can leave out, and the whole
module follows record sharing, so a parent can keep a child's Impfpass
under a record they look after. Every language the app has, including the
catalogue names, and never a brand name.
### Fixed
- A preventive-care reminder set to the longest interval the app allows no
longer loses its next date when you log the dose meant to push it forward.
It moves a full cycle ahead now instead of falling silent.
- The mood sliders show their value on its own instead of fused with the
reset control, which is a small icon beside the value now.
- Date and time fields wear the same focus ring as every other input, so it
hugs the field instead of painting wide or jumping left, and they no
longer set off the zoom on iOS.
- The add-mood sheet keeps its scrollbar clear of the endpoint labels, so
the longest wording is never clipped.
- The catalogue and practice pickers carry a name for a screen reader; a
control that only showed an icon was unnamed before.
- The health-score configuration lists its topics as one flat set with
dividers instead of grouping them under headings.
- In Settings, under Security, password reset sits last, after the
authenticator app, security keys and passkeys.
- Inviting someone to a shared record starts with nothing preselected, so
the choice between the whole record and single sections is one you make,
and each scope description is one line with the detail behind an info
icon.
- Sharing a record again with someone whose access had expired works. The
lapsed invitation no longer blocks a fresh one with a false report that
the person still has access.
## [1.37.2] — 2026-08-08
### Added
- **The mood record answers a second question next to your own check-in:
what this day would have looked like on your own past patterns.** Your
self-assessment leads and is never overwritten. The comparison sits beside
it with the number of days it rests on and a range rather than a single
figure, and it stays silent until there is enough of your own history to
stand on. It describes a connection, never a cause.
The rework grew from a concept Kai worked out in detail, from the
split between how you feel and the day around it to the idea that the app
should learn your own patterns instead of scoring you against fixed
weights. Thanks, Kai.
- **A mood entry can hold the shape of the day around it.** Four optional
sections cover work, contacts, leisure and one thing that stood out, each
folded away until you open it, so a quick check-in is still a face and
Save. Sleep, activity and vitals come from the modules that already hold
them and appear beside the entry instead of being asked for again, so a
correction at the source carries through. A figure nobody recorded reads
as not recorded rather than as zero, and a module you have switched off
leaves its block out.
If you write against the API, a mood entry carries the five values and the
day context as optional fields. An app that posts only the five-point
label still writes a complete entry, because the server derives the first
scale from that label.
- **Doctor visits have a place of their own.** A visit records its kind,
when it happened, its status, an optional practice from a contact list you
keep, an encrypted note, and links to the lab results, documents and
illness episodes it concerned. Those are links, not copies. A
preventive-care checkup can be closed by recording the visit that
fulfilled it, and a planned visit sets a reminder the server moves when
you move it and clears when the visit happens. It fires once and never
nags, and it carries no "done" button, because an appointment is not a
task you tick off. The visit list appears in the doctor report, and you
can leave it out.
- A workout recorded before the phone learned to send its heart-rate detail
can gain that curve later. When the phone re-sends a workout the server
already holds, the reading-by-reading heart rate attaches to the workout
that is already there, and nothing else about it changes.
### Fixed
- Marking an older missed dose as taken records it at the slot's own time
rather than the current moment.
- Cycle tracking leaves the Account page, since it is a module and one place
is enough, and changing your password moves to Security, where the second
factors and the passkeys already are.
- The dead space above each row in the Appearance list is gone, and the
module descriptions that ran far longer than their neighbours are back in
line.
- Research Mode is retired. The chart it gated stopped consulting it several
releases ago and has been visible to everyone since, so the switch changed
no screen in either position.
- The EKG page drops its lines about what the app does not do, shows the
device's own reading as a tag beside the date, opens a recording on its
own page with the back link above the heading, and shows the most recent
handful instead of every strip ever recorded.
- A shared record shows its owner's full name where the profile has one, and
the record switcher no longer gives the access level a line of its own,
where it was truncated more often than it was read.
## [1.37.1] — 2026-08-08
### Added
- **A mood entry now records five separate values instead of one.** The
quick check-in is unchanged: pick a face, save, done in a tap. Underneath
it, a section that stays closed until you open it holds five sliders, each
with wording at both ends: how the day felt, how much strain it carried,
how much energy there was, how connected you were, and how able to act you
felt. Anything you leave alone stays empty rather than landing in the
middle. The mood page trends all five over a week, a month or three
months, and the daily briefing and the assistant read them with their own
age, so a value from last week is stated with its date instead of as
today's.
- **A lab result keeps the range its report printed.** A lab report prints
its own reference range next to every value, and your physician reads the
value against that range, not against a general catalogue. Until now the
document reading extracted the value and discarded the range beside it. It
captures the printed range now, and that range decides the verdict
everywhere: the API, insights, the assistant, the doctor report and its
PDF, FHIR, backup and restore. The chart shows the printed window as a
second band and marks where it differs from the catalogue. Values without
a printed range keep the catalogue, and anything the reader cannot parse
with confidence lands verbatim in a text field instead of being guessed
at.
### Changed
- Factor ratings are no longer offered on a new entry. A separate score for
work stress and the strain slider are two answers to the same question,
and only the slider is asked now. Ratings already logged still show, still
edit, and still feed the breakdowns that read them.
- **Sync refuses readings the app itself calls impossible now.** A provider
could store a pulse in the millions, and one recently did. Every sync path
checks, and an implausible sample is dropped and counted, never clamped
and never allowed to fail the batch. The insights baseline applies the
same ranges before averaging, so an impossible stored reading no longer
skews its own comparison.
- The settings pages and the admin console had drifted apart wherever
nothing held them together. They share one page header, one action-row
shape, one card rhythm, and a rule that a card description is one
sentence, in every language the app has.
### Fixed
- Mood entries survive a restore whole. The backup carried only part of an
entry, so a restored one came back without its note, its tags and its own
timezone. Backup files written before this restore as they did.
- The workout detail draws the whole heart-rate curve: it runs to the end of
the workout instead of stopping at the moving-time mark, the min-to-max
band no longer squashes the shape by dragging the axis to zero, and the
peak marker renders again.
- The cycle module says "overdue" instead of "insufficient data" when a
period is late, deleting a period start no longer strands the previous
cycle in a state that stops all forecasting, a backdated period shows up
in the calendar month it belongs to, and a period logged only as flow
opens a cycle, which covers the whole Apple Health import path.
- Marking an older missed dose as taken lands on the slot it names instead
of a standalone row at the current minute.
- The top card no longer says "today" about a reading that is five days old.
- A hand-entered night of sleep is stored as hours, not minutes.
- Dialogs no longer scroll sideways on a phone, and the coach launcher no
longer shows a dot with an empty conversation behind it.
- **If you host this for others, the admin "wipe all data" action left most
tables standing.** It clears everything the per-account wipe clears now,
keeping only sign-in credentials, instance configuration and the grants
that keep a record without a login reachable.
- The geo resolver reads mounted GeoLite2 databases first and asks the
online provider only when they cannot place an address, which is what the
documentation promised all along. If you mounted the databases to keep
login addresses on your host, they do that now.
- SECURITY.md no longer claims the publish workflow runs on every push to
main; that trigger went away in v1.4.34.
### Security
- The PDF renderer behind the document vault moves past a script-execution
advisory, and the image build selects that renderer by name instead of
taking whichever copy the filesystem listed first.
- A dependency advisory on a small helper library reached through the
framework toolchain is closed, and scheduled scans of the source tree and
the published image now run alongside the blocking one.
## [1.37.0] — 2026-08-07
### Added
- **An invitation can name the sections it opens rather than handing over
everything:** readings, medications, lab results, health background,
illness, mood and mind, cycle, documents. Anything you do not pick stays
closed, and a section you kept back looks exactly like a record nobody
shared with you.
The invitation screen says two things out loud: a section can mention the
rest of your health in its notes, so sharing a section shares whatever was
written in it, and the dashboard overview, the health score and the daily
digest need the entire record, because a figure computed from part of one
would read as a figure about the person.
Invitations you have already sent or accepted open the entire record and
are untouched.
- **A third level of access.** Read access looks. Write access adds. Manage
access can also change and remove what is already there, including entries
you made yourself, and can read the insights generated from the record. It
stops at the account around the record: sign-in, second factor, connected
services, tokens, where notifications go, which modules are on, and who
else has access. Offering it asks for your second factor, so it happens in
a browser, and it always covers the entire record, because a note in one
section can be about any other. Everything a manager does is recorded
under their own name.
- **A record for somebody who does not sign in.** @balajiv113 asked in #360
for a way to look after a family member's record. Access between two
accounts covered half of that; the other half is a person with no account
at all, a child or somebody you care for. Such a record has no login and
no e-mail address. You give it a name, optionally a date of birth, and the
language and timezone its days and reminders are measured in. Creating one
asks for your second factor and puts you on it in the same step. You can
invite somebody else to look after it with you, and it can never be left
with nobody: the last person on it cannot hand it back and cannot be
removed. The way out is to add somebody else, or to delete the record.
Its reminders reach the people looking after it. A record with no login
has no phone of its own, so its medication, measurement, safety-floor and
low-stock reminders go to them, each in their own language and over the
channels they had already chosen. The message names whose record it is
about and carries no buttons; opening the app takes you into that record.
One of you turning off their own reminders does not silence anybody else.
You can keep that record's settings too, from modules and units to
thresholds and notification preferences, while your own screens stay in
your own language. Connected services show a status and nothing more for
now.
- Inside a shared record the document vault opens, and "take all due" and
marking a dose skipped are offered to somebody with write access.
### Changed
- Write access no longer lists logging a value on a metric you track
yourself, since no page a shared record opens could record one. Reading
those values stays.
### Fixed
- **Two tabs of one browser could disagree about which record they were
in.** Switching records applies to your whole sign-in, so a tab left open
on the record you just left went on reading and writing there until it was
reloaded. Every request says which record the tab believes it is in now,
and one naming a record you have left is turned down in a way the tab
recovers from on its own. Switching with several tabs open could also
leave every tab on a spinner for half a minute; that is fixed too. The
phone app was never affected.
- Somebody with write access can no longer overturn a dose you had already
recorded, on either path that reaches it. Re-sending the same decision
still works.
- Closing a document could leave it named in the address bar, so a reload or
a shared link re-opened the sheet you had just closed. The close checks
that the browser stepped back now, and tidies the address bar when it did
not.
- A control tapped before the app knew whose record was on screen no longer
strands you in a form the server would turn down.
- A dose recorded from the dashboard quick-add says which record it landed
in and no longer offers an Undo only the owner may use.
- The access-level options on the invitation screen are visible when reached
by keyboard.
- The customise link on the mood page no longer leads someone you gave
access to onto a page that tells them it was not shared.
### Internal
- On a server you run yourself, migrations run on start and there is nothing
to configure. Neither feature does anything until one account invites
another. The account-sharing page in the self-hosting guide is rewritten
for all of the above.
## [1.36.1] — 2026-08-04
### Added
- **Somebody you have given access to can now write into your record, not
only read it.** Usually the point of sharing is that another person is
doing something about it: a partner logging the blood pressure they just
took, someone recording that the evening dose went down. Read-only access
made them watch that happen and then ask you to type it in.
A grant carries a level now. Read access is exactly what it was. Write
access admits a short, closed list of things a second person may add: a
reading, a lab result, a biomarker to the catalogue, an illness entry, a
value on a metric you track yourself, a side effect against a drug, a
medication, and marking a dose taken or skipped. Editing, deleting,
restoring and importing stay with you, including on an entry the other
person added a minute ago.
Every entry the other person makes is stored as yours, because it is a
fact about your body and belongs in your record whoever wrote it down. Who
wrote it is in the activity trail, so "who put this here" still has an
answer months later. The access panel gained a feed of what has been
contributed, and you are told when somebody marks one of your doses.
- **A control the record will not accept is now absent rather than present
and broken.** Before, someone opened a page inside your record, saw the
button, filled the form, and the server said no. Nothing leaked, but it
taught them the product was broken. A control that is not there says "this
is not part of what you were given", which is the truth.
- An invitation can name the day the access lapses, set when it is sent.
### Fixed
- **The first page someone with access landed on was the worst page in the
feature.** Most of the requests behind the dashboard were turned down
inside somebody else's record, so the tile strip had no numbers and the
top card showed an error while every page underneath worked. They answer
now, except the ones that describe the person reading the screen, such as
the interface language, which keep answering about you.
- **The reference ranges were the wrong person's.** The dashboard drew
blood-pressure targets, the weight range and every shaded band on a chart
from the date of birth, sex and height of whoever was looking rather than
whose record was open, so a stranger's ranges were painted over their
readings for the whole visit. It uses the record's own now. The BMI chart
had the same problem, dividing their weights by your height; it draws
nothing there now rather than a number belonging to two different people.
- Deleting a medication's dose history removed the rows outright. A mis-tap
was final, and a phone that was offline at the time kept every deleted
dose forever, because a row that no longer exists has nothing to tell
anyone. The history is retired now the way every other deletion in the app
works.
- Recording a side effect against a medication and then restoring from a
backup lost it. Side effects travel with their medication now.
- Adding water by hand and undoing a deleted lab result both wrote health
data and left no durable record of who did it. Both leave one now, like
every neighbouring action, and the activity feed names the action when
someone with access marks a dose instead of showing a blank line.
- Marking a dose and postponing a reminder went through the same request, so
somebody with write access could postpone one of your medication
reminders, with no upper bound and without you being told, and the
activity feed called it marking a dose. Postponing a reminder is yours
alone now, and so is changing a dose you already recorded.
### Internal
- For anyone self-hosting, the library the server uses for outgoing requests
moves past high-severity advisories published since the last release, and
the notification senders were run against the new version rather than
assumed to work.
## [1.36.0] — 2026-08-02
### Added
- **You can give another account read access to your record.** @balajiv113
asked for a way to look after a family member's record in #360, and this
is the shape it took: everyone keeps their own account, their own
password, their own second factor. Nobody logs in as anybody.
The owner sends an invitation, and it opens nothing until the other person
accepts. Either side can end it at any time, and it ends on the other
person's very next request rather than at their next sign-in. An
invitation can also carry a date it lapses on, so access that was meant to
be temporary does not quietly become permanent because nobody remembered.
- **While you are looking at somebody else's record the whole app says so,
and it opens that record and nothing else.** A strip across the top names
whose record you are reading, and the navigation drops what does not
belong to them. Every write, and everything around the account, is turned
down by the server rather than merely hidden from the screen: credentials,
integrations, notification channels, share links, and handing the access
on. A few reads that look harmless stay closed too, because they sit next
to something that would widen access.
Two of those limits are permanent rather than pending. Someone you gave
access to can never create a clinician share link, because that would be a
door surviving their own revocation, and can never reach anything that
would widen their own access.
- **The owner can see what the access is being used for.** A panel lists who
has access, when they accepted, when they last opened the record, and
which days somebody else was in it. The invitation is also the consent
record: who offered the access, who accepted and when, when it lapses,
when it was last used, and who ended it. Revoking marks it rather than
deleting it, so the history of who had access between which dates survives
the access itself. The address the other person accepted from is not
stored; a consent record has to say who and when, and where adds nothing.
### Fixed
- **A backup holding a symptom name this instance no longer recognises now
restores.** It used to refuse the whole file over one retired name, so a
single renamed entry cost the entire account. The entry that cannot be
matched is dropped, and the restore says which names it dropped and how
many entries each one cost, rather than quietly losing them or refusing
everything.
- The About page said the project is licensed under the GNU Affero General
Public License. It has been PolyForm Noncommercial 1.0.0 since v1.15.19,
as the same page's footer said correctly. It also described the GeoLite2
terms as CC BY-SA 4.0 alone; those databases ship under MaxMind's own end
user licence, which incorporates CC BY-SA 4.0 and takes precedence where
the two disagree. Both are named now.
- The line under the top bar and the line under the sidebar header came
apart whenever a banner was showing, because the banners sat inside the
content column while the sidebar is beside it. The banners span the window
now, so both lines start below them.
- Chart cards on the dashboard and in Insights titled themselves one heading
level below where they sit, so a screen reader walking the page heard the
level jump. They sit at the right level now.
### Internal
- The backup and restore runbook now says what a backup does not carry, and
what that means depending on whether you restore onto the same instance or
a fresh one.
## [1.35.6] — 2026-08-02
### Fixed
- **The line under the top bar and the line under the sidebar header now
meet.** They were one pixel apart, because the two carried the same height
but painted their border on different elements. Both take height and
border from one place now, so they cannot come apart again. Still open, if
you host this with an offline, demo or unmaintained-language banner
showing: those banners push the top bar's line down without moving the
sidebar's, which needs the page shell rearranged rather than the bars
adjusted.
### Internal
- A piece of code that worked out a headline verdict for the dashboard is
gone. What read it was removed a while ago and nothing else was ever
connected to it, so it had been computing an answer nobody read. Every
signal it covered already has a live owner elsewhere.
## [1.35.5] — 2026-08-02
### Fixed
- **Cycle reminders no longer depend on whether you happened to open the
calendar.** The prediction a reminder is sent from was written only while
that page was being viewed, so an account that never opened it had nothing
for the reminder job to read. The prediction is refreshed nightly now for
every account that tracks a cycle, whether or not anyone looks at the
page. A prediction that fails to save is no longer discarded in silence
either.
- **The sync handshake no longer marks an account as synced.** Asking the
server where to resume is not the same as sending anything, and treating
it as a sync moved the resume point past data the app had not fetched yet.
The mark is set only by the paths that actually receive data now, so it
can only trail a real sync rather than run ahead of one.
- **Badges are recorded whether or not you open the app.** The date a badge
was earned was written only when the achievements screen was loaded, so an
account that earned one and did not look never had it written down, and
the date is the part that cannot be worked out again later. A nightly pass
records it now, with the date it was actually earned rather than the date
it was noticed.
### Internal
- Three reads no longer write. A read that writes widens what a read-only
token can reach. One such read stays for now: opening the cycle pages
creates a cycle profile for an account that has none.
## [1.35.4] — 2026-08-02
### Fixed
- **A checkup due later today is no longer described as overdue.** Two
places counted the distance to a checkup in hours and rounded, which is a
different question from how many calendar days away it is. On a Friday
evening, a checkup on Saturday morning and one on Sunday morning both
rounded to tomorrow. Both count calendar days now, in the timezone on your
profile.
- **The assistant and the screen beside it agree about when a checkup is
due.** The list the assistant reads flipped to overdue the moment the
clock time passed, while the page still said today until midnight. One
appointment could produce two different answers depending on which one you
asked.
### Internal
- The places in the server that work out who is calling are one fixed list,
including the ones that identify a caller by a credential in the address
rather than a login. A new one cannot be added without being listed with a
reason, and the one that turns an access token into an account had been
missing from it.
## [1.35.3] — 2026-08-01
### Added
- **Apple Watch ECG recordings can be sent directly by the phone app** now
instead of only arriving inside an export archive, so a strip you recorded
this morning no longer waits for your next export. For anyone writing
against the API, that is one recording per request at
`POST /api/insights/ecg`, behind the same module and status switches that
already govern reading ECGs, so an account with ECGs switched off is
unaffected. The limits are 32768 samples, a 2 MB body and 60 recordings a
minute, and an unknown field in the body is rejected by name rather than
ignored.
### Changed
- A recording carries its own identity, so sending the same one twice ends
up as the same entry rather than a duplicate or an error, and a retry
after a dropped connection is safe.
### Fixed
- **A page no longer grows a second scrollbar** for the moment between
appearing and becoming usable. Every screen with a toggle briefly drew a
hidden form control outside the scrolling area, so the whole window became
scrollable to about twice the height of the screen until the page finished
loading. On a slow connection or a busy phone the moment was long enough
to flick past the end of a settings page and land on nothing. It is fixed
for every page at once rather than the one it was noticed on.
### Migration
- **Back up before you update if your server has ECG recordings stored.**
Your database can hold more than one row for the same recording,
because an export archive and the app name the same strip differently. A
rule now prevents that, and the migration resolves what is already stored
before applying it. Where two rows describe the same recording it keeps
the one holding the curve over one holding only a verdict, and it carries
the removed row's link to its rhythm event across so the recording keeps
its place in the timeline. The rows that lose are deleted and cannot be
recovered. The migration runs on boot; if it fails, the container stops
with the migration named in the log and never serves traffic.
## [1.35.2] — 2026-08-01
### Added
- **The cycle ring gets its verdict from the server:** which day of the
cycle you are on, when the next period is expected, and whether one is
late and by how many days. The web app and the phone app each worked it
out from the raw dates and did not always agree. There is one answer now,
and both show it.
### Changed
- **Green on the Health Score starts at 70,** the same threshold the
readiness and sleep scores already used. A weak area still holds the band
down whatever the average says, so one poor pillar cannot hide behind good
ones.
- **The Fitness pillar is gone.** It could never produce a value, because
the record cannot tell a measured fitness test from a device estimate and
the pillar would not score on an estimate. It was a switch that did
nothing.
- The score settings page lines up with the other settings cards: same left
edge, no lines inside the card, and the save button where every other card
keeps it.
- The line on the Today card saying that something had arrived, with a time
but never what, is gone.
### Fixed
- A saved score selection comes back exactly as you sent it. One pillar was
dropped on the way to the server, so the page could never agree with what
it had just saved and the save button stayed lit as if there were unsaved
work.
- The cycle ring follows the timezone on your profile instead of the one
your browser happens to be in, so the day of your cycle no longer shifts
when you travel.
### API
- The share-link fields announced for retirement are no longer served.
- A repeated request that conflicts returns its error in the same shape as
every other route. One endpoint answered differently.
- The score settings route, which shipped in the previous release without
being described at all, is in the API reference.
## [1.35.1] — 2026-08-01
### Fixed
- A weekly injection you have already taken is no longer announced again. A
medication dosed on a rolling interval had its expected dose anchored on the
moment you actually injected, and was then checked off against the moment it
was planned for, with six hours of tolerance. Those two are allowed to sit days
apart, because a late dose still counts, so the dose your own injection created
could never be marked done. It stayed open, went overdue, and the app asked for
it again. Anyone on a weekly medication could hit this without ever editing
anything: taking a dose a few days late was enough. Push reminders were never
affected, because they read the plan rather than your entries.
## [1.35.0] — 2026-07-31
The Health Score becomes something you decide rather than something that happens
to you.
### Added
- You choose what counts toward your Health Score. A settings page of its own
lists the pillars, each one on or off. It starts with everything that can be
scored, so a narrower score is a decision you make rather than a default you
inherit.
- Each day's score is now recorded with the recipe that produced it: the value,
the method, which pillars took part, what each of them scored, and a
fingerprint of the inputs it depended on. Until now nothing was kept, and every
comparison was recomputed under today's rules.
- The score says when its composition was authored, so a number built from a
chosen set never looks like one built from everything available.
### Changed
- The module switches no longer decide what the score is made of. Turning off
Sleep or Labs used to remove that pillar from the score with nothing on screen
saying so. Those switches now govern what you see, and the score's own page
governs what it counts. An existing account keeps exactly the composition it
has today, so no number moves on upgrade.
- Changing what counts no longer reads as a change in your health. The comparison
stops at the point the recipe changed and says so, rather than drawing a line
across two different definitions. The per-pillar figures continue, because those
stay comparable.
### Fixed
- A settings change can no longer produce a message telling you your score
dropped.
- The score's own coverage figure now describes the pillars it actually counts
rather than every pillar that exists.
### Removed
- Two refusals that could not refuse anything, and a boundary condition no input
could ever reach.
### Operations
- Two migrations: one for the per-account composition, one for the recorded
scores. The recorded days travel through backup, restore and delete-all-data
like every other table.
## [1.34.5] — 2026-07-31
A release about the score explaining itself, and about sentences arriving in
the language you read.
### Added
- The health score sits beside the greeting again, at the top of the
insights page, instead of on a separate card pinned underneath it. It has
one place on the page.
- Every pillar inside the score takes its own colour. A pillar doing well
was painted in the overall score's colour, so a good result inside a weak
week looked like a bad one.
- A blood pressure score says how it came about. The pillar's detail now
opens with which of the two values set the number and how far it sat from
its boundary, what the scale means at that boundary, and that the pair it
works from is a weighted average over the window rather than a reading
anyone took.
### Fixed
- The Coach read strip writes both of its lines in the language you use. The
summary was translated and the line directly beneath it stayed English.
The translations existed the whole time. The request simply never named a
language, and the code behind it chose English whenever nobody did.
- The cycle insights read carried the same silence and the same English
line.
- Switching language reloads those lines instead of showing the previous
language out of the browser cache.
- Anything in the score that is not contributing is one quiet line instead
of a block of its own. A read that failed is named once for every pillar
it took down and keeps its retry. Safety guidance is shown in full.
Anything not recorded yet is counted, with the names one tap away.
### Internal
- A function that writes a sentence for a person has to be told the
language, and none of them may fall back to English on their own.
- The space the score holds open while it loads is measured against the
built panel instead of estimated. The declared minimum was smaller than
the blocks inside it, so it never bound anything, and a short score was
pushed further down the page than it needed to be.
- A widening of the pillar label column was taken back out. The measurement
behind it did not hold: the longest label in five of the six languages
overruns any column this panel can afford, so the extra width would have
come off the bar.
## [1.34.4] — 2026-07-31
A release about two places in the code answering one question differently.
### Added
- An inferred nap is now shown as its own band in the sleep stage view
instead of being blended into that night. A nap used to be counted into
the night's stages, so a day with a long afternoon sleep read as a longer
and shallower night than it was. Nights without a nap look exactly as they
did, with no empty band and no legend entry for something that is not
there.
### Fixed
- A checkup that is due tomorrow says tomorrow. The dashboard card worked
its due line out in rolling 24-hour steps while the checkups page used
calendar days, so the same appointment could read as due tomorrow on one
screen and in two days on the other. Both now read one shared rule.
- The due line and the date printed beside it are worked out on the same
clock. One used the device's timezone and the other the profile's, which
disagree by a day for anyone travelling or on a machine set to another
zone.
- Recording something no longer takes minutes to appear on the home page.
The page render and the API were each holding their own copy of the server
cache, so a write emptied one and left the other serving the state from
before it. They now share one.
- Returning to the dashboard no longer answers a refresh with a reading
fetched before your last entry.
- The onboarding profile step says when the server could not accept a field.
Valid answers are kept, rejected ones are named, and if nothing could be
saved the message says which field blocked it.
- A cycle symptom's intensity survives a backup and restore. It was recorded
and shown, and every export quietly dropped it. Older backup files still
restore, with the intensity honestly absent rather than filled in with a
zero.
### Removed
- The Health-Score explainer switch in the operator settings. It promised a
control that was deliberately removed two releases ago, so it had been
switching nothing for months.
### Internal
- The sleep composition and the single-night view now agree about which
session is the night, because they ask the same function rather than each
deciding.
- A dose taken before midnight stops reminding, and a genuinely missed dose
still escalates.
## [1.34.3] — 2026-07-31
A release about surfaces that knew something and did not say it.
### Added
- The workout page now shows the day around a session instead of linking away
from it: the day's pulse curve, the night before, and that day's mood, on the
page. It also offers the previous session of the same sport in one tap, says
how many workouts the list holds, and lets a sport narrow it.
- When a nutrient sync arrives and nothing can be stored, the nutrients page now
says so and why, in plain sentences, instead of showing the same empty page
someone would see who never synced at all.
### Changed
- The health score appears once. It is a single card below the greeting with the
breakdown behind one expander, rather than a compact copy in the greeting plus
nine full-width blocks underneath. Scores with too little input leave the grid
but keep their name and the reason they are not scored.
- The Coach input is one row tall until you type. It used to reserve a second row
for its buttons before anything had been written.
- Card headings sit closer to their content. A card with a title and no
description reserved space for a description that was never there, which
affected 45 of the 48 cards in the app.
### Fixed
- A profile save no longer discards everything because one field is invalid.
Valid fields are saved, rejected ones are named, and if nothing can be saved
the message says which field blocked it. Validator wording no longer reaches
the screen.
- A failed read on the nutrients page is no longer shown as "no data yet".
### Contract
- Share-link selection now publishes which leaves belong to which group and which
group is the sensitive one. The server always knew; a client that could not see
it had to withhold "select all" rather than risk sending psychometric scores in
a single tap.
- The dashboard snapshot schema declares three fields the route has been sending
all along. Because that schema is closed, a strictly generated client rejected
the real response.
- Rhythm events carry their own six-value classification, separate from the
three-value one on ECG rows.
- Six routes that clients consume were added to the published specification,
including sixteen skip reasons enumerated from the source rather than
transcribed. Three schemas were named in the specification's own prose while
never being emitted; all three are now published.
### Operations
- The optional worker in the bundled compose file inherited 14 of the app's 62
environment variables, missing every mail, push and notification value. It is
the process that sends notifications, so a split deployment ran a worker that
silently sent nothing. Both services now share one definition.
- Bounded log rotation on both containers, dropped capabilities and a memory
limit on the app, with the reasoning recorded, including why the database gets
neither.
- Every nutrient ingest call is now recorded, not only the ones that stored a
row. The single durable trace used to be conditional on success, so a batch
that was entirely rejected looked exactly like a batch that never arrived.
- The build carries its own memory ceiling, so a local run, CI and the published
image all invoke it the same way.
- The self-hosting notes warn that a pinned image reference stays pinned, and name
the two checks that catch a deploy which reports success while serving the
previous release.
## [1.34.2] — 2026-07-30
Point release. One workout that arrived more than once, and several surfaces
that were reading the same record differently from one another.
### Fixed
- Badminton recorded through Google Health keeps its sport instead of arriving
as "Other". Reported by @tarantila.
- A workout the same source sent twice is now counted once everywhere. The
workout list already collapsed the repeat, and the Coach narration, the
per-sport comparison and the workout deep link did not, so one session could
be folded two or three times into the averages a person is measured against.
The three that missed it were fetching the workout without its duration, and
the identity check needed that field.
- Insights prose renders at full contrast, and the two assessment cards share
one internal rhythm again after one of them drifted away from it.
- The dashboard headline survives for anyone who has switched the score lead
off.
- The service worker's fallback cache version follows the release again. It had
slipped a version behind, which only shows up in a browser that cannot load
the version file, and that browser then kept serving the previous release's
cached pages.
### Credit
Reported by @tarantila, directly rather than on the tracker: the badminton
mapping, following on from the workout-type loss in v1.34.0.
## [1.34.1] — 2026-07-29
Security and reliability point release.
### Added
- Google Health imports now show bounded progress and a clear final result.
- Workouts keep their provider details and can be enriched with available WHOOP
data without inventing pulse curves.
- Medication reminders retain overdue occurrences until their actionable window
has ended and identify each occurrence uniquely.
- Intake imports report completed writes and already-recorded entries clearly.
### Changed
- Dashboard scores, follow-up suggestions and hero metrics use the latest
authoritative readings.
- Public pages, documents and downloads now behave consistently on desktop and
mobile, with tighter spacing and clearer empty states.
- The administration and settings areas use a more consistent navigation and
grouping structure.
### Fixed
- Prevented duplicate medication intakes and reminders for the same occurrence.
- Fixed stale or future readings replacing newer measurements.
- Fixed cache isolation for authenticated PWA pages.
- Fixed passkey step-up, password-reset session revocation and refresh replay
edge cases.
- Restricted private-network integration requests to approved origins and final
sockets.
### Security
- Hardened MCP read permissions, refresh handling, passkey verification and
server-side request validation.
- Apple ECG data remains encrypted and source-labelled. WHOOP ECG is not
inferred from pulse data.
## [1.34.0] — 2026-07-28
Two things this release keeps saying to itself. A job, a sync, or a write
that fails can no longer report success by falling off the end of a
function. And a number the app grades you against is either yours or it says
plainly that it is not.
### A failure now looks like a failure
Every background job in this codebase, all 104 of them across five queues,
used to signal success by returning nothing. A handler that caught an error,
logged a warning, and returned was indistinguishable from one that finished
its work, so a queue could fail quietly for as long as it liked. Every
handler now has to construct and return the outcome of what it did, and the
type system enforces it: a handler that falls off the end no longer
compiles. Retry behaviour was reviewed queue by queue rather than flipped in
bulk, because a rethrow changes what runs, not only what shows up on a
screen.
The same fix reached the surfaces you actually watch. Six integration cards
and the lab import dialog used to key their tone off whether the network
call itself succeeded, so a sync that imported nothing because every reading
was refused still rendered a green tick, and a re-scan where every row
turned out to be a duplicate raised a success toast for zero saved readings.
They now render the outcome the server actually resolved: a tick only when
everything landed, a warning when part of it did not, an error when none of
it did.
### Grading you against your own numbers
The Health Score graded your weight against a target nobody set, silently
computed from your height and never shown anywhere. If you had entered your
own weight target on the targets page, the score ignored it. Both the
targets page and the score now read the same target you actually set, and an
account with no target of its own gets a plain trend reading instead of a
number graded against a hidden goal. The score's weight row now says
outright which of the two it used.
The Health Score itself is rebuilt on the same rules about coverage and
where a value came from that the rest of the app's derived metrics already
use. Every pillar now says what went into it and what did not, weights
redistribute openly when a pillar has nothing to grade instead of quietly
changing what the number means, and the card explains itself the way the
other Insights cards do. Because both of these changes move the same number,
they are explained once: the score carries a version marker, and the first
delta you see after updating is suppressed rather than reported as a real
day-to-day change, with a note on the card saying why.
Steps, active energy, distance, and flights now compare today's running
total against what is typical for you at this same hour, not just against
your latest reading, which never meant anything for a metric that only makes
sense as a full day. It needs at least two weeks of your own history before
it says anything, and until then it says so honestly instead of guessing
from three days of data.
### What was asked for, and built
An account-defined custom metric can now feed the same correlation engine
your built-in metrics already run through, so a value nothing else in the
app tracks can be checked against your own outcomes rather than sitting in
its own silo. Caffeine and nicotine join alcohol as taggable factors in that
same engine. A correlation or crosstab finding can be dismissed once it has
been looked at, and stays dismissed instead of recomputing itself back into
view on every run.
Three facts about you, smoking status, alcohol pattern, and shift or
rotating-schedule work, can now be recorded as dated, correctable entries
rather than buried in free text, with their own switch for whether they
reach the Coach's prompt at all.
The "worth a look" rail on the dashboard can now be trimmed to the kinds of
item you actually want to see there. Turning a tile off there only hides it;
it does not touch the medication reminder itself, which keeps firing on its
own channel regardless of what the hero card shows.
### Two defects, found by people who read the code and the numbers
A workout synced from Google Health with a sport type the app's mapping
table did not recognise lost that type outright and rendered as "Other,"
identical to a workout that genuinely had no category. The original label is
now kept on the row, the same way three other providers already keep theirs;
a full re-sync repairs workouts already written under the old behaviour.
A same-time baseline band for a metric that cannot go negative, such as step
count, could report a lower edge below zero. One call site is now required
to name which metric it is drawing a band for, so the floor comes from that
metric's own declared range rather than being patched in by hand at each
caller.
**Reminders for a multi-dose schedule stopped matching the dose they were
meant for.** The reminder tick computed one window length for a whole
schedule and applied it to every time slot in it, so a twice-daily schedule
gave its first dose a ten-hour grace period nobody configured while a
single-dose schedule with a narrow window escalated correctly but then had
almost no room to register a dose logged late. Each slot now resolves its
own window: an explicit per-dose override first, then the schedule's own
grace setting, then the same default every other unconfigured dose gets,
bounded so two doses in one day can never share a window.
A Fitbit workout kept only the category this app sorted it into and dropped
the raw activity name and type id it arrived with, unlike every other
provider this app syncs from. An unrecognised Fitbit activity had nothing
left to diagnose it by. Both are now kept on the row, and carried through
backup and restore the same way.
### The backup
Your custom metrics and their readings, and the encrypted health-profile
facts above, are now carried by export and restore. Neither was before; a
restore silently rebuilt an account without either and reported success. The
hourly shape behind the same-time baseline is also carried, because past a
short grace window it is the only copy of that curve anywhere. And the
summary shown after uploading a backup now counts every record class it
restores, including the nutrient day totals it was quietly leaving out of
its own total.
### For operators
Four migrations. `0283` adds the intraday cumulative profile table, `0284`
adds the effective-dated health-profile fact table and its inclusion enum,
`0285` adds correlation-pattern identity, `0286` seeds the caffeine and
nicotine mood tags. None deletes or rewrites a measurement.
### Credit
Reported by @lutzkind (#613, #614, #616, #617, #649, and a long run of
earlier reports this release also carries forward). Reported by @tarantila,
directly rather than on the tracker: the Google workout-type loss, and a
below-zero baseline reading that turned out to affect other metrics once
someone went looking. Built by @mathewcsims (Refs #662): the per-slot
reminder window fix.
### Dedication
This release is dedicated to the people who reported something, asked for
something, or noticed something and said so: @balajiv113, @ChaosExAnima,
@doenke, @DrGithubble, @Flieger37, @k-nacion, @kevinpapst, @livelylinux,
@lutzkind, @Manschk3rl, @mathewcsims, @muhdusama, @Nazza01, @nhemnt,
@StefanIndustries, @TalesFromTheStack, @tarantila, @Uvenstedt, and @zet1200.
None of you owe this project anything, and all of you gave it time anyway.
@lutzkind gets a line of his own. Twenty-four issues so far, several of them
arriving with the broken code path already traced, and a habit of pushing
back on a design decision until it actually gets better. More than one
feature in this release started as his idea, not mine, and I did not expect
that from a project I built to track my own blood pressure.
@StefanIndustries was the first person to send this project money. What was
sent stays between us; the amount was never the point. Deciding this was
worth paying for, and saying so out loud, was not something I expected from
a self-hosted app nobody asked me to charge for.
This release carries a good part of what came out of that list, among other
things a workout category that two providers were silently dropping, a
below-zero reading nobody else had caught, and a reminder window that
ignored half its own schedule. It does not carry all of it. What is still
open stays open because it is waiting its turn, not because it got set
aside.
More is welcome, from anyone: an idea, a rough thought about how something
ought to work, a screenshot of a number that looks wrong. Especially the
number that looks wrong.
## [1.33.2] — 2026-07-28
### The cycle-symptom fix in v1.33.1 did not work
v1.33.1 said it had fixed custom cycle symptoms in backups. It had not. The
fix was real in three places and missing from the one that decides what ends
up in the file. The code that reads your cycle data returned the symptoms you
created yourself. The file format declared a field to hold them. The restore
knew how to put them back. But the step that assembles the file copies its
contents across one field at a time, and that list was never extended, so
every backup written since carried an empty list of custom symptoms.
The same release also replaced a silent drop with an error. Before it, a
backup referencing a symptom the restoring instance did not have lost that
one link and finished. After it, the restore stops and reports a failure.
Together with the empty list, that took an account with a symptom of its own
from losing a single link to being unable to restore at all. That is the
group the fix was written for.
Nothing in your stored data was damaged. But a backup written by v1.33.1 or
earlier does not hold the symptom definitions you created, and restoring one
of those files on this version will fail at the first day-log referencing
one. Take a fresh backup after updating and keep it alongside the older
files.
The assembly step no longer lists fields by hand. It copies each section
whole, so a field added to a section reaches the file because of the way the
code is written rather than because somebody remembered. Both ends of this
were covered by tests that passed for the whole time it was broken, and
nothing covered the join between them, so there is now a test that writes a
real backup for an account with a symptom of its own, removes that symptom
from the database, restores the file, and checks the row and its day-log link
came back.
## [1.33.1] — 2026-07-27
A patch release about the same thing as the one before it: places where the
app knew something and said something else.
### Two that change what you receive
**Medication reminders arrived every fifteen minutes.** The rule that
decides whether a reminder has already been sent was keyed on a ledger that
could not tell one dose slot from another, so an overdue dose kept
re-announcing itself for as long as it stayed overdue. One account was
getting roughly fifty mails a day. A reminder now goes out once per dose,
per escalation step, per local day, and two doses due at the same time no
longer replace each other on the lock screen.
**"Handled by the app" now silences only the app.** The setting that lets
the iOS client take over medication reminders was suppressing every channel,
so switching it on quietly turned off Telegram, ntfy, webhooks, email and
web push as well. That was never what it said it did. If you have been using
it as a general mute, those channels will start delivering again, and the
setting does what its name says.
### Three that were reported
Someone self-hosting could not use an OpenAI-compatible gateway. LiteLLM,
OpenRouter and vLLM all speak the OpenAI wire, but the provider list had no
entry that honoured a base URL of your own, and an earlier answer from us
said otherwise. There is now a provider for exactly this, with its own key,
model and endpoint. The plain OpenAI entry stays pinned to OpenAI, so a key
meant for one never travels to the other.
Timestamps followed the browser rather than the profile in four places. The
worst of them: a shared report downloaded as a PDF could carry a different
date than the page it was downloaded from, for the same reading. Charts also
drew their week and month boundaries in one fixed timezone, so an evening
reading landed in the wrong bar for anyone living elsewhere.
Some dashboard tiles could be seen and not switched off. The mobile app
draws a handful of widgets the web has no render path for, and it writes
them into the same stored layout, so an account ended up holding rows it
never set and could not change from anywhere. `bmi` was one of them. They
now appear in the dashboard settings in their own labelled group, with the
same show and hide controls as everything else and no reordering, because
placement stays the mobile app's. The group only shows up for widgets your
layout actually holds, so an account with no phone client sees nothing new.
### The backup
Three things it carried and could not restore, or could not carry at all.
A cycle symptom you created yourself was never included, so on restore its
key resolved to nothing and the link was dropped without a word. Three lines
away in the same function, an unknown cycle reference threw an error. Same
kind of missing reference, two different answers.
A mood factor you created yourself had the opposite ending: the restore
looked only for the built-in ones, found your own missing, and stopped. The
file was complete and unusable.
Nutrient day totals have been written into every backup since v1.29 and were
discarded on restore, silently, while the restore reported success.
What a backup contains is now declared in one place, with a written reason
for every record type left out. Around thirty types are still carried by
neither the file nor the restore, and they are named there rather than left
to be discovered. The export card no longer promises "every record you own",
because it does not.
### For operators
Two migrations. `0280` adds the gateway provider's columns, `0281` turns the
integration ledger's single failing-part slot into a set. Neither deletes or
rewrites a measurement.
A provider that syncs several things separately can now remember every part
that is failing at once. Until now the last one to report overwrote the
rest, which is the limit named in the v1.33.0 notes and not fixed then.
Self-hosters can point `GEOLITE2_DIR` at their own GeoLite2 databases. The
variable was read by the code and missing from the compose whitelist, so it
never reached the container. The message in the admin area used to tell
self-hosters to set a secret in this repository, which is not something they
can do.
### Credit
Reported by @k-nacion (the OpenAI-compatible gateway and the timezone
rendering), @mathewcsims (the unreachable GeoLite2 setting, found by reading
the strings inside a running container), @Nazza01 (the dose-history import),
@doenke (the table under the chart, and for narrowing his own request until
it fit one release) and @lutzkind (the dashboard layout).
## [1.33.0] — 2026-07-26
Eight defects, and every one of them had the same shape: something failed
reliably, the code recorded it precisely, and no person ever found out.
**"Delete All Data" deleted 13 tables out of 92 and reported success.** What
survived included mental-health questionnaires, lab results, the menstrual
record, allergies, family history, the whole document vault, every AI
conversation, and clinician share links that kept working. On the account
row itself, 11 of 119 columns were cleared, so name, insurer, insurance
number, home coordinates, profile photo and every integration credential
stayed. The wipe now covers everything the account owns except what is
needed to sign in again, the list is derived from the schema so a newly
added table cannot quietly survive, and the confirmation says what will go
before you agree rather than after.
**A device sync failed 1149 times without anyone being told.** The
escalation had no rung above "temporary", so a connection could fail hourly
for weeks while the card read "connected". Worse, providers that sync
several things separately had one healthy part erase the error recorded by a
broken one, twice an hour, including a connection that had already been
parked for a dead credential. A failing connection now shows how long it has
been failing, escalates after a day, and stops retrying after two with a
reconnect button.
One limit worth naming rather than leaving to be discovered: the ledger
remembers a single failing part per provider. If two parts fail at
overlapping times, the recovery of the later one still clears the error of
the earlier. That is narrower than the case this fixes, and it is not
closed.
**The Environment module had never once saved a row, for anyone.** One
declaration in 49 was missing the line that maps it to the database, so
every write referred to a type that does not exist. The job failed every
night and recorded that failure faithfully, and no code anywhere reads a
failed job. Both halves are fixed: the module writes, and a failing
background job now names itself on the admin page and on the affected
screen. Past days can be fetched from the weather source; the nightly job
fills the last week by itself and the backfill button covers the rest.
**Sleep was counted twice.** A device that re-exports a night it already
sent left two overlapping in-bed periods, and both were added. One night
read ten hours in the headline and twenty in the legend. The same fault
inflated the time-asleep figure itself. Every stage total is now the measure
of its own period rather than the sum of overlapping ones.
**Twenty-two controls could destroy something without asking.** Among them a
single tap that cleared the allergy list along with a text note, and a
switch labelled "Custom range" that deleted the stored range when switched
off. Every destructive control is now registered, and a new one cannot ship
without being classified.
**Medication stock was written off that had not expired.** The thirty-day
clock that applies to an opened injection pen was being applied to a blister
of tablets, so pressing out the first tablet started a countdown and a month
later the rest were declared unusable. The container kind was recorded and
never read. Pens and ampoules keep the clock; blisters, bottles and inhalers
do not. A repair returns stock that was written off, and the dates that
drive it can now be seen and corrected on the item itself.
**Insights scrolled sideways on every phone.** The app scrolls inside an
inner area that absorbed the overflow, which is why it went unnoticed for so
long. It is gone now.
**A third value for gender was accepted by the app and missing from the
published API reference in five places, and dropped or coerced in nine
more.** A fitness calculation scored anyone without a recorded gender using
the male coefficients, and the doctor report labelled any unrecognised value
as non-binary. Every place the field is read, written, documented or
displayed was gone through.
### Four additions
Medication history from a widely used iOS export tool can now be read
directly, as the file actually is, rather than converted by hand. The
scheduled time and the time taken stay two separate facts, which is what a
hand-mapped file destroys. Every column is either used, reported, or named
as not read, and you see which before you import. Seven status values are
ruled on individually: only "taken" and "skipped" become doses, and a
snoozed reminder or an unanswered notification is counted and named rather
than turned into a decision you never made.
The Coach can tell "you never recorded this" apart from "you recorded it,
but outside the range I looked at". Someone with 1,597 imported glucose
readings from April 2024 was told there was no glucose data at all, and the
Coach then changed the subject, because one reason code covered both
situations and the instruction to move on fired on both. It now says what is
actually true and offers to look further back.
A mood tag can now be paired against the next morning's resting heart rate
and heart-rate variability. Until now a tag such as alcohol had three
channels to land in, and the only one that looked at the following day
needed a wearable recovery score that plenty of accounts never record, so
the question of what a tagged evening does overnight had no path at all. The
difference is printed without a colour verdict, because a rise reads as
worse on one row and better on the next, and an association cannot carry
that judgement.
Every chart on the metric pages now carries the points it drew as a table
underneath, collapsed by default with the count in the button. It is handed
the same points the chart draws and the same formatter the tooltip uses, so
a cell and a tooltip cannot print two different numbers for one reading. The
caption names the grain, so a weekly row says it is a weekly average, and
units sit in the header rather than in every cell, which is what you want
when the rows go into a spreadsheet.
### For operators
Four migrations. `0276` adds retention indexes, `0277` adds failure-streak
tracking, `0278` scopes the intake import key per account, `0279` repairs
inventory that was wrongly expired. None deletes or rewrites a measurement.
### Credit
Reported by @lutzkind (sleep double-counting, the gender field, the
Environment enum, the Coach retrieval window, the next-day pairing for mood
tags in #616, and a run of earlier reports), @Nazza01 (the intake import,
plus the export file and the format documentation that made reading it
possible), @doenke (#636, the table under the chart) and @DrGithubble (the
unit and precision work that led here).
Two of these defects were found because someone outside this project read
the code and said what was wrong. The Environment module had been broken for
every user since it shipped and nobody inside noticed.
## [1.32.39] — 2026-07-26
The doctor report now carries what you chose, and nothing else.
**Ten of the twenty switches did nothing.** The panel offered controls for
oxygen saturation, body fat, body composition, resting heart rate, heart-rate
variability, VO2 max, steps and distance. Switching them off changed nothing:
the value was folded away before it reached the report. Two more claimed less
than they did, and the medication switch left the dose-by-dose record in place
while appearing to remove the drug list.
**Allergies and family history went out regardless.** They were read straight
from the database in the export route, so a report could exclude them and the
structured file in the same download would still contain them.
**The mental-health questionnaires went out without being asked for.** The
depression, anxiety, wellbeing and sleep scores reached the structured export,
the automation interface and a shared link, gated only by a module that had
been switched on by default some releases ago. They now have their own entry
and are off unless you pick them.
**The cycle export shipped ten other things.** The card that promised to export
only cycle data sent an empty request, and an empty request fell back to the
defaults, so it also carried weight, blood pressure, pulse, oxygen saturation,
body fat, the medication list, adherence, lab results, allergies and family
history. The comment above that code said every other section was off.
**What replaces all of it.** One picker, on both surfaces that choose a scope.
Twelve groups, each releasable or excludable as a whole and openable down to
the individual value. All 77 measurement types and 13 record sections are in
it, each in exactly one group. Sensitive material sits below a divider with no
group control, so no single click can turn on more than one of those at a time.
Nothing is preselected. A new report starts empty and one button fills in what
a general visit usually needs, which you can then change. Generating is not
possible while nothing is chosen.
A metric added to the app in future cannot appear in a scope you saved
earlier. That is not a promise, it is a compile error: a new measurement type
without a group breaks the build, and every path that produces a report has to
name where its scope came from.
**Share links.** A link can now be fetched as a PDF or as a structured file
behind the same passphrase that already protects the view, which is what a
practice actually does with one. The old promise of an interface that never
existed is gone. Links created before this release carry no explicit choice
and are therefore closed; each one can be reissued in a click with its label,
period and expiry carried over.
**The written summary has left the report.** It could not be checked against
what you chose, and prose whose scope cannot be shown has no place on a
document whose value is exactly that.
**Structured export corrections.** Someone who records their gender as
something other than female or male was exported as female. FHIR has a code
for it and it is now used, and an unrecognised value is left out rather than
guessed. The capability document described a resource twice and omitted
another that was actually served; both corrected.
**Also in this release.** Roughly one page load in three showed raw text keys
for a moment instead of words, because the language file was racing the
application bundle and losing. It is now loaded before the first render.
**Converted values were shown with more precision than they have.** A weight in
pounds could read 207.896 where the conversion declares one decimal place. The
rounding was defined for every conversion and read by exactly one screen out of
all the ones that display a converted value, so everywhere else the raw result
of the multiplication went straight through. It now happens where the
conversion happens, so a surface cannot render an unrounded value even if it
forgets to ask. Metric values are deliberately untouched, because the metric
render is meant to be exactly what is stored, and the entry path keeps full
precision so a saved value cannot drift a little further each time.
Thanks to @DrGithubble, who reported this on #627 after confirming the earlier
unit fix. It turned out to be the second half of the same defect they had found
the first time.
**For operators.** Migration `0275` stores the new scope and removes the old
preferences. Existing share links are closed by it and need reissuing.
## [1.32.38] — 2026-07-25
Blood-glucose readings from anything other than the manual entry form were
never saved.
A rule added years ago required every blood-glucose reading to carry a
context: fasting, after a meal, random, or bedtime. That is a fair thing to
ask of someone typing a reading in by hand. It is impossible for a sensor,
which measures every few minutes and cannot know what you were doing.
So every automated path failed. A CSV import reported the rows as skipped and
still showed a green tick. Nightscout counted them as failed rows and carried
on. Apple Health has no context field at all, so not one of its readings could
ever have been stored. The same for readings written through the automation
interface, through Telegram, and through the JSON importer. None of it was
visible: no error reached anyone, and the affected charts and panels simply
stayed empty as though nothing had been measured.
A reading with no context is now stored with no context, which is what it is.
Where a context is given it is still checked. Manual entry still asks for one,
because the form offers the choices and nothing is lost by asking.
Absence is now shown as absence: a reading without a context is left out of the
per-context breakdowns rather than being counted as one of them.
**Imports no longer claim to have worked when they did not.** A run that wrote
nothing is shown as a failure. A run that wrote some rows and skipped others is
shown as a partial result. Only a run that actually landed something shows as
done. Skipped rows are grouped by reason with a count instead of listing the
first fifty identical lines, and the per-row detail is still there behind a
disclosure. This covers the CSV, JSON and Apple Health cards alike.
Reported by @lutzkind, who traced it to the parser, ran it in isolation to rule
out the timestamp and unit handling, and checked the database afterwards to
prove nothing had been written. Files that were rejected can simply be imported
again.
**For operators.** Migration `0274` relaxes the constraint. It adds no column
and rewrites no row. Readings that were refused in the past were never stored,
so they need a fresh import from the source.
## [1.32.37] — 2026-07-25
Two ways a record could quietly lose data, both closed.
An identifier that a client sends along with a reading is meant to say "this
is the same reading you already have". One client was sending a value that
changed every time the app restarted, so nothing ever matched, and every
sync created a fresh copy. On one instance that produced 23 medications
nobody had ever taken, none of them with a dose or a single logged intake,
because the doses were looking for a medication under an identifier that no
longer existed.
The server now refuses an identifier that cannot possibly be stable, with a
422 that says what is wrong instead of quietly making duplicates. A batch
rejects only the offending entry and still accepts the rest, so one bad row
cannot stop a sync. Twelve entry points check it. The identifier shapes that
real apps and devices actually send all still go through, so the new rule
cannot get greedy and start dropping valid data.
The second one was worse and had no symptom at all. The repeat-protection
key on a medication intake was unique across the whole database rather than
per account. Two people generating the same key independently meant the
second one could not save their dose at all. The database refused it, and
the intake was gone. The key is now unique per account, and every lookup
asks who it belongs to. Recording an intake through the automation interface
without sending a key had a related problem: the first one saved and every
one after it was discarded as a repeat. Also fixed.
The migration does not touch any existing intake. It relaxes a rule rather
than tightening one, so nothing that was allowed before becomes disallowed
now.
## [1.32.36] — 2026-07-25
Nothing here changes what you see. This release is about the checks that run
before a release goes out.
A recurring shape of mistake in this project is a half-built feature: something
is stored, or sent to the app, and the part that was supposed to use it never
gets written. Each end works on its own, so nothing complains. Three checks now
run on every build and refuse that. One walks the fields the app receives about
your account and insists each one is actually read somewhere. One compares the
measurement table against what a backup writes out, so a new column cannot go
missing from your backup file. The third fixes a check that had gone quiet: it
was meant to find translation entries no screen uses any more, but one line of
it had been waving through several thousand entries at once for a long time
without anyone being able to tell.
With that check honest again, 149 leftover translations turned out to belong to
screens that were rebuilt or removed some time ago. They are gone from all six
languages. Nothing that is on screen today was touched.
## [1.32.35] — 2026-07-25
The doctor report stops asking for your practice name every time.
You type a clinic or practice name onto the cover of a health record export,
and until now that was all it did. The field emptied itself again the moment
you left the panel, so next month's report started from a blank line. The name
you used last time is now kept and filled in for you. Clearing the line works
the same way and is remembered too, so a cover without a practice line stays
without one. The name is saved only once the report has actually been
produced, which means an export that fails partway leaves the previous value
alone.
A backup now carries the origin of your daily totals. Steps, distance and
similar figures that arrive from Apple Health as one number per day are stored
with a note of where that number came from, so a rough estimate read out of an
archive can never overwrite a total the phone measured itself. That note was
missing from the backup file, so a restore brought the figures back without it
and the protection went quiet. It rides the backup now. Restoring an older
backup written before this fix marks the affected daily totals as unclassified
instead of leaving them ambiguous, which is the state a later sync knows how
to repair.
Migration 0272 drops two columns that were written and never read. They held a
contributor count and a source fingerprint for archive derived daily totals,
and no screen, export or repair path has ever looked at either. The figures
themselves and the origin note above are untouched.
## [1.32.34] — 2026-07-25
Housekeeping on the interface. Most of it you will not see.
The sections on the insights overview had each been given their own card frame
by hand, and the copies had drifted apart on the one thing nobody thought to
write down: how much room to leave between the blocks inside them. Some used 8
pixels, some 12, one 16, all sitting one under the other in the same column.
They now share a single frame with a single rhythm, so a few of those cards
open or tighten very slightly and all of them pick up the same faint shadow
their neighbours already had.
When a metric page cannot load, the message now reads in the normal text
colour with a warning glyph beside it, instead of the grey reserved for
timestamps and units. A failure is what that surface has to say at that moment,
so it should not look like a footnote.
The rest is invisible from the outside. The sleep composition card uses the
same title row as every other card, a handful of one-off text sizes and
paddings went back onto the scale the rest of the app uses, and the spacing
rule that was already enforced in the build now has nothing left to catch.
## [1.32.33] — 2026-07-25
The moodLog integration is gone. It bridged mood entries to and from a separate
self-hosted mood app back when HealthLog had no real mood module of its own.
That stopped being true a long time ago: mood is tracked natively here now,
with tags, rated factors and notes, and the bridge had nothing left to add. Its
settings card was withdrawn a while back and the rest of it kept running in
the background against an upstream nobody was pointing at any more. This
finishes the job properly.
- **Your mood entries are untouched.** Every entry stays exactly where it is,
including the ones that originally arrived through moodLog. They keep their
source label, they still show up in the mood list, the filters, the charts,
the correlations and the doctor report. Nothing was deleted and nothing was
relabelled. Removing the pipe does not remove what came through it.
- **What was removed.** The hourly pull, the inbound webhook, the outbound
push, the per-account credentials and webhook secret, the admin toggle, the
entry on the integrations status feed, and the message strings behind the old
card. Migration `0271_drop_moodlog_integration` drops the six columns those
surfaces used and clears the retired sync schedule. It names no mood table.
- **One less admin switch that did more than it said.** The instance-wide
toggle was described as controlling mood-log reminders. It actually shut off
the whole integration, quietly, with no other visible effect. It is gone
along with the thing it gated.
If you were still relying on the bridge, export your mood data from the other
app and import it once; the entries land in the same place they always did.
## [1.32.31] — 2026-07-25
The integrations page could tell you a connection was failing. It could not
tell you a connection had stopped trying. Those look the same from the
outside: the page keeps showing whatever error was recorded last, and it
keeps showing it for as long as nobody looks, because nothing anywhere read
how old the last attempt actually was. This release adds that reading, and
every status on the page now comes from it.
- **A sync that has stopped running is labelled as stopped.** When a
connection has not even attempted to sync for a day, it says so, with how
long it has been. Previously it kept displaying the last error it managed
to record, which reads like something that is still retrying.
- **Per data type, when a value last arrived.** Each connected card now
opens a short list: every data type that connection has ever delivered,
and how long ago the newest one came in. A type that has gone quiet for
two weeks while the rest of the connection is healthy is called out, which
is the shape a single broken pipe takes: one revoked permission, one
endpoint that started returning nothing, while the sync itself keeps
reporting success. Types the connection has never delivered are simply not
listed. Nothing is invented.
- **Apple Health has a staleness threshold.** Any timestamp at all used to
paint the green "data received" badge, so a phone that stopped delivering
three weeks ago looked exactly like one that delivered this morning. After
a week of silence the card now says so and points you at the app's Apple
Health permissions. It also lists the same per data type detail as the
others.
- **A configured connection with no card of its own now appears.** If your
account has a connection set up that the page has no card for, it gets a
plain status row instead of being left off entirely. A connection can no
longer be running, failing and invisible at the same time.
- **The status colours mean one consistent thing.** Red is now reserved for
the cases your own action fixes, which is reconnecting or resuming. An
upstream outage or a rate limit carries the amber tone instead, because
clicking reconnect against a server error has never helped anyone. Every
card uses the same mapping. Three of them used to disagree with each other
on the same page.
- **Nightscout reads the same status source as everything else.** It was the
last card fetching its own status separately, which is why it could show a
different colour for the same condition.
The assistant tools report the new verdict too, so asking why your data
looks stale gets the real answer instead of "connected".
## [1.32.30] — 2026-07-25
Height was the last value still asked for in centimetres regardless of the
setting. Both previous releases closed with a note saying so. It sat outside
the fix because height is not a measurement at all: it is a field on your
profile, so the conversion the reading surfaces go through never reached it.
- **Height is entered in the unit you use.** With imperial selected, the
profile card and the onboarding step ask for feet and inches in two boxes,
the way a person actually states a height. With metric selected the single
centimetre box is exactly what it was.
- **The value you type comes back.** Feet and inches are kept to the nearest
whole inch, which converts to centimetres without a remainder, so saving
5 ft 11 in and reopening the page shows 5 ft 11 in again. The limits the
fields offer are the imperial equivalents of the ones the server enforces,
rounded inward, so a height you are allowed to type is never refused on save.
- **Storage is unchanged.** Height is still kept in centimetres, the API still
sends and receives centimetres, and BMI and the weight bands still work from
that same value. A metric account's stored height is untouched by this
release.
- **Switching the unit keeps what you were typing.** The unit selector sits on
the same card as the height field, so flipping it re-expresses whatever is in
the field rather than clearing it.
With this the preference reported in #627 reaches every value the app shows you
or asks you for.
## [1.32.29] — 2026-07-25
Fitbit re-scores a night hours after you wake up: blocks get split, merged or
relabelled. HealthLog identified each block by its position in the list, so a
re-score that renumbered the blocks looked like a set of brand new ones. The
previous version stayed on file, nothing removed it, and from then on the night
counted twice. It was quiet: the sync kept reporting success, because the sync
was in fact working.
- **A re-scored night no longer double counts.** Each sleep block is now
identified by the moment it starts, which does not move when Fitbit rescores
the night. A block that changes from light to deep updates in place instead
of leaving the old copy behind, and anything the new scoring dropped is
cleared from that night's window before the new blocks are written.
- **Nights already affected repair themselves.** A one off pass runs once per
connection after the update and rereads the past year of sleep, so the
duplicates collapse without you doing anything. It covers 365 days, the same
horizon the Fitbit history import uses. Nights older than that keep whatever
they already hold; they cannot gain new duplicates, since Fitbit does not
rescore year old nights. Migration `0270_fitbit_sleep_repair_marker` records
which connections have been through the pass.
- **Rate limits are handled instead of ignored.** When Fitbit asks for a short
wait, the request now waits and tries again once. A long wait is refused
outright and left to the next scheduled run, so one throttled account cannot
hold up everybody else's.
- **Fewer pointless writes.** Fitbit rereads the last 24 hours every hour. Rows
that came back unchanged were rewritten anyway, which showed up as needless
traffic for the iOS app. Unchanged rows are now left alone.
## [1.32.28] — 2026-07-25
Oura used to look back exactly one week and no further. If the sync had been
broken for longer than that, or if the ring sat unused and the older nights
only reached Oura's cloud later, those days fell outside the window and were
never imported. Nothing said so, because a sync that finds nothing still counts
as a sync that worked.
- **Oura catches up after a gap.** The window is now measured from the newest
Oura reading actually on file rather than from a fixed week. A ten day gap
pulls eleven days on the next run and closes itself; a month is the ceiling.
When data is arriving normally the window stays at a week and nothing about
the hourly run changes.
- **A fresh Oura connection imports a month** instead of a week. Deeper history
is a separate question and is not part of this release.
- **Sync now for Oura, Polar, Strava and Nightscout.** These four had no way to
ask for a sync; they only ever ran on their scheduled pass. Each connected
card gets the same button WHOOP and Fitbit already had, limited to a few runs
per minute.
- **Connecting starts the first sync straight away.** Oura, Polar and
Nightscout used to sit empty for up to an hour after connecting, waiting for
the next scheduled pass. The first pull now runs on connect, and the schedule
stays as the fallback.
## [1.32.27] — 2026-07-25
Target and threshold panels now follow the metric and imperial preference.
The previous release listed this as the one place still reading in kilograms
and Celsius: with imperial selected a weight chart read in pounds while the
target reference right below it read in kilograms.
- **Target ranges read in your unit.** On the weight insight page the target
band, the range bar, the status pill, and the 30 day average all read in
pounds when imperial is selected. Body water and bone mass follow the same
rule in the threshold settings.
- **Editing a target works in the unit you see.** The target editor and the
threshold settings seed their fields from the stored value converted to
your unit, check what you type against limits expressed in that same unit,
and convert back before saving. A value typed at the displayed limit is
accepted instead of being rejected against a kilogram bound you never saw.
Saving and reopening shows the number you typed.
- **Storage is unchanged.** Targets are still kept in SI units, and a metric
account's saved values are untouched by this release.
- **Known remaining.** Height is still entered in centimetres.
This finishes the job @DrGithubble started in #627.
## [1.32.26] — 2026-07-25
The metric and imperial display preference is now applied everywhere your
weight, body composition, waist, and temperature readings appear. Until now
the setting was saved but almost nothing read it, so the numbers stayed in
kilograms and Celsius regardless of the choice. Thanks to @DrGithubble, who
reported this in #627 and traced it himself, down to the hardcoded kilograms
in the entry form and the missing imperial branch in the display transform.
His follow-up was the more useful half: a reading genuinely tagged in
another unit was being relabelled by any surface that printed the type's
standard unit. That is what turned a one-screen fix into a proper one.
- **Weight and body composition follow the preference.** With imperial
selected, weight, muscle mass, body water, bone mass, fat mass, and grip
strength read in pounds across the dashboard tiles, the charts, the record
list, the insight pages, and the coach read strip. Body temperature, skin
and wrist temperature read in degrees Fahrenheit, and waist reads in
inches. Storage is unchanged: every reading is still kept in SI units, and
a metric user sees exactly the same numbers as before.
- **Manual entry converts at the point of entry.** When you log or edit a
value with imperial selected, the field is labelled in your unit and the
number you type is converted back to SI on save, so a pound reading is
stored as the correct kilogram value rather than a mislabelled one.
- **A logging tool can no longer store a foreign unit.** The measurement
logging tool used by connected assistants now converts a recognised unit
hint (such as pounds or Fahrenheit) to SI, and refuses anything it does
not recognise, so a reading is never saved under the wrong unit.
- **Known remaining.** Height is entered in centimetres for now. Target and
threshold panels also still read in kilograms and Celsius, so with
imperial selected a weight chart reads in pounds while its target
reference below it reads in kilograms. The stored values are correct
either way; both are separate follow-ups.
## [1.32.25] — 2026-07-24
Mirrored medications are now marked as such, and the server limits how many
medications one external source can mirror so a client bug can no longer fill
the list with duplicates.
- **Mirrored medications are marked on the list.** The medications list now
reports whether each entry was mirrored from an external source (today Apple
Health) or created in HealthLog directly. A synced row is no longer
indistinguishable from one you added yourself.
- **One external source can no longer flood the list.** The server refuses to
mirror more than 30 medications from a single external source per account. A
client that keeps re-sending the same medication under a changing identifier
can no longer create a fresh row on every sync. A repeat of a medication
already mirrored still resolves to the existing row, and medications you add
yourself are never affected.
## [1.32.24] — 2026-07-24
Polar sync is more resilient when one data type is unavailable. A single
collection that a device or account cannot serve no longer blanks the rest of
the Polar data on every hourly check.
- **One unavailable Polar data type no longer blocks the others.** SpO2 on a
watch without the sensor, or a data type an account is not entitled to, used
to fail the whole Polar sync and leave recharge, sleep, and activity empty.
Each collection now imports on its own, so the ones that are available still
come in and only the one that failed is held back.
- **A failing Polar collection is named on the integration status.** When a
collection cannot be read, the sync is recorded as failed with the failing
collection named, instead of showing a healthy state over missing data.
- **Unexpected Polar response shapes are logged, not read as empty.** If Polar
returns a body that does not match the expected shape, it is now recorded for
diagnosis rather than silently treated as "no data", which previously could
mark a sync healthy while importing nothing.
## [1.32.23] — 2026-07-24
Sync status now tells the truth for the connected wearables and CGM sources. A
few paths could report a sync as healthy when it had actually failed, which
reset the failure streak, hid the reconnect prompt, and let a dead grant get
retried on every hourly tick. This release closes those gaps so a failed sync
reads as failed, an interrupted history import is retried instead of marked
complete, and a couple of sources start pulling data sooner.
- **A dead WHOOP token no longer reports a successful sync.** When the stored
grant is revoked, the connection stays parked with its reconnect prompt and
the last-synced time stays put, instead of flipping back to connected and
hammering the revoked token every hour. This holds on both the manual sync and
the hourly background path.
- **An interrupted WHOOP history import is retried, not marked done.** A
full-history backfill that stops partway is no longer stamped complete, so the
gap is retried rather than frozen in place. To pull deep history after
connecting, run a full sync from Settings. It walks back to 2020.
- **Withings ECG now syncs on its own schedule.** A watch-only account with no
scale previously never had its ECG recordings pulled. An hourly check now
brings them in without waiting on a webhook.
- **Withings activity and sleep write failures now surface.** A failed write no
longer reads as a clean sync; it shows up on the integration's status instead
of leaving a green state over missing data.
- **Oura, Polar, and Nightscout sync errors that previously vanished now
surface.** A write that fails partway through is recorded on the integration
status rather than being silently dropped, and each error is classified and
redacted by the source that owns it.
- **Strava history import starts at connect time.** Deep history begins
importing right after you connect instead of waiting for the next server
restart.
- **WHOOP cycle data uses a wider catch-up window.** A day's strain and energy
figures that are still being revised are re-fetched for a week, matching the
other WHOOP collections, so a late revision is not missed.
## [1.32.22] — 2026-07-24
The concurrent-edit protection that reached the layout surfaces now covers
the rest of the preference records too. Notification preferences, module
toggles, coach preferences, and the mood-tag layout all get the same
protection, so a save based on an older read stops and reloads instead of
quietly reverting a change another writer just made. Two more races are
closed on the server side: a stock correction can no longer overwrite a dose
consumption that ran at the same moment, and a privacy-mode change is now
respected by an insight generation that was already running. Older clients
that do not send the new marker keep working exactly as before.
- **Notification preferences no longer clobber a concurrent change.** The
server merges each category into one stored record. A save based on an
older read now reloads instead of writing the whole record back without
the change another device just made. This is what closes the case where a
web save could silently switch off the app-managed medication reminder
flag and bring back a duplicate reminder.
- **Module toggles, coach preferences, and the mood-tag layout get the same
protection.** Each is one stored record that more than one surface can
write; a stale save is now stopped and reloaded rather than reverting a
newer one. The coach-preferences save covers both the tuning options and
the excluded-metric list together, so a conflict leaves both untouched.
- **A stock correction can no longer race a dose consumption.** Correcting
the remaining count on a container now runs under the same per-medication
lock a logged dose takes, so the two serialize instead of one overwriting
the other.
- **A privacy-mode change is respected by an in-flight insight generation.**
A briefing that was already generating when you switch the privacy mode no
longer writes its old-scope output over the cache the switch cleared. It
stops short of the write and the next run regenerates under the new mode.
- **Older clients are unaffected.** A request that does not carry the new
base marker keeps the previous save behavior, so nothing needs updating to
keep working. The new marker is documented in the published API spec.
## [1.32.21] — 2026-07-24
Insights and medications layout saves, and the coach about-me note, are now
protected against a concurrent edit overwriting a newer one. This is the
same protection the dashboard layout already had. If you save a layout in
one place while something else saved a change to the same record a moment
earlier, your save now stops and reloads the current state instead of
quietly reverting that other change. Older clients that do not send the new
marker keep working exactly as before.
- **Insights layout saves no longer clobber a concurrent edit.** Saving the
overview arrangement and saving the pill order are two separate surfaces
that write the same record. If one lands while the other was based on an
older read, the later save now reloads and asks you to redo it rather than
reverting the first.
- **Medications layout saves get the same protection.** The card/table view
toggle and the manual order both write one record, so a stale toggle can
no longer resurrect an order you just saved, and the reverse holds too.
- **The coach about-me note is protected as well.** A save based on an older
read is stopped and reloaded instead of overwriting a newer version of the
note.
- **Older clients are unaffected.** A request that does not carry the new
base marker keeps the previous save behavior unchanged, so nothing needs
updating to keep working.
- **The dashboard layout save is now documented in the published API spec.**
It was missing from the OpenAPI file even though it was the first surface
to get this protection.
## [1.32.20] — 2026-07-24
The medication-adherence insight now passes through the same safety screen as
every other insight card. It was the one card of eight that showed the model's
text without running that screen first. It already held back a reply that came
back broken or cut off, but a dose-change instruction or an invented figure
could still reach it. It now runs the same check the other seven cards run, and
anything that trips the check falls back to the plain, data-grounded line the
card shows when no model is set up.
- **The medication-adherence card is screened like the rest.** A reply that
reads as a dose change, or that cites a made-up risk figure, is held back and
the card shows its plain fallback. A normal grounded summary still shows as
before.
- **Two background notification lines can no longer print a raw model reply.**
The "just in" reaction line and the proactive coach nudge now catch a reply
that arrived as structured data or got cut off partway, and use their fixed
wording instead of printing it as is.
- **Smaller cleanups in the same area.** A stray marker left behind by a
cut-off coach reply is now stripped from the text, and an unreachable branch
in the adherence figure was removed.
## [1.32.19] — 2026-07-24
After you logged a dose, the Today view could keep showing it as still due
until you reloaded the page. The Today hero and the dashboard read from a
cache that refreshes on a timer, and a write made from another screen marked
that cache stale without actually re-reading it. The change reached the
server, but the screen you came back to still showed the old state for up to
two minutes, or until a hard refresh.
- **A dose you take now clears from Today at once.** This covers the "Take
all due" button on the medications page, editing or deleting a logged
dose, and importing intake history. The Today view and the dashboard now
re-read the moment the write lands instead of waiting for the timer.
- **A preventive-care reminder marked done leaves the Today rail
immediately.** Marking one off, editing it, or deleting it updates the
Today view straight away rather than after a refresh.
- **A lab reading you add updates its cards right away.** Adding a reading
refreshes that marker's assessment on the same page and the "what changed
since your last panel" card, instead of leaving them on their earlier
text.
- **A briefing you regenerate shows up on Today without a reload.** The new
briefing now reaches the Today hero and the dashboard as soon as it is
ready.
- **Saving a dashboard layout keeps its safeguard against a conflicting
edit.** A visit to the home page could leave the layout editor without the
token that protects a save, which quietly turned off the protection added
in v1.32.16 against two edits overwriting each other. The editor now
always loads its own copy, so the protection stays on.
Thanks to @lutzkind, whose run of reports surfaced the stale Today view.
Refs #581
## [1.32.18] — 2026-07-24
When a Fitbit token was revoked or expired, the hourly sync noticed the failure
but then stamped the same cycle as a success. The connection flipped back to
connected, the sync position moved forward, and after you reconnected the
catch-up window could no longer reach the days that were missed. So the data
was skipped for good, and in the meantime the failing token was retried every
hour.
- **A revoked Fitbit token no longer reports a successful sync.** A failed token
refresh now fails the whole cycle. The connection stays parked until you
reconnect, the sync position holds instead of advancing past real data, and
the once-per-failure alert stops repeating every hour.
- **Data after a reconnect is no longer skipped.** Because the position is held
back on a failed cycle, the next successful sync still covers the days that
were missed rather than starting after them.
- **A write failure during a sync holds its place too.** If rows were fetched
but could not be saved, the cycle keeps its position so the next run fetches
them again instead of letting them fall outside the catch-up window.
- **An interrupted history backfill no longer marks itself complete.** A partial
first-time import is retried until a full pass finishes, so the one-year
history walk fills in rather than freezing halfway.
If your Fitbit history already has a gap from before this release: reconnect
Fitbit if prompted, then open Settings, Integrations, Fitbit and run a full
sync. It re-walks the last year and fills the gap in place. Gaps older than a
year are outside the backfill window and cannot be recovered.
## [1.32.17] — 2026-07-24
The mood correlations on the Insights page could line up a day off. Your mood
is dated by the calendar day you logged it in your own timezone, but the
metrics it was matched against were still grouped by the UTC day. If you live
away from UTC, an evening reading slid onto the next UTC day, so a mood and a
reading from the same evening were paired as if a day apart. A regression from
v1.32.12, when the mood side moved to local days on its own.
- **Mood now pairs with metrics from the same local day.** The blood pressure,
weight, and resting pulse series that mood is correlated against are grouped
by the day you logged them in your own timezone, so both sides of the pairing
share one calendar. An evening entry no longer drifts onto the next day.
- **Non-UTC users get the correct same-day figures.** For anyone west of UTC an
evening reading used to pair with the wrong day's mood, turning a same-day
relationship into a one-day-apart one. Those correlations and their scatter
charts now reflect the day the data was actually recorded. UTC users see no
change.
## [1.32.16] — 2026-07-24
Changing which tiles and charts appear on your dashboard could get undone if
you also toggled a hero score ring at about the same moment. Both actions save
the whole layout, and the ring change was built from an older copy, so
whichever reached the server last won. Thanks to @lutzkind for the clear
report.
- **A saved layout now stays saved.** Your dashboard save is treated as the
authoritative one. If another change arrives after it but was based on an
older version of the layout, the server declines that write rather than
letting it overwrite your work. The ring toggle also carries only its own
fields, so it can no longer pull stale tiles along with it.
- **The two saves take turns instead of racing.** While a layout save is in
flight the score ring controls are held, and while a ring change is saving
the Save button waits, so the two writes can no longer overlap on the server.
- **A change underneath you reloads instead of getting lost.** In the rare case
where another tab saved first, the layout refetches from the server and a
small note asks you to make your change again against the fresh copy.
Refs #581.
## [1.32.15] — 2026-07-24
Your Polar training sessions now sync into HealthLog, alongside the recovery,
sleep, and daily activity that Polar already brought in. Thanks to
@StefanIndustries, whose report asked for this after the vitals sync landed.
- **Polar exercises arrive as workouts.** Every run, ride, swim, or gym session
you upload to Polar Flow shows up in your workout history with its sport,
start time, duration, distance, calories, and heart rate. The raw Polar sport
label and training load ride along in the background so nothing is lost. The
sync reads your last 30 days of uploads once an hour, so a session synced
late from your watch is still caught.
- **A Polar session does not double up with your other sources.** If the same
workout also reaches HealthLog through Apple Health or another connection, the
two are matched and only one is shown. A Polar watch counts as a direct
heart-rate recording, so it ranks above a Strava re-upload of the same run but
below your primary on-wrist wearables. You can change that order under
Settings, Sources.
- **The two Polar halves stay independent.** A hiccup fetching your training
sessions no longer holds back the recovery and sleep sync, and the reverse
holds too, so a problem on one side never masks the other.
## [1.32.14] — 2026-07-24
When the Coach removes a number it could not check against your data, the reply
now reads clearly and tells you what happened.
- **A withheld figure reads as a quiet omission.** The Coach checks every number
in its answer against the figures it was actually shown, and holds back any it
cannot verify so a drifted value never reaches you as if it were real. That
held-back number used to show up as an internal `[unverified]` marker in the
middle of the sentence. It now appears as a short editorial mark, and a small
muted line under the message tells you that some figures could not be checked
and were left out. The line is worded for each of the six languages and stays
out of the way until there is something to say.
- **Coach action cards and the tool trace survive a reload.** A suggested
reminder, a confirm-to-create action card, and the "what I looked at" tool
trace were saved with the message but dropped when the conversation was
reopened, so they quietly vanished on reload. They are now restored with the
rest of the message, so reopening a thread shows the same cards and trace it
showed the first time.
## [1.32.13] — 2026-07-24
An Insights assessment card no longer shows the raw model output when a
response comes back incomplete. If the underlying text was cut off, the card
now falls back to its computed summary instead of printing the unfinished
data.
- **A cut-off assessment stays hidden.** The per-metric assessment cards,
like the one for resting pulse, ask the model for a small structured reply
and then read the summary out of it. When that reply arrived truncated,
the reader could see the whole raw reply on the card, including the
property name, the braces, the quotes, and the escaped line breaks. The
card now recognises an incomplete or malformed reply and serves the
deterministic summary it already falls back to when no model text is
available, so the raw output never reaches you.
- **Plain-text assessments are untouched.** A model that answers in an
ordinary sentence rather than the structured shape is still shown as
written, so this change only affects broken replies and leaves normal ones
exactly as before.
- **The other cards behave the same way.** Medication compliance and the
derived-score assessments read their summary the same way, so they now
hold back a broken reply too rather than persisting it as the day's text.
Thanks to @lutzkind for reporting this one.
## [1.32.12] — 2026-07-24
A mood you log late in the evening now lands on the day you logged it, in your
own timezone, both on the dashboard and in the Coach's mood view.
- **Your late-night mood stays on the right day.** The pre-computed mood history
used to file each entry by its UTC date, so a mood logged after dark in a
timezone that runs ahead of or behind UTC could jump to the neighbouring day
once the nightly rebuild ran. Every mood now files under the calendar day it
was logged in your own timezone, matching the raw entry list.
- **The dashboard and the Coach agree.** The daily mood series behind the
dashboard tile and the Coach's mood view now read the same day for an entry as
the entry list does, so a value no longer shifts by a day when the faster
pre-computed path takes over from the live one.
- **The correction applies itself.** On upgrade the pre-computed mood history is
cleared and rebuilt under the corrected keying. While it rebuilds, mood reads
fall back to the live calculation, which was already correct, so there is no
window where the wrong day is shown.
## [1.32.11] — 2026-07-24
Signing in to the iOS app on a self-hosted domain now runs through your own web
login, so saved passwords and passkeys behave the way they should.
- **Sign in through your own domain.** On a self-hosted instance the app now
opens your instance's real web login page inside a secure in-app browser.
Because the login happens on your own origin, your password manager files the
credentials under the right site and passkeys can run at all, which the older
native form could not do on a custom domain.
- **A single-use handoff, not a token in a link.** Once you have logged in, the
server hands the app a one-time code that lives for about ninety seconds and
can be redeemed only once. The access and refresh tokens never ride a URL. The
whole exchange mirrors the existing native single sign-on handoff and reuses
its protections, including the per-request PKCE binding.
- **The login has to happen inside the flow.** The app receives a code only when
you actually authenticate during that browser session. A login left open in
the background from before cannot complete the handoff by itself, so a stray
link can never quietly sign the app in as you.
- **Your second factor still applies.** An account with an authenticator app or
a security key completes that step on the web page before any code is issued,
exactly as it does everywhere else.
## [1.32.9] — 2026-07-23
The Coach's numeric verifier now remembers the numbers you were actually
shown across a whole conversation, so figures you legitimately carry forward
stop getting marked unverified while an invented one still gets caught.
- **A figure from earlier in the chat is no longer flagged when you bring it
back up.** It now keeps a running record of every number the Coach was
given this turn and in earlier turns, plus your goal figures, your
medication doses, and the reference bands it was shown. A value it can
trace to one of those is left alone even a few turns later. A value the
Coach only wrote in an earlier reply is never trusted on that basis, so a
number that once slipped through cannot quietly become authoritative
later.
- **An educational sentence keeps its numbers.** A line that states a
general guideline, like the note that adults usually need seven to nine
hours of sleep, is recognised as a reference and left intact instead of
having its numbers stripped. This works in every supported language.
- **A dose you are actually on reads as a restatement, a different one does
not.** When the Coach says to keep taking a dose, the reminder is allowed
only when the amount matches a dose on your schedule. A wrong maintenance
amount, or any wording that nudges the dose up or down, is still held
back.
- **Fabricated risk numbers are caught in more languages.** The screen that
blocks an invented clinical risk figure or a named risk-engine result now
reads French, Spanish, Italian, and Polish to the same depth it already
read English and German, including a percentage written out in words.
## [1.32.8] — 2026-07-23
Health records written through the app now carry where they came from, and a
HealthKit sync can say what set it off.
- **A dose logged from the app is recorded as an app entry.** Every intake used
to be stored as a web entry no matter how it arrived. The server now reads the
transport it authenticated the call with and tags the dose to match, so a dose
logged in the native app reads as an app entry while one logged in the browser
stays a web entry. A reminder confirmation, an import, and an Apple Health
mirror keep their own labels. The value is derived on the server and can never
be set from the request body.
- **The dose history shows how each dose was recorded.** The per-dose history
returns a source label on every intake, so a client can show at a glance
whether a dose came from the web, the app, a reminder, an import, or an Apple
Health mirror. Rows written before the label existed report as unknown rather
than guessing.
- **A HealthKit sync can report what woke it.** The batch upload accepts an
optional trigger tag reading foreground, background, or push. It is recorded
for diagnostics only and never changes which readings are stored or how they
are deduplicated, so sending it or leaving it off produces the same result.
## [1.32.7] — 2026-07-23
The Coach's numeric verifier now reads numbers the way you actually write
them, so a correct figure stops getting marked unverified while an invented
one still gets caught.
- **A number you reformatted is no longer flagged.** It recognises a rounded
restatement, a delta narrated as a drop, an adherence rate written as a
percent, minutes read back as hours, a reformatted date, and a
thousands-separated step count, each checked against the figure the server
actually gave the Coach. A drifted number, like an average of 128 quoted
back as 138, is still caught.
- **A blood-pressure claim can no longer borrow a weight number.** Every
figure is matched inside its own unit family, and a raw reading never
earns the looser rounding reserved for a headline average, so a fabricated
average cannot slip through on a nearby sample.
- **A correct clinical-risk answer streams through untouched.** Naming ASCVD
or a ten-year risk to explain that a clinician computes it now passes.
What still blocks is an asserted figure or a categorical verdict, whether
it carries digits ("your risk is about 14%"), spelled-out words ("roughly
twelve percent"), or no number at all ("SCORE2 would put you in the
high-risk band").
- **A dose you already take reads as support.** "Keep taking your 7.5 mg as
prescribed" passes, while anything that moves the dose stays blocked.
- **A figure removed as unverifiable leaves the sentence intact.** The strip
touches only the exact token, so a year like 2023 is never clipped by a
flagged 23.
Refs #587, #591.
## [1.32.6] — 2026-07-23
Hardening and data-integrity fixes that were reviewed earlier and are now
folded into the trunk.
- **Outbound Web Push dials through the protected network path.** A push
endpoint now goes through the same safeFetch wrapper the rest of the app
uses, so an operator-configured endpoint cannot be pointed at an internal
address.
- **A bulk mood edit saves all at once.** The mood rows and their tag links
are written in one transaction, so a failure part way through no longer
leaves a partial save.
- **Nutrient entries are bounded on write and included in the backup.**
Values are range-checked when saved, and the per-day nutrient records now
travel with the full backup and restore.
- **Every response path carries the baseline security headers**, including
the early returns that used to skip them.
- **Provider re-sync tolerates an empty token-refresh body.** Oura, Polar,
and Fitbit treat an empty body as transient rather than a hard failure, so
a reconnect is not lost to a blank response.
- **The Coach reply stream aborts cleanly when you navigate away**, and the
daily metric series carries its own min and max.
- Also folded in: the WebAuthn relying-party origin gating (localhost only
in development), the medication-compliance point-window bound, and a
request-time future-date bound on the medication-intake import.
No migrations. No breaking changes.
## [1.32.5] — 2026-07-23
- **Browser SSO sign-in no longer fails on a duplicated callback.** With the
service worker's navigation preload enabled, a single-use OIDC callback could
be requested twice. The first request redeemed the authorization code and set
the session. The second hit the identity provider with the now-consumed code
and failed, so every successful sign-in landed on an error page. The service
worker now settles one-shot auth navigations itself, so the callback reaches
the server exactly once. The callback is also idempotent: a duplicate that
already holds a database-valid session is sent into the app instead of
erroring, while a forged or session-less state still fails closed and never
redeems a code.
No migrations. No breaking changes.
_Thanks to **@doenke** (#600), whose report pinned the cause (service worker
navigation preload plus the single-use OIDC code) down to the exact mechanism,
so the fix went in as reported._
## [1.32.4] — 2026-07-23
- **The Coach's numeric verifier stops flagging figures it was actually given.**
A restated difference, average, or percent change computed from two of the
numbers the model was shown, a sample count restated from the data inventory
that rides every turn, a calendar-date ordinal ("July 21st"), and a
numbered-list marker no longer trip the inline `[unverified]` rewrite that
was appearing in place of correct values. A genuinely fabricated figure, one
with no grounding at all, is still caught and stripped.
No migrations. No breaking changes.
## [1.32.3] — 2026-07-23
Security release. It ships on its own so the change to how a second factor is
proven can be watched in isolation.
- **A native client can manage its own second factor.** The iOS app can now
turn two-factor authentication on or off, add or remove a security key, and
regenerate recovery codes over its API session. Those actions were reachable
only from a browser before. It does so through a short-lived, single-use
elevation the app earns by re-proving a factor, never by presenting its
token alone.
- **The re-proved factor decides what the elevation reaches.** Re-proving with
a password reaches exactly what a plain browser session reaches. The actions
a browser gates behind a completed second factor now require the same over
the API: a re-proved TOTP code or security key, never the password alone.
Administrative endpoints remain reachable only from a browser session,
unchanged.
One migration (0267), additive. Addresses the native 2FA-management gap
(healthlog-iOS #57).
## [1.32.2] — 2026-07-23
Security release. Ships on its own and ahead of everything queued, because it
closes a set of freshly disclosed framework vulnerabilities that affect the
running instance today.
- **Next.js updated to 16.2.11**, closing four high-severity advisories in the
App Router: a middleware/proxy bypass, two server-side request-forgery
paths (rewrites and Server Actions), and a denial of service. The
middleware/proxy advisory is the most relevant here, since the request edge
enforces the public-path allowlist and the authentication redirect. No
application code changed; this is the dependency patch alone.
Carries the fixes already merged for 1.32.1 (honest health-score absence,
Coach score questions, Apple Health import and Pulse fidelity, three
silent data-loss races), since those reached the trunk first. No migrations
beyond what 1.32.1 declared (none). No breaking changes.
## [1.32.1] — 2026-07-23
- **The Personal Health Score reads "not enough data yet" instead of zero.**
An account with records that do not yet meet any score pillar's minimum
(for example a single mood entry, no paired blood pressure, one weight
reading) showed a red 0 rather than an honest absence. The score is now
withheld until at least one pillar is computable; a genuinely computed 0
is still shown.
- **The Coach stops refusing ordinary questions about your own scores.** A
question about a Sleep Score or Health Score no longer trips the block on
a fabricated clinical risk; it now matches the specific risk calculator it
was meant to catch, not the plain word "score", and still blocks a
genuinely invented risk figure.
- **The Apple Health import no longer stalls at "unpacking".** A large
export is now unpacked as a stream rather than decompressed whole into
memory on the shared event loop, and a job left mid-unpack by a restart is
picked up by a periodic reconcile instead of sitting stuck with no error.
A byte ceiling still guards against a decompression bomb.
- **The Pulse insights page shows pulse, not resting heart rate.** It no
longer silently swaps its main chart to resting heart rate when any
resting data exists; resting heart rate appears as a clearly labelled
second series when present, and the resting-calibrated target band no
longer paints behind raw pulse readings.
- **Dashboard layout changes can no longer be reverted by a concurrent
score-ring save.** The instant score-ring toggle now sends only the ring
fields instead of resending a cached snapshot of the whole layout, closing
a race where an in-flight ring update could land after a tile or chart
Save and silently restore the older layout.
- **Measurement-reminder cadence edits recompute against the cadence that
was actually saved.** Clearing an interval or a recurrence rule to switch
a reminder's schedule no longer leaves the next-due date computed from the
just-replaced value for one cycle.
- **ntfy notification settings keep a saved auth token across unrelated
edits.** Toggling the channel, or editing the server URL or topic, no
longer silently clears a previously configured token.
No migrations. No breaking changes.
_Thanks to **@lutzkind** (health-score zero-state, the Coach refusing score
questions, the dashboard layout race, the Pulse page) and **@Nazza01** (the
Apple Health import stall). These fixes came straight from their reports._
## [1.32.0] — 2026-07-22
- **Provider ingestion and daily reactions now preserve the correct
identity.** Arrival reactions are bound to the exact marker revision,
workout selection matches field donors to the correct surviving session,
long-range buckets honor the user's timezone, MCP pages stay stable, and
one Withings subscription repair can no longer block fallback polling for
other users.
- **Large reads and writes are bounded end to end.** Measurement exports
stream in pages without splitting sleep sessions, oversized projections
cannot evict the shared cache, dense reports and metric series use
database aggregation, upload reads have byte, time, and concurrency
limits, full-history backfills share admission control, and medication
imports use durable chunks with resumable, bounded final rollups.
- **Capture and history flows are complete on mobile and desktop.** Workout
history loads beyond the first page, metric pages open preselected capture
forms and return to their origin, failed water submissions retain their
draft, and confirmed discards clear it.
- **Interactive forms share one safe dismissal rule.** Dirty forms require
confirmation across tracked Back and Forward traversals, Apple Health
setup exposes successful native syncs, document selection is consumed on
Coach dismissal but preserved across full-page Coach navigation,
conversation titles can be renamed with rollback, and hidden tabs refresh
meaningful server state.
- **Accessibility coverage now exercises representative authenticated
routes, themes, dialogs, and sheets.** Small text keeps compliant
contrast, OCR uses a visible labeled control, workout sections use real
headings, and compact actions retain 44-pixel touch targets.
- **Client and API boundaries are explicit.** Localized stable error codes
cover measurement, mood, lab, and onboarding failures; client fetches and
query keys are lint-enforced; AI overrides are documented; and large
Coach, report, target, and provider modules now use one-way focused
builders.
- **Production image processing uses the patched Sharp 0.35 runtime.** This
closes the current high-severity libvips advisory chain inherited through
Next.js while retaining verified production-build compatibility.
Migrations `0264_whoop_owner_identity`, `0265_withings_subscription_state`,
and `0266_medication_intake_import_jobs`. No breaking changes.
## [1.31.8] — 2026-07-21
- **MCP search cursors keep lab results stable across pages.** Distinct analyte
names are ordered before offset pagination, preventing repeated or skipped
lab entries when a client follows `nextCursor`.
- **Opening the medication wizard no longer risks clearing entered fields.**
The one-shot URL flag is removed without starting a second page navigation,
so slower browsers keep the active wizard instance and its form state.
No migrations. No breaking changes.
## [1.31.7] — 2026-07-20
- **Current OpenAI reasoning models use the request fields they support.**
Official GPT-5, o1, o3, and o4 models receive `max_completion_tokens`
without unsupported `temperature` or `seed` fields; compatible custom
gateways retain the legacy parameters they advertise.
- **Apple Health XML cumulative totals no longer add overlapping sources.**
Imports aggregate each source/day independently and select the highest
source subtotal, while recording where each figure came from prevents XML
estimates or late legacy samples from overwriting authoritative native
HealthKit statistics. Existing legacy aggregates can be repaired by
re-uploading their archive.
- **Every currently actionable medication reaches Today.** Equal-time doses
no longer collapse to one row, overdue doses win deterministic ties, and
weekly or custom intake windows use the scheduling engine's real
availability boundary. The shared digest fixes both the web start page and
iOS Home.
Migration `0263_apple_health_aggregate_authority`. No breaking changes.
## [1.31.6] — 2026-07-21
- **The hardened runtime image can run database migrations again.** The
isolated Prisma CLI now exposes its pinned `dotenv` and `prisma/config`
dependencies to the copied production configuration while package managers
remain absent from the final image.
No migrations. No breaking changes.
## [1.31.5] — 2026-07-21
- **The production image no longer ships build-time package managers.** pnpm 11
verifies the lockfile under explicit build-script policy, npm and Corepack
are removed after migrations are prepared, and linked worktrees stay out of
local Docker build contexts.
No migrations. No breaking changes.
## [1.31.4] — 2026-07-21
- **Web and background processes now start and recover predictably.** Invalid
encryption-key configuration fails before the service accepts traffic,
worker-only processes report their real health, connection limits are
divided safely across replicas, and provider backfills keep their bounded
queues during reconnects.
- **Published images are reproducible and verified before promotion.** The
release pipeline builds one multi-architecture image, validates its digest,
startup, migrations, health, and reported version, and only then promotes
that exact artifact to production.
- **CI now uses isolated, deterministic test environments.** Integration tests
receive their own database and cryptographic settings before application
code loads, worktrees install hooks consistently, and browser tests use the
intended application process on every platform.
- **Concurrent iOS registrations no longer fail intermittently.** Device and
APNs notification-channel reconciliation now share one database lock and
transaction, so simultaneous first registration remains idempotent.
No migrations. No breaking changes.
## [1.31.3] — 2026-07-20
- **Document summaries now stay in the language you chose.** The same account
language is used whether a summary is created in the background or requested
from the document sheet. A saved summary remains visible and can be replaced
with **Generate again** after you switch languages.
- **The Coach composer has more room on phones.** The writing field now gets
its own line, with document and conversation actions below it and Send on the
right. The unreliable browser dictation button and its dead-end permission
states are gone.
No migrations. No breaking changes.
## [1.31.2] — 2026-07-20
- **Provider imports now commit identity and progress atomically.** Withings,
WHOOP, Oura, Polar, Strava, and Apple Health reconcile external and natural
measurement identities under database locks, preserve tombstones, emit
arrivals only for genuine inserts, and advance cursors only after durable
writes. OAuth refresh-token rotation is serialized per user and provider.
- **Webhook and import success now means the source data is durable.** moodLog
writes before acknowledging delivery, while Withings ECG callbacks enter a
durable queue with bounded retry and replay-safe processing. WHOOP backfills
commit bounded chunks without advancing failed resource cursors, and legacy
mood imports use deterministic, collision-free retry identities. Operational
JSON import failures are reported as failures instead of misleading
duplicate skips.
- **Recovery is complete, exact, and owner-safe.** Canonical backups round-trip
every supported record class, including encrypted document content,
measurement reconciliation identities, and tombstones. Restore rejects
account mismatches before writing, validates backup enums at the boundary,
processes stable measurement IDs in bounded batches, and preserves recovery
integrity across both uploaded and off-host payloads.
- **Device and mood edits no longer lose related state.** Same-user APNs
registrations merge transactionally without crossing account boundaries,
refresh-token bindings follow the surviving device, and mood edits preserve
rated factors unless the caller explicitly replaces them.
- **Polar sync follows the current AccessLink collection API.** Collection
requests no longer include the obsolete user ID path segment, and current
activity, Nightly Recharge, cardio-load, sleep, and SpO2 response shapes are
normalized without losing stable measurement identity.
- **The July dependency bundle is included.** PDF.js 6 now releases its loading
task correctly; Canvas 1.0, Radix UI 1.6.2, Tailwind CSS 4.3.3, the Tailwind
Prettier plugin 0.8.1, and setup-node 7 are integrated.
No migrations. No breaking changes.
## [1.31.1] — 2026-07-19
- **Arrival events now follow committed inserts across every writer.**
Provider re-sync updates stay silent, partial Apple Health imports retain
arrivals from earlier committed batches, all newly inserted workouts emit,
and sleep refreshes distinguish new stages from updates. Web-only
processes now retry their bounded send-only queue connection instead of
permanently losing arrival jobs after one transient startup failure.
- **Reaction and workout generation are single-spend operations.** Durable
database claims close worker races and provider-retry gaps, daily token
reservations are atomic and reconciled on every terminal path, and both
generation and display honor the user's Insights and Workouts module
choices plus server-managed consent.
- **AI context is narrower and more accurate.** Arrival reactions use the
record that actually landed, completed sleep uses the reconstructed night
rather than one stage segment, user-authored lab fields are fenced as
data, workout comparisons use one agreed session and date, and every
metric and derived-score coach entry carries its exact supported scope.
- **Dashboard state survives old and partial clients.** Saved comparison
range points and newly supported metric widgets are preserved, while Today
freshness remains aligned across local-day boundaries, DST, tab focus, and
split web/worker deployments.
Three migrations (0260, 0261, 0262). No breaking changes.
## [1.31.0] — "The record that reacts" — 2026-07-19
A milestone release: the record now visibly responds when something lands,
instead of waiting for a daily rollup to notice.
- **Today shows what just came in.** A completed night's sleep, a workout, a
first weigh-in of the day — the home screen now carries a calm "just in"
note for a few hours after each one, and at most one written line a day per
kind, grounded in your own numbers. Nothing changes if you run without an
AI provider: the sentence is the garnish, not the feature — the arrival
itself still appears, including on an otherwise-empty record, and an open
dashboard refreshes it as soon as the tab becomes visible again. The
underlying change that makes this possible without new polling or a flood of
background work: every write path that can produce a genuinely new reading
now emits a single typed event, day-scoped and de-duplicated, that a mass
import or a provider re-sync produces none of — a ten-year import or a
month of catch-up sync stays silent, by construction, not by luck.
- **A finished workout gets its own paragraph.** Opening a recent session now
shows a short, grounded read of that one activity — duration, effort,
where it sits against your own recent sessions for the sport — with no
training advice and no target zones. It is generated once, the first time
you open a fresh session, never on re-sync and never for older workouts
already in your record.
- **"Ask why" reaches every read-only card.** Every per-metric status card,
every derived-score page, and a finished workout now carries a way straight
into the coach, pre-loaded with just that card's own data so the
conversation starts already grounded — not with a blank prompt.
- **The lab-marker card and the offline metric texts speak with the same
voice as everything else** — meaning first, in your own language across
all six locales, checked by an automated tone pass that now runs on every
change to these surfaces so this kind of drift gets caught before it ships
again.
- **The dashboard layout stops discarding a comparison choice on save,** and
four clinical widgets stop disappearing from a saved layout — both from a
save made by a client that does not know those particular fields yet.
- **Mood and BMI reach the app's home screen**, and a medication pen can now
carry the maker and printed strength that show up in the native pen list.
- **Every new AI call this release can cause is metered and bounded**: a
daily cap per kind, a duplicate-content check that makes a re-post free,
and a token reservation that is always reconciled, including on every
failure path — the same discipline the rest of the AI surface already
holds to.
Two migrations this release (0257, 0259); an additional dashboard-widget
migration carried over from the prior line. No breaking changes.
## [1.30.35] — 2026-07-19
- **The metric texts you see without an AI provider now speak your
language.** They shipped in German and English only; French, Spanish,
Italian and Polish fell through to English. These are what nine metric
cards render whenever no provider is reachable — no key configured, an
outage, a spent budget — so for those readers this text was the app's
voice, in the wrong language. Written per language rather than
translated: where a sentence could not be formed naturally, that
language got its own sentence.
- **The lab-marker card leads with what a value means**, like every other
card already did. It was the last one still instructed to state the
number first.
- **The response length for a metric assessment is set in one place**
again, instead of one card carrying its own inline pair.
- **The fallback texts open on something honest** rather than on an
instruction, and the lab-marker card no longer shows the multi-metric
overview tip on a single value.
No migrations. No breaking changes.
## [1.30.34] — 2026-07-19
- **Rearranging your dashboard no longer wipes the comparison baseline.** A
layout save from a client that does not know about that setting — the
app never sends it — silently reset it to "none". Every field of the
layout now has to declare whether an absent value means "clear it" or
"leave it alone", so the next field added cannot repeat this.
- **Four clinical widgets stopped disappearing from a saved layout.** Pain,
grip strength, waist and waist-to-height were dropped by the save as
unknown, so a placed widget was lost every time.
- **Mood and BMI reach the app's home screen.** Both existed in the record
but were absent from the summary the app reads, so those two tiles could
not be shown while every other one worked.
- **A pen carries its manufacturer and printed strength.** Without them,
containers entered on the web could not be shown in the app's pen list.
- **Destructive controls ask first.** Signing every other device out,
revoking a session, and revoking a trusted device fired on a single tap.
Two more had neither a confirmation nor an undo: a Coach reminder and a
travel entry. Deleting a document still does not ask — it can be undone,
which covers the mis-tap.
- **Delete confirmations stop overclaiming.** Four of them promised the
deletion could not be undone while the same screen offered an undo.
Warnings that are wrong make the ones that are right easier to ignore.
- **The sleep-debt figure is described correctly again.** The calculation
changed to a running balance that credits surplus in v1.19.0; the label
still said "cumulative shortfall". The wrong description had spread to the
Coach, which was reasoning about the number on the old definition, and to
the published API description.
- **The glucose reference band can be chosen on the web.** The setting
already worked and already fed the Coach — nothing on the web set it. It
selects a reference range; it is not a diagnosis and is never inferred
from a reading.
One migration (0256), additive. No breaking changes.
## [1.30.33] — 2026-07-19
Security release. It ships on its own because it changes how a browser
session is recognised.
- **The session cookie is a purpose-generated secret.** It used to be the
session record's own database key — an identifier built to avoid
collisions, not to be unguessable, and one that appears wherever that
record is referenced. The cookie now carries 32 bytes from the system's
random source, and only its hash is stored, so the value in your
browser exists nowhere on the server.
- **You are not signed out by this.** Sessions from before the change
keep working and are simply not renewed, so they run out their normal
30 days and disappear. Every sign-in from this release onward is on the
new scheme.
- **The device list no longer hands out working credentials.** It
identified each device by that same record key — which, for a session
predating this release, is that device's cookie. It now shows an opaque
handle that is useless as a login, and the same handle is what signs a
device out.
- **A refresh token cannot be rotated from a different device**, and an
expired one is refused. The test that was supposed to prove the expiry
passed a token that had never been issued, so it proved nothing: with
the expiry check deleted, the suite still went green. It is a real test
now.
- **A Nightscout address is checked when you save it**, not only when it
happens to be probed first. The check itself already refused private,
loopback and cloud-metadata addresses.
Operators: rotating `API_TOKEN_HMAC_KEY` now also signs out every browser
session, not just API tokens. One migration (0255), additive.
## [1.30.32] — 2026-07-19
- **A withdrawn AI consent stays withdrawn.** Opening the AI settings
granted consent again if you had taken it back, and recorded it as though
you had done it yourself. A withdrawal is now a standing decision that
only an explicit act can lift.
- **The web can withdraw AI consent at all.** Until now only the app could;
on the web, consent could be given and never taken back. The AI settings
show what currently stands and offer the control that changes it.
Withdrawing takes effect immediately and deletes nothing you have
recorded.
- **Documents get read after you switch automatic reading on.** Reading was
only ever started at upload, so documents already in your vault stayed
unread no matter what the setting said. Turning it on now works through
what is already there, in batches, asking the same permission as any other
read.
- **A quoted passage in a document review is the document's own text.** The
quote shown for a finding was the wording that came back from the read,
not the line it was taken from — so it could be a paraphrase of your own
record presented as a verbatim quote. The passage is now looked up in the
document itself, and a finding whose source cannot be located says so
instead of showing a quote you cannot check.
- **Pages that arrive with your data pre-loaded now say so on the wire.**
They were never cacheable in practice, but nothing stated it. Anything
behind the sign-in wall is now marked private and uncacheable at the edge,
so no proxy or future change can hold one person's record and hand it to
someone else.
- **Deleting your last reading of a kind no longer leaves stale weekly,
monthly and yearly figures.** The daily figure went; the longer spans kept
the old aggregate indefinitely.
- **The documented API matches what the endpoints return.** Twenty
corrections, including a total that the measurements list reports in a
different place than the specification claimed, four medication figures
named wrongly, a missing intake source, and eleven error codes that were
never written down.
- **A retry after an unusable AI answer asks for the right shape.** The
correction prompt demanded fields that do not exist and named none of the
ones actually required, so the one corrective attempt steered away from
the right shape instead of towards it.
No migrations. No breaking changes.
## [1.30.31] — 2026-07-19
- **Today shows a dose before it is late.** The card only surfaced a
medication once it was already overdue, so it could report nothing
needing attention while a dose was due. A due dose now reads as a calm
note, an overdue one as a warning, and a day with everything taken
still shows the all-clear. Reminders are unchanged — they still only
fire once a dose is late.
- **The daily briefing stops disappearing.** The check that verifies
every figure against your own data was looking in fewer places than the
briefing draws from, so a briefing quoting your medication adherence or
a change since last month was thrown away as unverifiable. It now
checks against everything the briefing is built from. And when a figure
genuinely cannot be verified, yesterday's briefing stays up instead of
the section going blank.
- **The "read the full briefing" link is gone from Today.** Tapping the
health-score ring already opens the same page.
- **The close button on a document sits flush with the edge again.**
- **A document no longer says its summary is being generated forever.**
Summaries are only started when a document is uploaded while automatic
reading is on — there is no catch-up for documents you already had, so
the view promised a summary that was never coming, and hid the button
that would have made one. It now says what actually happened — not
attempted, unavailable, or held back — and offers to generate one in
every case. A summary you generate that way is kept.
- **Removed the "read by AI" note** at the bottom of a document.
One migration (0254). No breaking changes.
## [1.30.30] — 2026-07-19
Four languages get their own words; a failed read stops claiming you have no data.
- **Reminder notifications, the assistant's reply language, chart and calendar labels, and durations were English or German for French, Spanish, Italian and Polish.** The translations existed; the code that picked between them only knew two languages. It now uses the account's own.
- **Polish counts correctly.** Polish has a third plural form that a two-form scheme cannot express, so counted phrases read wrong. The other five languages resolve exactly as before.
- **A screening history that failed to load said "not taken yet".** On a mental-health surface that is a false statement about your own record — you could reasonably conclude you never completed something you did complete. That surface, and a nutrient card with the same flaw, now say the reading failed and offer to try again. Nothing retries silently: a failure you cannot see is worse than one you can.
- Three sleep-rhythm cards that share one reading each printed the same failure notice; the page now shows it once.
## [1.30.29] — 2026-07-19
- **A connector token that may only read is now refused at the write itself**, not only by never being offered the write. The protection was real but rested entirely on how the connection is assembled, so a future change to that assembly could have opened writing with no test noticing. Any write added later inherits the check.
- **Names and text read out of your records now carry an explicit boundary when handed to a connected assistant**, matching what the app already does for its own assistant.
- **Reading a medication list from a photo measured its cost against the server-wide limit even when you use your own key.** It now uses your own limit. The older way of counting that caused this — the third surface to have it — is removed entirely rather than fixed in place, so nothing can reach for it again.
- Removed a module that read as a check on generated recommendations but had not run for some time. It could not simply be switched on: it required fields the current format does not have, so enabling it would have rejected nearly every generation. The measurement it fed is unaffected and still runs.
No breaking changes.
## [1.30.28] — 2026-07-19
- **Daily briefings count against the AI budget.** They were the one
remaining kind of generated text that did not, so a morning's briefings
could run past the limit you set. On your own key they count against
your limit, not the server's.
## [1.30.27] — 2026-07-19
- **The resting heart-rate chart no longer stops partway.** Older
readings get condensed after a while, and the chart treated the
condensed days as the whole story — so recent days went missing. It now
shows both, and says when a day is an estimate.
- **Summaries recompute for the whole day or month.** Recomputing from a
point in the middle replaced the full period with a partial figure.
Affected summaries correct themselves the next time that day is written
to, or on the next nightly pass.
- **A repeated daily total in one upload keeps the newer value.** It kept
the older one.
No breaking changes.
## [1.30.26] — 2026-07-19
- **Text read out of your documents is treated as data.** Lab field
names were passed to the assistant unchecked, so a document whose
labels were written to look like instructions could influence later
answers. Those names are cleaned now, and document text is marked as
yours rather than as something addressed to the assistant.
- **A reminder the assistant picks up from a chat is proposed, not
active.** It waits for you to confirm it, like its other suggestions.
Reminders from before this release keep working unchanged.
No breaking changes.
## [1.30.25] — 2026-07-19
- **Generated cards count against the AI budget.** Metric cards,
biomarker cards, scores and the period narrative were not counted at
all. On your own key they count against your limit, not the server's.
- **Switching language no longer regenerates everything twice.** The same
card in six languages counted as six separate jobs.
- **Long assistant conversations get cheaper again.** Past a certain
length the full context was re-sent on every turn instead of once.
No breaking changes.
## [1.30.24] — 2026-07-19
- **The safety check on generated text covers every card, in every
language.** It ran on the assistant's replies but not on the metric
cards, biomarker cards or document summaries — so a sentence the
assistant would refuse to send could still appear on a card. It also
only knew English and German wording, leaving four languages
unprotected. Both are fixed.
- A document transcription is deliberately never filtered: it reproduces
your own document, and a letter from your prescriber is a record, not
advice from us.
No breaking changes.
## [1.30.23] — 2026-07-19
- **Switching a module off now also applies to connected assistants.**
With a module off, a connected assistant could still read that data —
including the full doctor summary — and so could a shared clinician
link. Both now honour the switch.
No breaking changes.
## [1.30.22] — 2026-07-19
- **Background jobs stop piling up on themselves.** Duplicate jobs were
meant to be collapsed but were not, so a large sync could queue
thousands of identical recomputations and a restart could re-queue a
running history import. Existing installations are updated on the next
start.
No breaking changes.
## [1.30.21] — 2026-07-19
- **Dose reminders land on the right hour across a clock change.** On the
day the clocks change, a reminder could resolve an hour off, and the
same dose could end up recorded twice. Nothing stored changed.
- **The next-dose day label was wrong** when your phone's timezone
differed from the one in your profile.
No breaking changes.
## [1.30.20] — 2026-07-19
- **A restore refuses when the backup file names a different account**
than the backup you selected.
No breaking changes.
## [1.30.19] — 2026-07-19
- **Switching a module off now holds everywhere.** The sync feed still
sent cycle data, the FHIR export still returned the full record, and
the dashboard summary still built glucose and sleep cards regardless of
the switch. Nothing is deleted — turning a module back on finds the
history complete.
- **Two assistant features sent data out without checking your consent
first.** Both now check, and fall back to their plain text when consent
is absent.
No breaking changes. The native app may see a 403 from the FHIR
endpoints where a module is off.
## [1.30.18] — 2026-07-19
- **Health data no longer reaches the logs or the backups.** Cached
responses are encrypted at rest — they echo what was just saved, so
cycle notes and mood text sat unencrypted in every backup for a day.
Diagnostic log entries are now scrubbed where they are written, and a
failed AI request no longer records what came back with it.
Log lines and backups written before this release may still contain those
values. No breaking changes.
## [1.30.17] — 2026-07-19
- **API tokens enforce their scope.** A token issued for medication
intake reached every endpoint, including the full backup export. Routes
that accept a narrow token now have to say so.
- **Revoke and re-issue any token you gave to a third party.** Treat it
as having had that reach for its whole lifetime.
- **The generic "create API token" button is gone.** The token it made
never worked for the medication intake it advertised, while reaching
everything else. Tokens are issued per medication from that
medication's own toggle. Existing tokens stay listed and revocable.
No breaking changes to the web app, the native client or the MCP
connector.
## [1.30.16] — 2026-07-19
- **Resting heart rate stops being lost.** Older heart-rate readings are
condensed on a schedule, and a resting value is derived before the raw
readings go. A faulty check stopped that derivation after the first
run while the raw readings were still cleared, so each run lost another
day's resting figure for good. Days already affected cannot be
recovered; from here nothing further is lost.
No breaking changes.
## [1.30.15] — 2026-07-18
- **Assessments come in the language you set.** French, Spanish, Italian
and Polish accounts got German or English text instead of their own.
Insight cards, the per-metric assessments, scores, the biomarker card
and the period narrative now follow your language. German and English
are unchanged. Stored texts in the four languages refresh on their next
nightly run.
No breaking changes.
## [1.30.14] — 2026-07-18
The workouts page joins the pages that paint on first load.
- **The workouts list opens with the sessions already there.** It was the
last high-traffic surface still fetching its rows after the page mounted,
so it showed a loading skeleton first and filled in a moment later. The
list is now read on the server and handed to the page populated, the same
way the dashboard, insights, medications, and coach pages already work.
The list logic moved into a shared read so the page and the API endpoint
go through one cached, deduplicated pass instead of two.
- **API docs: the heart-rate bucket example matches what the server
accepts.** The aggregated heart-rate `externalId` was documented with an
hourly example from its first version; the server has taken 10-minute
buckets since v1.30.7. The description, the example timestamp, and the
min/max field notes now describe what the server actually accepts.
No breaking changes.
## [1.30.13] — 2026-07-18
More pages that paint on first load.
- **The insights, medications, and coach pages open with their content already there**, the same way the dashboard now does — their above-the-fold data is read on the server and handed to the page populated, instead of a shell that fills in after a second round of requests. Nothing that would trigger AI work is pre-run: the daily briefing and the coach's own reads stay on-demand as before.
No breaking changes.
## [1.30.12] — 2026-07-18
Faster medication reads.
- **The dashboard's medication section stays fast as your log grows.** Its reads now use dedicated indexes instead of scanning the full intake history on every load, so a long history no longer slows down the daily view.
No breaking changes.
## [1.30.11] — 2026-07-18
Native single sign-on.
- **The iOS app can sign in through your identity provider (OIDC SSO).** Until now, single sign-on only worked in the browser — the native app couldn't complete it, which locked native users out entirely on an SSO-only instance. The server now supports a native sign-in handoff (an in-app authorization flow that returns the app's normal token, never a cookie), so the app can offer SSO once it adopts the flow. Existing password and passkey sign-in are unchanged. Multi-factor sign-in still applies, and an SSO session never satisfies a step-up prompt — those still require your own second factor.
No breaking changes.
## [1.30.10] — 2026-07-18
Localized insight text, and a richer workout view.
- **Correlation insights now read in your language.** The "this tends to go with that — a pattern worth watching, not a cause" lines under a metric were always written in English; they now render in your interface language (German, Spanish, French, Italian, Polish), with the association-not-causation framing preserved in each.
- **A workout you open shows more of what was recorded.** The detail view now draws the session's heart-rate curve over time, its route as a clean line, the time spent in each effort zone, per-kilometre splits, and how it compares to your own average for that sport — all from data already synced, with no new connection. Anything a given workout didn't record simply isn't shown.
No migration. No breaking changes.
## [1.30.9] — 2026-07-18
Faster dashboard first load.
- **The top card and the charts paint with their data on first load** instead of flashing empty and then filling in. The daily summary and the charts' 31-day series are now read on the server and handed to the page already populated, so there's no wait for a second round of requests after the page appears. The welcome/summary card, likely the slowest thing to show before, now lands with the first paint.
No migration. No breaking changes.
## [1.30.8] — 2026-07-18
Hardening for the new heart-rate upload validation.
- **A malformed bucket timestamp is rejected, not silently misfiled.** A bucket whose time rolled over (a 24:00, an impossible day) is now refused rather than stored under a non-standard key, where it would have failed to merge with a re-post and could land on the wrong day.
- **The per-window low/high is sanity-checked before it counts.** A window's low and high are kept only when they are in a plausible range and sit either side of the average; a glitched reading no longer skews the day's high/low band. The average is always kept.
No migration. No breaking changes.
## [1.30.7] — 2026-07-18
Groundwork for a tidier heart-rate history.
- **The server accepts a 10-minute aggregated heart-rate upload.** A watch records heart rate every few seconds, and uploading one row per raw sample fills the measurements list with the same value repeated many times a minute. The server now takes one row per 10-minute window — its average, with the low and high of the window — and the within-day pulse curve reads it directly, so the detailed 10-minute chart and the elevated-at-rest note are preserved. The de-duplication reaches the list once the mobile app sends the aggregated shape; existing per-sample history is unaffected and reconciles itself over time.
- **The daily high/low reflects the true spread.** A day built from 10-minute windows now reports the real intra-window extremes, not the range of the averages.
No migration. No breaking changes.
## [1.30.6] — 2026-07-18
Accessibility pass.
- **Heatmaps read to a screen reader.** The mood and medication-compliance grids now expose each recorded day — its date and value — through a hidden day list, so the per-day figures are no longer reachable by pointer only. The chart looks exactly the same.
- **Small labels meet the minimum size.** Muted meta text across settings, admin, the coach panel, and the insights cards is lifted off the sub-`text-xs` sizes; tabular figures and chart chrome are unchanged.
- **A couple of section headings.** Two metric-page sections became proper headings, so heading navigation lands where it should.
No migration. No breaking changes.
## [1.30.5] — 2026-07-18
Making a shipped feature findable, and a sync-status fix.
- **ECG has a home you can reach.** Recordings now have their own page in Insights, a pill in the Heart group that appears once you have a recording, and links to it from the resting-pulse and heart-rate-variability pages. Before this they were reachable from a single spot, and a deep link into the overview could land before the section had drawn. The page is unchanged in substance — the same non-diagnostic, device-attributed view.
- **The "last synced" time stops lagging.** A sync driven by an incoming update now records the connection's last-success time straight away, instead of leaving it frozen until the next full poll — so the Settings status and the assistant's integration read reflect a sync that already happened.
No migration. No breaking changes.
## [1.30.4] — 2026-07-18
The read-only assistant connector now reaches the full record.
- **More of your data is readable.** The connector gained direct reads for nutrition and vitamins, the intraday pulse curve, and stored ECG recordings, plus discovery for a set of device metrics that were collected but not advertised — wrist temperature, cardio recovery and load, day and workout strain, sleep score, breathing disturbances, fall count, the six-minute-walk distance, stair speeds, and energy expenditure. They can now be compared and baselined by name.
- **Correctness.** Heart-rate variability resolves whether the source records the SDNN or the RMSSD flavour, so a ring- or strap-only account is no longer reported as having none. A workout logged by two devices is counted once, matching the workouts list — this also corrects the figure the coach sees.
- **Exposure stays deliberate.** The mental-health screeners and the environmental signals remain off the connector by construction; a metric is only readable once it has been named for it.
No migration. No breaking changes.
## [1.30.3] — 2026-07-18
Follow-up fixes from a full quality re-check of the app.
- **Correctness reaches every surface.** The timezone, window, and grain fixes now also cover the coach's correlations, the per-metric cards, and the period narrative (they had kept an older copy). Daily insight cards roll over at your own midnight, not a fixed one. Personal records that a second device had inflated are re-derived on the next start, so a genuine record can be set again.
- **The metrics catalog** shows a loading state and a retry on error, instead of briefly reading as "not tracked".
- **Small.** One shared info popover, larger tap targets on the view toggles, a notch-safe height fix on the coach history page, and a few wording and translation gaps closed (a body-composition caveat, study citations, the backup scope note).
No breaking changes.
## [1.30.2] — 2026-07-18
- **Coach history is fully reachable.** The conversation list pages through
your whole history and search runs on the server (by title), instead of
only the first ~20 loaded chats.
- **Navigation and flows.** A document links to the lab values it produced;
the two "Recovery" pages are named distinctly and cross-linked; mood,
mental wellbeing, and mood insights link to each other; the nutrients card
moved out of "Source priority"; the onboarding checklist and tour no
longer point at dead ends; asking the coach from a metric or a plan
carries that context in.
- **Correctness.** Correlations, the intraday chart, achievements, and the
nutrients overview now bucket days by your own timezone and lead with the
most recent data; a multi-source day is no longer counted twice.
- **Performance.** The workouts list caches its de-duplication per filter,
so paging through a long history is quick.
No migration. No breaking changes.
## [1.30.1] — 2026-07-18
Polish and small fixes.
- **Mobile.** Coach and document text wraps instead of overflowing; the intake history is a card list on small screens instead of a sideways-scrolling table; tap targets meet the minimum size; two dialogs became bottom sheets; the workout detail header stacks cleanly.
- **Feedback.** Loading skeletons match the content that replaces them (no more layout jump), and the Today priority-card actions show a pending state and can’t fire twice on a double-tap.
- **Remembered choices.** A chart’s time range and the measurement type you last logged are remembered; a coach chat deletes with an Undo; a backdated dose can be undone; lab panels get “Save & add another”; pull-to-refresh works on more pages (and a scroll-position bug that made it fire anywhere is fixed).
- **Consistency, accessibility, wording.** Spacing and contrast tidied, more empty states use the shared component, headings and focus rings improved, a few German phrasings corrected, the daily line reads more calmly, and several explanations gained their guideline citation.
No migration. No breaking changes.
## [1.30.0] — 2026-07-18 — Quality pass
A broad correctness, performance, and polish release, plus two new features. Nothing new is required of you; existing data is untouched.
### Added
- **Document summaries.** An uploaded document can get a short plain-language summary, generated once at upload and stored encrypted. It runs only when the external-AI document reading you already opted into is on, and never touches older documents.
- **Efficacy chart, readable.** The medication efficacy chart scales its axis to the data (a real trend no longer looks flat), gains a “% since start” view, and the history tab reaches your full intake history, not just the last 90 days.
- **A metric catalog.** A quiet “All metrics” view lists everything HealthLog can track and which device supplies each — so a metric you don’t have data for stays out of your daily views but is still discoverable in one place.
### Fixed — correctness
- **The number shown matches reality.** The workouts and measurements lists now lead with the most recent entries (a long history no longer hid recent days), and a metric recorded by two devices on the same day is counted once, matching the dashboard — instead of being summed twice.
- **Strava workouts carry their real sport** (cycling, running, …) — like WHOOP already did — with a history backfill, and back-to-back rides no longer collapse into one.
- **Personal records and correlations** no longer double-count multi-source days or mix per-reading and daily-total values.
### Fixed — medical safety
- The hypoglycemia alert now uses your own glucose unit; the doctor-report PDF states your own timezone; the crisis card offers Austrian and Swiss lines, not only German ones; and resting-pulse labels drop clinical-verdict wording for calmer, neutral framing.
### Fixed — the daily view
- The Today view is faster (its heaviest reads are now cached instead of recomputed on every refresh) and updates immediately after you log a measurement, mood, or water — not only medication.
- The intraday heart-rate chart no longer draws a continuous shape across gaps in sparse data, and shows how many readings it’s based on. A failed load shows a retry instead of an empty state.
### Fixed — onboarding, dates, consistency, accessibility
- Connecting a device no longer dead-ends on a fresh account, and the setup steps (including the callback URL) are shown up front.
- A date from a previous year now shows the year, everywhere it’s displayed.
- Contrast, chart colours in the light theme, and a handful of duplicated components were tidied; keyboard access and headings improved on several surfaces; the insights and settings menus are grouped more sensibly.
### Changed
- **A fuller backup.** The full backup now also includes lab results, illness episodes, allergies, family history, workouts, and a documents list. What isn’t included (raw document files, workout GPS/heart-rate detail) is stated plainly.
### Performance
- The Today digest and the insights cards read through the pre-aggregated tier instead of scanning raw rows; batch imports write in one pass.
Two migrations (0250, 0251). No breaking changes.
## [1.29.5] — 2026-07-17
### Security
- **Hardened the same-origin redirect check.** A crafted relative post-login
`next` path (e.g. `/..//evil.com`) passed the same-origin check but left a
protocol-relative pathname that could re-resolve to a foreign origin when
re-used in a redirect or client navigation. The check now re-verifies the
reconstructed path resolves on-origin and rejects it otherwise. Covers the
OIDC callback and the password/passkey login redirect.
### Fixed
- **OIDC login redirects use the operator app URL, not the request URL.**
Behind a reverse proxy that does not forward the original host, the login
and callback routes built their browser redirects from the address the app
was reached at inside the container, sending users to an unreachable
internal URL after login. Every OIDC redirect is now built from the
operator-set `NEXT_PUBLIC_APP_URL`, matching the convention the other
integrations already use.
## [1.29.4] — 2026-07-17
- **The workouts list shows the most recent first.** The list was ordered
oldest-first, so on a longer history the newest sessions never made the first
page. It now leads with the latest workout.
- **Past-year dates show the year.** A workout from a previous year now carries
its year in the list, so a December date is never mistaken for this year's.
## [1.29.3] — 2026-07-17
- **Hydration and micronutrients now have a home.** Your fluid intake and the
vitamins and minerals your device records get a surface. A water tile on the
dashboard shows the day's total with quick-add amounts; a new **Nutrients**
page under Insights shows hydration over the last month, caffeine when you
record it, and the vitamins and minerals you actually have data for — each
against its reference daily intake where that applies, shown as context, never
as a verdict. Manual water entries and device-synced totals now coexist
instead of overwriting each other. Sparse logging is never read as a
deficiency.
The nutrients tracking stays opt-in — enable it in Settings, or with the
one-tap prompt on the page. Device-synced micronutrients flow once your client
sends them.
One migration (0249). No breaking changes.
## [1.29.2] — 2026-07-17
- **WHOOP workouts carry their real sport.** WHOOP was the only source that
didn't translate its activity into the app's own sport names, so
WHOOP-tracked rides and runs landed unlabelled instead of as cycling or
running. New and existing WHOOP workouts are now mapped, so your bike
rides show up as cycling — history included. When the same session comes
in from two sources, the surviving row now keeps the richest reading
(heart rate, energy, distance) instead of dropping it.
- **Scroll back through the day's heart rate.** The intraday heart-rate view
gains a day navigator to step through previous days. The fine-grained
history now stays for 90 days (was 14); older days show the coarser hourly
shape.
- **Clear a card from Today.** A milestone, a new ECG, or a tension note can
be dismissed from the Today rail once you've seen it; the section tightens
when the last one is gone. The cards that need an action stay until you
act.
- **Vitals context moves into an info tap.** The vitals tiles carry an (i)
with the context — your reading against your personal range, and against
your age where that applies — instead of a permanent caption. The VO₂
max heading no longer gets cut off.
- **Oura cycle phases feed the cycle tracker.** Where your Oura connection
provides cycle-phase data, it now fills the cycle tracker as a background
source; your own entries always win.
Two migrations (0247, 0248). No breaking changes.
## [1.29.1] — 2026-07-17
A round of fixes and small refinements from living with the daily view.
- **Today refreshes right after a dose.** Logging or undoing a medication now
updates the day's read immediately — the medication line no longer lingers on
a stale count until the next reload.
- **A calmer Today hero.** The extra score rings are gone; the hero keeps the
one health-score ring and a tighter layout.
- **The Coach names the document you attached.** Asking about a document now
shows its title in the chat instead of a plain "1 document" count, and the
attach menu stays within its popover on long labels.
- **A fresh install starts with tracking on.** New installs now enable the
tracking modules by default — you can still switch any of them off. The
external-AI options and the daily push stay off until you turn them on.
- **Insights vitals lose the explainer line.** The "your personal normal range
is the median…" caption is removed from the baseline tiles.
- **Illness journal: set and edit the end.** You can mark an episode's end date,
clear it back to ongoing, and backdate the start. When nothing strayed from
your personal range, the course section now reads a short settled line instead
of appearing empty.
- **The morning refresh covers every sleep source.** The refresh that folds last
night's sleep into the day now triggers from Google Health, Fitbit, Oura, and
Polar as well, not only Withings, WHOOP, and Apple Health. Each skipped refresh
is now recorded so a stale morning read can be traced to its cause.
No new data is collected and there is no migration.
## [1.29.0] — 2026-07-17 — A daily health companion
This release draws a line under a run of work that turns HealthLog from a place
you record your health into one that meets you each day — and it completes that
picture with the pieces that make it feel whole.
Over the last releases the dashboard gained a **Today** view that leads with the
day's read, a health score that refreshes the moment last night's sleep syncs, a
coach check-in when a plan is due for a look-back, and an optional morning note.
Documents came closer to hand — the Coach opens beside them, you can bring
several into a conversation, and lab values move between a document and your
labs. This release adds the layer that ties it together:
- **Your heart's rhythm, in context.** ECG recordings are no longer a page on
their own — they're referenced where your resting heart rate and rhythm are
discussed, always as the reading your device produced, never a reading of the
app's own. A new recording surfaces gently in Today.
- **The shape of your day.** Heart rate is now drawn across the day, not just as
a daily number — so you can see it settle and climb. When it stays elevated
while you're at rest and not moving, Today notes it plainly as possible
tension, carefully and never as a verdict.
- **Quiet milestones.** Reaching a steady stretch in your target range, or a new
personal best, is marked with a calm moment — states you've reached, not
streaks to keep, so there's nothing to break and nothing to nag.
- **One piece throughout.** The Today view brings back the health rings you
choose, each surface carries its metric's own calm colour, and the whole thing
reads as one design rather than a stack of features.
Nothing new generates when you open the app — every daily surface reads the
insight prepared overnight. No new data is collected; this makes more of what's
already yours.
Four changes bring documents and your record closer together:
- **Ask the Coach opens beside your document.** Asking about a document now
opens the Coach in a side panel rather than jumping to a full page — expand it
to full screen whenever you like, and it keeps its place.
- **Bring documents into a Coach chat.** From the composer's "+", attach one or
more documents — pick from your vault or upload a new one, at the start or
mid-conversation — and the Coach answers grounded in them. A conversation with
a document runs in its fenced mode: grounded in those documents only, with no
access to your health data or tools, so nothing in an uploaded file can reach
anything it shouldn't.
- **Lab values from a document, proposed for your labs.** When a document you've
had read looks like lab work, its values are detected and proposed — one tap to
add them to your labs; they're never added without your say-so.
- **A lab scan is also kept.** Scanning a lab report into your labs now also
files the document in your vault (encrypted, like every upload) and links the
two, so the reading and its source stay together.
Migration 0245 (attaching multiple documents to a conversation) accompanies this
release; existing document chats carry over unchanged.
Three pieces turn the Today view into a daily loop:
- **A coach check-in.** When a coach plan is due for a look-back, it surfaces as
a calm card in Today with keep, adjust, or let-go — one tap. Letting go retires
the plan quietly; silence never changes it.
- **A morning that reflects last night.** The score and read are prepared
overnight, but last night's sleep usually hasn't synced by then. Now, the
moment it arrives, the sleep-dependent parts refresh on their own — so by the
time you look, the day is up to date. Until sleep is in, Today says so plainly
instead of showing a stale score.
- **An optional daily nudge.** A once-a-day morning notification with the day's
read, off by default and opt-in per channel under Notifications. It carries the
refreshed digest when sleep is in, never repeats within a day, and is a calm
note — never an alert.
All three read the digest prepared overnight; nothing is generated when you open
the app.
With the Today view now leading the dashboard, the older opt-in hero card it
replaced is retired, so the two never stack. The greeting stays, the tiles and
charts are unchanged, and a layout stored with the old hero switched on simply
ignores that setting — nothing to redo.
## [1.28.53] — 2026-07-16 — A "Today" overview at the top of your dashboard
The dashboard opens with a Today view: your health score, the day's lead read,
and a short "worth a look" list — a dose window that's open, an integration to
reconnect, a check-up coming due — each a single tap to the right place. The
familiar tiles and charts sit just below, unchanged.
It reads the insight that's already prepared overnight, so it loads instantly
and never generates anything on open. When last night's sleep hasn't synced yet,
it says so plainly and fills in the sleep-dependent parts once the data arrives,
rather than showing a stale or empty score. This is the first piece of a single
daily view that later surfaces — a morning refresh, an optional daily note —
will all build on.
The dashboard gains tiles for readings that were synced and charted but never
had a place on the home strip: heart-rate variability, blood oxygen, breathing
rate, wrist temperature, and — for anyone with a body-composition scale — muscle
mass, body water, and bone mass.
Each tile appears only once you have that reading, so nothing new shows up on an
account that doesn't track it, and every tile can be toggled or reordered under
Settings → Dashboard like the rest. Tapping one opens its detail view. Nothing
changed in how the readings are stored or synced — this only surfaces what was
already there.
## [1.28.51] — 2026-07-16 — Chatting about a document lives in the Coach now
Asking the Coach about a document used to open a separate, half-finished chat
panel that never showed up in your Coach history. It now opens the real Coach:
the conversation appears in your Coach list like any other, marked with a small
document badge and a "Chatting about:
" note so it's clear which
document it's scoped to.
The safeguards that make document chat safe are unchanged. A conversation about
a document still runs in its fenced mode — grounded in that document's text
only, with no access to your health data or any tools — so text inside an
uploaded file can't reach anything it shouldn't. Only the surface moved into the
Coach; the boundary did not.
ECG recordings synced from a compatible watch were stored but never shown. They
now have a place in Insights: a list of your recordings, each opening the full
waveform on a familiar ECG grid, with the date, duration, average heart rate,
and the result your device recorded.
The result shown is the one your recording device produced — HealthLog does not
read or interpret the trace and does not provide a diagnosis. A recording is
single-lead and for personal awareness only; if a result concerns you, a note
alongside it points you to discuss the recording with a clinician.
## [1.28.49] — 2026-07-16 — Apple Health re-import no longer stops on a duplicate reading
Re-importing a cumulative export.zip could run for several minutes and then fail
outright on a unique-constraint error when two readings shared the same time,
type, and source but carried different internal ids — a single such reading
aborted the whole import. The importer now recognises that case: it updates the
existing reading in place (and brings back one that had been removed) instead of
failing, and if it still can't place a single reading it skips just that one and
carries on. A re-import completes cleanly.
## [1.28.48] — 2026-07-16 — Security follow-ups
Two security fixes:
- On a brand-new instance with single-sign-on configured, two people signing in
for the very first time at the same moment could both be made admin. The
first-admin selection now takes the same lock as password registration, so
exactly one admin is created no matter which sign-in path or how concurrent.
- A background nutrient-import error could echo a raw database message back to
the client; it now returns a fixed status and logs the detail server-side only.
## [1.28.47] — 2026-07-16 — Job queue engine update (pg-boss 12.26)
Updates the background-job engine (pg-boss) from 12.20 to 12.26, which adds
schema-drift detection, self-healing for background index builds, and a set of
correctness fixes. Some of those fixes surface errors that earlier versions
silently swallowed; the app already attaches an error listener to the queue and
does not use the direct calls affected by the change, so the upgrade is
transparent. No configuration change is needed.
## [1.28.46] — 2026-07-16 — Server performance on large instances
Performance work for accounts with a lot of history, none of it changing what
any number means:
- Insight generation computed its 30-day comparison averages by pulling every
matching row of the last 400 days into memory and averaging in code. It now
asks the database for the averages directly — the same figures, without
materialising the rows. (Sleep keeps its per-night reconstruction path.)
- The background scans that run at worker startup used an in-memory
de-duplication over full-table reads. They now de-duplicate in the database
and are backed by partial indexes (migration 0243, additive), so startup on a
large instance no longer does full scans.
- The coach chat re-rendered every message on every streamed token; now only
the message being streamed re-renders.
- The doctor-report mood read no longer pulls fields it doesn't use.
## [1.28.45] — 2026-07-16 — Doctor report + privacy fixes
- Sleep now actually appears in the doctor-report PDF. The sleep section was
enabled by default and its data was prepared, but the PDF's vitals table left
it out, so the section rendered empty — it now shows time asleep in hours.
- Glucose can now be included or withheld from a shared report like every other
clinical section. The toggle existed in settings but did nothing; it now
gates the glucose data end to end.
- Turning the environment module off now also keeps its data (audio exposure,
daylight, skin temperature) out of the coach, matching how the other modules
behave.
## [1.28.44] — 2026-07-16 — Loose ends: a broken link, the API contract, module guards
- The "connect a device" link on the sleep insights empty state pointed at a
settings page that doesn't exist and returned a 404; it now opens the
integrations settings.
- Six endpoints the iOS app already uses were missing from the published API
reference (`docs/api/openapi.yaml`); they are now documented, so the
reference is complete again.
- A few module pages didn't redirect away when their module was turned off,
the way the others do; they now behave consistently.
## [1.28.43] — 2026-07-16 — Visual consistency polish
A pass over small visual inconsistencies:
- The insights header now uses the standard heading weight and spacing
scale.
- Two dashboard tiles that hand-rolled a dimmer header now use the same tile
header as their neighbours, so the cards read as one family.
- A few tiles sat a step denser than their siblings; they now match.
- Two header icons that used the accent colour are back to the standard.
- Several edit dialogs on phone-reachable surfaces now open as the same
bottom sheet the rest of the app uses.
A new lint rule keeps card spacing on the standard scale so this drift can't
quietly return.
## [1.28.42] — 2026-07-16 — Snappier lists, correct mood refresh, safer first sign-up
Performance and correctness fixes:
- The shared date/number formatters rebuilt a fresh locale formatter on every
single call, which stalled the main thread across long history lists. They
now reuse cached formatters — the same output, without the per-row cost.
- Long measurement lists (a year of dense readings) rendered every row twice —
a desktop and a mobile copy, one hidden with CSS. They now render one layout
for the current viewport.
- Logging a mood entry now refreshes the dashboard immediately; the mood tile
and score could previously stay stale for up to two minutes after a change.
- On a brand-new instance, two people signing up at the exact same moment could
both be made admin. First-admin selection is now atomic, so exactly one does.
## [1.28.41] — 2026-07-16 — Complete the translations, and guard the gap
One reminder-adjacent label — the recovery-driver "functional impact"
track on the illness insights card — was showing its internal name instead
of a translation in every language; it is now translated in all six. The
class of bug behind it (a label resolved through a computed key, so a
missing translation ships silently, the same seam as the well-being labels
fixed a release ago) is now closed: a translation missing from one of these
labels can no longer reach a user.
## [1.28.40] — 2026-07-16 — Insight assessments lead with meaning, not a number
The per-metric insight assessments read like a data readout — they opened
on the current value. The overview texts already lead with the day's verdict
in plain words and land the warm, motivating tone; the per-metric cards were
told the opposite (name the level with a number first) and never received
the opener-variation the other insight surfaces use. Now every metric
assessment opens with what the reading MEANS and brings the number in right
after as support, in the same warm register as the overview. A shared
opening shape keeps the surfaces from drifting apart again, and the
deterministic first-paint fallbacks lead with the verdict too. No change to
what the numbers say — only how the sentence is built.
## [1.28.39] — 2026-07-16 — Sync data-loss hardening
Four fixes to sync paths that could lose or corrupt data in edge cases:
- The nightly pass that folds raw cumulative samples (steps, energy, distance)
into daily totals no longer re-counts a sample you had deleted — it was the
one such pass missing the deleted-row filter its siblings all carry.
- That same pass is now safe when two runs overlap (a deploy-timing overlap, or
a manual admin trigger racing the nightly job): an advisory lock plus an
in-transaction re-read make the fold idempotent, so an overlap can no longer
permanently double a day's total.
- A re-scored Withings sleep segment (its end time shifts) now updates the
existing row in place instead of colliding with it and wedging that night —
the same re-key rescue the other providers already carry.
- Withings measurement sync now holds its position when a reading fails to
write, instead of advancing past it and stranding the reading; the next sync
retries it.
## [1.28.38] — 2026-07-16 — Documents polish, waist reminders, bulk sharing
The document vault gets a round of overdue polish. The list cards drop a
stray glyph — the "read by AI" mark it carried moves into the document's
detail view — and give the real filename its own line so it stops being cut
off; the detail view's close button is aligned correctly (a shared sheet
primitive was reserving space for a button it wasn't showing). The action
that opens a document in the coach now reads "Coach fragen".
From a multi-selection in the overview you can now share several documents
at once — one link holding the selection, with the same documents-only
privacy as a single share (up to 50 per link).
Reminders can now be linked to waist circumference. And the well-being
reminder types (WHO-5, sleep-condition) that were showing their internal
names instead of a label are now translated in every language.
## [1.28.37] — 2026-07-15 — Provider step totals survive a restart
A boot-time maintenance pass that folds genuinely old raw step samples into
daily totals had too broad a reach: it also matched the daily step totals that
Google Health, Fitbit, and Withings write, and soft-deleted them on every
worker start. Recent days came back on the next sync, but older days stayed
gone until a full re-sync — which the next restart swept again. The pass now
skips any row that is already a daily total and only ever touches the ingest
paths that produced real legacy raw samples, so a provider's step history is
left alone. A one-time repair restores the totals removed by earlier restarts
(within the retention window; a single full sync recovers anything older) and
cleans up the placeholder totals the bug minted in their place.
## [1.28.36] — 2026-07-15 — Re-importing after a failed Apple Health import actually retries
The v1.28.33 rollup and staging fixes were correct but never reached the
reporter's case: the upload deduplicates by content hash, and the lookup
matched a previously FAILED job too — so re-uploading the same export.zip
returned the old failed job and replayed its stale error without ever running
the import again. The dedup now ignores failed jobs; the same file retries
cleanly. A redundant staged upload is also cleaned up on a dedup hit instead
of lingering in the staging directory.
Two related hardenings: the boot-time reconcile that marks interrupted imports
failed now performs a real liveness check (queue state plus a progress
heartbeat) instead of unconditionally failing every in-flight job — so a split
or multi-worker deployment no longer kills an import running in another worker.
And the "staging file not found" message now names the separate-container cause
explicitly, with a matching note in the scaling guide.
## [1.28.35] — 2026-07-14 — Timestamps honor the profile timezone
Client-rendered times previously fell back to a hardcoded display zone
(Europe/Berlin) no matter what the profile timezone said; a user in Manila
saw Berlin clock times on every card, chart, and table. Display now follows
the profile timezone everywhere, with a strict fallback to the legacy
display zone — never the browser zone, so screen, PDF, and export can no
longer disagree. An invalid stored timezone can never break rendering; it
falls back safely.
The same pass closed the remaining inconsistencies: the "Updated today"
cards no longer mix one zone for the day boundary and another for the clock;
calendar-date fields no longer shift a day for profiles west of UTC; the
dose-history day headings and weekday labels render in the display zone;
chart day/month labels stay correct for every zone (bucket math is
untouched); and the clinician share view renders in the patient's timezone
instead of the server container's. Day groupings, schedules, and aggregates
were already timezone-correct server-side and are byte-identical.
## [1.28.34] — 2026-07-14 — Nutrient intake lands as quiet context (server side)
Supplement-style intake written to Apple Health by nutrition apps —
vitamins, minerals, water, caffeine — can now sync to HealthLog as daily
totals. This is deliberately not a nutrition feature: no food diary, no
calorie tracking, no dashboard tile. The data lands in a dedicated store (26
nutrients with EFSA reference values), is visible on a read-only card under
Settings → Sources, and will feed the Coach as context in a later release.
The module is opt-in and off by default; with it off, the server refuses the
data entirely. Re-posting a day replaces it, units are guarded against
µg/mg mix-ups, and implausible values are skipped per entry. This release
is inert until the iOS companion app ships its reading side (coordination
ticket filed); the request format is in the OpenAPI spec.
## [1.28.33] — 2026-07-14 — Re-importing a cumulative Apple Health export works again
Re-uploading a fresh export.zip over existing data could fail with a duplicate-key
error in the rollup tier: a concurrent recompute (a live sync, the nightly drain)
racing the import's full fold could collide on the same aggregation bucket — and
the losing side silently dropped its freshly computed aggregate, leaving a stale
bucket. Rollup writes are now conflict-safe upserts with deterministic lock
ordering: a race can neither fail the import nor lose an aggregate, and buckets
left stale by earlier races self-heal on their next recompute.
The misleading error is gone too. A long import could outlive its queue timeout;
the redelivery re-opened the already-consumed staging file and overwrote the real
failure — or even a successful completion — with a raw ENOENT. Import jobs now
finish terminally (no redelivery after the staging file is consumed, six-hour
timeout), a delivery for an already-finished import is a no-op, and a genuinely
missing staging file reports an honest "upload the export again" message.
## [1.28.32] — 2026-07-12 — Fitbit: re-keyed re-imports can no longer be silently dropped
Single-fix release for the Fitbit integration, mirroring the Google Health
fix from v1.28.28. The measurements table carries a second unique index on
the natural key (type, time, source, sleep stage) that also covers
soft-deleted rows; when an import carries new external ids for rows that
were previously removed — the re-keyed re-import case — the insert collided
with the old row and was dropped without a trace. Planned inserts now get a
second probe by natural key: a match, live or removed, is updated in place
with the fresh value and the new id instead. If the probe itself fails, the
sync reports the failure and holds its watermark so the next run retries —
nothing is lost either way. No other code path is touched in this release.
## [1.28.31] — 2026-07-12 — Heart-rate history keeps its daily shape
Long-term retention for the dense intra-day signals (heart rate, HRV, blood
oxygen) now folds to hourly means instead of a single daily mean. The last
14 days keep every raw sample as before; older days keep up to 24 points —
enough to see how a day actually went, at roughly nine thousand rows per
metric per year instead of an unbounded raw stream.
Because the old fold soft-deleted rather than erased, a one-time rebuild
reconstructs the hourly shape for every already-folded day from the retained
raw samples, then retires the old daily row in the same transaction — no
reader can ever double-count a day. The rebuild runs automatically once per
installation after the update; days whose raw samples are no longer
available keep their daily mean. Daily min/max/mean statistics captured at
fold time stay untouched throughout.
## [1.28.30] — 2026-07-12 — The daily briefing stops silently skipping days
The recurring "no briefing today" had a chain of causes, now closed end to end.
Every failure path in the nightly generation names its cause in the logs
(several classes previously failed without a trace). A failed night retries
itself about forty-five minutes later instead of waiting for the next night.
A cached insight payload that carries no briefing no longer counts as fresh —
previously it could satisfy the daily window and even the unchanged-data check,
so a once-stripped briefing could block its own regeneration indefinitely. And
the briefing card now reports persistently why a briefing is absent (withheld
by the number check, generation failed, or not yet attempted) with a retry
button that always generates.
Documentation corrections, verified against the code: Withings credentials are
per-user in the app (the documented environment-variable path never existed —
and the webhook secret is now actually passed through in the compose file);
the classic Fitbit portal no longer accepts new registrations (new users:
Google Health); the backup-restore example no longer generates a fresh
encryption key (which could never decrypt an existing backup) and uses the
invocation that works in the production image; the certificate-pinning guide
now describes the real model (two CA-level pins, leaf renewals need no app
build); scaling and admin-endpoint references corrected.
## [1.28.29] — 2026-07-11 — Dashboard edits show up on the way back; one scrollbar again
Changing the dashboard tile selection now shows up immediately when you
navigate back — the third and final layer of this bug. Saving marked the
dashboard's cached data stale but, with the dashboard page unmounted,
nothing re-read it until the next poll or a window-focus flick. The save now
refreshes that cache directly, mounted or not.
The settings pages with sortable lists (dashboard layout, medication order,
modules, mood tags) showed a second page scrollbar: an invisible
screen-reader hint below each list escaped its container and silently
lengthened the document. All five editors are fixed, and these pages no
longer scroll past their end.
## [1.28.28] — 2026-07-11 — OpenAI-compatible gateways work; a re-keyed night can no longer vanish
The user-level "Local (OpenAI-compatible)" provider now speaks the standard
JSON wire: it sends `response_format` and, when an endpoint rejects it, falls
back once and remembers that endpoint's dialect — so LiteLLM, OpenRouter,
vLLM, LM Studio and plain Ollama all work from the same settings form. The
form and the provider docs now say so explicitly. Gateways that wrap a
Claude-family model behind a synthesized tool call are parsed correctly
instead of yielding silently empty insights.
The briefing token budget is configurable (`INSIGHTS_MAX_TOKENS`, default
raised so full briefings stop truncating), and a reply that was cut off
mid-JSON now says exactly that instead of a generic parse error. When the
number-grounding check withholds a briefing, the card now explains why
instead of pretending nothing was generated. The provider test button
distinguishes "the endpoint rejected the request" from "could not reach the
endpoint".
Sleep repair follow-up: re-syncing a history whose sleep rows predate the
stable segment keys could erase those nights — the old rows were swept while
their replacements collided with a second uniqueness rule and were silently
dropped. Re-imports now recognise such rows by their natural identity
(type, instant, stage) and migrate them in place: fresh value, new key,
restored if deleted. A full sync after updating heals any history affected.
## [1.28.27] — 2026-07-11 — Runs on CPUs without AVX2
Self-hosts on older x86-64 CPUs — Celeron/Atom-class NAS boxes, pre-2013
Xeons, and VMs that mask newer CPU flags — crashed in a restart loop since the
document renderer arrived: the bundled rasterizer uses AVX2 instructions, and
a CPU without them kills the whole process the first time a thumbnail or a
scanned-PDF render runs. The renderer is now gated on a one-time CPU-feature
check: on unsupported hardware, thumbnails and scanned-PDF rasterization
simply switch off (the vault shows type icons; PDFs are read as text) while
everything else runs normally. `NATIVE_CANVAS=off|on` overrides the detection
if ever needed. Reported by a self-hoster with kernel traps in hand — thanks,
that made it a same-day fix.
## [1.28.26] — 2026-07-11 — Internal restructuring for maintainability
No user-facing change. Eight of the largest source files were split along
their natural seams into focused modules — mood analytics calculators, the
insight status-invalidation machinery, feature extraction blocks, the doctor
report's types and helpers, the Google Health mapping layer, the coach chat
bubbles and read-aloud controls, the Telegram webhook handlers (the route
now holds only auth and dispatch), and the coach snapshot's cache, series
helpers and largest per-metric blocks. Every move is verbatim with stable
import paths, and behavior is unchanged.
## [1.28.25] — 2026-07-11 — Every integration held to the same standard
A platform-wide hardening pass: the failure classes found live this week were
hunted down across every integration, not just where they first surfaced.
Fitbit carried the same silent wedge fixed for Google Health — a soft-deleted
reading permanently blocked its own re-import, hidden behind a comment
describing a database index that never existed. Fixed the same way: a re-import
revives the deleted row in place.
Sleep segments now keep stable identities across re-scoring for Withings,
WHOOP, Polar and Oura — previously a source refining a night could duplicate or
orphan its stage rows, quietly inflating the night's total (the class fixed for
Google earlier this week). Each sync now also sweeps rows a re-score left
behind, so already-affected nights heal themselves as they are re-read.
One platform rule for deleted rows: a reading owned by a connected source comes
back when the source reports it again — across Withings, WHOOP, Oura, Polar,
Nightscout, the mood webhook, CSV re-imports and Apple Health day totals.
Deleting an Apple Health sample in the Health app still sticks, as the paired
client expects. Import counts and per-entry statuses now tell the truth in
every one of these paths.
Google Health lifecycle: an expired connection no longer lets the hourly sync
stamp success while importing nothing (it parks cleanly for reconnect instead
of retrying against a dead token forever); history backfills and the sleep
repair only mark themselves done after a clean pass, so a transient error now
retries instead of silently leaving a gap; and a failed database write holds
the sync watermark so the affected window is re-fetched.
Scale: the doctor report reads only the columns and sections it renders;
exports and backups read the measurement table in pages instead of one giant
query; long-window charts for sensor-dense metrics (glucose, pulse) aggregate
per day in the database beyond 90 days instead of shipping every raw sample;
blood-pressure pairing, achievement tallies and several "which types exist"
scans moved from in-memory loops to the database. A year of continuous sensor
data can no longer stall or crash a request.
## [1.28.24] — 2026-07-11 — A deleted Google reading no longer blocks its re-import
A Google Health reading that had been soft-deleted could never be imported
again: the sync treated the deleted row as absent and planned a fresh insert,
but the database's uniqueness rule still saw the deleted row and silently
dropped the insert — every sync, forever. A self-hoster's step days were stuck
exactly this way. The sync now recognises the deleted row and revives it in
place with the freshly fetched value: Google remains the source of truth for
its own readings, so a re-import deliberately brings a deleted one back. To
remove Google data permanently, disconnect the integration.
## [1.28.23] — 2026-07-11 — Sleep disagreement visible on the dashboard; Apple Health medication groundwork
When two sources disagree about last night's sleep — a sleep mat's time in bed
against a watch's time asleep — the dashboard sleep tile now carries a small
marker next to the value. The tooltip names each source and its total, so a
surprising number explains itself instead of silently picking a side. The same
marker backs the sleep panel; the shown total is unchanged either way.
Groundwork for importing medications and their taken doses from Apple Health
(iOS 26+): the server now accepts medications mirrored from an external source
and dose events carrying a stable external identity, imported idempotently —
re-syncs never duplicate. A mirrored medication is source-exclusive, so an
imported dose can never double-count against a manually logged one. The
matching client support arrives with a future app update.
Hardening: the doctor-report aggregation and PDF charts now compute their
ranges with loops instead of spread calls, so a report over a sample-dense year
(per-beat heart rate, sensor glucose) cannot overflow the call stack — the same
failure class fixed for the Google Health full sync.
## [1.28.22] — 2026-07-10 — Full sync survives dense histories; quieter document chrome
A full Google Health sync no longer fails on sample-dense accounts. Collecting
the rollup bookkeeping for a multi-year heart-rate history overflowed the call
stack, which aborted the metrics pass mid-cycle — exactly on the accounts with
the most data. Reported by a self-hoster whose sync log made the diagnosis
immediate; the new sync diagnostics from the previous release confirmed every
other read on that account healthy.
The documents vault gets quieter chrome: the "read by AI" tag no longer takes
its own line on each card — a small glyph rides inline on the date-and-size row
instead. The document view drops the status bar under the preview entirely;
when reading is automatic and nothing needs review, the content starts straight
with the document's fields.
## [1.28.21] — 2026-07-10 — Sleep history heals itself; plateau context; sync diagnostics
Three follow-ups to the recent sleep fix, plus better Google Health diagnostics.
Nights that were stored twice before the sleep fix now repair themselves: a
one-time background pass re-reads each connected account's Google Health sleep
history and collapses every re-scored night to its true total. No operator
action needed — it runs once after the update and marks itself done.
The sleep night read now reports when two sources disagree about the same night
(say, a sleep mat's ten hours in bed against a watch's seven and a half asleep).
The served total is unchanged — the disagreement rides along as an annotation so
clients can mark the number instead of silently picking a side.
A GLP-1 medication whose weight has plateaued on the current dose now says so
where the estimated drug level is shown — a short, factual note with the
observed change over the window, next to the curve in the efficacy view and in
Insights. It states the association and nothing more.
Google Health sync diagnostics: a daily-totals read that returns rows but
imports nothing now says so in the sync log, distinguishing an empty answer
from one the importer could not read — and the connection test's structure
probe reports the raw response field names when a type parses to zero, so a
per-account naming drift is visible from a single report.
The estimated active-substance curve for a GLP-1 medication — the modelled drug
level from your logged doses — now also appears in the efficacy view and in the
Insights medication overview, not only under the injection tab. It is the same
estimate shown there, surfaced next to the efficacy readouts so the drug level
and its effect sit side by side. Non-GLP-1 medications are unaffected.
## [1.28.19] — 2026-07-10 — Document and card polish
Small refinements across a few surfaces.
In the documents vault, each card now carries the date, size and an "read by AI"
marker directly under the document's title, instead of the meta line sitting off
under the thumbnail.
The document view gets a larger, easier close button, drops a redundant top-right
shortcut and the inline "read with AI" row under the preview, and gains a button
to ask about the document right next to share and download.
The mental-wellbeing cards no longer wash the whole tile on hover — only the
title is the tap target now, matching how the medication cards behave.
The medication API settings render inline instead of behind an expander that
never had anything else beside it.
Sleep imported from Google Health could read far too long — a night of about
seven and a half hours showing as ten. Google re-scores a night after the fact,
and the sync re-reads recent nights to pick that up. The re-read was landing as a
second, parallel copy of the night rather than replacing the first, so the
night-total added both together.
Each sleep segment now keys on a stable identity (the session's own id plus the
segment's start) instead of a value that shifted every time Google refined the
night, so a re-read overwrites in place. And before writing a re-read night, the
sync clears any superseded rows left in that night's window — so a re-scored
night reads its true total, and a mere re-classification of a block (light to
deep) updates in place instead of duplicating.
Nights already stored with duplicates heal as they are re-read; operators can
repair the full history at once with a one-shot re-sync.
Fixes two bugs and hardens two edges.
Dashboard layout changes now take effect immediately. Reordering or toggling
tiles and pressing save could appear to revert to the previous selection for a
few minutes: the saved layout was written correctly, but the home screen kept
serving a cached snapshot that the save never evicted. The save now clears that
snapshot, so the dashboard reflects the new layout on the next paint.
A share link created with a narrowed set of report sections now serves exactly
that set. Previously a link built with a reduced selection could fall back to the
full default set of sections when viewed — the opposite of what the owner chose.
The shared view now reads the frozen selection faithfully, so a section switched
off stays off for the recipient. Documents-only shares were never affected.
Hardening: the shared-document download path and the owner download path now use
identical filename sanitising, and thumbnail generation enforces its size ceiling
a second time after decoding, so a malformed image that slips past the up-front
check still cannot exhaust memory.
## [1.28.16] — 2026-07-10 — Documents-only shares stay documents-only
Hardens the "share a document, not the record" guarantee. A link created as
documents-only now carries an explicit frozen flag, and the shared view reads
that flag directly instead of inferring it from "are all report sections off?".
The old inference would have re-opened an existing documents-only link the day a
new report section was added — the link's frozen settings never mention the new
section, so it would have defaulted on. The flag closes that path: a
documents-only link can never begin serving health data later, whatever the
report grows to include. Existing documents-only links are migrated to the flag.
## [1.28.15] — 2026-07-10 — Document thumbnails; admin-shared AI access
The documents library now shows a thumbnail for each file — a small preview
rendered on the server, downscaled and re-encoded so location and camera
metadata are dropped, and served only to the file's owner over an authenticated
request. Files that cannot be previewed keep their type icon, and a file that
fails to render simply shows the icon rather than an error.
An administrator can connect one central AI access on the server and offer it to
the people who use it: each user decides in their own settings whether to use the
shared access or keep their own provider. It is set up in the admin area and
stays off until an admin turns it on, so anyone already using their own key is
unaffected.
Also trims two now-redundant pieces of the documents view: the caption noting
that search covers file contents, and the per-file searchable/read badges. The
same actions remain available without the extra chrome.
## [1.28.13] — 2026-07-10 — Sharing a document shares only the document
Fixes a scope bug: a link created from a document carried the full health record
(vital signs, labs, medications) because an empty report scope was read as "all
sections". A document share is now document-only — it serves the attached
document and no health data — while sharing from Settings keeps the full record
form. NOTE: revoke and re-create any document link made before this release; an
earlier link cannot be narrowed retroactively.
## [1.28.12] — 2026-07-09 — Lab scans use your AI provider; tidier dashboard
Scanning a lab report now uses the AI provider you already configured — including
a scanned PDF, which is rendered to images the same way documents are, so a
subscription or local vision provider reads it instead of only a Claude one. The
separate lab-scan provider setting is gone; there is one place to configure AI.
On the dashboard, the "log dose" action for an overdue medication now sits
directly under its line instead of far to the right.
## [1.28.11] — 2026-07-09 — Strava workouts + Garmin guidance
Connect Strava to bring your activities in as workouts. Like the other wearable
connections, you register your own Strava application and paste its keys, so no
shared account is involved. A run that also arrives through Apple Health is not
double-counted — the existing source-priority picker collapses the duplicate.
Strava is a workout source only (no sleep, recovery, or body metrics). Also adds
a Garmin page and an in-app note: Garmin has no direct connection, but its data
flows in through Apple Health or Google Health Connect.
## [1.28.10] — 2026-07-09 — Onboarding shows the AI value early
New accounts now meet the AI value up front instead of discovering it as a cold
"no provider connected" error. The finish screen shows a clearly-labelled sample
briefing — a static example, nothing is sent anywhere — and lays out the setup
choices local-first (a local model, your own API key, or a signed-in account),
with a plain reminder that the app is fully useful without AI. The getting-started
checklist gains a matching step that satisfies itself once a provider is
connected (or when the operator has configured a shared one).
## [1.28.9] — 2026-07-09 — Document chat in the coach dialog
Chatting about a document now opens the coach chat dialog, scoped to that
document ("Chatting about: …"), instead of a separate inline panel. The entry
is a neutral icon in the top-right of the document, matching the insights
surface. The document chat keeps its stricter footing unchanged — it reads only
the one document, carries no health-record context, uses no tools, and renders
as plain text.
## [1.28.8] — 2026-07-09 — Documents: automatic when set, cleaner when not
With automatic AI reading turned on, a document is read on upload — so the
per-document AI action buttons no longer appear (nothing to press). They return
when automatic reading is off, for manual per-document control. The AI-suggestion
review also drops its heading and grey sub-caption — just the fields to review
and apply.
## [1.28.7] — 2026-07-09 — Scanned-PDF reading actually works
The prior fix made the PDF image library loadable, but the page renderer was
still a bundled copy that failed on a real document with a bare error, so reading
a scanned PDF on a non-Anthropic provider still fell back to "PDF scanning needs
a Claude vision provider". The renderer now runs the real, un-bundled module —
the same one that renders these documents correctly in isolation — so scanned
PDFs are read on a subscription or local vision provider. Also drops a redundant
per-view note from the document summary panel.
## [1.28.6] — 2026-07-09 — Fix scanned-PDF reading in the container
Reading a scanned PDF with a non-Anthropic AI provider (a signed-in subscription
model or a local vision model) failed with "PDF scanning needs a Claude vision
provider" because the PDF page renderer could not load its image library inside
the standalone container image. The library is now resolvable where the renderer
looks for it, so a scanned PDF is rendered to images and read as intended.
## [1.28.5] — 2026-07-09 — Read documents with a subscription AI, cleaner surfaces
Documents can now be read by a signed-in (subscription) AI provider — the
capability check no longer wrongly treats those models as unable to see images,
so "read with AI" works on that path instead of erroring. Scanned PDFs and
photos are read the same way. Two now-redundant blurbs are gone from the document
panel, and on the recovery insights page each metric shows its explanation inside
its own card instead of repeating the heading above it.
## [1.28.4] — 2026-07-09 — Medication efficacy
A per-medication "effect" view: see whether a medication is moving the outcome
it targets, against your own data. The detail page gains a tab that plots the
target metric (or lab) with the start date, dose changes, and pauses marked, a
before/after-start comparison, and your adherence overlaid — strictly
descriptive, never a verdict or dose advice. A compact version surfaces on the
medication insights area. The target is resolved from the drug's class or its
name, and you can override which metric a medication is tracked against.
## [1.28.3] — 2026-07-09 — Consistent spacing and alignment
A consistency pass across the app. Insight cards now share one left edge —
headings and body text line up instead of drifting from card to card (worst
on mobile). Settings and admin pages no longer scroll past their content
into empty space. On mobile, Settings and Notifications live only in the
account menu instead of being duplicated in the overflow menu. The dashboard
greeting keeps its action beside the text rather than pushing it onto its
own line. Preventive-care, cycle, and onboarding cards adopt the shared card
anatomy. The spacing and alignment rules behind these are written down so
they hold going forward.
Also folds in the document PDF rasterization runtime: the image renderer now
loads in the container, so a scanned PDF is read on an image-only AI
provider (previously it quietly fell back to text-only).
## [1.28.2] — 2026-07-09 — Automatic AI document reading (opt-in)
One opt-in in AI settings: read every uploaded document automatically with your
configured AI provider — no per-document tap, the same way a lab photo is read.
Off by default; the vault stays local-first for everyone who does not turn it
on. When on, scanned PDFs and photos are read too — a PDF's pages are rendered
to images so a provider that only accepts images can still read them — and the
turn-on records a standing consent entry. The background indexer now follows the
same opt-in, so nothing leaves the machine automatically unless you enable it.
## [1.28.1] — 2026-07-09 — Document vault load fix
Fixes documents failing to load — old and newly uploaded alike — on
deployments where local content extraction is present. The local PDF text
reader imported its parser at module load, which pulled a browser-only global
into the server bundle and threw during evaluation, taking the document routes
down with it. The parser now loads lazily on first extraction; the document
list, upload, and content search work again, and local text extraction is
unchanged.
## [1.28.0] — 2026-07-09 — Document intelligence
A milestone that closes the document line end to end. The vault stopped
being a place to _store_ files and became a place to _understand_ them —
read, searched, shared, and answered — with the privacy posture held at
every step. The document capabilities shipped incrementally across
v1.27.17–v1.27.33; this release marks them, together with a full
correctness/resilience/quality pass, as one coherent whole.
### The document intelligence era
- **Vault** — every letter, report, and scan in one place, encrypted at
rest, byte-classified serving (inline vs download), opt-in.
- **AI reading + automatic search** — documents are read and indexed
automatically on upload; a local reader keeps the file on the machine, an
AI provider reads richer (including scans) with consent. Content search
matches whole words _inside_ documents over an encrypted blind token index
— nothing readable is ever stored.
- **Sharing** — hand a clinician a time-boxed, revocable link with a
mobile QR code; camera metadata (EXIF/GPS) stripped on the way out.
- **Chat about a document** — ask a document questions in plain language,
grounded and cited, with a security model that treats the document as
untrusted: fenced as data, no tools, no health-record context,
numeric-grounded, never a diagnosis.
- **AI provider governance** — for documents, a local-first /
no-training-API / subscription-last provider order, explicit consent for
any external egress, and a vendor-blind notice before a document leaves
the machine.
### Correctness, resilience and polish
The same line carried broad correctness work: DST-safe medication windows
and user-timezone compliance; graceful degradation (worker timeouts,
per-metric sync freshness, offline-write safety, per-source isolation,
honest "unreadable" markers); no more dashboard dead-ends and honest
error/empty states; accessibility contrast and semantics; and cold-start +
performance fixes.
## [1.27.33] — 2026-07-09 — Chat about a document
### Added
- Open a document and ask about it in plain language — a short, grounded conversation about that one document's text ("What does the Impression say?", "Which medications are listed?"). Answers cite where in the document each statement comes from and say plainly when the document doesn't contain the answer.
### Security
- The feature is built so a hostile document is harmless: the document text is fenced as data (never instructions — a document that says "ignore your instructions" is treated as content, not obeyed, and embedded fence markers are scrubbed); the chat has **no tools** (nothing an injected instruction could do); **no health snapshot** is ever sent (the only context is that one document — your health record can't leak, even if asked); fabricated numbers are stripped so only figures present in the document appear; and replies render as plain text (no markup a reply could contain is executed). It never diagnoses — it describes the document and defers clinical decisions.
- Available for indexed documents only, using your configured AI provider (local-first, keeping the document on your machine; any external provider requires explicit AI consent and shows the egress notice). Document chats are kept separate from the Coach and encrypted at rest. Migration 0230.
## [1.27.32] — 2026-07-09 — Quality-of-life, accessibility, cold-start and performance polish
### Changed
- **No more dead-ends:** the dashboard metric tiles (weight, blood pressure,
pulse, glucose, mood, sleep, steps, VO₂max) now link to their Insights
detail; the daily-briefing finding rows are tappable again; the "no
notification channels" prompt points to where channels actually live now;
and Coach Plans are reachable from the conversations header.
- **Honest states:** the Achievements, Coach-conversations, Notifications,
custom-metrics and Insights surfaces render a retryable error card on a
failed load instead of a confident empty state, and auth-gated pages show
a layout-reserving skeleton instead of a bare spinner.
- **Medication dialogs** move to the bottom-sheet pattern on phones
(consistent with the rest of the app), and secret/config inputs suppress
password-manager autofill.
### Accessibility
- Fixed several contrast shortfalls (muted text/icons below AA, phantom
colour utilities that emitted no rule), added an accessible name to the
admin toggle, and `aria-pressed` to the chart range switches.
### Fixed
- Documentation now matches what compose actually expects for
`DATABASE_URL`; a `SESSION_COOKIE_SECURE` transport mismatch is diagnosed
(was a silent login loop); a boot readiness summary prints per-secret
status before fail-closed loaders throw; and a nudge appears while public
registration is open.
- CSV import batches its writes; the off-host backup pages its large reads;
the theme provider closes a hydration seam; and hot per-row Intl
formatters are cached.
## [1.27.31] — 2026-07-09 — Document AI provider governance
### Security
- Document AI now routes through a document-scoped provider order — local extraction first, then no-training API providers (BYOK OpenAI/Anthropic), and a subscription/OAuth provider only last. Sending any document to an external provider requires explicit AI consent (a gap where the subscription path was previously ungated is closed), and a vendor-blind notice appears before a document leaves the machine. Coach and Insights provider behaviour is unchanged. (Follows a three-part investigation of the subscription/OAuth path: capable of reading documents, but the consumer-tier data policy allows training on content, so it is never the default for health documents — only an explicit, consented choice.)
## [1.27.30] — 2026-07-09 — Correctness and resilience sweep
### Fixed
- **Time zones / DST:** the "today" medication window is now DST-safe — a dose near midnight on a 23h/25h transition day no longer drops off the cards, dashboard, or PWA badge. Per-day medication compliance now buckets in the user's own time zone (was hardcoded to one zone), and the punctuality classification and the Insights greeting hour follow the user's zone too.
- **Resilience:** background job workers now run with a statement timeout and pool cap (a heavy nightly fold can no longer wedge the worker pool). Offline mutations fail fast and surface their state instead of hanging and silently losing the write. Each sync source isolates its per-collection/per-metric-type work, so one bad response can't blank a whole integration, and a per-metric-type freshness signal distinguishes a dead pipe from a healthy-idle one. The rollup read falls back to live SQL on any read error. Undecryptable rows now log a diagnostic and, where a clinician or user reads them, surface an honest "unreadable" marker rather than a silent blank.
## [1.27.29] — 2026-07-08 — Automatic document indexing and AI reading
### Added
- Every uploaded document is made searchable automatically — no "index" button. On upload, a background job reads the document (the upload finishes instantly and never fails because of indexing) and adds it to the private, encrypted search index. It picks the best reader: an AI provider transcribes the original (including scanned pages and photos) when one is configured and consented; otherwise HealthLog extracts a PDF's embedded text layer locally, in milliseconds, with no AI and nothing leaving the machine — so digitally-generated PDFs (lab reports, letters) become searchable even with no AI.
- The document detail sheet gains a prominent "Read with AI" block with a status pill — Read by AI, Searchable, Making searchable…, or Not searchable yet — plus the existing suggest-details / summarise / show-text actions, consolidated. With no provider it shows a calm pointer to AI settings, and any locally-indexed document stays searchable. The old manual per-document index buttons are gone (indexing is automatic).
### Fixed
- An upload whose background index job failed to enqueue (a transient database hiccup) could return a 500 even though the file was stored; enqueuing is now fire-and-forget and never fails the upload.
## [1.27.28] — 2026-07-08 — Share a document from the document
### Added
- A document now carries its own Share action: open a document and tap Share to create a clinician share link with that document already attached — the picker, expiry, EXIF note, and the one-time QR (with the passphrase in the link) all appear in the same sheet, so sharing no longer requires a detour through Settings. The Settings → Sharing flow is unchanged.
## [1.27.27] — 2026-07-08 — Reorderable hero rings
### Changed
- The dashboard hero rings now lead with the health score by default, and the order is yours to set: drag (or use the up/down arrows) to reorder the rings in Settings → Appearance, and the order persists across the mobile carousel and the desktop row. The health score is pinned as the anchor ring — it can be repositioned but not removed.
## [1.27.26] — 2026-07-08 — Hero and wellbeing card polish
### Changed
- The dashboard hero tightens the gap between the ring row and the briefing another step, and each ring is now a link to its detail — readiness / recovery / sleep open their Insights score view, the dose ring opens medications, the health-score ring opens the Insights overview. On a phone the carousel keeps swiping; a tap navigates.
- The mental-wellbeing surface is titled "Mental wellbeing" (was a longer check-in phrase) and its instrument cards follow the medication-card anatomy. The questionnaire source/licence note moves out from under the Start button into a discreet info control in the card header, so the Start button stands alone.
## [1.27.25] — 2026-07-08 — Documents filter dropdowns
### Changed
- The Documents page filters (type, condition, year) move from an inline chip row into compact dropdowns beside the search field, so the filter bar stays on a single line at every width — on a phone the dropdowns collapse to icons rather than wrapping or scrolling. Selection behaviour is unchanged (type is multi-select; condition and year pick one).
## [1.27.24] — 2026-07-07 — Share documents
### Added
- A share link can now carry a hand-picked set of your stored documents, so a clinician can open your reports and letters at the public `/c/` view without an account. Pick up to 50 documents when you create a link; the set is frozen at creation (revoke and re-share to change it).
- The share-create screen shows a prominent QR code that encodes the link and its passphrase together — a clinician scans it to open the shared record and its documents on their own device in one step (still shown only once, alongside the link and passphrase text).
- Recipients see the documents as a metadata list: images and PDFs preview inline, other formats download. Only the exact documents attached to that link are reachable, each fetch re-checks the link's live state, and revoking or expiring a link cuts off access to the documents immediately.
### Security
- Shared JPEG, PNG, and WebP images are stripped of embedded camera metadata (EXIF, XMP, GPS) on the way out; the stored original is untouched. PDF, TIFF, HEIC, and Office documents pass through as stored — a documented limit.
- The public document route is token-scoped to the link's frozen set, re-validates revocation and the passphrase before any decrypt, serves non-previewable types as opaque downloads, and carries its own strict framing policy.
## [1.27.23] — 2026-07-07 — Dashboard hero polish
### Changed
- On phones, the dashboard hero's wellness rings become a swipeable carousel — one ring shown at a time, centered, with dot indicators; swipe left/right through readiness, recovery, doses, and the health score. Calmer, and no vertical space spent on a cramped row. The desktop layout keeps every ring in the row.
- The desktop hero tightens the gap between the greeting/ring row and the briefing by one step.
## [1.27.22] — 2026-07-07 — Document assist and content search
### Added
- The document vault gains an optional AI layer that only runs with a configured AI provider (bring-your-own-key or local) and your consent — nothing runs automatically, nothing is interpreted, and nothing is saved without an explicit action.
- **Suggest details** proposes a filing title, type, and date for a stored document. Suggestions are drafts: the title seeds the editable field, type and date apply only on an explicit tap, and nothing is written until you act.
- **Summarise / show extracted text** returns a short plain-language description or the raw transcribed text, shown once for that view only — never saved into the coach, snapshots, records, or the search index, and never a diagnosis.
- **Content search** now matches whole words inside a document's body, not just its title and filename. The extracted text is stored encrypted at rest and turned into a blind, one-way keyed token index — no readable text and no readable word is ever stored; a search matches hashes. Whole-word only (substring matching stays on title and filename). Index one document at a time or run a bounded background backfill over the rest.
## [1.27.21] — 2026-07-07 — Hero spacing and one green
### Changed
- The dashboard hero's greeting anchors to the top edge instead of drifting to the vertical centre of the taller ring column, and the briefing follows closely instead of sitting a full step below — the wide gap under the greeting is gone.
- A "green" score ring now uses the same green as every positive signal and trend delta beside it (the band-fallback ring green landed on its own hue before). Green reads as one green across rings, signals, and deltas — the change is app-wide (insights rings included), with computed contrast 11.9:1 dark / 6.1:1 light.
## [1.27.20] — 2026-07-07 — Coach and cycle toggle from the modules hub
### Changed
- The Coach and cycle-tracking rows under Settings → Modules now carry a real on/off switch, like every other module, instead of only a "manage in …" link. The switch drives the same underlying setting the dedicated page does, and a "manage" link stays beside it for the settings those pages carry beyond on/off. When the operator has turned a module off server-wide, its switch shows as disabled with a note rather than a control that can't take effect.
## [1.27.19] — 2026-07-07 — Vault page polish
### Changed
- The Documents page now uses the standard content width like every other page instead of a wider container.
- The standing dashed drop-zone is gone: the header Upload button and page-wide drag-and-drop already cover uploading, so the zone no longer takes a band of space on every visit (the quota bar still appears above 80% usage).
- Search shares one row with the type tags on desktop instead of sitting on its own line above them.
- In a document's detail view, Delete moved to the bottom-left as a quiet text button — the bottom-right slot reads as the primary action, so a destructive control no longer sits there; Download keeps the trailing edge.
## [1.27.18] — 2026-07-07 — Late doses no longer jump the evening slot
### Fixed
- Recording a dose late — after its catch-up window has closed, when the
card has already advanced to the next scheduled time — no longer binds
the intake to that later slot. Previously a morning dose taken in the
early afternoon could be recorded against the evening slot, which then
made the evening dose look already taken, dropped it from the day, and
pushed the next reminder to tomorrow. A late intake now records on its
own, leaving both the missed and the upcoming slot intact, and the
taken-count reflects only real intakes. The same rule applies to the
bulk-intake path.
## [1.27.17] — 2026-07-07 — The document vault
### Added
- A document vault: an opt-in module (Settings → Modules) that stores the paper trail a household accumulates — doctor letters, discharge summaries, lab and imaging reports, prescriptions, referrals, insurance letters, vaccination records — encrypted at rest in one searchable timeline, with no folders to maintain.
- Upload by drag-and-drop anywhere on the page, paste from the clipboard, or the file picker; multiple files upload in parallel with per-file progress and instant optimistic entries. Files are classified by their actual bytes, not their extension: PDFs and photos (JPEG, PNG, WebP, GIF) preview inline; Office, text, CSV, RTF, TIFF, and HEIC files are stored verbatim and served only as an opaque download; executables, HTML/SVG, and archives are refused at upload. Nothing a user uploads can execute in the app's origin.
- Find a document in seconds: filter by type, by linked condition, or by year, or search titles and filenames — every filter lives in the URL, so a view is shareable and the back button restores it. Each document opens in a detail view with inline preview, editable title, type, and date, and links to the conditions it belongs to; the illness journal shows and links its documents both ways.
- Bulk selection (shift-click, long-press on touch) with bulk type assignment, condition linking, and delete; delete is undo-able with a 30-day grace before a nightly job reclaims the space. Full keyboard navigation and screen-reader labelling throughout.
- Operators tune two limits on the admin settings page: a per-file cap (default 25 MB, hard ceiling 100 MB) and a per-account quota (default 1 GB, with an optional per-user override). Documents live in Postgres, so a normal database backup includes them; the off-host JSON backup deliberately stays data-only.
## [1.27.16] — 2026-07-07 — Offline that survives, and a faster first paint
### Fixed
- The installable app now survives being offline. A network failure on the
auth probe was treated as a logged-out session — it wiped the offline
caches and redirected to the login page, defeating the entire offline
layer. Offline relaunches now render the dashboard from cache with the
offline banner; only a real 401/403 or an explicit logout clears anything.
- Chart text is readable in the light theme again — 23 chart surfaces drew
their ticks and axis text in colours measuring near-invisible contrast;
they now use the semantic text tokens (measured 13.2:1 in light, dark
byte-identical).
- On the mood insights page, correlation captions no longer escape their
tiles on phone widths. Row checkboxes gained a 32 px touch target at
unchanged visual size.
- The dashboard hero breathes: the score rings space at 24 px on desktop, on
phones the four rings spread evenly across the full width instead of
clustering in the centre, and the briefing section separates from the
greeting row at the same rhythm as the card padding.
### Changed
- Every page sheds roughly 100 KB of compressed JavaScript: the translation
catalog no longer ships twice (once per route bundle, once inlined into
the page document — the dashboard document alone shrinks from 139 KB to
25 KB compressed); catalogs now load once as a cached static asset. All
sixteen charts share a single chart runtime chunk instead of up to eight
copies.
- The dashboard's first paint carries real tile values straight from the
server (measured −25 % largest-contentful-paint), and a new bundle-size
budget gate in CI keeps route weights from regressing silently.
## [1.27.15] — 2026-07-07 — Complete Google Health coverage
### Fixed
- HRV, resting heart rate, SpO₂, respiratory rate, and VO₂max now import
from Google Health. All five daily-summary types filtered with a
field-name style the live service never matches — the requests returned
200 with zero rows while the data sat visibly in the Google app. The
reference documentation contradicts itself on the style; the sync now
sends the form the documentation's own worked example uses and falls back
to the alternative automatically, and the connection test reports which
one the service accepted.
- Respiratory rate additionally read a response field that does not exist,
and height parsed metres where the API sends millimetres — both silent
zero-imports since the integration launched.
- Workout recognition covers the full documented exercise catalogue (~35
additional activity types — trail runs, pool swims, rowing machines, and
the like no longer land as "other").
### Added
- Three more Google Health data types import into existing measurements:
blood glucose, core body temperature, and the nightly wrist temperature
Fitbit bands record during sleep.
- All 40 API data types are documented alongside the integration: every type
is now either imported or an explicitly reasoned skip.
## [1.27.14] — 2026-07-07 — Cross-device dose sync and phone-width polish
### Fixed
- Marking a dose on one device now wakes the others within seconds: every
intake mutation — logging, editing, undoing, bulk actions, imports —
dispatches a silent sync push to the user's other iPhones, so a
lock-screen Live Activity no longer keeps counting down after the dose was
taken on the web. Rapid mutations coalesce into one ping (a five-dose bulk
action sends one push, not five), the originating device is skipped, and
the payload carries no health data — previously it leaked the medication
id, and five of the seven mutation routes sent nothing at all.
- On phone widths, the dashboard's briefing-signal rows and the health-score
delta stack vertically instead of squeezing a wrapping headline beside its
value — a signal headline measured seven lines tall at a third of the
tile width before, and on narrow phones the delta spilled past the tile
edge.
## [1.27.13] — 2026-07-07 — Assessments that interpret
### Changed
- The per-metric AI assessments now interpret values instead of enumerating
them: where the current value sits on guideline reference bands (resting
heart rate, SpO₂, respiratory rate, body temperature, sleep duration,
waist measures, pulse-wave velocity, BMI, visceral-fat rating — each
derived from cited primary sources), what that band means in plain words,
and a trend judged by its position — a shift deep inside a healthy band
reads as a footnote, the same shift near a boundary leads the text.
Metrics without an established general reference band say so honestly and
interpret against the person's own baseline.
- Two rules now bind every AI text surface: measurement counts and logging
cadence are not insights (they may only appear when they carry a
consequence), and the tone standard is encouraging and dignified —
celebrates what is genuinely good, names what deserves attention, never
alarms or moralises.
- Band positions are computed server-side and handed to the model as context
— the model states them, it never computes them; no diagnosis language
anywhere.
## [1.27.12] — 2026-07-06 — Google Health daily totals
### Fixed
- Steps, distance, floors, and active energy now import from Google Health. The daily-totals request counted one calendar day too many per window (an exclusive end bound where the API expects the last covered day), which the API rejected outright; the request now matches the documented example exactly, with a self-reporting fallback to the tighter window size should a stricter limit apply.
- One data type's failure no longer blocks the remaining ones — each cumulative type fetches independently, failures are reported per type, and the sync watermark is withheld so the next run refetches what failed.
- Google API errors now carry the offending field names in the logs (redacted to field paths), so a future rejection explains itself; the connection test's structure probe also exercises a minimal daily-totals request.
## [1.27.11] — 2026-07-05 — Consistency, colour, and locale fixes
### Fixed
- An explicitly chosen display language now sticks on every device,
permanently. Two causes were fixed together: the server never consulted
the profile's language when the locale cookie was missing, and Safari
deletes script-written cookies after seven days — which silently flipped
the UI back to the system language every week. The cookie is now set
server-side and the profile choice wins whenever the cookie is gone.
- Every insights and dashboard card now shares one measured geometry: the
same text edge, the same header-to-body distance, the same foreground
colour for body prose. A measurement pass over all pages found and fixed a
dozen drifted surfaces, including the "usual range" strip (rebuilt on the
standard card) and the blood-pressure explainer.
- The dose ring on the dashboard paints in the medication hue used across
the insights instead of green.
### Changed
- The last raw theme-palette colour utilities are gone — every surface
reads from semantic tokens now. A small brand token covers the few places
that genuinely carry the brand accent, with computed AA contrast in both
themes.
- Card padding stays on the standard spacing scale, so per-card overrides
cannot drift back in.
## [1.27.10] — 2026-07-05 — Two new check-ins: wellbeing and sleep
### Added
- The WHO-5 wellbeing check-in joins the mental-wellbeing page: five positively worded questions about the last two weeks, scored 0–100 per the WHO's official scheme. A lower result gently points to the PHQ-9 check-in — a hint, never a diagnosis. The questionnaire uses the WHO's own official translations in all six app languages.
- The Sleep Condition Indicator (SCI) adds a sleep check-in: eight questions, aligned with how sleep problems are clinically described, shown with neutral wording. The validated English questionnaire is used; languages without a validated translation show the English items with an honest note.
- Both scores persist as measurements (server-computed, like the existing screening scores) and can be scheduled as preventive-care reminders that satisfy themselves when a check-in is completed.
### Changed
- The mental-wellbeing page now leads with the four instrument cards; the result history moved off the landing page. Each card opens a detail view with the score trend chart, the dated history, and the start button.
- Dependency refresh: Next.js 16.2.10, TanStack Query 5.101.2, nodemailer 9.0.3, react-hook-form 7.81.0, @types/node 26, and current Docker build-action pins.
## [1.27.9] — 2026-07-05 — Coach plans you can accept
### Added
- When the Coach works out a concrete if-then plan with you, it now appears as a card right in the conversation — accept it or decline it with one tap. Accepted plans become part of what the Coach remembers: follow-up conversations refer back to them, and a plan with a review date comes back for review on its own.
- A new plans page in the Coach area lists your proposals, active plans, and finished ones — mark a goal as met, end a plan, or clean up past ones.
## [1.27.8] — 2026-07-05 — Dashboard follow-ups and design-system sweeps
### Changed
- The hero's medication ring now shows today's doses — "1/3 taken" as a
filling ring — instead of a seven-day percentage. No doses scheduled
today means no ring.
- Ring choices under Settings → Dashboard apply immediately when toggled
and the selection can be reordered; the order carries over to the hero.
Ring colours match the Insights rings exactly.
- The "daily briefing" heading on the dashboard is itself the link to the
full briefing (no underline), the verdict sentence links to the metric it
talks about when it has no action button, and the separate "open Insights"
text link is gone.
- The light theme now tunes all remaining highlight colours to readable
contrast on light cards — admin pages, the Coach thread, and confidence
meters no longer render neon-on-white.
- Failed data loads show the same retry card everywhere; a dozen surfaces
previously showed a bare error line without a retry.
- Insights tiles share one header anatomy (icon and title in the standard
size and colour) — the sleep, glucose, cycle, and correlation tiles had
drifted into hand-rolled variants.
- Card paddings across ~20 surfaces now come from the card primitive alone,
removing stale per-card overrides from an older spacing era.
### Added
- The doctor report can include structured allergies and family history as
toggleable sections, and the Coach considers both in its picture of you.
- The PHQ-9 check-in regained its optional closing question ("how difficult
have these problems made daily life?") as a regular tenth question —
answering is optional.
- The settings pages no longer scroll past their end, and raw palette
colours can no longer come back through theme utilities, colour props, or
arbitrary values.
## [1.27.7] — 2026-07-05 — Score rings on the dashboard
### Added
- The dashboard hero can now show up to three score rings of your choice beside the health score — readiness, recovery, sleep, or medication adherence — picked under Settings → Dashboard. Rings only offer themselves when their module is on, and a ring without data simply stays away. The default shows medication adherence, which replaces the old "doses taken" text line.
### Changed
- Tapping a briefing signal on the dashboard now opens that metric's own page instead of the Insights overview.
- The hero's broad "Open Insights" button is gone; the health-score card already links there. Verdict buttons with a specific action (take dose, view blood pressure, …) stay.
## [1.27.6] — 2026-07-05 — A calmer mental-wellbeing check-in
### Changed
- The mental-wellbeing page is a judgment-free space now: the score history chart is gone from the page itself. Trends stay available through Insights and Measurements for those who want them.
- The PHQ-9 and GAD-7 cards follow the same card anatomy as medications and preventive care: title, when you last tested, your last result, the time since — and one start button.
- The check-in itself is just the questions: the question-overview strip at the top is gone, and after the last answer you land directly on your result — no summary step in between. The back button stays.
- The page intro now mentions a comfortable rhythm: both tests cover the last two weeks, so checking in every two to four weeks is plenty.
### Added
- Mental-wellbeing check-ins can be planned under preventive care like any other reminder, with a four-week default interval. Completing a check-in satisfies the reminder automatically, and its card button starts the check-in directly.
### Fixed
- Completed screening scores are now recorded as server-computed readings, so they cannot be confused with hand-entered values; existing score entries are re-attributed once during the update.
## [1.27.5] — 2026-07-05 — Insights polish and honest states
### Fixed
- The cycle wheel no longer counts into implausible territory when the last logged period lies far back. Beyond a grace window past your typical cycle length it pauses the day count and says plainly that the period is later than usual, with a shortcut to log it.
- Injection medications no longer show the overdue notice twice on one card; the line under the streak row is gone, the escalated next-intake line stays.
- The preventive-care measure button keeps one constant look; a due checkup no longer tints it green. Due-ness still reads from the next-due line.
- Editing a measurement now runs the same per-type plausibility bands as every other entry path, and readings owned by a connected device or the server can no longer have their value edited by hand. Signed metrics accept negative values on edit again.
- The daily briefing card opens straight on the day's signals: the recall/outlook paragraph above the list is gone, the section titles read in the regular text colour, and the card's Ask-the-Coach entry is removed.
- Assessments and the briefing overview are asked to write short paragraphs instead of one connected block — takes effect as texts regenerate.
- Dimmed low-contrast text is gone from informational lines (integration cards, notifications, sleep chart, module rows, and more); your own mood notes now read in the regular text colour and size.
- Sleep-phase and voice copy no longer promise features "coming in v1.x" — those texts now describe the actual state.
### Added
- The recovery score's detail view shows how the score is composed — the same ranked factor rows the readiness and sleep scores already have (shown for scores computed from your vitals; watch-native scores stay as delivered).
## [1.27.4] — 2026-07-05 — Google Health sync fixed against the live API
### Fixed
- The Google Health sync now actually imports data. Against the live service the v4 responses differ from what the connection was built against: payload fields arrive in a different naming form, large numbers arrive as text, and several data types only answer on other read methods. Steps, distance, floors, and active energy now read as true daily totals; sleep, workouts, blood oxygen, HRV, respiratory rate, and cardio fitness use the read method and data type the service accepts, so their requests no longer fail. Weight and height convert from the service's units correctly.
- When Google rejects a request, the sync log now carries the service's own error message (shortened, without credentials), so a failing setup can be diagnosed from the integration status instead of a bare status code.
### Added
- The Google Health connection test can return a structure probe: one sample request per data type, reduced to field names and value kinds — never actual readings. Useful when reporting sync issues.
## [1.27.3] — 2026-07-05 — Briefing resilience and Insights readability
### Fixed
- The daily briefing no longer disappears when the AI provider fails mid-generation. If the number-grounding recheck cannot run because the provider is unreachable, the previous day's briefing stays in place until the next successful run — a briefing that restates unverified numbers is still withheld, as before.
- Assessments show their full text right away; the three-line cut with the "show more" step is gone.
### Changed
- Insights read better: the assessment, the daily briefing, the page explainers, and the "usual range" note now use the regular text colour instead of grey — grey stays reserved for meta lines such as timestamps.
- The "usual range" note is a proper card now, with the same icon-and-title header as every other insights card.
- The page heading and explainer on each metric page line up with the card text below them instead of hanging to the left of it.
- The health-score card drops its "Ask the Coach" corner button.
## [1.27.2] — 2026-07-04 — Readable AI texts
### Changed
- The AI texts — Coach replies, the daily briefing, and the insight assessments — now read as structured prose instead of a single block: short paragraphs with real spacing, a proper list when a reply genuinely enumerates options or steps, and the one key takeaway may be set in bold. The rendering stays deliberately minimal: those two shapes are the only ones the app accepts, everything else remains plain text, and there is still no markdown engine anywhere in the tree.
## [1.27.1] — 2026-07-04 — iPad layout fixes
### Fixed
- On tablet-width screens — an iPad held upright — the app now starts with the compact icon sidebar, so the content keeps the width it needs instead of being squeezed into a narrow column next to the full navigation. Expanding the sidebar still works and the choice is remembered, as before.
- The measurements page no longer pushes the whole page into a horizontal scroll on tablet widths; the history table scrolls within its own frame instead.
## [1.27.0] — 2026-07-03 — Fitbit and Pixel Watch through Google Health
### Added
- A new Google Health connection reads your Fitbit, Pixel Watch, and Fitbit Air data through your Google account: heart rate and resting heart rate, heart-rate variability, blood-oxygen, respiratory rate, steps, distance, floors climbed, active energy, cardio fitness, sleep, and weight. It runs alongside the existing Fitbit connection and is the path forward as Google retires the older Fitbit Web API in September 2026 — connect through Google and your devices keep syncing. You register your own Google client once, and the setup guide walks through it step by step. Two things are stated plainly up front: stress and readiness scores are not offered by this interface, and Google may ask you to reconnect from time to time — the connection shows a clear prompt when it does.
## [1.26.1] — 2026-07-02 — Fitbit setup guide fix
### Fixed
- The Fitbit (Fitbit & Pixel) setup guide described the wrong OAuth app. HealthLog connects through the classic Fitbit Web API, so you register a **Fitbit developer app at dev.fitbit.com** and paste that Client ID/Secret — but the guide walked you through creating a **Google Cloud** OAuth client instead, which the Fitbit sign-in rejects with `unauthorized_client — Invalid client_id`. The guide now describes the correct dev.fitbit.com setup (app type, HTTPS callback, scopes), and flags that Google is retiring the classic Fitbit Web API in September 2026.
## [1.26.0] — 2026-07-02 — Light theme and a consistent interface
### Changed
- The light theme is calmer. Text now sits on a soft off-white instead of pure white, the near-black body colour is lifted to a dark tinted grey, cards separate from the background by a gentle step rather than a hard edge, and charts and status colours draw from the theme's own palette — so nothing glares and light mode reads as one harmonious whole. The dark theme is unchanged.
- Every page shares one header now: the same title size, the same one-line description beneath it, and the same back control. Admin sections, cards and list rows follow the same spacing everywhere, so the app feels of a piece as you move between modules.
- The remaining forms that still opened the device's own date picker (allergies, environment, documents) now use the in-app calendar, matching the rest of the app.
- Wording is consistent across the interface — one word per action and one term per concept throughout.
- Mental Wellbeing now shows a visible page title, like every other module.
### Fixed
- When a list can't load, it shows a clear "couldn't load — retry" card instead of looking like there is no data; lists also show a placeholder skeleton while loading rather than a blank spinner.
- The Coach's medication-adherence storyline now reports the same figure as the dashboard tile. An account that only logged doses it took could previously read as fully on track and hide a real decline.
- Charts over a custom historic date range now return the range you asked for; a range far in the past could come back showing the most recent stretch instead.
- Logging out now always clears the session on this device, even if the server hits a transient database error while doing so.
### Security
- Error reports sent to an operator's monitoring service no longer carry the webhook path secret. This only applies if outbound error reporting is enabled.
- The workout import endpoint no longer accepts a device-owned source from a client, so a synced-device label cannot be forged.
## [1.25.13] — 2026-07-01 — Briefing and medication fixes
### Fixed
- The daily briefing no longer vanishes when it mentions one of your own vitals. It could quote a blood-pressure average, a resting pulse, a weight, a mood or a sleep figure that the server had computed but the briefing's number check did not recognise; a single unrecognised figure dropped the entire briefing while the rest of the insight refreshed, so the briefing looked like it had stopped working. Every figure the server pre-computes for those signals is now recognised, and a genuinely invented number is still caught.
- A medication's dose now reads as a tag next to its category — `7,5 mg` beside `Other` — instead of a separate grey line under the name. Blood-pressure tablets and GLP-1 injections present their strength the same way.
- The custom on-time window switch in a medication's schedule can now be turned on. Turning it on was previously undone in the same step, so it snapped straight back off and no custom window could be set.
- The Appearance sub-pages no longer carry a "Back to appearance" link that pushed the layout down; each sub-page is a dead-end by design.
## [1.25.12] — 2026-06-30 — Follow-up fixes
### Fixed
- The left navigation no longer briefly flashes entries for modules you don't have enabled while a page loads.
- The daily briefing could time out before finishing on an account with a long history; it now has a longer window to generate. (If it still clips, raise the AI response timeout in Settings → AI and/or add a second AI provider so a slow one falls back.)
- Pre-existing and chronic conditions — the ones the Coach watches — now live in Medical history (Anamnese), alongside allergies and family history, instead of the personal-context panel. Where they are stored is unchanged.
### Changed
- Tapping a mental-wellbeing assessment card (PHQ-9, GAD-7) now opens that instrument's trend, the same way a medication card opens its detail.
## [1.25.11] — 2026-06-30 — Consistent settings and cards
### Added
- A date field now opens an in-app calendar that looks and behaves the same on every browser and phone, instead of handing off to the device's own date picker. The time field — and your 12-/24-hour preference — are unchanged.
- Appearance settings are now a hub: a list of the modules you can arrange (dashboard, insights, medications, and the rest). Pick one to open its own page with that module's view and ordering options, plus a link back to the list.
### Fixed
- Medication doses now show their unit in your language ("1 Stück", "2 tablets") instead of an English code, and the dose sits on its own line under the name instead of running into it — on the cards, the detail view and the dose lists.
- The integrations settings could intermittently render with non-interactive notification controls after a slow load; the notification card now renders identically on the server and on the first client paint, so its controls stay responsive.
- A short settings section no longer over-scrolls into an empty band below its last card — the content now fills the visible height exactly, and switching sections stays width-stable.
- The admin sign-in overview truncates a long IPv6 address instead of forcing the table to scroll sideways, and clicking an address copies it in full.
- The admin system snapshot now names the geo provider you actually configured, rather than always showing the default.
- Several insight charts no longer leave a lone tile stranded at half width, and a chart's rolling-average line no longer sits flush against the card edge.
### Changed
- Settings section headers, card layouts and add-form spacing are now drawn from shared building blocks, so they stay consistent across the app — the medication card's shape is the template every card follows.
- Configuration and secret fields — API keys, tokens, integration URLs — no longer prompt your password manager to fill or save them, while real sign-in and password fields still do.
- The custom-metrics add button carries an accessible label on mobile, and the custom-metrics section reads as its own block on the measurements page.
## [1.25.10] — 2026-06-30 — Mobile add-form fix
### Fixed
- On a phone, opening an add form (measurements, mood, medication, …) could push the layout wider than the screen, so the page scrolled sideways. The date-and-time field now stacks its date and time halves on narrow viewports instead of forcing them onto one row, so the form stays within the screen width. They still sit side-by-side from tablet width up.
## [1.25.9] — 2026-06-30 — Custom metrics
### Added
- **Custom metrics.** Define your own measurement — a name, a unit, optional target range, decimal places, and a short description — then log values against it and chart the trend over time, right alongside the built-in measurements. Useful for anything the standard catalog doesn't cover (a symptom score you track, a device reading, a habit count). Each definition and its values are private to your account; they are a deliberately separate, log-and-chart store, so they are not synced to Withings or Apple Health, not exported to FHIR, and not used by AI Insights.
### Fixed
- The settings pages could intermittently render with non-interactive controls (a disclosure that wouldn't open, an export button that did nothing) when the section navigation resolved a different set of entries on the server than on the client. The navigation now renders identically on both passes and applies its module-based filtering once after load, so every settings control stays responsive.
## [1.25.8] — 2026-06-29 — Carrier in the sign-in overview
A focused configuration release. No schema change.
### Changed
- The admin sign-in overview now resolves the network operator (carrier) from the online IP-location provider's ISP field, so it is populated even on a host without the optional offline ASN database. The carrier moves out of a chip stacked under the login method into its own column, next to the location.
- The location backfill now also revisits rows that already had a location but no carrier, so the column fills in across existing history on the next pass — not just for new sign-ins.
## [1.25.7] — 2026-06-29 — Settings, tidied
A settings information-architecture cleanup. No schema change; every page keeps a working address (old links 301-redirect).
### Changed
- **Appearance** is now the single home for how each area looks and is set up: the dashboard, insights, medications, mood, labs, illness journal, and preventive-care surfaces render inline as sections there (each with its full view/sort and management cards), instead of being scattered as their own left-hand entries. A disabled module's section simply doesn't appear. The per-area pages keep their addresses and redirect into the matching Appearance section.
- **Delivery channels** (Telegram, ntfy, web push, …) now live under **Integrations** alongside the other external services, instead of under Notifications. Notifications keeps the reminder types.
- **Share links** move into **Health record**, next to the export they belong with — the standalone Sharing entry is gone.
- **Account** no longer repeats active sessions and security activity; those live under Data & Privacy.
These changes only relocate existing surfaces — nothing was restyled, and every deep link keeps working.
A focused configuration release. No schema change.
### Added
- A self-hoster can now opt into a plain-HTTP IP-location provider by setting `IP_GEO_ALLOW_INSECURE=true` alongside an `http://` `IP_GEO_LOOKUP_URL`. This makes the free ip-api.com endpoint usable (its HTTPS form needs a paid key, but its geolocation is often more accurate, and it includes the carrier). The default stays HTTPS-only with ipwho.is — plain HTTP is refused unless the operator explicitly opts in, since the looked-up IP then travels unencrypted over the server's own egress.
## [1.25.5] — 2026-06-29 — Mental-wellbeing polish, location lookup
A focused polish release. No schema change.
### Changed
- **Mental wellbeing** reads cleaner: the screener trend now matches the rest of the app's charts (a single line with the same soft gradient fill and spacing as the dashboard, no surface-only background bands — the severity band still shows on hover); the page uses the full content width like the other tracking surfaces; and the redundant top heading, its tooltip, and the repeated self-test disclaimer are gone. The instrument card's last-test line now reads like a medication card: the label on the left, the day (today / yesterday / date) on the right.
- **Location lookup** defaults to ipwho.is again — free, no key, resolves a city/country for the login overview out of the box. The offline GeoLite2 databases stay strictly optional (used only when an online lookup misses). Operators whose network blocks ipwho.is can point `IP_GEO_LOOKUP_URL` at another provider without any code change.
## [1.25.4] — 2026-06-29 — Time picker honors the 24-hour preference
A focused fix release. No schema change.
### Fixed
- **The time picker now follows your 24-hour preference.** The earlier fix corrected the times shown across the app, but tapping a date/time field still opened the browser's own time picker — which renders its clock (and the AM/PM toggle) by the browser's language, not the app's. On an English-language browser you saw AM/PM even with 24-hour selected. The field now uses its own time picker, so hours/minutes always follow your preference (24-hour, 12-hour, or automatic) in every browser. The date half keeps the familiar native calendar.
- The doctor-report PDF's "generated at" timestamp now honors the time-format preference as well, instead of falling back to the locale default.
- When adding a blood-pressure reading, the systolic / diastolic / pulse field labels no longer wrap onto a second line in the three-column layout — the metric name stays prominent and its unit (mmHg / bpm) sits beside it as a compact affix.
## [1.25.3] — 2026-06-29 — Mental wellbeing, labs depth, and fixes
A patch release: a clearer mental-wellbeing check-in, more useful lab pages, two visible bug fixes, and a couple of settings tidy-ups. No schema change.
### Added
- Each biomarker now reads with a short explanation of what it measures and why it matters, in every language — not just a generic line.
### Changed
- **Mental wellbeing** is reworked: the PHQ-9 / GAD-7 check-in is now a calm step-by-step flow (one question at a time, with progress) instead of a single long form; the screen reads as cards with a clear title and a short explanation, the voluntary-self-test note stays on the overview rather than interrupting the test, and past results show as a proper trend with severity context. Crisis-support always surfaces for an at-risk answer.
- **Labs** biomarker pages: the redundant "adjust target range" control is gone (it already lives in Edit); the controls now read Delete · Edit · Show all values · Add value, with tooltips, and the "show all values" control matches the others.
- **Settings**: delivery channels now live inside Notifications (one place for "what you receive" and "where it lands"); the former "Dashboard" settings area is renamed **Appearance** and gathers links to the view settings of the dashboard, insights, medications, labs, illness journal, and checkups.
- The daily briefing now scales its generation budget to your configured AI response timeout, so raising the timeout for a slower model actually takes effect instead of being capped — and when a refresh genuinely fails, the message points at the right lever (raise the timeout / re-check the provider) instead of a generic error.
### Fixed
- **The 24-hour time preference is honored everywhere again** — several screens (sleep timeline, workout times, sign-in activity and sessions) were showing AM/PM even with 24-hour selected. Time now follows your preference across the app.
- The readiness / recovery insight tile no longer renders broken — the method note was overlapping the score and contributing factors; the card now reads top to bottom with the factors visible, on every screen size.
- The weekly and monthly review now use the same top-right "ask the Coach" control as the other insights, and the Coach conversations list spans the normal content width instead of a narrow column.
### Removed
- The Documents feature is temporarily switched off pending a deeper rebuild; the existing data and code are untouched.
## [1.25.2] — 2026-06-29 — Flat settings navigation
### Changed
- The settings navigation returns to the familiar flat list down the left, reversing the grouped layout introduced in 1.25.1. Two-factor and passkeys stay under Account; active sessions, trusted devices, and login activity return to Data & Privacy; account deletion returns to Advanced. Every page keeps its own address. The recent quality-of-life touches stay: active sessions and security activity remain collapsed by default, the personal-context page links to the medical-history form, and your chronic conditions show there.
## [1.25.1] — 2026-06-29 — Documents library, tidier settings
A patch release that turns the document inbox into a real library, reorganises the settings, closes a cross-account data-isolation gap on shared browsers, and carries a broad quality pass across the new surfaces. One additive migration (`0222`) applies automatically on start.
### Added
- A documents library: file any clinical document — a report, discharge letter, lab result, imaging, prescription, and more — without needing an AI provider. Give it a title, a category, and a date, then search, filter, sort, and browse it grouped by date. AI fact-extraction is now a separate, optional step you run on a document you have already filed, and you can view or download the original at any time.
- Richer reference descriptions for every biomarker — a few plain sentences on what each marker measures and what it relates to, in all six languages.
### Changed
- Settings are reorganised into nine clear groups — Account, Tracking, Display, Integrations, Notifications, AI & Coach, API & Access, Data & Privacy, and About — with in-page tabs. Far fewer entries down the left, and every page keeps its own address. Sign-in security — two-factor, passkeys, active sessions, and trusted devices — now lives together under Account.
- "Inbound documents" is now simply "Documents".
- Several repeated medical disclaimers were removed in favour of one clear, non-diagnostic statement, and the mental-wellbeing check-in now stands on its own page.
- The daily briefing can draw on grip strength, waist, and pain alongside your other signals, and the grip-strength and waist reference bands are age- and sex-aware.
### Fixed
- Signing out and back in as a different account on a shared browser no longer shows the previous account's data: the in-memory cache is now cleared at every sign-in and sign-out, alongside the offline caches.
- A once-weekly medication no longer triggers a false "low adherence" alert — the alert now uses the same cadence-aware calculation as the rest of the app.
- A lab value read from a filed document is no longer saved under the wrong unit: a unit mismatch is refused and reported rather than silently mis-recorded.
- Moving between insight pages now starts at the top; scroll resets on navigation throughout the app.
- A broad pass of accessibility, mobile-layout, date-format, and wording refinements across documents, settings, and the biomarker pages.
### Security
- Cross-account data isolation on a shared browser: no cached reading from one account can survive into another's session.
## [1.25.0] — 2026-06-28 — Clinical depth
A milestone release. It broadens tracking into clinical territory — validated mental-wellbeing self-assessments, grip strength, a pain score, waist measurements, a longevity lab panel, and respiratory rate as a first-class vital — adds structured allergy and family-history records, an optional weather-and-daylight context module, and the ability to file a doctor's letter and pull its facts into your record after you review them. It also makes the Coach's check-ins warmer, restores assistant-connector sign-in, and carries a broad correctness, performance, security, and accessibility pass. Additive migrations (`0208`–`0220`) apply automatically on start.
### Added
- Mental-wellbeing self-assessments (PHQ-9 and GAD-7) — opt-in, alongside your mood tracking and never a diagnosis. Item answers are encrypted; a non-zero self-harm response surfaces calm, locale-aware crisis-support contacts. Enable the Mental wellbeing module to use it.
- New signals you can log and chart like any other, each with its own detail page, reference range, and trend: grip strength, a 0–10 pain score, waist circumference and waist-to-height ratio, and respiratory rate.
- A longevity lab panel in the biomarker catalogue — ApoB, Lp(a), hs-CRP, HbA1c, fasting glucose and insulin, eGFR, GGT, ferritin, and the omega-3 index — with reference ranges.
- Structured medical-history records: allergies and family history, encrypted at rest, gathered under a new Medical history section and included in the health-record export. Immunizations are not part of this release.
- An optional environmental-context module: daily weather, daylight, and temperature for your location, correlated against mood, sleep, and vitals — off by default, with a home location, dated location periods, and backfill.
- Inbound documents: file a doctor's report or discharge letter and review the facts it contains before they enter your record. The app transcribes what is written and never interprets it; off by default.
- Biomarker detail pages now match the metric pages — description, summary, chart, trend, and a one-tap question to the Coach.
### Changed
- The Coach's proactive check-in is warmer and in your language: it greets you by name, keeps to one calm thought, never quotes your own words back, and never arrives two days running. A setting turns the daily suggestions off; another lets the Coach compose the check-in itself.
- Sleep debt now follows a configurable source order — a wearable's own figure or the computed one — and explains what the computed number means.
- One consistent assessment card, with a one-tap Coach question, across every insight page.
- Sortable history tables and real pagination where long lists used to truncate.
- A clear, non-diagnostic description of what HealthLog and the Coach are — and are not — and an explicit statement of where your data stays.
### Fixed
- Assistant connectors (such as Claude Code) can sign in again over the Model Context Protocol — an outbound-connection fix that also hardens every outbound request aimed at a host you configure.
- The daily briefing, health score, and weekly review keep their last good text instead of going blank when a model is slow.
- The clinician-share page renders again for newly created share links.
- Wearable daily figures, paused-medication adherence, and several long-window calculations are corrected.
### Security
- Free-text on the new records and on filed documents is encrypted at rest, and key rotation covers it.
- Mental-wellbeing answers stay out of the assistant connector, the Coach, and exports by default; their scores export only once you enable the module.
- A fresh second-factor check is required when changing the password.
## [1.24.0] — 2026-06-28 — Model Context Protocol server
A connector release. HealthLog can expose your own health record to MCP-compatible assistants (Claude, ChatGPT, and others) over a standard, OAuth-secured Model Context Protocol server — off by default, behind a module switch and a connector token you mint yourself. Two additive migrations (`0206`, `0207`) apply automatically on start.
### Added
- An MCP server — remote over `/mcp` and a local stdio command — that serves your own records to a connected assistant: metric series, glucose, sleep, workouts, medication compliance and schedule, labs (latest and history), correlations, baselines, level-shift detection, recovery and illness, cycle, integration status, and the preventive-care due-list. Every value carries its unit and reference range, with honest "no data" instead of a fabricated zero.
- A confirmed, scoped write surface: log a measurement, a blood-pressure pair, or a mood entry from an assistant — preview first, then confirm. Read-only by default; writing needs a token minted with the write scope.
- Installable prompt "skills": doctor-visit summary, weekly review, medication check, recovery / glucose / sleep review, and a lab-trend brief.
- Browseable resources and resource templates for metrics, labs, medications, and the doctor-visit report.
- An OAuth 2.1 connector flow (PKCE, audience binding, dynamic and URL-based client registration) so Claude.ai and ChatGPT can connect; mint read-only or read-and-write connector tokens under Settings → MCP. Enable and connect guides live in `docs/`.
### Security
- Off by default — the module gate plus a configured `APP_URL` are required, or the surface returns 404. A connector token is audience-bound to the MCP surface: it can never write or delete over the REST API and can never reach the admin surface. Writes are append-only, idempotent, range- and timestamp-bounded, and audited.
## [1.23.0] — 2026-06-27 — Account security and data sovereignty
A security release. It adds two-factor authentication, brings every sign-in and sensitive action under a stronger check, encrypts free-text health notes at rest, lets you download an encrypted copy of your record, and gathers export, deletion, and privacy controls into one place. Seven additive migrations (`0199`–`0205`) apply automatically on start; `0205` removes the retired bug-report tables.
### Added
- Two-factor authentication: an authenticator app (TOTP) or a hardware security key as a second factor, with one-time recovery codes — set up under Settings → Security.
- Sign-in asks for the second factor when it is enabled; you can trust a device for 30 days to skip it, while the password is always still required.
- Passkeys now show when they were last used and can be renamed, with a gentle prompt to add one.
- An active-session list with sign-out-everywhere, and an alert when a new device signs in.
- A security activity log of recent sign-ins, second-factor changes, and exports.
- A passphrase-encrypted export, so a downloaded copy of your record is never plain text — keep the passphrase safe, there is no recovery.
- A Data & Privacy page that gathers export, deletion, retention, encryption status, sessions, and activity in one place.
- A check that warns when a chosen password has appeared in a known breach.
- Admin controls to require a second factor and to review encryption coverage and key-rotation progress.
### Changed
- Free-text notes on measurements and mood entries are now encrypted at rest.
- Sensitive actions — account deletion, data reset, encrypted export, disabling the second factor, and key rotation — require a fresh second-factor check.
### Removed
- The bug-report integration has been removed.
### Security
- A second-factor session is bound to the credential it was issued for; an API token can never satisfy a fresh-factor check or reach the admin surface.
## [1.22.1] — 2026-06-27 — Mobile and Coach polish
A reliability and quality-of-life patch focused on the mobile interface and the Coach.
### Added
- Each lab biomarker shows a short description under its heading, the way Insights describes its metrics.
- Hover a Coach message for an action row: copy it, read it aloud, rate the reply, regenerate it, and see the time it was sent.
### Changed
- Read-aloud now selects a natural device voice instead of the default robotic one.
- The Coach prompt stays on one line and the conversation search field spans the full width.
- The settings and admin section navigation stays fixed while only the content scrolls, on both desktop and mobile.
- The admin "Module availability" section is now simply "Modules".
### Fixed
- The settings and admin side navigation no longer drifts while scrolling.
- Selecting a settings section on mobile no longer animates a scroll from the top of the page.
- The profile menu rows are evenly spaced, and the "More" menu labels are no longer clipped on narrow screens.
- Charts no longer crush the layout in landscape on small screens.
- Sleep insights no longer repeats the nightly average above its tiles.
## [1.22.0] — 2026-06-27 — A Coach that speaks, remembers, and charts; richer narratives across the app
A feature release. The Coach reads its replies aloud, can draw the metric it is discussing, remembers what you ask it to and resurfaces those reminders, and proposes checkups or measurement reminders you confirm inline. Insights and Coach assessments are rewritten for connected, motivating narratives instead of restated figures, and the daily read now folds in labs, preventive care, workouts, and glucose. Four additive migrations (`0195` hidden biomarkers, `0196` coach reminders, `0197` coach plan outcomes, `0198` AI settings); applied automatically on start — no operator action required.
### Added
- The Coach can read its replies aloud, and a reply can include a chart of the metric under discussion so the number has its trend alongside it.
- The Coach remembers things you ask it to keep and resurfaces a reminder when it comes due; a reminders list in settings shows what it is holding and lets you clear any of it.
- The Coach can propose a checkup or a measurement reminder, which you confirm inline before anything is saved.
- Hover a message to see the date and time it was sent.
- A configurable AI response timeout, and a separate optional provider dedicated to scanning lab documents.
- Steps now appear in Insights, and selecting a measurement type opens its insight.
### Changed
- Insight and Coach assessments are rewritten to connect signals into a motivating narrative rather than restate figures, with substantially expanded score summaries; generated text now renders in paragraphs and carries clearer "updated" timestamps — today with the time, yesterday, and a date for anything older.
- The daily read folds in labs, preventive care, workouts, and glucose, and correlations surface emerging signals over a recent window alongside more cross-signal links.
- The preventive-care area is now "Checkups", reached at `/checkups`; the German interface keeps "Vorsorge".
- Settings pages share one consistent layout, and the settings and admin sidebar stays pinned while the content scrolls.
- Lab biomarkers are a compact, editable list with hide and restore.
- Local AI servers stream their replies and honour the configurable timeout, so slower self-hosted models finish cleanly.
### Fixed
- The daily briefing and the insight trend no longer go blank when generation runs long.
- The blood-pressure "usual range" no longer shows a nonsensical band.
- A Coach notification now opens straight into the conversation it began.
- Insight headings drop the trailing info icon in favour of a short description.
- Lab values no longer link out to an article.
## [1.21.4] — 2026-06-27 — Coach surface refinements
A patch release. No migrations, no breaking changes.
### Changed
- The Coach settings link now lives in the composer's "+" menu, next to New chat and Conversations, and the redundant gear in the page toolbar is gone; the drawer keeps its own gear. The composer is the single control hub.
- Conversations open as a dedicated page with the search field at the top and the list grouped by recency — today, yesterday, this week, and earlier — instead of a slide-in panel. Both the "+" menu and the drawer's conversations control route there.
- The seeded "worth a look" opener spans the full width of the composer and carries a dismiss control that hides it for the rest of the day.
## [1.21.3] — 2026-06-27 — Coach memory and a standing quality harness
A patch release. One additive migration (`0194` coach plans); no breaking changes.
### Added
- The Coach can remember a goal or an if-then plan you confirm — "if my resting pulse is up in the morning, I'll keep the evening easy" — and carry it across conversations. It proposes the plan; it is saved only when you accept, and you stay in control of what it keeps.
- A standing quality harness runs in CI for the Coach: a graded set of reference cases plus an adversarial battery checks that every cited number traces to your own data, that the warmth never tips into over-validation, and that a red-flag symptom always escalates. An optional nightly model-judged pass is wired in and stays off until a key is configured.
### Fixed
- The dashboard and insights snapshot is cached per language, so switching language no longer briefly shows the previous language's briefing text.
## [1.21.2.1] — 2026-06-27 — Coach + briefing provider hotfix
A hotfix. No migrations, no breaking changes.
### Fixed
- The Coach and the daily briefing failed for accounts signed in through the ChatGPT provider: the provider rejected the multi-step tool-call request, and the failure surfaced as a server error instead of a graceful message. A provider failure now always degrades to a clear in-chat notice — with the correct reconnect or rate-limit prompt where those apply — rather than a 500, and the provider request follows the documented tool-call wire format so the Coach's multi-round reasoning and the briefing generate again. The provider's own error detail is now captured for diagnosis.
### Changed
- While the Coach is thinking, the bubble shows only the three animated dots — the "Thinking…" word is gone (it remains for screen readers).
## [1.21.2] — 2026-06-27 — Ambient Coach presence
A patch release. The Coach surfaces what it already computes — at the metric, on the score, in the briefing — instead of waiting to be asked. No migrations, no breaking changes.
### Added
- A "Coach read" on every metric page: one line placing today's reading against your own usual range, and — when the data supports it — one line naming the strongest connected signal in plain association language. While your range is still forming it says so rather than guessing.
- Opening the Coach from a metric or card now shows that it is already on that metric, with a tappable opening question; the unscoped Coach opens on the day's most notable signal instead of a blank box.
- The daily briefing recalls the prior period and points ahead.
- The health score surfaces an honest "internal read" when its contributors disagree — good sleep but a rising resting pulse — and notes when a metric has come back inside your usual range after a dip.
### Changed
- The Coach's motivational-interviewing repertoire gained develop-discrepancy and roll-with-resistance moves and a clearer anti-persuasion boundary.
- The prose number-check now runs on the local-model path too, so grounding holds regardless of provider.
## [1.21.1] — 2026-06-26 — Dialog footer reachability
A patch release. No migrations, no breaking changes.
### Fixed
- Pop-up forms now scroll only their body on desktop, keeping the action row in view. A tall form — or one made tall by browser zoom, display scaling, or a longer-language label — could push the primary button below the fold and force a scroll to reach it; the centred dialog now matches the bottom sheet, which already pinned its footer. Every form the app mounts through the shared sheet surface inherits the fix.
## [1.21.0] — 2026-06-26 — Date-format preference, a far more connected Coach, and broad correctness work
A feature release. The Coach reaches every data domain on demand, surfaces the cross-metric patterns the analytics tier already discovers, opens in context from any screen, and speaks with a warmer, forward-looking voice on one shared set of safety thresholds. Dates render in your chosen format everywhere. Two additive migrations (`0193` rollup x-rescale, `0192` date format); no breaking changes.
### Added
- A date-format preference in the profile — automatic (follows the language), day-month-year, month-day-year, or ISO — honoured across the app, including every date and date-time field, which now render in your chosen order regardless of the browser's locale.
- The Coach answers from the full data picture: every metric domain on demand plus cycle and workouts, and it surfaces the discovered cross-metric correlations — medication adherence against symptoms, short sleep against the next morning's vitals, and so on — citing only what the analytics tier actually found.
- Open the Coach in context from any metric page or insight card and it arrives already scoped to what you were looking at, with a relevant opening question.
- "Learn more" pointers on the vitals tiles, the glucose panel, the resilience tile, and the lab biomarker detail link out to the matching guide; the Coach references the same guides and can no longer offer a link that doesn't exist.
- Medication compliance and symptom severity are now first-class signals in the correlation engine, so an adherence dip that tracks a symptom flare can finally be surfaced.
### Changed
- The Coach connects signals into one story instead of listing metrics, looks ahead with gentle, ranged outlooks, ends an action turn by checking your confidence and offering a single doable step, and keeps affirmation earned. A closed acute-symptom clause points to prompt medical attention for crisis signs.
- Critical-threshold numbers — blood pressure, fever, glucose — now come from one source, so the dashboard banner, the Coach, the status cards, and the notifications always state the same thresholds.
- The trend regression is composed on an origin-rescaled, mean-centred basis, removing a floating-point cancellation on long windows; the rollup now matches the live computation to the last digit.
- The daily AI usage budget is provider-aware: usage on your own OpenAI key, ChatGPT plan, or local model is no longer limited by the server-cost ceiling that applies to an operator-provided key.
- The Coach builds its data snapshot once per turn and may take an extra reasoning round for a deep cross-metric question.
- Discovered correlations are held to an effect-size floor and a confidence tier, and sparse personal signals are shrunk toward the baseline, so a real-but-trivial association is no longer stated as a confident driver.
### Fixed
- ChatGPT/OpenAI sign-in users no longer hit a spurious "daily limit reached" after only a couple of messages.
- The symptom journal's recovery-return and the SpO2 red-flag count consecutive calendar days and are order-independent; the "two or more vitals out of band today" flag is keyed to your calendar day rather than UTC.
- Withings activity sync and the medication-intake dedup issue far fewer queries on a long backfill.
- The trend read returns the same boundary day whether served from the rollup or from a live query.
- The native date primitives that ignored the locale are gone — every field routes through the format-aware inputs.
## [1.20.2] — 2026-06-26 — Hardening across insights, safety flags, and integrations
A patch release. A broad pass over the server tier and the AI surfaces: a few correctness and safety fixes, a numerical hardening of the trend tier, and several smaller robustness and cost improvements. No schema changes.
### Added
- The lab-report review screen marks low-confidence rows so an uncertain reading is easy to spot before it is saved.
### Changed
- The trend regression (slope, r² and variability) is composed from mean-centered sums, which removes a floating-point cancellation on long, near-flat windows and keeps the rollup result in line with the live computation. Each accumulator now uses the exact stored sum rather than reconstructing it.
### Fixed
- The symptom journal's "sustained fever" flag — which can raise an urgent prompt to seek care — now counts only consecutive calendar days. A sparse series of isolated febrile entries no longer trips it, so the alert reflects a genuine multi-day run.
- Insight generation no longer reads a full measurement history per metric to compute its 30-day comparisons; it reads only the window those averages need. Generating insights on a long-history account is markedly faster, on both the page request and the overnight pre-generation.
- Photo and PDF scanning is no longer offered for text-only reasoning models that cannot read an image, so a scan attempt fails clearly up front instead of erroring at the provider.
- A notification channel whose stored configuration cannot be read now disables itself immediately with a precise reason, instead of retrying a permanent error several times and ending on a misleading message. This surfaces during an encryption-key rotation gap.
- The Coach cancels its in-flight model call when the browser tab disconnects mid-answer, rather than paying the full cost into a closed connection.
- Local-OCR text structuring reserves a budget proportionate to its real cost instead of the larger vision-scan ceiling, and refunds cleanly on a failed read.
- The admin diagnostic endpoints return a 422 for a malformed query string instead of a 500.
## [1.20.1] — 2026-06-23 — Dashboard render-loop fix
A patch release. One fix; no schema changes.
### Fixed
- A user reported the dashboard crashing on returning to a backgrounded tab. The on-focus snapshot refetch re-rendered the page, and each chart's "data ready" notify — keyed on a handler that the page recreated every render — re-fired on every commit, driving the chart row into an unbounded update loop until the browser tripped its render-depth guard and the page fell back to its error card. The notify now fires once, when a chart's initial query settles, regardless of how often the page re-renders.
## [1.20.0] — 2026-06-22 — Coach on-demand retrieval, deeper recovery insight, Fitbit, and a leaner data tier
A feature release. The Coach fetches what it needs on demand instead of carrying a full snapshot; the recovery-gap learns from the symptom journal and sleep; Fitbit connects over its own Web API; the trend tier serves slope and variability without a live scan; and several built surfaces become discoverable. Two additive migrations (`0190`, `0191`); no breaking changes.
### Added
- The Coach answers from on-demand retrieval: a small base context plus a data inventory, with tools it calls to pull the exact series, lab, sleep, medication or recovery figures a question needs. It cites only what it fetched and says so plainly when there is no data. First-turn cost drops substantially while the grounding and cross-metric reasoning are unchanged.
- The recovery-gap reads the illness journal's own symptom-severity curve and adds a sleep-context line, and it is now relapse-aware — it reports the final sustained return, not the first.
- Fitbit connects experimentally over the Fitbit Web API with PKCE, so a self-hoster can link it with an instant developer app.
- The sleep, steps and glucose dashboard tiles and the preventive-care row are surfaced by default (each self-hides without data), so built features are easier to find.
### Changed
- The AI client layer moves to a structured message format with per-provider prompt-caching, which lowers cost on repeated context and lays the groundwork for retrieval.
- Trend slope, r² and standard deviation are served from the rollup tier via stored regression accumulators instead of a live scan on every request; the result matches the live computation.
### Fixed
- The fever red-flag now reads days in chronological order, so an episode logged from both a thermometer and the journal can no longer raise or hide a sustained-fever flag incorrectly.
- A vital that settles and then shows a single stray out-of-range reading keeps its recovery day instead of reporting none.
- Fitbit sleep segments are placed on the correct day for users outside UTC.
- Concurrent syncs for Withings/WHOOP/Oura/Fitbit no longer park a connection at a spurious reconnect when a one-time-use refresh token is rotated.
- The Coach reply is no longer corrupted into raw JSON on some providers, and a tool request is no longer dropped on an unexpected stop reason.
- The trend tier records when slope is briefly unavailable pending backfill rather than reporting it silently.
### Refactor
- The Coach's per-request reads are shared across the retrieval fan-out instead of repeated per tool.
### Security
- Dependency bumps: next, pg-boss, radix-ui, pg, @testcontainers/postgresql, and the CI actions.
## [1.19.2] — 2026-06-21 — Recovery-gap symptom signal, resilience tile, long-range charts, richer device + Telegram data
A follow-up that folds the illness journal's own symptom curve into the recovery-gap, surfaces Oura resilience, fixes multi-year chart ranges, and widens the heart-rate and Telegram data paths. Two additive migrations (`0188`, `0189`); no breaking changes.
### Added
- The recovery-gap now reads the illness journal's own daily functional-impact curve (and linked symptom severity), not just passive vitals. When symptoms lead the recovery, the card names them — "you tend to feel better before your logged symptoms ease". Sparse journals withhold rather than guess.
- An Oura resilience tile on the recovery surface, showing the latest resilience band with a quiet above/in-line/below-average cue and a calm learning state until enough days are recorded.
- Hourly heart-rate uploads can carry a per-bucket low and high alongside the average, so a high-frequency client no longer loses the within-hour spread.
- A numeric reply to a Telegram measurement reminder is captured as a reading (for example a weight or a glucose value), bound to the linked chat and self-cleaning like the mood path. Blood pressure stays a simple "done" for now, since it needs two values.
### Changed
- A multi-year "All" chart range now renders the whole history, stepped up to a weekly or monthly tier, instead of being silently truncated to the most recent year.
### Fixed
- `telegram_chat_id` is now unique, with a one-time clean-up of any stale duplicate bindings (no account is removed).
- Migration `0187` is rerun-safe.
- The sleep peer-card comment no longer overstates window parity; each card discloses its own night count. Dead streaming state left after the reasoning-disclosure removal is gone.
## [1.19.1] — 2026-06-21 — Coach flow, sleep and mood polish, navigation, recovery-gap
A follow-up to 1.19.0 that refines the Coach conversation flow and cost, adds a sleep average card, completes the mood colour pass, tidies navigation, and sharpens the illness recovery-gap. No schema changes; no breaking changes.
### Added
- A sleep average-per-night card sits beside sleep debt and chronotype as a third peer, computed over the same scorable-night set so the three cannot disagree. It stays calm until enough nights are scored.
- The recovery-gap names the vital someone tends to recover ahead of — for example "you tend to feel better before your resting heart rate settles" — rather than a single bare line.
### Changed
- The Coach opens on a fresh chat by default. It resumes a past conversation only when the Coach has written something unread; an explicit `?c=` deep-link still opens that conversation. This refines the 1.19.0 behaviour, which always reopened the most recent thread.
- The chat view carries an always-visible toolbar with a clear way into past conversations and a new-chat action, and reaching conversations from the dashboard tile now always lands on the conversation list instead of an empty pane.
- The Coach sends the full health snapshot once per conversation (and again only if the conversation grows long enough to scroll the original context out of view); short follow-ups ride the existing transcript. A one-word reply no longer re-pays the whole snapshot, cutting per-message cost on follow-ups by roughly an order of magnitude while keeping the same grounding and cross-metric correlation.
- The streaming reply shows a typing indicator that gives way to the streamed text, replacing the reasoning-status disclosure.
- The settings navigation entry that opens the dashboard layout is labelled "Dashboard" to match where it leads.
- The main sidebar order groups the care surfaces together: medications, preventive care, lab values, illness, then insights, the Coach, and achievements.
### Fixed
- The mood time-of-day chart and the mood calendar now use full colour saturation in line with the other mood surfaces; only no-data cells stay muted.
- The cross-episode recovery-gap baseline counts only days that deviated in the illness-adverse direction, so a neutral reading (such as a weight drift) can no longer pad the sample or be named the recovery driver.
- The sleep average-per-night field is part of the documented sleep-rhythm response contract.
## [1.19.0] — 2026-06-21 — Coach context, a leaner AI pipeline, new device signals, interactive reminders, and wider self-hosting
A feature release. The Coach gains the lab values and a direct way in; the daily AI work is restructured to do less and reuse more; several new device signals are captured; reminders become interactive over Telegram; charts and the cycle and sleep surfaces are corrected and polished; and the project ships ready-made templates for one-click self-hosting. Three additive migrations (`0185`–`0187`); no breaking changes.
### Added
- The Coach can answer about lab results — the most recent value per biomarker (last twelve months) is provided as grounded context, with reference range, in/out-of-range status, and date. Qualitative results are included; the Coach quotes them verbatim and never invents a value.
- The Coach entry points open directly into the most recent conversation, the same thread across devices, rather than always starting blank. A `?c=` deep-link opens a specific conversation; the new-chat start remains.
- The daily briefing is surfaced on the dashboard as a short spotlight strip, lifting its key signals above the fold for a fresh briefing.
- Mood can be logged straight from a Telegram reminder — a 1–5 inline choice or a short written note — and the reminder chat self-cleans about thirty minutes later so old prompts do not linger or bias the next entry. Mood and measurement reminders also carry calm "remind me later" / "done" actions.
- Withings ECG records the device's atrial-fibrillation screening result and the full ECG waveform (stored encrypted at rest).
- Oura cardiovascular age and resilience are recorded.
- A configured multimodal model reads a scanned lab document directly; where the model is text-only, the on-device OCR path still applies.
- Ready-made self-hosting templates: an Unraid Community Applications template and a Portainer stack template, a hardened `docker-compose.yml`, and a NAS quick-start guide.
- A go-forward aggregated heart-rate upload contract (hourly buckets) so high-frequency clients stop accumulating one row per raw sample.
### Changed
- The AI pipeline does less, more coherently: the comprehensive briefing reads a bounded recent window (all-time figures stay accurate); per-domain status summaries generate once on the nightly pass instead of per metric through the day; a shared feature snapshot is computed once and reused; slow movers (weight, BMI) skip regeneration when their inputs are unchanged; illness and cycle state fold into the briefing context at no cost for users without them. Reproducibility, consent, and grounding checks are unchanged throughout.
- Medication remaining-supply is computed server-side and rendered from one canonical value, so the web and the iOS client always agree.
- High-frequency heart rate and blood oxygen are consolidated server-side into daily aggregates (mean with the daily minimum and maximum preserved; a resting-heart-rate value is derived where the source has none), matching the cumulative-metric consolidation already in place.
- Day-level bucketing for recent readings honours the user's timezone, so a late-evening reading lands on the correct local day.
- Sleep debt is a recovering rolling balance over recent nights instead of an ever-growing sum, so it reflects what is owed now and shrinks after a good night.
- The cycle view aligns its columns, uses consistent typography and equally sized toggles, and shows the prediction disclaimer once — clearly, at onboarding — rather than repeated across the feature.
- The sleep card's tiles share one consistent layout, and mood charts paint at full saturation.
- Sleep-night minute values are returned rounded; the notifications page opens on a single heading.
### Fixed
- Charts honour the 90-day and All ranges — they were stuck on roughly the last month regardless of the selected range.
- Medication remaining-supply can no longer display a nonsensical negative figure; the canonical readout is floored and a would-be underflow is recorded for diagnosis.
- Notification settings now label the measurement-reminder, medication-intake-sync, and coach-nudge events instead of showing raw keys.
### Performance
- Achievements and the dashboard summary serve from a stale-while-revalidate cache, the achievements builder precomputes day boundaries, three charts defer their library, and the analytics aggregate folds its canonical self-join into a single shared expression.
### Security
- The ECG waveform is encrypted at rest and covered by the key-rotation script, with a schema-driven test that fails the build if any encrypted column is ever left out of rotation.
- A Telegram interaction is strictly bound to the linked account before it can write, and the chat identifier is redacted from logs.
### Refactor
- Oversized modules are split into focused modules behind a barrel with no change in behaviour: the OpenAPI medication and insights registries, medication validations, the import panel, and the account settings section.
### Deferred
- Garmin: blocked by an external gate (the developer programme is invitation-only and not accepting new applicants), so there is nothing to build until access opens; only the official hosted instance could pursue it.
- Briefing map-reduce over the per-domain summaries, the cycle/sleep companion-line edge cases, multi-year "All" chart ranges, and the Oura resilience tile remain follow-ups.
## [1.18.10] — 2026-06-20 — data-loss fix, measurement consolidation, grounding, and the full-page Coach
A correctness-and-polish release. It closes a silent data-loss path and a stalled measurement-consolidation job, hardens AI output grounding, and acts on a broad round of UI feedback. Two additive migrations (a covering index and a labs setting); no breaking changes.
### Fixed
- Editing one field of a cycle or condition-journal day entry no longer wipes an encrypted note that cannot be decrypted with the current key — the stored value is preserved untouched unless that field is explicitly changed.
- The nightly consolidation of cumulative metrics (walking/running distance, flights climbed, active energy) no longer aborts on the first conflicting day, so raw per-sample history collapses to daily values as intended. A one-time compaction of the existing backlog runs after deploy.
- The daily briefing refreshes when regenerated, and the home screen reflects a saved tile layout without a manual reload.
- The Coach dictation button reports when speech input is unavailable instead of doing nothing.
### Added
- The Coach has a full-width page (no panel-within-a-panel), reachable conversation history, and a clean new-chat start.
- Scan a lab report without a vision model: optional on-device OCR (off by default) reads the image in the browser and sends only the extracted text to the configured AI provider, then the same mandatory per-row review applies. The labs "Add" action now offers "scan a document" or "add a value manually".
- Withings sleep now records heart rate, respiratory rate, blood oxygen, sleep HRV, and the sleep score (previously discarded at sync).
- Oura VO2max is recorded, and the VO2max tile is shown on the dashboard by default.
### Changed
- Preventive-care cards show the cadence on the metric row rather than beside the heading, and the heading opens the measurement history filtered to that metric.
- The danger zone and the daily-briefing card match the rest of the surface; the feedback tool is marked deprecated; the preventive-care and labs settings follow the standard section layout.
- Lab values: delete moved to the value's detail view, the overview tags are quieter, and the reference-range confidence is surfaced when a new biomarker is created.
- Location lookups default to the online resolver; the offline database is optional.
- Mobile: the full-page Coach composer stays clear of the bottom navigation, long navigation labels stay readable, and touch targets meet 44 px.
### Performance and integrity
- Blood-pressure in-target and grade read identically whether served warm or cold (one canonical source per day).
- A per-session statement timeout stops a single stuck query from holding a connection; a soft-delete-aware measurements index; the all-time fallback scan is bounded.
### Security
- Daily-briefing numbers are checked against the underlying signals; the Coach reply is screened for fabricated values or dose instructions; the period narrative is grounded before it is stored; the metric status cards state precomputed trends rather than re-deriving them.
### API
- The labs OCR capability response carries a `mode` (vision or text) and a new local-OCR preference; additional measurement types are recorded server-side. All additive and back-compatible.
## [1.18.9] — 2026-06-20 — Coach surface, lab-report scanning, and fresher dashboards
A broad UI and data release. It adds two migrations (both additive and nullable — qualitative lab results and Coach token columns) and three additive, back-compatible API changes (see the note below).
### Added
- Coach opens on a calm new-chat surface: a centred greeting and a single composer, then each answer streams in word by word with a soft fade, a collapsible thinking disclosure, and a quiet per-message footer showing the tokens used and the model. The quick-access drawer is unchanged.
- Scan a paper lab report. With an AI provider connected, upload a photo or PDF and the reader proposes the measurements and biomarkers it finds. Every row goes through a mandatory review — confirm, edit, or discard each one, with duplicate and reference-range hints — and only what you confirm is saved. The uploaded file is held in memory for the read alone and is never stored.
- Record a qualitative lab result — positive/negative or another text value — where a number does not apply.
- Sort lab values alphabetically, and delete a biomarker directly from the labs list and detail views.
- VO2max shows on the dashboard by default, and Oura's VO2max reading is now ingested alongside the existing sources.
### Changed
- The labs surface defaults to the list view, and its tiles share the medication-card layout. The labs and condition-journal settings now follow the same section pattern as the rest of Settings.
- Preventive-care cards read the last-completed date as "today"/"yesterday" and the next due date as a discreet "today" (green) or "overdue" (orange), and drop the small trend strip.
- Sleep-debt and chronotype sit side by side, and the daily-briefing card aligns with the surrounding type.
### Fixed
- The dashboard reflects new readings without a hard refresh: an added or background-synced measurement now refreshes the home snapshot — including on Withings and Apple Health sync, and on returning to the tab.
- The condition journal opens quickly again. The cross-episode correlation runs only when an entry's analysis is opened, and the typical-recovery-gap insight appears only when there is enough real data to support it.
- The latest-report area shows its age and, when no AI provider is connected, points to where to connect one, instead of presenting an old report as current.
- The Coach quick-access icon uses a softer tone.
### API
- The Coach stream's `done` frame now carries an optional token `usage`, and a Coach message can carry `tokensUsed` and `model`; an optional `reasoning` frame is reserved. New `GET/POST /api/labs/ocr/{capability,extract,commit}` endpoints; rows committed from a scan carry `source: "OCR"`. All additive and back-compatible.
## [1.18.8] — 2026-06-20 — coded condition and lab terminology in the health-record export
An additive export-only release: the FHIR R4 health record now carries standard terminology codes where it can assert them honestly. No migration, no API contract change.
### Changed
- Condition resources for journal entries carry a SNOMED CT category coding per illness type (infection, allergy, injury, mental health, autoimmune, chronic, other) alongside the user's free-text label. These stay broad categories, not diagnoses — the patient-reported, unconfirmed guard rails are unchanged.
- Lab Observations now emit a LOINC code and a canonical UCUM unit for the common biomarkers (HbA1c, lipid panel, ferritin, TSH, vitamin D, creatinine, eGFR, liver enzymes, CRP, fasting glucose, hemoglobin), resolved through an analyte alias table. An unrecognised analyte, or a unit that does not match the canonical UCUM symbol, keeps the honest text-only behaviour rather than asserting a code it cannot validate.
## [1.18.7] — 2026-06-19 — dashboard, Coach and insights polish
A broad UI, insights, and AI-efficiency release. It ships database migrations (see the migration note below) and two client-facing contract changes: Coach budget responses now arrive as a stream error rather than a 429, and the measurement-reminder push carries the reminder id.
### Added
- The daily briefing surfaces "signals of the day" — today's readings against your 7- and 30-day trend, emerging slopes and recent outliers — and leads with one concrete, present-focused nudge.
- Preventive-care cards show a quiet seven-day strip of the metric, and their menu jumps straight to the measurement history filtered to that type.
- The Coach reads your active illness state and Rest Mode, can point you to the relevant in-depth guide, and draws on a tiered view of your history (recent days in full, older periods progressively summarised with peaks preserved).
- Dictate to the Coach by voice.
- The share-link section is back as its own Settings entry, next to the health-record export: mint a time-boxed, revocable, read-only link to share your record with a clinician.
- Share links now carry a passphrase second factor. A new link mints a one-time passphrase embedded in a QR code (in the URL fragment, never sent to the server); the public view stays locked until the passphrase is verified. Links created before this release keep working without one.
- Labs, preventive care, and the condition journal each have their settings as a proper section of the Settings shell, in the same layout as every other section, instead of a separate page.
### Changed
- The daily briefing regenerates every day and reads warmer and more motivating.
- The Coach interface is rebuilt around a calm, centred reading column with a collapsible conversation list, a thinking indicator that hands off to streaming, and a quieter scrollbar.
- The health-score rings are flattened to match the chart style — the glow, sheen and pulse are gone.
- Sleep drops the redundant "last night" card; the chronotype reads as a prominent summary with your natural sleep midpoint and an expandable detail.
- Every integration card follows one layout — a short description ending in an inline setup-guide link, with the credential hint above the save button.
- A warm insight cycle now sends far fewer AI requests: the per-metric status assessments are batched into a single call, and the comprehensive path recovers from a malformed reply with one corrective retry instead of failing outright.
- The daily briefing now varies its wording day to day while the underlying findings stay stable, so a quiet day no longer reads as a frozen cache.
- Prompt construction is consolidated: grounding, tone, and the medication-safety rules come from one shared source across the briefing, status cards, narratives, and the Coach.
- Settings cards share one card primitive and a consistent padding step, and touch controls across the app meet a 44-pixel target on phones while staying compact on wider screens.
- The local-AI private-host switch accepts an explicit host allowlist instead of an all-or-nothing flag (the previous `true` still means "any private host").
- Coach conversations are pruned on a schedule, alongside the existing notification-attempt retention.
### Fixed
- The daily briefing no longer goes stale: a fresh daily signal forces regeneration instead of re-stamping an unchanged narrative.
- The sleep assessment no longer shows the "connect an AI provider" prompt when a provider is already connected.
- A medication's supply now updates on the next read: adding a container or registering a new medication evicts the cached list instead of serving the pre-change stock for the cache window, and the card and table refresh together.
- The seed for the public demo no longer produces an impossible blood-pressure reading, and reads as a current, healthy, fully-used account.
### Security
- The Coach budget gate reserves spend atomically before each request, so concurrent requests can no longer slip past the cap together, and tokens burned on an empty or refused reply are still counted.
- New share links require a passphrase to open the record; the passphrase is stored only as an HMAC hash, verified in constant time behind a rate limit, and unlocks a short-lived view cookie scoped to that one link.
- The session and authentication-challenge tables are indexed on their expiry column for the reaper.
- The development key-padding fallback fails closed unless the environment is explicitly development or test, so a missing `NODE_ENV` can no longer silently derive a weak key.
### Migration
- This release adds three migrations: expiry indexes on the session and challenge tables, a daily-briefing reroll marker, and the nullable share-link passphrase hash. Run `prisma migrate deploy` (the entrypoint does this automatically). All three are additive — no backfill, no downtime.
## [1.18.6.1] — 2026-06-18 — settings and Coach UI follow-ups
A small UI and settings patch on top of v1.18.6. No migration, no API contract change.
### Fixed
- The settings and admin left menu now aligns with the top of the first card, instead of sitting slightly offset.
- The Coach conversation scrolls within a bounded area with the composer pinned to the bottom, instead of overflowing the panel.
- The health-record settings section (PDF and FHIR export) is reliably reachable again — its entry no longer disappears from Settings.
### Changed
- The guided tour is first-run only. The per-page and Settings "show tour" triggers are removed.
- Preventive-care cards now mirror the medication card: the cadence sits as a chip beside the name, due and last dates read as text, and "last done" always shows — with an em-dash when there is nothing yet.
## [1.18.6] — 2026-06-18 — offline reads, grounded guidance, and one consistent app
A large release that makes the app genuinely useful offline, gives every metric cited reference context, grounds the AI Coach and Daily Briefing in that same context, adds responsible safety alerts for clinically urgent readings, and brings the newer modules (preventive care, illness, labs) and the whole settings area in line with the established patterns. A guided tour now explains what each module does and how they connect. No breaking changes; all schema changes are additive.
### Added
- **A module-by-module guided tour.** After onboarding, a short tour walks each module — what it does, one capability you might miss, and how it feeds the others — so the whole app makes sense on day one. Replayable any time from Settings, or per module from its page.
- **Cited reference ranges everywhere.** Metrics now carry plain-language explainers and the general reference range they sit in (blood pressure, resting heart rate, glucose, blood oxygen, temperature, and more), framed as general guidance and sourced from recognised guidelines — never a diagnosis. Blood pressure follows the ESH range. The blood-pressure detail now also shows pulse pressure and mean arterial pressure, derived from your readings.
- **A Coach that knows the ranges.** The AI Coach and Daily Briefing are now grounded in those cited ranges, so a comment about a value reflects where it sits in the general range — still general guidance, never a diagnosis, with no third-party attribution.
- **Proactive Coach check-ins you can actually see.** A proactive nudge now appears as a message in your Coach conversation (not only as a push), with a discreet unread dot on the Coach button that clears when you open it — so it works even without notifications set up.
- **Responsible safety alerts.** A confirmed, re-measured reading in a clinically urgent range (very high or low blood pressure, low or high glucose) now raises an alert that says to re-check or contact your doctor — never a diagnosis, and only for the modules you use.
- **A diabetes setting.** An explicit opt-in switches glucose targets to the tighter clinical goal range; it is never inferred from your readings.
- **Per-module settings for preventive care, illness and labs.** Each gets its own settings page to reorder items and switch between card and list view; labs additionally lets you manage biomarkers and choose the sort order.
- **Mark a reminder done.** A measurement reminder can be completed server-side (for the apps), instead of only being dismissed locally.
- **Real offline reads.** Opened without a connection, the installed app shows your last synced dashboard and recent data with a "showing your last synced data" note, rather than blank placeholders. Changes you make offline still save when you reconnect.
### Changed
- **Preventive-care cards match the medication cards.** A clear "measure now" action, the interval as a chip, and progress toward the next due date — with measurement-appropriate wording (no more "intake" for a blood-pressure check) and the same calm, neutral card.
- **One consistent "add" and a settings wrench** across preventive care, illness and labs; the illness journal's resolved entries are collapsed by default.
- **Every Settings and Admin page now has a clear heading and description**, in one shared frame, with the side menu lined up to the content — so the whole area reads as one piece. The AI section is now labelled "AI provider", and module availability has its own admin page.
- **No more AI dead-end.** When no AI provider is connected, onboarding and the Coach/Insights/Briefing screens explain what the feature does and how to set it up, instead of a bare error.
- **Clearer sleep, recovery and insights.** The estimated sleep-stage timeline is shown only when it reflects measured stages (with an honest note otherwise); the chronotype reads as labelled text; the sleep-quality assessment is more useful; the recovery page is de-cluttered; and insight cards link through to their detail pages, including steps.
- **A faster dashboard.** All charts load in one batched request instead of one per chart, with target ranges computed on the server.
- **Fewer disclaimers, one clear acknowledgment.** The repeated "not medical advice" banners are gone in favour of a single acknowledgment at onboarding (reachable any time in Settings).
- **Mobile polish.** Small tap targets were brought up to a comfortable size, and a global reduced-motion safeguard quiets animation when your device asks for it.
### Fixed
- **Reduced motion no longer hides content.** Charts and list rows that animate in now stay visible when the device requests reduced motion, instead of disappearing.
- **Read-after-write.** A reading or medication you just added now reliably appears in its list.
- **A timezone bug** that anchored the local day at UTC midnight for one intake path.
- **Contrast.** The preventive-care action button now meets AA contrast in dark mode.
### Security
- **The offline cache is hardened.** It stores only a safe, read-only allowlist (never anything tied to sign-in or sensitive AI conversations), is bound to the signed-in account, and is wiped when the session ends — not only on explicit sign-out.
- The container scan ignores a known `picomatch` advisory (CVE-2026-33671) that exists only in the npm CLI bundled inside the base image, not in the app's own dependencies (pinned to the fixed version).
### Operator note
- Additive migrations only (`0173`–`0177`: tour progress, diabetes opt-in, disclaimer acknowledgment, Coach last-seen, a Coach-message index). The optional `APNS_CRITICAL_ENTITLEMENT` env var (introduced in 1.18.4) gates native critical alerts; urgent alerts ship as time-sensitive without it. The service-worker cache version bumps with the release, so clients refresh their offline cache automatically on update.
## [1.18.5] — 2026-06-18 — filter by value, a tidier advanced panel, and a titration timeline for injectables
A small polish release. You can now filter your readings by value, the advanced medication settings collapse into sections instead of one long page, and injectable medications show their dose-escalation plan as a timeline. No breaking changes; no migration.
### Added
- **Filter readings by value.** Search now takes a numeric range — show only readings whose value is between two numbers (or just above / just below one), alongside the existing type, source and date filters. Useful for finding outliers or a specific episode.
- **A titration timeline for injectables.** Injectable medications now show their planned dose-escalation (e.g. 2.5 mg → 5 mg → 7.5 mg over the weeks) as a timeline with a "you are here" marker, on the injection tab. Built from the dose-change history already recorded — nothing new to enter.
### Changed
- **The advanced medication settings read more calmly.** The advanced and API panels group into collapsible sections (lifecycle expanded, data and danger areas collapsed by default) instead of one long wall — the settings themselves are unchanged.
## [1.18.4] — 2026-06-18 — urgent alerts that reach you on whatever you've set up, and a stronger free notification path
This release makes notifications work well for self-hosters who don't run Apple push. Genuinely urgent health signals — a sustained low-oxygen or fever pattern flagged by the condition journal — now go out at the strongest level each notification channel you've configured supports, and reach you even with no Apple Developer account: max-priority ntfy, high-urgency Web Push, a webhook, or a time-sensitive iOS alert if you do run push. The installed web app gains real reminder handling: a dose reminder clears itself once you mark it taken, and an app badge counts what's still due. No breaking changes; no migration.
### Added
- **Urgent alerts, on every channel you have.** A condition red-flag (sustained low SpO₂ or fever — "seek care", never reassurance) now sends an urgent alert that escalates per channel: time-sensitive on iOS (critical only if you've been granted and enabled Apple's entitlement), max priority on ntfy, high urgency on Web Push, flagged on the webhook. No Apple account required — an instance with only ntfy or Web Push still gets the urgent treatment. De-duplicated per condition.
- **A real reminder experience in the installed web app.** Dose, preventive-care and low-stock reminders reach the home-screen web app over Web Push; marking a dose taken clears its reminder (no leftover notification), and an app badge shows how many doses are still due today — the closest the no-Apple-account path gets to the native lock-screen behaviour.
### Changed
- **Notifications degrade gracefully without Apple push.** The channel cascade and every urgent path are now explicit that an instance without APNs still delivers through its other channels at their top tier.
### Docs
- A self-hosting notifications guide: what each free channel (Web Push, Telegram, ntfy, webhook, email) can and can't do, the recommended PWA + Web Push setup, and a step-by-step path for running your own signed iOS build with your own Apple Developer account and APNs key for native push and the lock-screen widget.
### Operator note
- UI/API-only; no migration. Optional new env var `APNS_CRITICAL_ENTITLEMENT` (default off) — set it only if your own iOS build carries Apple's Critical Alerts entitlement; otherwise urgent iOS alerts use the time-sensitive level, which needs no Apple approval.
## [1.18.3] — 2026-06-17 — the condition journal you can scroll back through, and a clearer view of what's tracked
A small follow-up that finishes a few rough edges from the previous releases. The condition journal's day-by-day timeline now scrolls through the whole illness, not just today; deleting a condition can be undone; the condition journal is on by default like every other area; and medication inventory that was never set now reads as "unknown" instead of a misleading zero. No breaking changes; no migration.
### Added
- **Restore a deleted condition.** Deleting a condition now offers an Undo — its day-by-day log and note come back exactly as they were, nothing is re-created from scratch.
- **The full condition timeline.** A condition's detail page now lists every day you logged, newest first, instead of only today.
### Changed
- **The condition journal is on by default.** It now behaves like every other optional area — present unless you switch it off under "What you track" — rather than starting hidden. Self-hosters can still make it unavailable instance-wide.
- **Unknown medication stock reads as "unknown".** A medication whose inventory was never set now shows "unknown" rather than 0, so it can't be counted down into a confusing negative. A genuine zero still shows as zero, and low-stock reminders ignore the unknown case.
### Operator note
- UI- and API-only; no schema change and no migration. The companion-app contract gains a date-less day-log list endpoint, a condition restore route, and nullable medication-inventory fields (`null` = unknown) — all additive.
## [1.18.2] — 2026-06-17 — preventive care, the same as your medications
This release brings preventive care into line with how medications already work, and clears up the condition journal. Each preventive-care item is now its own card in the same style as a medication, and marking one done behaves the way you'd expect: a check-up you plan for yourself is a simple "done", while a reminder tied to a measurement opens the actual entry form — completing a blood-pressure reminder records the reading rather than just ticking a box. Items take a first-due date and a custom interval, the page gained a settings control like the labs and condition pages, and preventive care can now sit on the dashboard as its own tile. The condition journal was reorganised so it reads at a glance. No breaking changes; no migration.
### Changed
- **Preventive care looks and works like your medications.** Every reminder is its own card with one clear action. A self-planned check-up is marked done in a tap; a reminder linked to a measurement opens the matching entry form, so finishing it captures the value and clears the reminder in one step. Reminders take a first-due date and a custom interval, can be edited from the card, and the page carries a settings control to manage them. Preventive care can be placed on the dashboard like any other area.
- **The condition journal reads at a glance.** Conditions are grouped into active and resolved, flares sit under the condition they belong to, each row carries a single clear action with the rest behind a menu, and a condition's daily timeline now lives on its own page. The retrospective summary sits up top where you'll see it.
### Operator note
- UI-only release — no schema change and no migration. Preventive care ships on the dashboard switched off by default; enable it from the dashboard layout settings.
## [1.18.1] — 2026-06-16 — labs that hold a catalog, a condition journal, and reminders that only nudge when you forget
A large release that turns two thin features into proper ones and adds two more, all in the existing visual language. Lab results gain a biomarker catalog: define a marker once with its unit and reference range, then log a value by picking it — with a full dashboard-style chart, edit and undo. A new condition journal records an illness from onset to recovery and, once it has enough of your own history, reflects back how it announced itself and how long recovery took — retrospective only, never a diagnosis. Preventive-care reminders now fire only when a measurement is actually overdue and clear themselves the moment a reading arrives from any source, and the coach can suggest an evidence-based measurement cadence you accept with one tap — both feeding one shared reminder engine. The settings menu, the dashboard header, the recovery page and the coach view were tightened so every module reads as one app. Medications can now be switched off like any other module; the core vitals — weight, blood pressure, pulse — stay always on. No breaking changes.
### Added
- **Lab biomarkers with a reference range you set once.** Define a biomarker — name, unit, lower and upper bound, an optional note — from a suggested common panel or your own, then record a value by picking the marker instead of retyping its range every time. Each biomarker gets a proper chart with its target band, a full reading history, and edit, correct and undo. Every reading links to a catalog marker, so none is a dead end.
- **A condition journal.** Track any illness or condition — acute, chronic, recurring, or a flare hanging off an earlier one — with a daily symptom-and-severity log and an encrypted note. Once there is enough of your own data, a retrospective view shows how often you have been unwell and your typical recovery gap (when your body returned to baseline versus when you felt better), computed from your own baseline and withheld until it is sure. It never predicts or diagnoses, and a sustained red-flag pattern points you to care rather than reassuring you. The area ships switched off; turn it on under "What you track".
- **Rest Mode.** While a condition is active, your scores, recovery and streaks are annotated rather than penalised and cadence nudges pause — an active illness never changes a measured number, only the narrative around it. A calm banner explains it on the dashboard and recovery view, and condition episodes carry into the doctor-report PDF and the FHIR export as patient-reported conditions.
- **Coach measurement-cadence suggestions.** When a real change warrants it, the coach can suggest an evidence-based cadence — daily weight, morning-and-evening blood pressure for a week, structured glucose — that you accept with one tap to create a reminder. It stays non-naggy: capped, cooled-down, dismissible, and willing to say you already measure enough.
### Changed
- **Preventive-care reminders only nudge when you forget.** A reminder now clears itself the moment a matching measurement arrives — entered by hand or synced from a device — and reschedules its next due date, so the self-disciplined are left alone and the reminder is a safety net for gaps. Coach suggestions and preventive-care reminders run on one shared engine; reminders can be edited, carry a quiet "Coach" badge and course-window end date, and the linkable measurement set now covers the full range of types.
- **Every module reads as one app.** Labs, the condition journal and preventive care now use the same add-entry sheet, overflow menus and confirm-before-delete guards as medications. The settings sections drop their leading blurbs and align consistently, integrations split into Connections, Channels and Sources as their own entries, the coach setting reads as "activate the coach" (on by default), and the "About me" health context moved into the account profile.
- **A calmer dashboard and a consistent recovery page.** The daily-overview header now sits flat alongside the other tiles instead of as a glowing hero. The recovery page follows the same shape as every other metric page — heading, a short explanation, history with high, median and mean, a chart with a 7- and 30-point view, a target band and an assessment — and the duplicate score block was removed. The coach view was rebalanced for symmetry and space.
- **Medications is now an optional module.** It can be switched off like cycle, sleep or labs; weight, blood pressure and pulse remain the always-on core.
- **A richer cycle log.** The day sheet gains a phase-context header, inline field explanations, and a fertility section that opens itself when your goal calls for it.
- **Admin tidy-up.** The coach feedback, insight-quality and assistant-surface panels consolidate into one coach area, the backups panel leads with its counts, and the danger zone now requires typing a confirmation like the less-destructive restore already did.
### Fixed
- The illness recovery-gap engine no longer reports a return before a vital deviated, escalates a red-flag for rock-steady oxygen or temperature, reads journalled fever, and keys days by your own time zone rather than UTC.
- Lab readings logged without a catalog marker are linked on write instead of stranded; a failed note-load no longer wipes the note on edit; and a partial reference-range edit can no longer save an inverted range.
- The morning-and-evening blood-pressure protocol now schedules both times instead of one. Reminder satisfaction is race-safe across the cron and event paths.
### Security
- The encryption-key rotation script now covers every encrypted column in the schema — including the new lab, biomarker and condition notes and a tail of previously-missed integration and profile fields — driven from a single registry that a test asserts is complete, so a future encrypted column cannot silently miss rotation. The admin data-wipe is gated behind a typed confirmation and a per-user rate limit.
- Pin `hono` to `≥ 4.12.25` through an override, clearing the CORS-middleware advisory (GHSA-88fw-hqm2-52qc). The package is pulled in only by Prisma's and the shadcn CLI's tooling; the app is Next.js and sets no CORS headers anywhere, so the issue was never reachable at runtime.
- Pin `picomatch` to `≥ 4.0.4` through an override, clearing the ReDoS advisory (CVE-2026-33671). The package is a transitive build dependency.
### Operator note
- Additive migrations only (`0169`–`0172`): reminder course-window fields, the biomarker catalog and lab link, the condition-journal tables, and a partial unique index for coach-minted reminders. No backfill and no deploy-time action required. The condition journal ships disabled by default. **If you rotate `ENCRYPTION_KEYS`, re-run `scripts/rotate-encryption-key.ts` after upgrading** — it now re-encrypts several columns it previously skipped; do not drop a legacy key until it reports zero remaining rows.
## [1.18.0] — 2026-06-16 — turn off what you don't track, and a settings menu that finally reads as one app
This release makes the app yours to shape. Every optional area — cycle, mood, sleep, glucose, workouts, recovery, labs, achievements, the coach, AI insights and the doctor-report — can be turned off, and when it is, it disappears everywhere at once: out of the navigation, off the dashboard, gone from insights, the coach, your reminders, achievements and the exported report. Your data is kept; turn the area back on and it returns. The settings menu was redesigned alongside it, so notification channels, integrations, sources and per-area pages each sit where you'd look for them instead of spread across overlapping hubs. The core vitals — weight, blood pressure, pulse and medications — are always on. No breaking changes.
### Added
- **Turn modules on and off.** A "What you track" settings hub lets you switch off any area you don't use. The switch is honoured by one server-side gate, so a disabled area is hidden from the navigation and the dashboard and is also refused at its API — the surface and the route vanish together, with no window where one leaks without the other. Re-enabling restores everything; nothing is deleted.
- **Operator-level module availability.** A self-hoster can make an area unavailable for the whole instance from the admin settings; an operator "off" wins over any personal preference. Both layers project onto the same module map the web app and the companion app already read, so neither client needs special handling.
### Changed
- **The settings menu reads as one app.** Notification channels moved under Integrations alongside the connection and source panels; the duplicated "Reminders" hub collapsed into Notifications; mood, medications and the health-record export each became a clear top-level entry; and every section now carries one consistent heading. Old deep links (`/settings/reminders`, `/settings/sources`, the standalone coach page) redirect to their new homes.
- **One add-entry pattern across every feature page.** The cycle, labs, mood, medication and preventive-care pages now share the same header and primary "add" affordance, so a page built later doesn't read differently from one built earlier.
### Operator note
- Migrations 0167 (per-user module preferences) and 0168 (operator-level module availability) are additive — a single nullable JSON column each, no backfill. Both default to "all on": an area is disabled only by an explicit choice. No deploy-time action is required.
## [1.17.1] — 2026-06-15 — preventive care, lab results, and more of the data you already have
This release closes the loop from tracking to acting. Preventive-care reminders tell you when to measure or check what, structured lab results give your bloodwork a home, and a new recovery view surfaces signals that were already being collected but never shown. Sleep timelines now read in real clock times, marking a dose on one device clears its reminder on the others within seconds, and self-hosters gain a generic webhook channel, email, one-tap Web Push setup and a proper notifications guide. Every new number is computed once on the server and read the same way on the dashboard, the coach, the doctor-report and the companion app. No breaking changes.
### Added
- **Preventive-care reminders.** Set a reminder to measure your blood pressure on a cadence or to schedule an annual blood panel, with a clear next-due date and where to do it. A matching measurement marks it done on its own; the rest you tick off. Reminders reach you over every notification channel, and the cards stay calm — status is a quiet badge, never an alarming colour.
- **Structured lab results.** Record bloodwork and biomarkers with their reference range, see each one trend over time, and carry them into the doctor-report PDF and the FHIR export. An out-of-range value is shown plainly, not in red.
- **A recovery view, and sleep quality in depth.** A new recovery page gathers strain, training load and autonomic-charge readings, and the sleep page gains an efficiency, performance and sleep-score block — metrics a connected device was already sending that nothing surfaced before. Each appears only once it has data and stays calm until it has enough to be sure.
- **Import and backdated entry.** Import a CSV of past measurements with a previewed, per-row result and unit conversion, and log an entry with a past date and time — the cold-start escape hatches for bringing existing history in.
- **Onboarding that does what it says.** A short, skippable health-baseline step, and the goals you pick now actually seed your dashboard.
- **Polar and Oura credentials in the browser.** Both connect with your own developer-app credentials entered in settings, like the other integrations — no environment file needed.
- **More ways to be notified.** A generic webhook channel reaches a self-hosted notifier or a chat service, an email channel sends over your own SMTP server, and an operator can see delivery health across every channel. Web Push keys can be generated in one click from the admin panel.
### Changed
- **Sleep reads in real clock times.** Per-stage rows now carry their own start and end, so the "last night" timeline lays out across the night instead of stacking — measured where the device reports stage timing, and an honestly-labelled reconstruction where it only reports stage totals. Nights logged by more than one source resolve to a single total.
- **A dose taken on one device clears the others in seconds.** Marking a dose sends a silent sync to your other devices, so a lock-screen reminder ends without waiting for the next app open.
- **More of a connected device's data flows in.** Readiness contributors, body-temperature deviation, blood-oxygen, a sleep score and autonomic-charge and training-load now come through where the source provides them. Body weight is never taken from a wearable strap.
- **One product across every screen.** Desktop and mobile navigation now tell the same story, the coach has a single home, the layout and reminder settings each gather under one hub, and every integration card links to its setup guide.
### Fixed
- Sleep nights from some sources were stamped at the wrong instant and sat shifted earlier in the day; corrected, with a one-time backfill that re-syncs affected nights.
- The doctor-report sleep figure now matches the dashboard and the companion app, reading the same reconstructed per-night total as every other surface.
- Polish across the new surfaces: consistent loading and empty states, design-token colours, responsive grids, and a calm confirmation for regenerating Web Push keys.
### Security
- The webhook channel pins a public host and refuses a private or loopback address unless explicitly allowed; webhook secrets, SMTP credentials and the Web Push private key are kept out of any recorded error; the key-generation and delivery-health endpoints are reachable only from an authenticated admin session, never a token.
### Self-hosting
- A notifications guide spells out that no Apple account is needed — Web Push, Telegram, ntfy, the new webhook and email all work without one — alongside a backup-and-restore callout and a clearer note on which variables must be whitelisted to reach the container.
### Operator note
- Migrations 0162–0166 are additive. A boot-time backfill re-syncs sleep nights from the affected sources to the corrected timeline once; it is idempotent and bounded.
## [1.17.0] — 2026-06-14 — clinical depth for glucose and sleep, and three new sources
This release builds new depth on the coherent foundation laid over the v1.16 line. Blood glucose gains a clinical panel, sleep gains a debt and chronotype reading, and three new data sources — Nightscout, Polar and Oura — join Withings, WHOOP, Fitbit and Apple Health. Every new metric is computed once on the server and read the same way on the dashboard, the coach, the doctor-report and the companion app, and each holds back a confident reading behind a calm "still learning" state until it has enough data. No breaking changes.
### Added
- **Blood-glucose clinical panel.** Beneath the glucose chart, a panel reads time-in-range on the consensus bands, the glucose management indicator and an estimated A1C, and the coefficient of variation with an instability flag — with the J-index and the low/high blood-glucose risk indices behind an advanced view. It is honest about its limits: a panel built from spot readings says so, and it waits until enough readings over enough days have accrued before asserting anything.
- **Nightscout.** Connect a self-hosted Nightscout instance and its continuous glucose readings flow in — the density that makes the clinical panel meaningful. Public instances work by default; a per-connection toggle allows a private or home-network instance.
- **Sleep debt and chronotype.** The sleep page now shows a rolling sleep debt against your age-based need and an MCTQ chronotype with your mid-sleep, social jetlag and a type band — the deeper detail behind an advanced view, and the type held back until enough free-day nights are recorded.
- **Polar and Oura.** Connect either over OAuth to bring in sleep, heart rate, respiratory rate, activity and recovery. Recovery now reads from the strongest connected source — WHOOP, then Oura, then Polar, then the computed proxy.
- **Reorder-aware medication supply.** The low-stock warning now accounts for how long a refill takes, so a weekly medication is flagged before its last dose rather than after, with a concrete "runs out on … — reorder by …" date. A reorder lead time is configurable globally and per medication.
- **Invite deep-link.** Invites open through a link the companion app can intercept to start a prefilled registration, with a browser fallback.
### Changed
- **The coach reasons from the new signals.** Glucose time-in-range, sleep debt and chronotype, and a connected device's native recovery and strain now reach the coach, which prefers the device's gold-standard number over a computed proxy and respects each metric's still-learning state.
- **Mood and cycle entry is one sectioned sheet.** A quick row of your recent tags stays open, the rest folds into collapsible sections with a count of what's set inside — fast by default, deep on demand.
- **Wearable data you already had now surfaces.** Heart-rate variability from a ring or strap appears on the HRV view, and Oura and Polar steps, energy and pulse are ranked as real sources.
- **Onboarding tells the truth.** The setup flow now presents the sources that actually shipped, signposts importing existing history, and sets the expectation that some insights sharpen over the first week or two — instead of promising a configuration that did not happen.
### Fixed
- The sleep "last night" timeline bar returns for nights whose stages share an end instant, and the dashboard clears a recorded dose without a reload (carried forward from v1.16.17).
### Security
- Outbound Nightscout calls pin a public host by default with an explicit private opt-in, the stored token is kept out of any recorded error, and the new OAuth connections verify a signed, expiring state before exchanging a code.
### Operator note
- On the first nightly low-stock check after deploy, a medication already flagged low re-notifies once as the warning threshold moves to its lead-time-aware value. Expected and bounded.
## [1.16.17] — 2026-06-14 — the sleep timeline returns, and the dashboard clears after a dose
Two fixes to behaviour that surfaced in daily use. No breaking changes.
### Fixed
- **The "Last night" sleep timeline bar returns.** The staged hypnogram — which lays the night out across deep, core, REM and awake by clock time — had collapsed to just the numbers for nights whose stages share an end instant, leaving the breakdown without its bar. The bar now appears whenever the night carries timed stages; only a session recorded on a single instant falls back to the breakdown alone.
- **The dashboard clears a dose the moment it is recorded.** After recording an intake from the medication card or detail page, the dashboard kept prompting for that dose until a full page reload, because the home view was refreshed only while it was on screen. It now refreshes as soon as the dose is recorded, with no reload.
## [1.16.16] — 2026-06-14 — one engine for sleep, recovery and glucose
This release continues the work of making one number mean one thing everywhere. Sleep, recovery and blood glucose are now read from a single source on every surface — the dashboard, the coach, the doctor-report, the CSV and FHIR exports, and the companion app — so the figure you act on is the same wherever you look. Several displayed numbers become correct-but-different; each is called out below. No breaking changes.
### Added
- **Per-stage sleep on the night feed.** The sleep-night feed now rounds every duration to whole minutes and carries the per-stage breakdown (deep, core, REM, awake), so the companion app renders the same night the web does.
### Changed
- **Sleep is reconstructed once, from one engine.** The sleep score now reads each night through the same reconstruction the rest of the app uses, with source de-duplication: a night recorded by more than one source (for example a watch and a ring) is counted once instead of summed. For multi-source nights the score — and the readiness that builds on it — drops to its true figure rather than an inflated one.
- **WHOOP recovery is the recovery you see.** When a WHOOP-native recovery exists it is now the single canonical value on the tile, the chart, the doctor-report, and the coach; the computed proxy is the fallback when there is no native score. A connected account no longer sees the proxy and the native value as two competing numbers, and the night each belongs to is resolved consistently, so one night reads as one recovery. A WHOOP user's recovery figure can change to the native one.
- **Blood glucose speaks your unit everywhere.** Glucose is converted once, at the point it is served, through a single helper — so the series the companion app charts, the CSV export, the dashboard tile, the detail page, and the coach all read the unit you chose. The CSV export now matches the FHIR document instead of emitting raw mg/dL against a converted clinical export. If you read in mmol/L, these surfaces now show the converted value (for example 100 mg/dL as 5.5 mmol/L); mg/dL readers see no change. The blood-glucose median no longer claims a fixed window its value does not hold.
- **The coach reads the canonical numbers.** The coach now quotes recovery from the same resolved series as every other surface rather than a blend of sources, and states blood glucose in your unit instead of a bare mg/dL number.
- **WHOOP sync keeps a cursor per data type.** Recovery, sleep, workouts and cycles each track their own sync position, so a single slow or rejecting collection no longer holds the others back; a change notification refreshes just the affected record, and a full sync can backfill the deep history an incremental tick skips.
### Fixed
- **The coach appears when a shared model is configured.** A self-hoster who runs the coach from a server- or operator-managed model — with no personal key — now sees the coach instead of an "unconfigured" state, because the provider endpoint reports whether the coach can actually answer.
- **A consent receipt cannot double-mint.** Two requests arriving together can no longer leave two active consent grants; the active receipt is enforced unique and minted atomically, and a concurrent first grant resolves to the winning receipt instead of an error.
### Security
- **Consent endpoints are rate-limited.** The consent grant, read and revoke routes share a per-user limit, so the receipt path can no longer be driven in a tight loop.
## [1.16.15] — 2026-06-14 — an honest calendar that learns before it predicts
A focused release on the cycle calendar: it leans on observed data, names its
uncertainty plainly, and reads your charts the way the Sensiplan method does.
No breaking changes.
### Added
- **Mark a reading as disturbed.** A temperature taken after a fever, a late night, or any off day can be flagged "disturbed" and is dropped from the evaluation, so a single skewed point no longer drags the curve.
- **A positive ovulation test anchors the prediction.** When you log a positive test, it refines the predicted ovulation day — it sharpens the estimate but never overrides a temperature shift you have already confirmed.
- **Choose your secondary fertility sign.** Advanced cycle settings now let you track either cervical mucus (the default) or cervix observation alongside temperature, so the symptothermal evaluation follows the sign you actually record.
- **Warmer phase descriptions.** Each cycle phase gained a second paragraph of plain-language context, so the calendar explains what is happening, not just which phase you are in.
### Changed
- **The calendar learns before it predicts.** Below three logged cycles it shows a calm "still learning your cycle" state instead of a confident fertile-window or ovulation guess — and the same gate now applies on the API, so every client reads the same honest signal rather than painting a window the rest of the app holds back.
- **Temperature evaluation no longer under-detects ovulation.** The symptothermal reading gained the two Sensiplan exception rules — the slow rise and the single fall-back day, each confirmed on a fourth reading — so a real shift is recognised in the patterns that the simple rule used to miss.
- **The mucus peak settles before it is trusted.** A mucus peak is now confirmed only after three drier days follow it, so a stray late entry no longer shifts the peak backward.
### Fixed
- **Longer cycles are no longer clipped.** Cycles up to roughly sixty days are evaluated in full instead of being cut short, so a naturally long cycle reads correctly.
## [1.16.14] — 2026-06-14 — one number everywhere: the score, the coach, and the doctor-report agree
### Changed
- **Blood-pressure in-target speaks one window everywhere.** The in-target tile, the health-score blood-pressure pillar, and the coach now read a single 90-day window — labeled on the tile, so it is no longer hidden. The tile shows its real span ("· 23 T") until 90 days of history have accrued, and holds back a percentage ("sammelt noch Daten") until it has at least a handful of readings, so a single measurement can never imply a confident share. The all-time view stays on the detail page. The headline figure can differ from the previous unlabeled 30-day number — it is now the correct, consistent one.
### Fixed
- **The dashboard ring and the insights card show the same health score.** Both assemble the score from one shared input builder over identical windows, so the same account can no longer see two different scores or a pillar present on one surface and absent on the other; the ring's day-delta now also reflects blood-pressure movement.
- **The coach quotes the adherence the card shows.** Medication compliance routes through the same authority as every other surface instead of the coach's own per-day tally, so it can no longer reason from a number you never see — it matters most on irregular, rolling and as-needed schedules.
- **The doctor-report matches the app.** The adherence in the PDF/FHIR export is computed from the same dose history — slot attribution, cadence and cross-source dedup honoured — as the detail page, instead of a raw intake-row count, so the figure a clinician reads on paper agrees with the screen.
- **Measurement and mood entries reject implausible timestamps.** A future date beyond a small clock-skew allowance, or anything before 1900, is refused — matching the medication-intake guard — so a stray future-dated reading can no longer distort the recent-window metrics.
## [1.16.13] — 2026-06-14 — installable and offline, doses that find the right day, and headers that hold their line
### Added
- **The app installs and works offline.** The service worker now registers for every visitor, not only after notifications are switched on, so the app installs to the home screen, its shell and assets are cached, and a lost connection shows an offline page instead of a dead tab. Authenticated pages are never cached — their data always loads live — the share-by-link page is kept out of the cache entirely, and signing out clears the page cache. The running app still heals itself across deploys, and the cached-version marker can no longer drift stale between releases.
### Changed
- **The default mood tags are curated.** The redundant `happy`, `excited`, water and music defaults step aside, `overtime` returns to the work picker, a new `praise` ("Lob") tag joins it, and the manual sleep-quality rating retires — a measured night already covers it. Tags already recorded against any of these keep resolving their labels; nothing on existing entries changes.
- **Page headers hold one line on a phone.** The medications header no longer drops its actions to a second row, the insights "Ask the coach" action sits to the right where it belongs, and a long label in the more-menu stays inside its tile. The mood, measurements and dashboard headers get the same treatment so none of them can wrap either.
### Fixed
- **A historical dose edit credits the schedule that was live then.** Editing a past intake on a medication whose schedule has since changed now attributes it to the dosing era valid at the dose's own time rather than today's schedule — the same era the history and compliance already read it under.
- **A CSV intake import moves the tracked stock.** It was the one path that recorded taken doses without decrementing the supply, so the runway read high; it now consumes exactly like every other path, once per dose, and a re-import never drains the shelf twice.
- **A workout that reaches WHOOP late is no longer lost.** Workout ingest used a one-hour overlap window while recovery and sleep used a far wider one, so a phone that synced to the WHOOP cloud more than an hour after a session left the workout permanently outside the window. It now shares the wider window; the idempotent upsert keeps the widening from duplicating anything.
- **Sleep night totals read as whole minutes** instead of a long fraction, matching the contract the clients decode.
- **A tiles-only layout save no longer wipes section customization.** Reordering the navigation tiles without sending the section layout keeps the stored sections instead of resetting them to defaults, and the same holds the other way around.
- **The web AI surfaces honour consent again.** The web client now records the consent receipt the server requires before any model call, so insights and the coach stop falling back to the empty no-provider state for people who have granted it, and the status reads "consent missing" honestly when it is genuinely absent.
- **The coach button in the public demo no longer errors** — it steps aside there instead of offering a send the demo blocks.
## [1.16.12] — 2026-06-13 — split doses, steady cards, and a read that stays warm
### Added
- **A dose can consume a fraction of a unit.** Split-pill medications now declare ½, ⅓ or ¼ of a tablet per dose — the wizard offers a curated set of fractions alongside the whole numbers, the most error-resistant input. The supply tracks the fractional remainder (30 tablets dosed at a half each reads 29.5 after one dose), the days-left projection follows, and undoing a dose refunds exactly what it consumed. Existing whole-number doses are untouched; the inventory unit counts widen to decimals across the API contract. (#316)
### Changed
- **The medication cards hold one shape.** The low-supply notice moves onto the adherence row beside the streak — a slot every card already reserves — so a card that is low on stock no longer pushes its bars out of line with the card beside it. The gap between the drug class and the first line tightens at the same time.
- **The dashboard and insights stay warm between visits.** The analytics and derived-insight reads keep a served-stale snapshot for an hour rather than ten minutes, so returning after a normal break paints instantly and refreshes in the background instead of paying the full cold rebuild — which a visit spaced more than ten minutes apart hit almost every time.
### Fixed
- **The medications list refreshes when you return to it.** Navigating back to the page — or reopening the supply tab — now refetches the list, compliance and stock instead of serving a client cache that could be minutes stale, so a dose logged on another device shows up without a manual reload.
## [1.16.11] — 2026-06-12 — the supply speaks up, tags find their groups, and nothing asks twice
### Added
- **The app says so before the pack runs out.** A daily pass projects every tracked supply against its schedule and notifies on the configured channels when the runway falls below a per-account threshold — once per crossing, re-armed by a refill, re-announced when the threshold changes. The threshold lives beside the reminder settings (1–60 days, default 7, or off), the push deep-links straight into the supply tab, the table's stock column carries the projection as "≈ X Tage", and the card states the remaining runway once it crosses the threshold — only then.
- **Mood tags get groups, icons and a home of their own.** Custom groups with encrypted labels, a searchable 75-icon picker, and a manage page under settings; tags archive without touching the entries that carry them — editing an old entry can no longer drop its archived tags either — the picker offers inline creation right where a tag is missing, custom labels finally render in the picker AND across insights, and the arrangement persists per account. The whole surface ships in the public API contract.
- **Every due dose in one confirmed action.** When two or more doses are due — including a dose escalated past its catch-up window — a button on the medications list collects them into one confirm dialog and records each through the same path as the card buttons: slot attribution, stock consumption and every dependent surface included; failures are counted, not hidden.
- **A medication can declare itself as-needed.** No schedules, never due, never reminded, excluded from every compliance rate — but intakes log normally, consume inventory exactly like scheduled doses, and the history stays. The wizard offers it as a cadence choice; switching it onto a fixed schedule later never repaints the schedule-less stretch as missed doses, and the doctor report lists it without inventing a rate. (#316)
- **Units-per-dose and the default pack size are editable from the supply tab** — a manufacturer switch with a different blister size no longer requires walking back through the wizard.
### Changed
- **One wrench marks every customize entry point.** Dashboard, insights and medications share the glyph, the table corners clip to their container again, and the card/table view choice moves into its own settings section.
- **The coach opens in place.** The button brings the side drawer over the page you were reading instead of navigating away, and grew to a comfortable tap size.
- **The card's dose state lives on the next-intake row.** Take-now, late and the overdue escalation speak from the line that names the dose — with their time beside them, on mobile too — instead of a separately reserved status line that left a hole on every settled card.
- A source ladder saved before a newer integration existed now lists and ranks the later-added sources everywhere — settings, charts, rollups and the sleep picker — instead of silently pinning them below every ranked one.
- WHOOP's self-reported profile weight is no longer ingested as a measurement; the stale entry stops resurfacing as the newest weight, and existing accounts heal on the next sync.
### Fixed
- **The hypnogram keeps its timeline when a summary writer joins the night.** A stage summary stamping all its rows on one instant no longer evicts a per-segment night; when only summaries exist, the card shows the stage breakdown instead of a fake skyline.
- **A dose taken on the dashboard updates the dashboard.** The snapshot band refetches with every intake path instead of waiting out its polling interval.
- The supply tab can delete a container, its controls carry the outline they always implied, each container states its condition inline next to its figures, and the table's stock warning follows the user's runway threshold instead of a leftover four-dose constant.
- Every wide event carries the recent event-loop lag, and a stalled loop emits its own warning event — the instance-level signal per-request durations cannot carry.
## [1.16.10] — 2026-06-12 — the stock follows every dose, and the list takes your order
### Added
- **A medication can declare how many units one dose consumes.** Two 2 mg tablets for a 4 mg dose register as one dose taken and decrement two units, the supply dialog accepts quantities in doses or units with live conversion, containers carry a type (pen, ampoule, blister pack, inhaler, bottle) and hold up to 1000 units — a 200-dose inhaler canister finally fits. (#316)
- **The medication list has a table view and an order of your choosing.** A toggle beside the add button switches between cards and a compact, per-column sortable table; both views share a manual order, and the whole arrangement lives in its own settings section behind the same gear icon the dashboard and insights pages use. The choice persists per account. (#316)
### Changed
- **The daily-overview band is opt-in now.** New accounts and accounts that never chose it start without the band and keep the plain greeting; it switches on under dashboard settings.
- **Expired containers count as available nowhere.** Every surface — overview row, supply tab, list, table, GLP-1 card and the runway estimate — agrees on the same figure and shows expired stock separately instead of mixing it in.
- The wellness rings sit exactly on the theme palette now — readiness green, recovery cyan, sleep purple, stress orange, strain pink — instead of neighbouring tones, and the light theme follows the same five hue families.
- The inventory endpoints speak one wire dialect: request fields renamed to `unitsTotal` / `unitsRemaining` to match the responses they always returned.
### Fixed
- **Every taken dose now moves the tracked stock.** Only one of the six intake paths decremented the supply before — web, the status toggle, the phone sync, the external API and the Telegram button all consume now, the next container opens automatically, and undoing, skipping or deleting a dose refunds exactly what it consumed. Each dose carries its consumption stamp, so a double tap racing a sync replay decrements once, and a re-posted history never drains the shelf. Doses recorded before this release stay untouched — the count starts moving with the next dose, or correct a container once under the supply tab. (#316)
- **A dashboard full of readings no longer claims "not enough data".** While the score warms up the band says it is being computed, and a first reading of a new data type — a fresh wearable metric, a first glucose value — can no longer knock the score out, because the gate checks only the types the score actually reads.
- **The score stops flickering on busy accounts.** The pre-aggregation rebuild deleted its window before inserting the replacement without a transaction — a concurrent sync write could abort the insert and leave whole types empty until the next pass. Delete and insert run atomically now, and the boot-time backfill shares the fold horizon, so an account with years of history stops re-backfilling on every deploy.
- **The filter pills on the mood and measurements lists open their menu on screen again.** The menu opened off-viewport below the page since v1.16.1 — selecting a mood, type or source read as a dead control.
- **The coach button stops blinking out on chart hovers.** It no longer hides whenever a chart tooltip is open anywhere on the page, and it sits evenly in the corner.
- The pre-item GLP-1 supply ledger reads again: accounts that tracked pens through the older ledger see their count on the card and in the coach until they register containers, which then take over.
## [1.16.9] — 2026-06-12 — the day opens with a verdict, and every dose path tells the truth
### Added
- **The dashboard opens with a daily band.** Greeting, the one item that matters right now — a fresh crisis-level blood-pressure reading, an overdue or upcoming dose, weight drifting from its range, a run of short nights, a quiet stretch, a score drop, or the briefing's key finding — with a single action, today's dose tally beside the next due time, and the health score as a ring. The verdict resolves from a fixed ladder with crisis floors that ignore personal target ranges, it never invites a dose the server has not called due, and the band can be turned off under dashboard settings, where the plain greeting then returns to the header.
### Fixed
- **An intake recorded through the external API now credits the due slot.** The ingest endpoint was the one intake path that stored a bare row beside the pending slot — the card showed the intake as the last dose while still demanding it, and the ledger later counted a miss. It attributes to the dose band like every other path now, the Telegram confirm and skip buttons converge onto the reminder's own slot, and a nightly pass heals the pairs this has already produced — both days' history and compliance recompute, no manual repair needed.
- **A cached page can no longer contradict an action you just took.** Invalidation now fences in-flight cache builds, so a read can neither join a build that started before your write nor have a background refresh re-store the pre-write state as fresh.
- **Dose prompts err on the safe side everywhere.** A take shortly before the window credits the slot instead of floating loose, an off-schedule intake can no longer silently resolve a different slot and hide a genuinely due dose, a long-interval dose taken on an earlier day downgrades the slot-day prompt to "last dose n days ago" instead of inviting a double dose, and the overdue badge no longer disappears because the day's pending entries were counted as actions.
- Intake times entered retroactively carry the same plausibility bounds as edits, moving a dose across a slot boundary keeps its recorded injection site and dose, both affected days recompute, and schedule corrections, data wipes and backup restores refresh the caches they feed.
- The medication cards, the quick-entry preselection and the dose pills compute their clock in the profile timezone instead of a fixed one.
- WHOOP credentials are trimmed on save — a stray space from the portal's copy button read as an unknown client — and an empty redirect override falls back to the derived callback URL.
## [1.16.8] — 2026-06-12 — texts that follow your data, charts that never strand
### Added
- **The coach is one tap away on every page.** A round button sits bottom right, stays put while you scroll, shows a quiet dot when a nudge is waiting, and steps aside for the selection bar and the welcome tour.
- **Tell the coach to remember something.** A remember action under your own chat messages stores the statement in the matching self-context field — allergies land under allergies, diagnoses under conditions — visible and editable in the settings, and fed into every future conversation. Statements phrased casually are recognised again, and remembered facts no longer drop out of the prompt on data-rich accounts.
- **Mood notes in full.** Notes expand to their complete text right in the list — the toggle only appears when something is actually clipped — and the CSV export carries a note column.
- **The hypnogram names its source** and paints only the stages the night actually contains.
### Changed
- **Insight texts regenerate when your data changes, not on a timer.** Every generated text stores a fingerprint of the data it describes; nightly runs, page visits and sync bursts skip the model call when nothing moved. A notable new reading is narrated the same morning, a manual regenerate refreshes the cards along with the briefing, and texts can no longer claim freshness over readings that crossed a staleness boundary. An active day drops from hundreds of model calls to a handful.
- **Medication cards arrive in one piece.** All cards share a single compliance request instead of one each, the heavy date math runs about five times faster, and the status line, adherence bars, streak and cycle line keep reserved slots — nothing shifts when data lands. A dose logged from the phone app is reflected immediately.
- **A cold start paints the page you'll actually get.** The chart row reserves its space before any data arrives, the tile silhouettes match the real layout, and the health-score card holds its column while the numbers compute. The insights overview runs its reads in parallel and serves repeat visits from cache — the targets and derived endpoints answered in well over a second before.
- **Sleep nights pick the richest recorder.** Several apps writing through the same health platform no longer blend into one night — the writer with actual stage data wins, time in bed unions every writer's window, and nights that reach the sleep service late are fetched up to seven days back, so a wrong night heals itself on the next sync.
### Fixed
- Charts no longer strand on a skeleton after a deploy: the update check re-arms per version, a failed chart chunk retries and then degrades to a single card with a reload button, stalled requests time out and retry, and a failed data read shows an error with a retry instead of "no data in this period".
- Screen readers hear chart failures and coach nudges, the floating button leaves the tab order when it hides, focus survives the remember confirmation, and the icon cluster on insight pages no longer has overlapping touch areas.
- The duplicated customize entry and the per-chart target link are gone, show-all-values became an icon beside the target control, pill borders stop clipping in the tab strip, and the mood tag bars adopt the muted palette of the charts above them.
- Changing a target range, profile detail or source priority is reflected in the analytics on the next read, AI consent is honoured before any stored text is reused, and CSV exports neutralise spreadsheet formula prefixes in free-text cells.
## [1.16.7] — 2026-06-11 — insights answer instantly, every evening
### Changed
- **Insights always answer instantly.** A stale briefing is served immediately with a regeneration kicked off in the background — the response carries a revalidating flag and the open page polls briefly until the fresh content lands, instead of blocking the visit on up to ten seconds of inline generation.
- **The nightly pre-warm no longer skips evening readers.** The staleness threshold drops from twenty hours to one — previously an evening visit stamped the cache fresh, the night run skipped it, and it expired exactly the next evening, so the people who read insights after work were the ones who always hit cold generation. The visit-triggered warm fills both locale families and collapses to one job per user.
- **The session resolves faster.** The profile read inside `/api/auth/me` runs in parallel with the cookie refresh, the bug-report status answer comes from a ten-minute cache without a write per request, and the medications page is prefetched the moment its nav entry is touched.
- **The insights route ships less JavaScript.** The schema validator loads lazily and the mood charts split out of the main chunk, trimming the cold download that dominated an uncached first visit.
### Fixed
- A manually entered measurement is reflected in the analytics aggregates on the next read — interactive writes evict the cache instead of marking it stale, while background sync keeps the cheaper path.
- The analytics script proxy degrades to a silent no-op instead of failing every page when the upstream is unreachable, and records the failure reason where the operator can find it.
## [1.16.6] — 2026-06-11 — corrected eras, an honest weekly status and a faster first paint
### Added
- **Schedule eras can be corrected.** A recorded era can be edited through a superseding revision that keeps the original as an audit trail; manual eras edit in place, all era writes serialise per medication, and duplicate dose times collapse in validation.
- **The server-wide assistant key has an admin home** — status, provider, model and base URL with the BYOK-first and consent rules stated where the operator works.
### Changed
- **The first paint got faster again.** The dashboard snapshot fires the moment login succeeds instead of waiting for the page chunk — its request starts about half a second earlier under throttled conditions — handed over so the server and client render never disagree.
- **Guided coach follow-ups speak to your data.** Questions are personalised from your own snapshot when a provider is available, and an answer receives the coach's streamed reaction before the next question.
- **Settings and admin finally share one face.** Twenty-seven hand-rolled card headers move onto the shared pattern, sixty containers normalise to one chrome, descriptions get a consistent one-sentence voice, and the one duplicated coach preference collapses onto a single owner.
### Fixed
- A rolling medication whose next dose is tomorrow no longer reads as heavily overdue — the card status derives strictly from the served next-due.
- The adherence bars keep a fixed geometry in every state instead of growing with their status text.
- The remaining hydration warnings on the dashboard, insights and AI settings are gone.
## [1.16.5] — 2026-06-11 — a guided coach dialog and a schedule that shows its past
### Added
- **Open coach follow-ups run as a guided dialog.** A quiet card offers to start, the coach asks one question at a time with visible progress, answers can be adopted into the matching self-context field, skipping keeps a question pending, and a closing summary says what was taken over.
- **The schedule shows its history.** Past eras appear as a collapsible timeline on the schedule tab, and an earlier era can be added manually for old history — validated against overlaps and bounded so it can never swallow tracked live history.
- **Smarter proactive nudges.** Three new deterministic signals — weight drifting from its target band, a run of short nights, an active user falling silent — plus per-group toggles, a frequency preference and wording that addresses the focus you asked the coach to watch.
- **Invites mint up to fifty uses** with a scrolling redemption list.
### Changed
- **A documented path away from the moving latest tag.** The compose image reference accepts a pinned digest via env var, and a promote script resolves the published digest, verifies its cosign signature and hands the operator the exact pinning call.
### Fixed
- The self-context adopt runs in one transaction, the admin area renders nothing before the role is confirmed, and the json-body helper keeps caller headers intact.
## [1.16.4] — 2026-06-11 — half the bundle, a typed client, and stock you can record
### Added
- **The inventory tab records stock** — packs and pens with optional expiry, corrections and withdrawals. The detail page gains a dedicated API tab, an editor entry under lifecycle, and a danger zone without the red drama.
- **Record the dose you actually took.** Intakes carry an optional actual dose, editable in the quick-add and log dialogs and shown as a quiet deviation in the history.
- **Undo for deletes.** Removing a measurement or mood entry can be taken back from the toast.
- **Pull-to-refresh** on the measurement and mood lists, and the customize entry on every insights sub-page.
- **Answered coach follow-ups can flow into your self-context** with one tap.
### Changed
- **The app got dramatically lighter and faster.** Only the active language ships to the browser — the dashboard route drops from 2.1 MB to under 1 MB parsed (288 KB gzip), with no language flash. The shell paints immediately while the session resolves, a redundant request stage is gone, and insight sub-pages prefetch on approach: a first tab switch lands in tens of milliseconds instead of nearly a second.
- **Every client call goes through one typed API client** with the envelope, error and ok-handling in a single place, enforced by lint. The route registry, query keys, background worker and dashboard page split into feature-located modules behind thin indexes.
- **Settings behave one way.** Provider and integration cards live in focused subcomponents, every setting persists server-side at exactly one home, save conventions are documented, and duplicated controls collapsed into single owners with links.
- **Published images are signed**, dependabot merges only behind the required checks, a pre-commit secret scan guards every commit, the deploy webhook can demand a fresh timestamp, and a monthly drill proves the newest backup actually decrypts.
### Fixed
- Server and first client render now agree everywhere query state used to leak into the chrome.
- Delete confirmations admit the undo they offer, step counts drop their decimals, streaks know their singular, the medication header speaks your date format, and a failed coach turn keeps your message and says what went wrong.
- A language switch updates pending status notes and only flips once the new language is actually loaded.
## [1.16.3] — 2026-06-10 — schedules remember their history
### Fixed
- **Changing a schedule no longer rewrites the past.** Every material cadence change now archives the outgoing schedule as a dated revision, and history, compliance and write attribution evaluate each period against the schedule that was valid at the time — a dose era taken at its then-correct times stays on time forever. The repair script can infer historical eras from consistently dated old anchors behind an explicit flag.
- **The welcome tour can no longer strand its buttons below the fold.** The target scrolls into view first, the popover flips above it when space runs out, and a centred sheet takes over on any viewport when neither fits.
- The avatar menu drops its about entry; the section lives in settings.
## [1.16.1] — 2026-06-10 — a chat worth using, honest sleep stages, stock you can record
### Added
- **The inventory tab records stock.** Packs and pens with optional expiry, plus corrections and withdrawals — no API detour. The medication detail page also gains a dedicated API tab, an editor entry under lifecycle replacing the misleading header button, and a danger zone on the shared card surface with only its buttons in red.
- **A shared filter bar.** Measurements and mood lists filter through one pill-rail language — active filters read as removable chips, date ranges live in a popover, every page feels the same.
- **Six care achievements.** Miss-free dose streaks (7/30/90 days), a four-week measurement routine, a complete self-context and a week of sleep logging.
### Changed
- **The coach chat behaves like one.** Single-line composer that grows with the text, a typing indicator, no inline disclaimer, a context rail that hides until asked, conversations in the full view, and coach preferences in Settings. Clearly stated allergies and conditions are remembered immediately.
- **Settings and admin share one alignment language.** Icon column left, content flush on its own column, neutral header icons, no duplicated titles, and every select on the account-style idiom with a visible chevron.
- **First paint shows silhouettes, not bare cards**, and charts reveal within 1.2 seconds even when one widget lags.
### Fixed
- **Sleep stages show again next to coarse sources.** A night carrying granular watch stages alongside coarse phone samples picked the coarse source and showed only awake/asleep; the granular source now wins its session.
- **The next-dose line tells the truth after a schedule change.** It reads from the dose calendar instead of a stale schedule window; the repair script reconciles drifted windows and clears pending rows on abandoned anchors.
- **Nightly insight texts are actually there in the morning.** The warm pass no longer skips the status cards when the briefing step is budget-blocked or fails, and one hanging provider cannot stall the whole run.
- The welcome tour stays inside the viewport — no more sideways scrolling to reach its buttons, with a bottom-sheet fallback on narrow screens.
- The dashboard customize icon matches insights, the target-settings link leaves the home charts, and the mood heatmap adopts the damped fills of its sibling charts.
## [1.16.0] — 2026-06-10 — a coach that knows you, invites, and a faster, calmer app
### Added
- **Tell the assistant about yourself.** A new encrypted self-context in Settings → AI — free text plus structured fields for conditions, allergies and what the coach should watch — feeds the coach and the daily briefing as clearly fenced context. After saving, the coach may ask up to three targeted follow-up questions, surfaced as tappable chips above the chat composer.
- **Proactive coach nudges.** A nightly pass watches four signals — low 7-day adherence, blood pressure above its target band, a falling recovery trend, and a stale self-context — and sends at most one gentle nudge per week through your configured notification channels, with its own opt-out. Off whenever the coach is disabled.
- **Invite links with QR codes.** Operators on closed registrations can mint expiring invite links from a new admin section: status, uses, expiry and a redemption history of who joined when. Revoking keeps the history; the link and QR are shown exactly once at creation.
- **Attribute an off-schedule dose to its slot.** History rows that landed off schedule now show when the dose had been due and offer to attribute the intake to that slot — visibly badged, always counted as taken late, reversible at any time.
- **A time-format preference.** Automatic (follows the language), 24-hour, or 12-hour — set once in the profile, applied to every clock the app renders.
- **Insight pills are manageable in one place.** The settings list that orders the navigation pills now also hides and shows them, with the same eye toggle the overview tiles use; the page-side manager folded into a link.
### Changed
- **The medication detail page calms down.** Six tabs instead of seven (reminders live with the schedule they gate), every section on the card surface, an overview tab that actually summarises — next dose, reminder state, supply runway — and a history where settled rows carry one quiet menu instead of a button wall. Charts load on demand.
- **The dashboard loads as one.** Structured skeletons replace bare tiles, and charts reveal together once their data is in instead of popping in one after another.
- **The medication list stops hammering the server.** Compliance for the cards is computed in one calendar pass per request, cached for fifteen minutes and invalidated on every write; the dashboard drops two redundant queries; the BMI chart reuses the weight series; expensive analytics endpoints gain a shared per-user rate limit.
- **Nightly assistant runs respect every gate.** Status generation skips users who disabled the coach and honours the operator kill switch, deleted measurements never feed a prompt, the mood card gets the nightly run the other cards always had, non-German locales consistently fall back to English, and worker failures finally reach the error tracker with real retries behind them.
- **The README says what matters in one screen**, and the repository description and topics match it.
### Fixed
- The wellness ring's glow is no longer clipped to the circle.
- An overflowing insight-pill row is reachable by mouse — chevrons and wheel scrolling joined touch.
- Safari favourites no longer render the icon on a white plate.
- A rate-limited briefing regenerate stops claiming success, and a missing AI provider shows as exactly that instead of an eternal "preparing".
- Editing schedule times migrates the open dose rows of the old anchors and reconciles a stale intake window instead of stranding both.
- Auto-missed verdicts respect each schedule's real catch-up window, clear when the dose is taken after all, and skip deleted rows.
- The intake repair script runs inside the production container.
- Every workflow action is pinned, dependabot merges only patch and minor updates behind required checks, vulnerability gates block instead of warn, and the published image carries its build SHA so a deployment can prove what is running.
## [1.15.19] — 2026-06-10 — honest dose states, bounded bodies, new licence
### Changed
- **The project licence is now PolyForm Noncommercial 1.0.0.** HealthLog stays free to use, modify and self-host for noncommercial purposes; commercial use needs a separate arrangement. Releases up to and including v1.15.18 were published under AGPL-3.0 and remain available under that licence.
### Fixed
- **An open dose no longer reads as missed the moment its time passes.** A pending dose now shows as upcoming until its intake window actually closes — hours for daily schedules, days for weekly ones — so the history tab and the compliance rate stop punishing doses that are still takeable. Phantom "ad-hoc" lines for tonight's not-yet-due doses are gone too.
- **Logging the same dose from two places no longer doubles it.** A dose acknowledged via a reminder and also logged from another client produced two live rows on the same slot, inflating the day's schedule count (4 of 4 on a two-dose day). Intake writes now converge onto the existing slot row regardless of source, the compliance rollup counts slots instead of rows — which also corrects historical days on the next recompute — and the duplicate sweep runs nightly instead of only at worker start.
- **A weekly dose taken a few days late counts as late, not missed.** The write path only searched ±1 day around the intake time for a matching slot, so a weekly injection taken inside its multi-day catch-up window was stored as off-schedule while the slot stayed missed. The search now covers the schedule's full window reach.
- **An intake edit can no longer park a dose on an impossible date.** Editing a dose's time now rejects future timestamps and dates before the medication existed, the picker caps at now, the dialog shows the scheduled slot next to the field and warns when the new time sits more than 48 hours away from it — a day/month typo no longer corrupts the today view silently. The edit dialog's note field is removed; it was never saved.
- **One poisoned day no longer halts the nightly consolidation for everyone.** The daily-mean pass could collide with a row parked exactly on its canonical timestamp and abort the whole run on the first conflict, leaving every later user and metric unconsolidated night after night. Collisions now resolve deterministically and a failing day is logged, counted and skipped.
- **Every JSON request body is now size-bounded.** Raising the upload limit for large Apple Health archives had removed the implicit ceiling on all other JSON routes; each one now enforces an explicit cap sized to its payload and answers 413 beyond it.
### Added
- **An operator repair script for historical intake anomalies.** `scripts/repair-intake-anomalies.ts` lists duplicate rows sharing a dose slot and intakes whose taken time sits implausibly far from their slot; with `--fix` it merges the duplicates the same way the nightly sweep does and recomputes the affected compliance days. Runbook: `docs/ops/intake-repair.md`.
## [1.15.18] — 2026-06-08 — a traceable medication dose history
### Added
- **A traceable dose history for every medication.** The Verlauf tab now shows every expected dose slot with a clear status — taken on time, taken late, skipped, or missed — and tags any off-schedule intake as ad-hoc. The history reads from the same ledger the compliance percentage is built on, so the rate you see can no longer disagree with the timeline beneath it.
- **A configurable intake window per dose.** Each dose time can carry either a point time or an explicit range (for example 07:00–09:00); the default stays ±1 hour. The window defines the on-time band, so a take that lands inside it counts as on time rather than late. The window is editable on the schedule and in the medication wizard.
- **An Insights settings page.** A cog at the top right of the overview opens a dedicated Insights section in Settings, where you arrange the overview sections and sort the navigation pills. The old inline "Anpassen" toggle is gone — the cog is the single entry point.
- **A dose-history endpoint.** A read endpoint returns the resolved per-slot status over a date range, so the history view and any client render the same picture.
### Changed
- **A redesigned medication detail page.** The detail page is now a tabbed layout — Übersicht, Zeitplan, Erinnerung, Bestand, Verlauf, Erweitert — and the old stacked advanced-settings modal is dissolved into tidy, grouped sections under Erweitert. Card actions open straight to the matching tab.
- **The medication card keeps a constant surface.** The card no longer changes colour by dose status; the current status shows as a discreet label instead of tinting the whole card.
- **Intake writes and edits attribute by the real intake time.** Marking a dose taken records it against the slot whose window it falls in; a take outside every window records as ad-hoc instead of snapping to a far-off slot. Editing a dose re-attributes it, a near-miss take offers "attribute to this dose anyway?", and marking taken or skipped recomputes the history and rate immediately.
### Fixed
- **The daily briefing's regenerate no longer times out before the text is ready.** A slow but successful regeneration used to be discarded by a short timeout, leaving the old text in place while the toast still claimed success. The regenerate path now waits for the warm text and the toast is honest — it reports a refresh only when one happened, and tells you to try again when generation is still running.
- **The sleep-phase chart no longer double-counts time in bed.** Time-in-bed was being stacked as if it were a separate phase on top of the individual phases, roughly doubling the bar and the per-night total. It is no longer counted as a phase, so the bar and tooltip total read correctly.
## [1.15.17] — 2026-06-07 — large imports, demo chart toggles
### Fixed
- **Large Apple Health exports import correctly.** A request-body size limit in the middleware layer truncated uploads over ~10 MB before they reached the importer, so a multi-megabyte `export.zip` lost its ZIP structure and failed with an "end-of-central-directory" error. The limit is raised so real exports arrive intact. (GitHub #281)
### Changed
- **Chart display toggles work in demo mode.** On a demo instance, the comparison-baseline selector and per-chart overlay toggles above charts are no longer blocked — these are harmless display preferences.
## [1.15.16] — 2026-06-07 — wellness ring glow no longer clipped
### Fixed
- **The score ring's glow shows in full.** On a wellness score detail page the ring's soft glow was being clipped at the card edge. The card no longer clips its contents (its background gradient still follows the rounded corners), so the glow renders uncut on every side.
## [1.15.15] — 2026-06-07 — one warm, motivating assistant voice
### Changed
- **The whole assistant speaks in one voice.** The daily briefing, key findings, the insight write-ups, and the Coach now share the same warm, motivating, advisory tone the metric and wellness assessments adopted last release — encouraging and grounded in your data, honest about what needs attention, never generic. (Already-generated briefing text refreshes within about a day; the Coach speaks the new way immediately.)
## [1.15.14] — 2026-06-07 — manage every detail page, calmer mood charts
### Added
- **Manage every detail page from the overview.** "Anpassen" → "Detailseiten verwalten" now lists every insights sub-page, grouped by category, so you can reorder and hide any of them — not just the vitals tiles. The top navigation follows your choices: hide a page and its pill leaves the nav too.
### Changed
- **Calmer, more compact mood charts.** The mood distribution and weekday-average charts on the mood page are smaller and use softer colour, so the page reads more easily. Their axis labels are now legible in the light theme too.
## [1.15.13] — 2026-06-07 — search, filter, and bulk delete
### Added
- **Filter your measurements and mood entries.** Both lists now filter by source (manual, Apple Health, Withings, …) and by a date range, alongside the existing type / mood filter and sorting.
- **Select and delete in bulk.** Tick several entries on a page and remove them in one step, with a confirmation step. Deletions stay reversible on your devices the same way a single delete does.
## [1.15.12] — 2026-06-07 — a fairer health score and a richer overview
### Fixed
- **Blood pressure is scored fairly.** The Health Score's blood-pressure share used to be all-or-nothing per reading: a reading slightly over target counted exactly like one far over, so being consistently a little high collapsed the score even when you were well-controlled. It now grades by how close your readings sit to your target — recent readings weighted more — so "borderline" reads as borderline, not a crisis, while genuinely high pressure still scores low.
- **Workout heart rate no longer counts against your resting pulse.** Apple Health files every heart-rate sample — including the hundreds recorded during a workout — under one type, and the resting-pulse view was scoring all of them against a resting range, so an active day looked "elevated." Resting-pulse surfaces now use your true resting heart rate (with a careful estimate when that isn't available), and raw heart rate is no longer judged against a resting target.
- **The "in review" section is named "Rückblick"** and the spacing above it matches the rest of the overview.
### Added
- **Tap a wellness score for the full picture.** Readiness, Recovery, Sleep, Strain, and Stress now open a detail view with an explanation of what the score means and why it matters, the breakdown of how it's built, and a personal assessment of where you stand.
- **A warmer, more motivating assessment voice** across the insight assessments — encouraging and grounded in your data, never generic.
- **The running version on the admin overview**, with a hint when a newer release is available.
### Changed
- **Premium polish on the wellness detail view** — the detail card carries the score's colour, the ring has room to breathe, and the info control sits in the top corner.
- **Self-hosted instances resolve a login's location online by default**, using a bundled offline database only when one is configured — so the admin login overview shows a location out of the box.
## [1.15.11] — 2026-06-07 — a customizable overview
### Added
- **Arrange your overview.** Tap **Anpassen** on the Insights overview to rearrange it: drag sections into the order you want, hide the ones you don't, and — inside Vitals — reorder or hide individual metric tiles. Your layout is saved to your account and carries across devices. Tap **Fertig** to apply, **Zurücksetzen** to return to the default arrangement.
### Fixed
- **Self-hosting docs now generate a URL-safe Postgres password.** The setup examples used `openssl rand -base64 24`, which can emit characters (`/`, `+`, `=`) that break the unescaped `DATABASE_URL` and stop a fresh instance from connecting to its database. Every example now uses `openssl rand -hex 32`.
## [1.15.10] — 2026-06-07 — Insights overview, medication follow-ups, health score
### Fixed
- **Medication compliance now pairs doses to the right slot.** For a twice-daily medication, a dose logged off its usual time could be matched to the wrong slot — so a deliberately-skipped dose might read as taken, or a late evening dose as missed. Doses now snap to their own scheduled slot first, so the percentage reflects what actually happened.
- **The card advances to your next real dose.** After you log a dose, the "next dose" line moves to the next upcoming, unresolved slot instead of lingering on one that's already taken or past.
- **A single dose time reads cleanly.** A dose with one target time shows "07:00" rather than "07:00 to 07:00".
- **Your weight trend counts toward the Health Score even without a target.** The weight pillar used to need a height/target to score; it now scores your weight trend on its own when no target is set, so it stops dropping out of the score.
### Changed
- **A consistent Insights overview.** Every section now has its title above its card with a matching icon and even spacing throughout — the daily-briefing icon matches the rest, Trends has an icon, and the period-in-review and cycle sections carry proper headings.
- **A clearer cycle ring for new trackers.** With only a little cycle data the ring now shows the four phases in sensible proportions instead of collapsing to a single arc.
- **Mobile polish on Insights and Cycle** — the cycle ring fits narrow screens, the cycle tabs no longer crowd, and calendar days meet the tap-target size.
- **Your photo in the top bar.** If you've uploaded a profile photo it now shows in the top-right avatar; otherwise the icon remains.
- **The notifications area is just "Notifications"** (shorter), and admins get a link to the admin area from the mobile menu too.
- **Dark theme by default** for a fresh visitor; an explicit theme choice is still remembered.
## [1.15.9] — 2026-06-07 — medication compliance and cards
### Fixed
- **Compliance now counts a forgotten dose as missed.** A scheduled dose that was never taken or skipped used to quietly drop out of the calculation after a day, which made adherence read higher than it was. It now counts as missed, so your 7 / 30 / 90 / 365-day figures reflect what actually happened — and genuinely differ from one another instead of all looking the same.
- **The dashboard medication chart is computed against your schedule.** It previously measured taken doses against the doses you had logged (not the doses you were due), which pushed every window toward 100%. It now measures against the schedule.
- **Consistent skip handling.** A dose you deliberately skip is treated the same everywhere (a deliberate pause, not a miss); a dose simply forgotten counts as missed.
### Changed
- **The two medication cards are now truly identical.** Both styles render from one shared card body, so structure, spacing, labels, and the streak row line up exactly — only the value content differs (a weekly injectable keeps its relative-day phrasing, a daily dose shows a time). The bottom-spacing mismatch between cards is gone.
- **Cards highlight the dose that's due.** When a dose is in its take-now window the card is highlighted; once it is past due the card shows an overdue note, escalating to "Stark überfällig" near the cutoff. After the cutoff the dose counts as missed and the next dose becomes the focus.
- **Dose windows are cadence-aware.** A daily dose has a short on-time window then an overdue grace before it counts as missed; a weekly injectable follows a wider, clinically-appropriate window.
- **Tidier card.** The compliance bars show the percentage on its own, and the injection-site line was removed from the card (site tracking is unchanged).
## [1.15.8.1] — 2026-06-07 — large Apple Health imports
### Fixed
- **Large Apple Health `export.zip` uploads no longer fail with "Could not locate ZIP End-Of-Central-Directory record".** The multipart upload reader could drop part of a file body when a network chunk ended exactly on the multipart boundary — which only showed up on larger, multi-chunk uploads (a small export imported fine). The reader now handles the boundary across chunks correctly, so the uploaded archive arrives byte-for-byte intact. (#281)
## [1.15.8] — 2026-06-07 — medication cards
### Changed
- **Consistent medication cards.** Both medication card styles now use the same structure and the same labels for the same thing — "Next intake" and "Last intake" everywhere — so the two cards read the same way. The GLP-1 card keeps its relative-day hint ("Saturday 13 Jul (in 7 days)").
- **Compliance bars show the counts.** Each compliance bar now shows the doses behind the percentage (for example "100% · 12 / 12"), so two windows that genuinely land at the same percentage are legible at a glance instead of looking like a glitch.
## [1.15.7] — 2026-06-07 — data import in the browser
### Added
- **Import your data from the browser.** Settings → Export is now **Export & Import** and carries a proper import area with two paths:
- **Apple Health** — upload your `export.zip` directly. It streams server-side (handles multi-GB archives) and is idempotent, so re-uploading the same archive merges rather than duplicating; progress and the imported/skipped totals are shown as it runs.
- **Generic JSON** — upload or paste a JSON document of measurements and mood entries (with a downloadable example and a documented schema) for manually-prepared or converted data.
- The data-import schema, the full measurement-type/unit reference, and a "convert a CSV into this" how-to are documented at `docs/integrations/data-import.md`.
- **Cycle export.** When cycle tracking is on, the Export area offers an explicit cycle export.
### Changed
- **A more compact health-record export card.** The health-record export keeps its prominence but takes far less vertical space.
### Fixed
- **The Apple Health import is reachable again.** The importer existed on the backend, but the web page the docs pointed to was never present; there is now a real import UI in Settings → Export & Import. (#281)
## [1.15.6] — 2026-06-06 — settings, insights, and medication-card polish
### Fixed
- **The notifications settings panel no longer crashes.** Opening Settings → Notifications could throw a client-side error on some accounts; the panel now renders safely whatever shape the data arrives in.
- **Even chevron in dropdowns.** The arrow on the right of selects (timezone, language, units, and everywhere else) now sits with the same inset from the right edge as the label has from the left, instead of pushing against the edge.
### Changed
- **Clearer Insights overview.** The period-in-review card now carries a proper heading, and the "signals of the day" section only appears when there's an actual anomaly to flag — when every vital is within your personal range it stays hidden rather than showing an empty header.
- **Tidier medication cards.** The next-dose line no longer appears twice on a card, and the injection-site suggestion text has been dropped from the card surface (site tracking and the post-dose picker are unchanged).
- **The avatar menu lands in Account.** Clicking your avatar now opens the Account settings directly.
- **Shorter integration save button.** The integrations credential form's button now simply reads "Save".
- **Tidier API-tokens panel.** The token endpoint URL is shown inline with a copy button, and the "generate another token" button matches the rest of the page rather than standing out in a different colour.
- **The export panel's create button is right-aligned**, consistent with other primary actions.
### Removed
- **The moodLog integration** has been removed from the Integrations page; it is no longer used.
- **The standalone sharing (create-link) feature** has been removed from the Export area.
- **The doctor report is no longer offered twice** — it has been removed from the main Export page, where it duplicated the health-record export's own doctor report.
## [1.15.5] — 2026-06-06 — insights layout, navigation, password-reset CLI
### Added
- **Reset a password from the command line.** Operators can reset a user's password from inside the container — useful when someone is locked out — via `docker compose exec app node scripts/reset-password.mjs `. See `docs/ops/password-reset.md`.
### Changed
- **A tidier health-scores row.** With cycle tracking on, your cycle ring takes the place of the strain ring in the Insights health-scores row, so the row stays compact rather than growing an extra tile.
- **Cycle sits with your other trackers.** The Cycle entry in the sidebar now appears between Medications and Insights, rather than at the very bottom.
### Fixed
- **No more empty gap on the Insights overview.** The cycle summary card on the Insights page was held invisible by a style rule yet still took up space, leaving an odd gap above and below it; it now appears as intended and the spacing is even.
### Added
- **Your cycle ring in the health-scores strip.** With cycle tracking on, the Insights health-scores row now includes your cycle ring — current phase and day — alongside the other scores. It shows only when cycle tracking is enabled.
### Changed
- **A calmer cycle calendar and cards.** Predicted-period days are now a soft filled marker instead of a thin underline; the "what's happening now" card sits only under the ring (no duplicate above the calendar) and drops its own log button since logging is one tap away at the top; and the predictions and insights tabs lost their repeated "estimates / associations" footers (the caveat is stated once, where it matters).
- **Tidier settings.** Section descriptions line up under their headings; the web-push enable action sits in the card header; and the standalone cycle-data export/delete actions are gone from the cycle settings (cycle data is already in the full export).
- **Calendar tiles keep their size.** The mood and medication calendar tiles no longer balloon to several times the height of their neighbours when only a short window or a single medication is shown.
### Fixed
- **Per-score explanations load faster.** The dashboard's batch read now carries each score's deterministic explanation, so the score sheets show their "why" without an extra request.
### Added
- **Your cycle at a glance on Insights.** If you track your cycle, the Insights page now shows a compact card with your current phase and cycle day plus the one standout phase relation, with a tap-through to the full cycle insights. It appears only when cycle tracking is on, and the detailed phase analysis stays in the cycle section.
## [1.15.2] — 2026-06-06 — cycle insights crash fix
### Fixed
- **Cycle insights no longer crash on mood or blood-glucose relations.** The insights tab could throw and fail to render when a mood or blood-glucose pattern cleared the significance check, because those two metrics were missing their on-screen labels and units. They now render correctly, and an unrecognised metric can never crash the tab again.
## [1.15.1] — 2026-06-06 — cycle premium parity, reminder polish, fixes
### Added
- **Know your phase.** A "what's happening now" card next to the cycle wheel names your current phase, describes it in plain language, and surfaces the symptoms you yourself tend to log in that phase — drawn from your own history, never generic claims — with a one-tap shortcut to log today. It stays in a calm "still learning" state until there's enough of your data to be honest about.
- **Your own symptoms.** Add custom symptoms beyond the built-in set, with a name and an icon, straight from the log sheet. Custom symptom names are encrypted at rest, included in export, and removed by the cycle purge.
- **Fertile-window reminder.** An opt-in reminder a couple of days before your predicted fertile window — shown only when your goal is to conceive, off by default, and discreet-mode aware. It is an estimate, never a contraceptive or "safe day" claim.
- **A richer cycle calendar and history.** Period days now shade by flow intensity, a confirmed ovulation is marked distinctly from a predicted one, and a new cycle-length history chart shows your recent cycles with average, variability, and a regular/irregular read.
### Changed
- **Reminders read as plain text.** Medication, mood, and cycle reminder notifications no longer carry decorative emoji; emoji are kept only for system and failure alerts.
### Fixed
- **Blood-pressure-in-target reads accurately.** The "in target" share on the home health score now pairs systolic and diastolic readings the same way the rest of the app does, so imported readings whose timestamps differ by more than a few minutes are no longer dropped from the calculation.
- **A dose is never shown as taken before its time.** Logging a dose can no longer attribute it to a later slot still in the future — a late morning dose will not appear as the evening dose taken early.
### Security
- **Closed a server-side request risk in Web Push.** Push subscription endpoints are now validated against internal-network addresses on save and again before delivery, and the cycle-insights endpoint is rate-limited.
## [1.15.0] — 2026-06-06 — cycle tracking
### Added
- **Cycle tracking.** A new, private menstrual-cycle vertical. Log your period (flow and intermenstrual spotting), basal body temperature, cervical mucus, ovulation and pregnancy/progesterone tests, contraception, intercourse, and graded symptoms — all from one quick-entry sheet. A month calendar marks logged days, the predicted period window, the fertile window, and ovulation; the cycle wheel shows your current day and phase at a glance, and a logged day can be edited or deleted.
- **Honest predictions.** Next-period and fertile-window forecasts are always shown as a range with a confidence level — never a single certain date — and carry a clear, non-medical caveat (with a stronger one when you track to avoid pregnancy). A "read your body" mode suppresses every interpretation if you prefer to see only your raw data. The forecast is computed from your own data so the iOS client produces the identical result offline.
- **Basal-body-temperature chart** for the current cycle, with your fertility signs in the tooltip and the estimated — or confirmed — ovulation marked.
- **Phase insights.** An insights tab relates your cycle phase to your vitals — resting heart rate, HRV, sleep, steps, weight, blood glucose, mood — showing where each differs between phases with statistical significance, and which of your logged symptoms cluster in which phase. Associations only, never cause. A per-cycle history lists your recent cycle and period lengths.
- **Privacy first.** Cycle tracking is off by default except for accounts set to female, and anyone can turn it on or off from settings. The intent-revealing fields are encrypted at rest behind a default-on toggle; a discreet-notifications mode keeps any reminder generic on the lock screen; period reminders are opt-in per channel; and a one-tap purge hard-deletes every cycle row, the cycle audit trail, and reminder records, and clears your tracking intent.
- **Apple Health import.** Reproductive-health categories from an Apple Health export fold into your cycle log, and cycle data is included in the health-record export (PDF and FHIR).
## [1.14.0] — 2026-06-06 — animated health scores, score explanations, mood-factor correlations, log past doses
### Added
- **Each health score now explains itself.** The Readiness, Sleep, Recovery, Strain and Stress score sheets carry a short, plain-language read of why the score landed where it did, drawn from the inputs that moved it. It fills even without an AI provider configured, so the explanation is always there.
- **See how your rated factors track your body.** When you rate factors like work, sleep quality or stress on a mood entry, HealthLog now relates those ratings to your vitals — for example how your sleep or resting heart rate differs on the days you rate work low versus high — as a new card that the assistant can also cite. Associations only: each is shown with a confidence level, computed over a stable window, and never presented as cause.
- **Log a dose you took earlier.** The medications "Add" button now offers a choice — create a new medication, or log an intake — and the intake path accepts a backdated date and time against an existing medication.
### Changed
- **The health scores got a signature redesign.** The score strip reveals with a calm, staggered animation — each ring springs to its value while the number counts up — with a distinct colour per score (readiness green, recovery teal, sleep blue, stress amber, strain violet), a soft glow, and a faint "vs your normal" reference arc on the scores that carry a history. The motion plays once per visit and fully respects reduced-motion; the colours hold their contrast in both light and dark themes.
- **Sparse medications show where you stand at a glance.** A weekly injection or other "every N days" medication now shows its open-cycle status on the card — "next dose in N days", "due today", "overdue", or "no cycles yet" — instead of leaning on a percentage that says little between doses.
- **Smaller refinements across Insights.** The three trend cards (mood, weight, blood pressure) are now exactly the same height; the daily-briefing heading sits above its card; and the assistant tone and length controls sit side by side to cut scrolling.
## [1.13.3] — 2026-06-05 — health score scores blood pressure again, steps in Insights
### Fixed
- **The health score rates your blood pressure again.** The score was reading your blood-pressure-in-range rate from the trailing 30 days, but the score is meant to use your full history — so an account with blood-pressure history but no readings in the last month lost the whole blood-pressure pillar and showed "no rating". The score now reads the all-time rate (the at-a-glance tile headline still shows the last 30 days). If your blood pressure or weight still shows no rating, check that your date of birth (needed for the blood-pressure target) and height + target weight are set in your profile — those are required for the respective pillars.
### Added
- **Steps now have their own Insights page.** "Activity / Steps" was tracked and shown on the dashboard but never appeared in the Insights metric navigation; it now has its own Insights sub-page like the other activity metrics, surfaced whenever you have step data.
## [1.13.2] — 2026-06-05 — honest weekly-med compliance, correct sleep average, calmer wellness scores
### Fixed
- **Weekly injections (and other "every N days" medications) report honest adherence.** A rolling-cadence medication — the typical weekly GLP-1 injection — only ever counted its single next-due dose, so the rate flipped between a meaningless 100% and a hard 0% on a single dose and ignored a faithful history of shots. Compliance now reconstructs the full per-cycle history across the window: every logged dose counts, a genuinely skipped cycle reads as one miss, and the current cycle that simply hasn't come due yet no longer drags the number to zero. A new per-medication "current cycle" state (on track / due today / overdue / no cycles yet) lets the card show "next dose in N days" instead of a scary 0%.
- **The sleep average is correct again.** When a device wrote both a combined "asleep" block and the detailed light/deep/REM breakdown for the same night, the average double-counted them and could show an impossible figure (e.g. ~20 h). The average now reads from the de-duplicated per-night totals. The night-detail reconstruction is also hardened so it never errors on an unusual night.
### Changed
- **The wellness scores look calmer and clearer.** The "Your health scores" tiles (renamed from the old label) now use a gentle, card-toned surface instead of a heavy saturated slab, and each score draws a larger, thinner ring with a soft single-colour gradient tuned per score (readiness greener, sleep bluer, recovery and strain turquoise). The number and band stay fully legible in both light and dark themes.
## [1.13.1] — 2026-06-05 — wellness rings show the real score
### Fixed
- **The wellness-score rings now fill to the actual score.** Each ring was sweeping a full circle regardless of its value, so every score looked like 100%. The ring now fills proportionally — a 74 fills roughly three-quarters of the circle — across the overview tiles and the score detail view.
## [1.13.0] — 2026-06-05 — your own mood tags
### Added
- **Make the mood-tag list your own.** You can now create your own custom mood tags, give each a label and an icon, and hide any of the built-in tags you don't use — so the tag picker shows exactly the set that fits you. Custom tags work everywhere a built-in tag does: logging, history, and the analysis. Custom tag names are encrypted at rest like the rest of your personal content. Deleting a custom tag keeps it on your past entries by default; a separate purge removes it from history too.
## [1.12.12] — 2026-06-05 — the period retrospective is never empty
### Added
- **The period retrospective fills in even without an AI provider.** When no AI provider is configured (or before the first AI summary has been written), the "period in review" card now shows a concise, factual summary built from your own data — the biggest changes versus the prior period, any vitals that moved outside your typical range, and an honest, strictly non-causal count of statistical associations. It reads in German or English and is replaced in place the moment an AI summary is generated.
## [1.12.11] — 2026-06-05 — settings notices read the same everywhere
### Changed
- **Settings notices and status colours are unified.** Every callout, badge and status note in Settings now draws from the same set of meaning-based colours — success, caution, info — so a green "connected" or an amber "needs attention" looks identical from one card to the next and stays legible in both light and dark themes. Caution notes share one consistent boxed style with a leading icon.
## [1.12.10] — 2026-06-05 — a "taken" tap no longer back-fills the wrong dose
### Fixed
- **Logging a dose "taken now" no longer marks a far-away scheduled dose.** For a medication taken more than once a day, a quick "taken" log without a specific time (including a synced record) was snapping onto the nearest scheduled slot up to six hours away — so a midday log could mark the morning dose taken. A timeless "taken" log now records as its own entry and never back-fills a distant slot; logging a specific dose by its time is unchanged.
## [1.12.9] — 2026-06-05 — a tighter mood catalogue, clearer cards, and a fixed retrospective
### Changed
- **The mood factors are down to three sliders** — work, sleep quality, and a new sadness slider — and the day-tag list is trimmed to a tighter, higher-signal set. Tags you've already used stay on your past entries; they just no longer clutter the picker.
- **Medication cards read cleaner.** The next-dose and last-dose lines are now their own rows: the label on the left, the time flush to the right.
### Fixed
- **The period retrospective generates again.** A non-measurement metric was leaking into a measurement query and failing the background job; it's now filtered out.
### Documentation
- **Every code directory has a README**, the main README embeds the architecture diagrams and a documentation index, and the contributor guide reflects the current trunk-based release model.
## [1.12.8] — 2026-06-05 — fuller WHOOP data, charts that follow your selection
### Added
- **WHOOP now records more of what it measures.** Daily average heart rate, daily max heart rate, and per-night sleep disturbances are imported alongside the recovery, sleep, cycle and workout data already synced.
### Changed
- **The stats above a chart follow the range you pick.** Choosing 7 / 30 / 90 days or a year now recomputes the min / max / median / average for exactly that span — the numbers always match what the chart shows. The separate draggable range slider is removed.
- **Every chart leads with the same titled header** as the rest of the Insights tiles, for one consistent layout.
- **The wellness dials are refreshed** — a white ring on a gradient tile, matching the greeting card.
- **On the pulse page, the assessment sits above the cardio-fitness link**, and the assessment card's heading sits closer to its text.
### Fixed
- **AI insights use the current default models consistently** across every path, so the shared-key experience matches the rest.
### Added
- **The stats above a chart now follow your selection.** Brush a window on a metric chart and the min / max / median / average recompute for just that span; clear the selection to return to the full range. Works the same on every metric page, and blood pressure now shows systolic and diastolic in one card, side by side.
### Changed
- **The mood page reads top to bottom the way you'd walk it.** Summary, then the mood calendar, then the chart, then the target, then the assessment — followed by the breakdowns. The two "what stands out" sections are merged into one card, the statistical footnote is gone (the detail lives in a small info tooltip), and the mood colours are aligned to the rest of the charts.
- **VO₂max opens its full chart.** The compact row on the pulse page now links to the dedicated cardio-fitness page instead of showing a chart-less summary.
- **AI assessments vary more across your metrics.** The anti-repetition context now reaches the main cards (blood pressure, weight, pulse, mood, BMI, medication, overall), a step is only suggested when the finding implies one, and the default models were refreshed so the shared-key fallback isn't a quality drop.
### Fixed
- **Status colours stay legible on the light theme.** Connection states, recommendation badges, score provenance, and the settings success messages now use tokens that meet contrast in both themes.
- **A late device sync no longer shrinks a day's step or energy total.** Trailing samples are folded into the existing daily total instead of replacing it.
- **The medication streak is correct across time zones**, and the dashboard now serves its snapshot instantly after a sync while refreshing in the background.
- **Metric pages and the Insights overview show a retry on error** instead of a blank or a false "no data yet".
- **Account profile:** the timezone selector fills its row, and the "globally excluded injection sites" card matches the other cards.
## [1.12.7] — 2026-06-05 — Insights that react, read clean, and stay legible
### Added
- **The stats above a chart now follow your selection.** Brush a window on a metric chart and the min / max / median / average recompute for just that span; clear the selection to return to the full range. Works the same on every metric page, and blood pressure now shows systolic and diastolic in one card, side by side.
### Changed
- **The mood page reads top to bottom the way you'd walk it.** Summary, then the mood calendar, then the chart, then the target, then the assessment — followed by the breakdowns. The two "what stands out" sections are merged into one card, the statistical footnote is gone (the detail lives in a small info tooltip), and the mood colours are aligned to the rest of the charts.
- **VO₂max opens its full chart.** The compact row on the pulse page now links to the dedicated cardio-fitness page instead of showing a chart-less summary.
- **AI assessments vary more across your metrics.** The anti-repetition context now reaches the main cards (blood pressure, weight, pulse, mood, BMI, medication, overall), a step is only suggested when the finding implies one, and the default models were refreshed so the shared-key fallback isn't a quality drop.
### Fixed
- **Status colours stay legible on the light theme.** Connection states, recommendation badges, score provenance, and the settings success messages now use tokens that meet contrast in both themes.
- **A late device sync no longer shrinks a day's step or energy total.** Trailing samples are folded into the existing daily total instead of replacing it.
- **The medication streak is correct across time zones**, and the dashboard now serves its snapshot instantly after a sync while refreshing in the background.
- **Metric pages and the Insights overview show a retry on error** instead of a blank or a false "no data yet".
- **Account profile:** the timezone selector fills its row, and the "globally excluded injection sites" card matches the other cards.
## [1.12.6] — 2026-06-05 — one consistent Insights surface
### Changed
- **Every Insights card now reads the same way: an icon, a title, then the content.** Targets, the systolic and diastolic stats, the wellness and vital tiles, mood, and medications all lead with the same titled header in the same weight, so the surface is easier to scan and find your place in. The target tile is simply titled "Target".
- **The wellness scores sit above the daily briefing, with legible numbers.** The score numbers now render in the foreground colour against the dark cards (the band still shows on the ring), and the strip leads the overview above the briefing.
- **The min / max / median / average stats moved above each chart.** They now lead the metric page rather than trailing it, and blood pressure shows the full stat strip for both systolic and diastolic.
- **The mood page leads with where you stand, then what fits your better days.** The classification (in-range share and stability) comes first, followed by the better-days read; the mood colours are aligned to the rest of the charts.
- **The medications page is tighter.** The dose-by-dose therapy list is gone, the per-medication compliance tiles are denser, and the assessment sits directly under the target.
### Fixed
- **The medical disclaimer appears once.** The "describes your own data, not a clinical assessment" line was repeated across many tiles; it now shows once as a single page footer (and no longer doubles up on the Coach page, which carries its own note).
- **Medication status counts meet contrast on the light theme.** The taken / skipped / missed figures now use the semantic colour tokens, which stay legible in both themes.
### Documentation
- **Provider setup is documented end to end.** The WHOOP guide now pins the exact OAuth scopes and the webhook URL format, and a new Google Health (Fitbit & Pixel) guide covers the Google Cloud OAuth client, scopes, and redirect URI. See `docs/integrations/`.
## [1.12.5] — 2026-06-05 — correct sleep summary and readiness contributors
### Fixed
- **The sleep summary now reflects the whole night, not a single stage.** The Insights sleep figure and prose were reading one sleep stage (~16 minutes) instead of the reconstructed night total; they now use the night's time-asleep, the same number the dashboard and charts already show.
- **Readiness contributors no longer read a misleading 0.** Resting heart rate, heart-rate variability, and respiratory rate are hidden (rather than shown as 0) when there isn't yet enough history to establish a baseline; a genuine low sub-score still shows.
## [1.12.4] — 2026-06-05 — a calmer, more consistent Insights
### Changed
- **The Insights overview is reordered and easier to read.** Top to bottom: the morning greeting + Coach, then your wellness scores (now filling the row), vital values, trends, the period retrospective, and the signals of the day. The wellness numbers are legible on the dark theme, the steps figure in the daily briefing now opens its readings, the Coach suggests two focused questions, and the duplicate "prepare assessment" button at the foot of the page is gone.
- **Every metric page now follows one layout.** Intro → chart → target → assessment → the min / max / median / average stats below it. The target tile matches the other cards, blood pressure gains the same stat strip the other metrics have, the redundant range / "vs. previous" row is removed, and the "last measurement" card no longer clutters the page.
- **The mood page is tighter.** The little insight one-liners now sit side by side instead of each taking a full row, and the statistical explainers collapse into a small info icon you can hover or focus for the detail.
## [1.12.3] — 2026-06-05 — mark the dose you're looking at
### Fixed
- **Marking a dose "taken" now records the dose on screen, not the one nearest the clock.** For a medication taken more than once a day (e.g. morning and evening), the "Taken" button now targets the specific dose the card is showing, instead of snapping to whichever slot is closest to the current time — so a morning tap can no longer land on the wrong dose.
## [1.12.2] — 2026-06-05 — WHOOP connect from the app, consistent assessments and medications
### Added
- **WHOOP can be connected from the native app.** The OAuth handshake now works for an app that holds no web session: the client obtains a short-lived one-time connect token and opens the WHOOP authorization in an in-app browser that returns straight to the app. (No change for the web flow.)
### Changed
- **One word for "assessment", everywhere.** The AI assessment was labelled four different ways across the app; it now reads consistently in every language, and the assessment is always the final block on a metric page.
- **Every medication behaves the same when you log a dose.** Marking a GLP-1 dose taken now shows the same failure notice and one-tap undo that the other medication types already had, and medication status colours, the compliance percentage, and tap-target sizes are consistent across the cards.
### Operations
- **An alarm when the TLS certificate's public key changes.** Self-hosters whose native clients pin the server certificate now get a loud, logged alert (and an admin notification) when the served leaf key rotates, so the pin can be refreshed before it lapses. Set the expected pin(s) via `TLS_LEAF_SPKI_PINS`; see `docs/ops/tls-cert-pin.md`.
## [1.12.1] — 2026-06-05 — security, data-integrity, and insight-quality hardening
### Changed
- **Consent is now enforced on the server before any health data reaches a server-managed AI provider.** When the app uses an operator-provided AI key to generate insights, the assistant, or medication extraction, it first requires an active consent receipt — consent no longer depends on the client asking nicely. Bring-your-own-key and local providers are unaffected (the key is your own).
- **Assessments read less repetitively and more relationally.** A per-metric card can now bring in a genuine correlation from your own data (shown as an association, never a cause), varies its wording instead of repeating a near-identical paragraph when a metric has been steady for weeks, and hedges honestly when there is little data rather than over-claiming.
- **Settings feel like one surface.** The integration, account, sharing, and API cards share one header, the same button names and behaviour (so "Sync now" / "Sync all data" / "Test connection" mean the same thing everywhere), consistent tap-target sizes, and consistent spacing.
- **Faster settings and insights.** The integrations screen now loads its status in a single request instead of one per provider, the mood insights stay warm instead of recomputing from cold on every entry, and below-the-fold charts load on demand.
- **Sturdier Google Health / Fitbit sync.** A reading you delete stays deleted instead of reappearing on the next sync, large backfills write in batches, and the hourly poll no longer lets one slow account stall the rest.
### Fixed
- **Re-taking a dose after deleting it works again.** The medication intake history no longer blocks a legitimate re-entry against a deleted row.
- **Mood entries can round-trip without duplicating.** Mood entries now carry a stable identifier, so re-importing the same entry updates it in place instead of creating a second copy.
- **Tag-to-metric comparisons no longer double-count.** When the same day's steps, energy, or sleep arrive from more than one source, the mood tag comparison counts them once.
- Logging out now revokes a native session's token immediately, a long-lived refresh token is never handed to a browser, and a few authentication edge cases were tightened.
## [1.12.0] — 2026-06-05 — Google Health sync, rated mood factors, and a Coach and insights overhaul
### Added
- **Google Health (Fitbit & Pixel) — experimental.** Connect a Google Health account through your own Google client and pull in activity, body, sleep, and workout data: steps, distance, active energy, floors, and VO₂max; weight, body fat, SpO₂, resting heart rate, heart-rate variability, respiratory rate, and wrist temperature; sleep stages; and workouts. The card works like the others — test the connection, sync now, sync everything, and reconnect after a pause. This integration is experimental: Google's health API is young and coverage for some data types is still being verified.
- **Rated mood factors.** Alongside the yes/no mood tags you can now score factors on their own scale per entry — work, social, sleep quality, stress, conflict, and family — so a day can carry "stress 4, sleep quality 2" rather than a plain tag.
- **Mood logging, rebuilt.** Capturing a mood now opens with a five-face picker, then a category-grouped tag picker and the factor ratings, replacing the old form.
- **Hobbies and nutrition mood tags** — films, reading, gaming, music, outdoors, and fast food, no sweets, a big meal.
- **How your tags line up with your health metrics.** The mood Insights now compares a health metric on the days you tagged an activity against the days you did not — active energy on workout days, sleep length on the nights you tagged sleep, next-day recovery after a tag like alcohol or food. Each row shows the difference, its confidence, and is framed as an association in your own data, not a cause; only relationships that clear the same statistical filter the rest of the relations surface uses are shown.
- **A full-page Coach.** The Coach conversation can expand to its own full page from a maximize control.
- **A quicker way to log.** Navigation gains a central capture action that opens a measurement, medication, or mood entry from anywhere, and a "More" hub gathers the less-frequent sections.
- **A dashboard settings shortcut** — a wrench beside "Add" jumps straight to the dashboard layout settings.
### Changed
- **The Coach reads like a chat.** Replies stream in as they are written, past conversations and the context behind an answer collapse out of the way, and the disclaimer appears once.
- **Insights, de-duplicated.** The overview no longer repeats what the per-metric pages already show, and each metric page follows one consistent layout — a primary value, the last reading, the average/min/max/median strip, the chart with its range control below it, the metric's own correlation, and the assessment last — with clearer "what stands out" wording.
- **A refined injection-site body map** with corrected proportions and a legend for the recommended and last-used markers.
- **Export, reordered.** The health-record (FHIR) export is now the primary export, the doctor-report PDF moves below it, and the included-data list collapses by default.
- **The unit system lives in your profile** as a dropdown beside timezone, treated like a preference such as language.
- **Google Health and the other wearables share one source-priority ladder**, so when the same metric arrives from several sources the most appropriate one wins per metric.
- **The standalone moodLog integration is deprecated.** Mood is now tracked fully inside HealthLog with entries, structured tags, and rated factors, so the external bridge no longer adds anything. It keeps working for existing setups but is slated for removal in a future major release.
### Fixed
- **Weekly medications no longer read 0% compliance.** A weekly-cadence medication with recorded intakes now reports the correct rate instead of zero across the 7-, 30-, and 90-day frames.
- **Twice-daily oral doses record independently.** Marking an evening dose no longer collapses onto the morning slot, so both land in the history.
- The Google Health sync reads from a single point in time per cycle, so a gap after downtime isn't silently skipped, and a connection returned without a refresh token is rejected cleanly rather than saved half-formed.
## [1.11.5] — 2026-06-04 — mood relations, sleep depth, and a clear-the-decks polish pass
### Added
- **What lifts your mood, and what your better days have in common.** Insights now learns from your own logs: it compares your average mood on days with a given activity or tag against days without it, surfaces a ranked "what's associated with your better days" board across both your tags and your tracked metrics, and lists the day-to-day relationships that survive a statistical filter. Every result shows its strength and is framed as an association in your own data — a pattern worth watching, not a cause.
- **Last night, stage by stage.** The sleep section now draws a hypnogram of the most recent night — the progression through deep, core, REM, and awake — colour-coded by stage, alongside the per-stage breakdown.
- **WHOOP: test the connection and reconnect, just like Withings.** The WHOOP card now has a "Test connection" button that checks the link live, and — if the connection was paused after repeated errors — a one-tap reconnect.
### Changed
- **Adjust your targets from the header.** The target-range editor now opens from a gear next to the Coach button instead of a link buried in the tile.
- **Account settings gather the personal preferences.** The unit system (metric or imperial) and your globally excluded injection sites now live under Account rather than Dashboard.
- **Sleep reads as one value per night everywhere.** The measurements list, the per-metric sleep view, and the CSV export now collapse a night's individual stages into the night's total, with naps surfaced separately, matching what the tile and chart already showed.
- Full Spanish, French, Italian, and Polish coverage for the remaining Insights, admin, and HealthKit-label copy that previously fell back to English.
### Fixed
- **Sleep no longer double-counts an Apple Health night.** When Apple Health writes both an overall "asleep" figure and the deep/core/REM breakdown for the same night, the per-stage view and hypnogram now use one or the other rather than adding them together, and a nap on the same day no longer falls out of the night's total.
- **The Insights assessment reads cleaner.** The "cached" tag is gone, "show more" appears only when there is more to show, and the last-updated time is right-aligned.
- The "Signal of the day" and "Period at a glance" cards no longer leave a tall band of empty space below their content.
- A revoked WHOOP authorization is now recognised as needing reconnection instead of being retried as a transient error, and a WHOOP plan that shares none of a sync cycle's data no longer looks like a successful sync.
- The quick-entry sheet now asks before discarding mood ratings and dropdown changes, not only typed text.
- Settings: copying an API token confirms the copy, the destructive-action dialog shows progress while it works, and a failed token revoke surfaces an error.
## [1.11.4] — 2026-06-04 — sleep totals, trend captions, and a round of fixes
### Fixed
- **Sleep now shows the whole night.** The sleep tile and sleep chart previously showed a single sleep stage's minutes; they now show the night's total time asleep, grouped by sleep session so a night that crosses midnight counts as one night, with the unit shown explicitly. When two sources report the same night, the higher-priority one is used rather than adding them together.
- **The daily briefing no longer makes the Insights overview wait.** The overview reads the existing briefing immediately and refreshes it in the background instead of blocking the page on generation.
- **Recent achievements no longer flash in and vanish.** Cards that depend on your layout or your data now wait for that to load before appearing, so nothing shows up only to be retracted a moment later.
- **Charts with only a day or two of data now show those points** instead of withholding them behind a "more days needed" card. A short series still notes that more days will fill out the trend.
- **A reading logged by hand and mirrored to Apple Health no longer appears twice.** The same physical reading arriving from both sources is merged into one.
- Long wellness-score labels (for example "Sleep score") no longer clip.
- The mood-by-time-of-day chart no longer overflows its card.
- The Trends row cards are now equal height and the mood card's axis labels stay inside the card.
### Changed
- **The Trends row now describes the actual trend.** When there is no written summary yet, each trend card states the direction and size of the change over the period (for example "rising over 30 days") drawn from your own data, instead of always showing "awaiting more data."
- **Settings → Advanced:** the erase-data and delete-account buttons now sit beside their descriptions, matching the rest of the app.
## [1.11.3] — 2026-06-04 — WHOOP body data, quality-of-life polish, fuller translations
### Added
- **WHOOP body data.** WHOOP weight and maximum-heart-rate context now flow in alongside recovery, sleep, cycle, and workout data. Weight is ranked below a real scale by your source priority, so a connected scale or manual entry still wins; your profile height is filled in from WHOOP only if you haven't set one yet.
- **Stop a Coach reply mid-stream.** A Stop control appears while the assistant is answering, and closing the Coach drawer now ends an in-flight response instead of leaving it running.
- **Undo a logged dose.** The confirmation after taking or skipping a dose now offers an Undo, so a mis-tap is one tap to reverse instead of a trip into the history.
### Changed
- **More of the app speaks your language.** Spanish, French, Italian, and Polish now cover the Settings, Medications, Insights rhythm-event, and Achievements copy that previously fell back to English.
- **A WHOOP plan that doesn't expose every data type no longer breaks the connection.** A data class WHOOP declines to share is skipped on its own while the rest keeps syncing.
- The first Coach reply on a cold cache arrives faster — its inputs are now gathered in parallel.
### Fixed
- **A failed dose log is no longer silent.** If recording a dose doesn't go through, you now see an error instead of a confirmation for something that didn't happen.
- The quick-entry sheets only ask before discarding when you have actually typed something.
- A failed export now shows a clear message instead of a raw error code.
- Saving feedback on an Insights recommendation surfaces an error if it can't be stored.
- Several loading placeholders now reserve their final height, removing the small layout shifts on the Insights and dashboard surfaces as content lands.
### Accessibility
- Reduced-motion now suppresses the Insights card entrance animation.
- The API-token revoke button carries an accessible name and a larger target, and the medication scheduler's step controls have a larger touch target.
### Notes
- WHOOP weight is the value stored in your WHOOP profile, not a scale reading; it is ranked last among weight sources and only surfaces when nothing higher-priority is present.
## [1.11.2] — 2026-06-04 — Coach-memory controls, privacy hardening, more pinnable metrics
### Added
- **Review and clear what the Coach remembers.** A new "What the Coach remembers" settings section lists the durable facts the assistant has learned about you, grouped by category, and lets you forget a single one or clear all of them. Encrypted at rest; available wherever the Coach is enabled.
- **More dashboard metrics you can pin.** Cardio recovery, six-minute walk distance, stair ascent/descent speed, breathing disturbances, wrist temperature, fall count, and walking steadiness can now be pinned to your Home layout.
### Security
- **Outbound request hardening.** The AI provider clients (when pointed at a custom base URL) and the optional IP-geo lookup now reject any address that resolves to a private or internal host, closing a server-side request-forgery / DNS-rebinding gap.
### Fixed
- A rare write race when two updates recompute the same day's rollup at once is now handled cleanly instead of surfacing a transient error.
- Analytics fast paths resolve your source-priority preference once per request instead of repeatedly.
## [1.11.1] — 2026-06-04 — source-aware vitals + Coach long-term memory
Closes the three follow-ups left open by v1.11.0: cross-source vital de-duplication, the Coach's conversation memory, and durable personal facts.
### Added
- **Coach long-term memory.** The assistant now keeps a private, encrypted rolling summary of long conversations and remembers durable facts you have told it — standing preferences, goals, constraints, and conditions you have stated about yourself. Both are descriptive recall, never a diagnosis, and ride the same Coach setting; turning the Coach off stops all of it.
- **A facts surface to review and clear what the Coach has learned.** List your active facts, forget a single one, or forget everything (`GET` / `DELETE /api/insights/coach/facts`, `DELETE /api/insights/coach/facts/{id}`). Facts are stored encrypted at rest.
### Changed
- **Measurement rollups are now source-aware.** When two sources report the same standard vital on the same day (for example a resting heart rate from two devices), charts, summaries, and insights now show the higher-priority source's reading instead of a blend of the two. Cumulative metrics such as steps and energy continue to total per source. Your source-priority settings decide which reading wins.
### Notes
- The "latest" value shown for a vital with two competing sources now follows your source priority for the most recent day, so it matches the chart line. For some metrics this may surface a different device's reading than before.
- After clearing a Coach fact it can take up to a minute to disappear from the assistant's working context, in line with the existing snapshot refresh window.
- On first start after upgrading, each account's rollup cache is rebuilt once in the background; charts fall back to a live read until it converges.
## [1.11.0] — 2026-06-04 — WHOOP, a longitudinal coach, and a clinician-grade record
A multi-feature milestone across three fronts: a second connected provider, deeper Insights, and a shareable clinical record.
### Added
- **WHOOP integration.** Connect a WHOOP account with your own developer keys to bring its recovery, day- and workout-strain, sleep-performance, HRV (RMSSD), and energy readings into HealthLog alongside Apple Health and Withings — synced on a schedule and via signed webhooks, each value kept distinct by its source so it never overwrites a reading from another device.
- **A longitudinal Insights coach.** A weekly/monthly narrative summarises what changed over the period and the likely contributing factors (descriptive, never causal); the Coach now carries a rolling profile of your recent baselines and trends; and a short-horizon trajectory projection shows where a metric is heading with an honest, widening confidence band — shown only when there is enough history to mean something.
- **A clinician-grade health record.** A read-only FHIR REST API (`GET /api/fhir/*`) serves your data as standard FHIR R4 resources, and you can mint a scoped, time-limited, revocable share link that opens a clean read-only clinician view — no account needed. Wellness figures stay fenced off from the clinical ones with a plain "not a clinical assessment" note.
- **More resilient AI generation.** A durable provider-health record skips a known-bad credential instead of failing every run and surfaces an expired credential proactively, with a local model as a guaranteed fallback.
### Known limitations (planned follow-ups)
- When two sources supply the same standard vital (e.g. WHOOP and an Apple Watch both report resting heart rate), both readings may currently appear for a day until a source-aware aggregation update resolves them to your preferred source. WHOOP's own scores are unaffected; a note explains this on the WHOOP card.
- The coach reflects your rolling health profile and trajectory; full conversation-summary memory and saved personal facts are a later refinement.
## [1.10.4] — 2026-06-03 — Strain honesty + six-minute-walk caveat
### Changed
- **The Strain reading now says which scale it used.** When the score is anchored to your own recent training load it reads "relative to your typical effort"; while there is too little history and it falls back to a general reference, it says so — so the framing always matches the number actually shown.
- **The six-minute-walk band notes its reference range.** The reference equation was established for ages 40–80; outside that range the percent-of-predicted is now flagged as an extrapolation.
## [1.10.3] — 2026-06-03 — Personalised strain, a daily signal card, deeper derived metrics
### Added
- **A "Today's signal" card.** The coincident-deviation read — which notices when several of your vitals sit off their personal baseline at once — now leads the Insights overview as a calm daily card instead of a flag buried in the grid. It shows an all-clear on a normal day, names the vitals to keep an eye on when a few drift, and always frames them as possible factors, never a cause or a diagnosis.
- **Derived bands for more of what your watch measures.** Overnight wrist temperature and stair-climbing and stair-descent pace each get a personal typical-range band, and your device's estimated six-minute-walk distance is placed against a published reference for your age, height, weight and sex. Each is shown with its method and its cited standard, and only once there is enough history. They appear under a new "Mobility & body" group.
- **Trailing trend sparklines** on the derived tiles, drawn from the readings the tile already uses.
### Changed
- **The Strain score is now anchored to your own training load.** It reads how hard a day was relative to your own recent training-day effort rather than a fixed population figure, so a genuinely hard day reads high even while you are building back up; with too little history it falls back to a general reference and says so.
- Age-banded reference norms now interpolate across bracket boundaries instead of stepping, and the sleep midpoint is computed in your own timezone.
## [1.10.2] — 2026-06-03 — Honest AI connection test, consistent insights, retention re-enabled
### Fixed
- **The AI connection test now tells you what actually went wrong.** A failed test used to surface a cryptic parse error ("unexpected token") because the server answered with a gateway-level error page the settings screen could not read; worse, the test probed a different provider than the one your nightly insights are generated with, so it could report a problem with a fallback you do not even use. The test now always returns a readable, translated reason — rejected credentials, rate-limited, a provider server error, or unreachable — and it checks the same provider your insights run on, so the result is the truth about your setup.
- **Every insight metric page carries the same time-range controls.** The core-metric pages (weight, blood pressure, pulse, BMI, sleep) were missing the period selector that the other metric pages already had; they now match. Drill-down back-links are consistent and a "show all values" link returns you to the metric you came from.
- **The derived-metrics dashboard no longer collapses or jumps while loading.** It shows a placeholder row at the right height, keeps its heading hidden until there is something to show, and offers an inline retry if the data fails to load instead of silently vanishing.
- **More status colours meet contrast on the light theme** across the medication and admin surfaces.
### Changed
- **The intra-day retention drain is back on.** The storage optimisation behind the stress reading was switched off in 1.10.0.2 after a daily-summary collision kept the app cycling post-deploy. The fold now adopts the existing daily row deterministically and recovers cleanly if a concurrent write reaches the same slot first — verified against real data — and the start-up defer that protects the connection pool stays in place.
- **The batch-ingest and health-record export endpoints cap request-body size**, rejecting an oversized payload before parsing it.
### Added
- **A capability endpoint for API clients.** `GET /api/meta/capabilities` returns the server's live metric, tile, ingest and FHIR vocabularies alongside a contract version, so a client renders what it recognises and ignores what it does not — no more transcribing each release's new identifiers by hand.
## [1.10.1] — 2026-06-03 — Consistent medication cards
### Fixed
- **Every medication card looks the same now, whatever the type.** Oral, injection, GLP-1, as-needed, cyclic and one-time medications previously drew their "next / last intake" line, dose accent, spacing and compliance bars slightly differently, so cards in the same row could sit at different heights with the action buttons misaligned. The cards now share one layout: the same intake line, a reserved-height compliance area, and the take/skip buttons pinned to the bottom — so a row of mixed medications lines up cleanly. The dose accent also meets contrast on the light theme.
## [1.10.0.2] — 2026-06-03 — Disable the intra-day retention drain
### Fixed
- **Stops the post-deploy restart loop for good.** The intra-day retention drain (a storage optimisation behind the new stress reading) folded older samples into a daily summary, but on real data that summary collided with an existing daily row and the drain failed on every run, which kept the app cycling after a deploy. The drain is now switched off until it is reworked and tested against real data; the nightly pass is skipped, nothing is queued at start-up, and any work already queued completes harmlessly. No user-facing feature is lost — the stress reading simply falls back to its sparse-data behaviour.
## [1.10.0.1] — 2026-06-03 — Defer the intra-day retention drain past startup
### Fixed
- **The app no longer turns unresponsive shortly after a deploy or restart.** The new intra-day retention drain ran its catch-up pass the moment the background worker started, at the same time as the database migration, the other start-up backfills, and the first health checks — all sharing one connection pool. On an instance with a lot of history this exhausted the pool, so the app stopped answering until it restarted, then repeated. The catch-up now waits until start-up has settled before it runs; it is unchanged otherwise and the nightly pass is unaffected.
## [1.10.0] — 2026-06-03 — Derived wellness metrics, device-event awareness, deeper ingestion
### Added
- **Derived wellness metrics, each shown with how it is worked out.** From data you already sync, the Insights surface now derives a personal typical range for a vital, a cardio-fitness band, a vascular-age delta, an HRV (SDNN) balance read, body-mass index, a sleep score, a readiness index, and a coincident-deviation flag that notices when several vitals sit off their personal range at once. Every metric states its inputs, its method in plain language, and the published standard it is grounded in, with an honest read of how much data backs it — never a bare number. A metric appears only when there is enough history to mean something.
- **Recovery, Stress, and Strain scores, computed each night.** Recovery blends your resting-heart-rate, HRV, sleep and respiratory trends against your own baseline; Strain reads the heart-rate load of your workouts; Stress is an HRV-derived proxy and is labelled as exactly that — not a sensor-measured stress reading. Each is stored as a first-class value you can chart, and each opens to a breakdown of its contributors and its cited basis.
- **More of what your watch already measures.** Cardio recovery, overnight wrist temperature, fall count, six-minute-walk distance, stair ascent and descent speed, and the sleep breathing-disturbance index are now ingested and surfaced.
- **An awareness timeline for device-flagged events.** Irregular-rhythm, high- and low-heart-rate, walking-steadiness, and breathing-disturbance notifications your device produced are shown as a timeline — strictly as awareness of your own device's certified result, never re-assessed and never a HealthLog diagnosis, with a permanent disclaimer to that effect. The section stays hidden until there is something to show.
- **A per-workout heart-rate series**, so an indoor workout with no GPS still yields a training-load signal.
- **A wellness summary in the doctor report.** The derived scores and metrics appear in their own section, kept out of the clinical vitals table, labelled descriptive rather than clinical, and exported to FHIR as `survey`-category observations with the same note — so a clinician's system never mistakes them for a vital sign or a diagnosis.
### Changed
- **The health-record export carries richer medication codings.** A `MedicationAdministration` now adds a SNOMED route and injection-site coding alongside the existing plain text, a German-locale export can append the German BfArM ATC coding next to the unchanged WHO ATC entry, and the number of administrations an export can carry is raised and made configurable by the operator.
- **The Insights overview reads its metrics through a single request** instead of one per tile, so opening the page no longer fans out into a burst of concurrent reads.
- **Score-band colours meet contrast on the light theme**, and the device-notification copy — including its medical disclaimer — is translated across every supported language.
## [1.9.3] — 2026-06-02 — Medication card layout polish
### Fixed
- **The medication cards look right again.** A recent alignment change pushed each card's action buttons to the very bottom of the cell, which left an empty gap between a shorter card's content and its buttons when it sat next to a taller card in the grid. The buttons now sit directly under the card's content again; the cards still share a height and the dose rows still line up, without the gap.
## [1.9.2] — 2026-06-02 — Document the medication compliance endpoint
### Changed
- **The medication compliance endpoint is now in the public API contract.** `GET /api/medications/{id}/compliance` was missing from the OpenAPI document, so a client generating its types from the contract could mistake the separate cadence endpoint (a different shape) for it. The route is now documented with its exact response — the 7- and 30-day adherence summaries, the per-day compliance grid, and the two-row display block — with the field a client should read for the headline 30-day rate and how to draw the per-day history. No behaviour change; the handler was already correct.
## [1.9.1] — 2026-06-02 — No assessment warm on a page visit
### Fixed
- **Opening the Insights overview no longer regenerates every assessment.** A returning visit fired a one-per-session background warm that asked the provider to regenerate the comprehensive briefing and every category and metric assessment at once — a burst that could run for minutes and make the whole app feel unresponsive while it ran, recovering on its own once it finished. Assessments are kept fresh by the nightly pass, and each metric's text refreshes gently on its own when you open it; the "prepare assessments" button still regenerates everything on demand. A page visit now only reads the cached text.
## [1.9.0] — 2026-06-02 — Insights time-ranges, deeper mood, medication coding, stability
### Added
- **A time range you can choose on the Insights pages.** Each metric now carries range pills (week, month, quarter, year) and shows how the period compares with the one before it — the change in your average, stated plainly with its direction — so a trend reads at a glance instead of by eye.
- **Deeper mood insights.** The mood page now surfaces your time-of-day pattern (when your mood tends to sit highest and lowest), a stability read, and correlation cards that line mood up against weight and blood pressure — each shown only when there are enough paired days to mean something.
- **Standard drug codes on a medication.** A medication can carry an ATC and an RxNorm code (entered, never guessed). These flow into the health-record export: the exported `MedicationStatement` now codes the drug with the WHO ATC system and RxNorm alongside the plain name, and the export adds a `MedicationAdministration` for every dose you actually took or skipped, so a clinician's system can read the record without relying on free text.
- **A deploy-verification script** (`scripts/assert-deploy.ts`) that checks a target reports the expected version after a release.
### Changed
- **The medication advanced-settings page is rebuilt.** Import, intake-import and export sit in their own group, the external-API endpoints are listed one by one with collapsible request examples, and the layout is tidied so the technical settings read clearly instead of crowding together.
- **Targets are edited inline where they belong.** The separate `/targets` page is retired — you set a metric's target range from the metric itself, with no detour to a standalone editor.
- **The health-record export emits insurer coverage from a bare insurance number**, not only when a full insurer organisation is present, and carries a top-level narrative summary.
- **Notification settings wording** is tightened for clarity.
### Fixed
- **The app stays responsive under load.** The database connection pool was sized far too small by default and could starve while a background insight warm and a foreground request competed, which showed up as the occasional "server not responding" that recovered on its own; the pool is now sized for real concurrency and tunable for larger self-hosts.
- **A background insight warm no longer blocks a page**, and a burst of Apple Health / Withings sync no longer triggers a storm of regenerations — the warm is decoupled and the invalidation is debounced.
- **The medication card rows stay aligned.** "Last dose" and "next dose" could break onto different heights and look misaligned; the rows now hold an equal height.
- **Glucose entered in mmol/L converts correctly** in the editor instead of being stored against the wrong unit.
- **The admin and global layout no longer shifts** when a scrollbar appears, by reserving the gutter.
- **The legacy status endpoints are documented** — six older `*-status` routes were missing from the API contract and are now included.
## [1.8.7.1] — 2026-06-02 — Assessments for every HealthKit metric, one-tap pre-generation
### Added
- **A plain-language assessment on every HealthKit metric page.** The HealthKit metric pages (resting heart rate, heart-rate variability, blood oxygen, glucose, body composition, gait, audio exposure, sleep, and the rest) showed charts but no written assessment, because the core metrics' bespoke per-metric prompts don't scale to thirty more. Each page now carries an assessment, generated from the metric's own normal range and the direction a healthy value moves, anchored on your individual baseline — the same treatment the core metrics already had. A metric with no data keeps its existing insufficient-data note.
- **One-tap pre-generation of every assessment.** A button on the Insights overview generates all assessments in the background so they are ready immediately the next time you open them. The overview also warms a cold cache on its own, and the nightly pass keeps everything fresh — so a page opens to its assessment instead of a wait.
### Changed
- **"Auswertungen" is now "KI-Auswertungen" / "AI Insights"** in the settings heading and the menu, so it's clear the section is AI-generated.
- **Targets and Sources are two separate settings pages** instead of one combined page, each with its own heading.
- **The back link sits above the heading** on a metric's detail page.
- **Dropdown fields have symmetric padding**, so the chevron no longer crowds the right edge.
- **The Insights layout accepts the full metric set**, so a saved layout (order and visibility) can include every metric page, not just the core ones.
### Fixed
- **Assessments generate again.** The assessment provider's accepted model identifiers had rotated, so generation failed and an assessment could sit on "preparing" indefinitely; the model ladder is updated and falls through to the current identifiers.
- **The Active Energy assessment loads.** Its page requested an assessment under a mismatched identifier and silently fell back to "no analysis yet"; the identifier is corrected and now type-checked, so the same class of mismatch is caught at build time.
- **The source-priority editor is fully translated** for Spanish, French, Italian and Polish — it was English-only after the settings split.
## [1.8.7] — 2026-06-02 — Instant insight assessments, GLP-1 blood-level chart, tile + caption polish
### Fixed
- **Insight assessments are instant again.** A category's assessment was being re-prepared on almost every visit: the constant Apple Health / Withings sync kept evicting the cached text, and a miss only ever deleted-and-waited. Assessments now serve the last good text immediately (stale-while-revalidate) and refresh in the background — "preparing" shows only when an assessment has genuinely never been generated. The background refresh is also debounced per metric, so a sync burst no longer triggers a storm of regenerations, and it warms only the active language (halving the work).
- **The dashboard tiles never truncate the value.** In a dense tile strip the number could clip or wrap mid-value (e.g. "130 mmHg" showing as "13"); the value now always renders in full and the unit yields the space instead.
- **GLP-1 medications show the drug-level-in-blood estimate by default.** The estimated concentration curve was hidden behind the Research-Mode opt-in and below the dose-strength curve; for GLP-1 medications it now shows by default, above the dose-strength curve, with the "educational estimate, not a measurement" disclaimer kept clearly attached.
- **The Trends captions read consistently.** Metrics without an advisor note previously rendered their caption in a different, plainer style that broke the row; every Trends caption now uses the same treatment.
### Changed
- **The mood assessment sits directly under the first chart** on the mood insights page.
## [1.8.6] — 2026-06-01 — Compliance two-row return, insights polish, targets deprecation
### Added
- **Mood narrative takeaways above the charts** — a ranked feed of plain-language findings (the weekday your mood dips, the overall trend, the tags that lift or lower it, your in-target share, your logging streak, the weekend effect), now drawing on the structured tags as well as free-text ones.
- **A GLP-1 dose-strength curve** that plots the titration history (2.5 → 5 → 7.5 mg …) in place of the previous empty therapy-course block.
- **A FHIR `Coverage` resource in the health-record export**, with an optional insurer institution number (IKNR) captured on the profile — making the insurer machine-resolvable for German systems.
- **An optional `source` on the measurements batch endpoint**, so a client backfilling manually-entered history can tag it correctly instead of defaulting to Apple Health.
### Changed
- **Medication compliance returns to two rows for every medication.** The cadence-aware timeline is gone; the two windows now scale to how often a medication is due — a daily medication keeps 7-day and 30-day, a rarely-taken one steps up to longer windows (up to twelve months) so both rows stay meaningful and the layout stays steady.
- **The medication advanced settings are a single, calmer column** with a symmetric danger zone (matched delete-history / delete-medication actions), replacing the cramped two-column layout.
- **The medication wizard uses a jumpable dot stepper** with a "next step" hint; in edit mode every step is reachable in one tap.
- **Insights category headers are quieter.** The question-mark circle is gone (the metric definition stays inline beneath the heading), the measurement-diversity hint moved to a heading lightbulb with the explanation on hover, the assessment border is de-emphasised, and "Coach fragen" moved to the header as an icon.
- **Target ranges are now edited inline from Insights.** The Targets page is deprecated — it carries a notice, is removed from the menu, and will be retired in a future release; editing happens where you read the trend.
### Fixed
- **Every briefing-driven Trend card now shows its caption** — additive metrics no longer render a chart with empty space below it.
- **A long mood note no longer stretches the entries table** — it truncates with the full text on hover.
- **The mood in-target share is shown once**, not duplicated between the headline tile and the takeaway feed.
- **Medication compliance no longer counts days before a medication existed**, so a newly-added medication's expected-dose figures reflect its real age.
## [1.8.5] — 2026-06-01 — Insights reference panels, mood depth, injection tracking
### Added
- **A target reference panel on every Insights category page.** Each metric now shows the target band, where the latest reading sits inside it, a plain-language status, the guideline source behind the range, the 30-day average and a seven-day consistency strip — the same reference the Targets page carries, now on the page where you read the trend. Blood glucose surfaces one panel per context (fasting, post-meal, random, bedtime).
- **A statistics strip per metric** — minimum, maximum, median and mean — plus a dedicated page that lists every underlying reading with edit and delete, reachable from each Insights category.
- **Injection-site tracking and rotation.** Opt-in per medication (injections only): record the site after a dose, see the history on a body map, and get a rotation suggestion. Sites can be restricted per medication, and an account-level exclusion always wins.
- **Structured mood tags and a per-entry note**, with a notes timeline so the context behind a mood is kept alongside the score.
- **A richer mood page** — calendar heatmap, score distribution, weekday pattern, and correlations with sleep and activity.
- **A measurement-diversity nudge** that suggests logging at other times or days when readings cluster, for a fuller picture.
### Changed
- **The medication compliance display adapts to how often a dose is due** — a percentage for frequent schedules, a dose-by-dose adherence timeline for sparse ones, so an every-few-weeks medication reads sensibly.
- **The Trends section now follows the daily briefing**, charting the metrics the briefing actually flags rather than a fixed set.
- **Tightened the Insights category pages and rebalanced the overview** for a denser, calmer layout.
- **Gait and mobility metrics from Apple Health are consolidated to a daily mean overnight**, while discrete clinical readings (weight, blood pressure, glucose, mood, pulse) stay raw.
- **The health-record FHIR export codes HealthKit-only metrics under a dedicated code system** instead of the LOINC namespace, for FHIR R4 conformance and alignment with the native client.
### Fixed
- **A rolling-interval medication now surfaces its first dose at the start date** — including when that date is already in the past — so the dashboard and reminders no longer skip the opening dose.
- **The medication wizard no longer shows each time-of-day suggestion twice.**
- **Deleting a medication no longer hangs on a spinner.**
- **Mood entries with tags save atomically** — the entry and its tags commit together, and create and update return the saved tags immediately rather than after a refresh.
- **Accessibility:** the injection-site picker has a visible focus state and screen-reader labels; mood tag chips and the status pill meet the contrast and minimum tap-target guidelines.
## [1.8.4] — 2026-06-01 — Medication card accuracy + Insights polish
### Fixed
- **The medication card shows the correct next intake for every schedule.** Flexible "interval after the last dose" and one-time medications previously showed a next-intake derived from a simplified day-of-week rule that ignored the interval and the last logged dose — so a medication taken every few weeks proposed tomorrow and never moved after a dose was logged. Both the standard and the injection card now read the schedule engine's computed next-due, so the next intake is right on creation and advances the moment a dose is recorded.
- **Insights status polling is bounded.** When a model is configured but generation stalls, the per-category assessment card no longer polls indefinitely on an open page — it stops after a fixed ceiling and shows its static state.
- **Clean deploys no longer hit a missing settings column.** Four `app_settings` columns existed in the schema but had only ever been applied ad-hoc, so a migrations-only deploy could fail on first read; an idempotent migration now adds them (a no-op on instances that already have them).
### Changed
- **Logging a dose confirms itself.** Marking a medication taken or skipped now shows a brief confirmation, and the card's next-intake updates immediately.
- **Each Insights category explains itself inline.** The short metric definition now also appears as a caption under the category heading, alongside the existing question-mark tooltip.
- **Medication history shows where each dose was logged** — web, the iOS app, a reminder, or an import — as a small origin label on each entry.
## [1.8.3] — 2026-06-01 — Insights no longer block the interface
Opening an Insights category and switching between them could freeze the whole interface for several seconds while the per-category assessment was generated. The interface now stays responsive throughout.
### Fixed
- **Switching Insights categories no longer freezes the interface.** Each category's short assessment is now served from cache as an immediate, read-only response; when it is not yet ready the page shows a brief preparing state and the assessment is generated in the background, so navigation and taps stay responsive. The client request is capped, so a slow or unreachable model can never block the interface — the worst case is a preparing state, never a frozen screen.
- **The overnight pre-generation warms the assessment actually shown.** The nightly warm pass keyed its cache differently from what the page requests, so a returning visit often fell back to an on-demand generation; it now warms the languages the interface uses and runs even when the daily summary was already cached, so a returning visit lands on a ready assessment.
- **A stalled model no longer re-blocks every visit.** A generation timeout is briefly remembered so the next visits return immediately instead of retrying the slow path each time.
- **Streamed loading states.** Insights and the dashboard now render a skeleton while their first segment loads, rather than waiting on a blank transition.
## [1.8.2] — 2026-06-01 — One intake row per dose slot
A multi-time medication could end up with two entries for the same dose — one still open, one marked taken — for a dose the user had not taken. This release guarantees a single entry per scheduled dose.
### Fixed
- **A dose slot can no longer hold a duplicate entry.** Logging a dose now updates the slot's existing entry in place instead of inserting a second row, regardless of where the log came from, and the logged time is aligned to the scheduled dose rather than the exact moment of the tap. A twice-daily medication no longer shows the same time twice (once open, once taken), the daily count is no longer inflated, and the "due now" prompt is no longer suppressed for a dose that has not actually been taken.
- **A recorded dose is never silently un-recorded.** A background re-sync that replays an open dose onto a slot already marked taken is now ignored rather than clearing the taken mark, so an offline sync can never flip a taken dose back to missed.
- **Concurrent logs converge.** Two logs landing on the same dose at once resolve to one entry instead of failing or duplicating.
- **Existing duplicates are cleaned up on startup.** A one-time pass collapses any duplicate dose entries already stored, keeping the recorded dose and correcting the affected compliance figures.
## [1.8.1] — 2026-06-01 — Multi-time medication compliance
A follow-up to the multi-time compliance work in 1.7.3, closing the two surfaces it did not reach.
### Fixed
- **Every scheduled time of a multi-time medication is now loggable** — a medication taken more than once a day on a single schedule with several times (e.g. 07:00 and 19:00) surfaced only its first dose in the today tile and the logging sheet, so the second dose could never be recorded and the day's compliance was counted against a single expected dose. The today-window projection now creates one pending dose per scheduled time, matching how reminders are issued.
- **The compliance heatmap classifies each dose against its own time** — a twice-daily dose logged in the evening was measured against the morning time and read as very late, painting the day amber even when both doses were taken on time. Each logged dose is now matched to its nearest scheduled time, so a day with every dose taken on time reads as on time. Single-time schedules are unaffected.
## [1.8.0] — 2026-05-31 — Insights: reliable data-driven assessments, embedded targets, explainers
The per-metric Insights pages become the coherent heart of the app. The data-driven assessment — a short, honest reading of your own trend — now reliably reaches the page: it previously timed out and cached a generic placeholder for the rest of the day, so most users only ever saw the platitude. The generation budget is aligned with the provider, the placeholder is never cached, and the AI payload is compressed into a graded shape (recent days raw, then weekly, monthly, and yearly aggregates drawn from the rollup tier) so a heavy logger no longer ships thousands of raw points. Each metric's prompt is rewritten to a consistent house style — name the finding, place it against your own baseline, offer one concrete step; honest on poor values, never alarmist, never diagnostic. Target ranges move onto each category page beside the chart, every category gains a plain-language "what is this?" tooltip, and the assessment is warmed overnight so the page is a cache read instead of a live call. Route slugs and tile ids move to English (German tile ids stay accepted as aliases, so existing clients keep working).
### Added
- **Per-category target ranges** — each Insights category page shows its numeric target range and the share of recent days within target, beside the chart and the assessment, with a link to adjust the range. The dedicated targets page remains the editing surface.
- **Per-category explainer tooltips** — a question-mark affordance next to every category heading opens a short, plain-language definition of the metric, available offline in all six languages and reachable by keyboard and touch.
- **Nightly assessment pre-generation** — a budget-gated overnight job warms the per-metric assessment caches (and invalidates them when fresh measurements arrive), so opening a category page is a cache read rather than a synchronous model call.
- **Graded measurement compression for assessments** — a shared compressor folds a metric's history into recent (daily), weekly, monthly, and yearly buckets, drawing the monthly/yearly tiers from the rollup store, so the model receives a compact picture instead of the full daily series.
- **Naming decision record** — `docs/adr/0001-insights-naming-convention.md` documents the convention: localised UI, English internal keys and route slugs.
### Changed
- **Data-driven assessments reach the page reliably** — the per-metric assessment generation budget is aligned with the provider (was capped well below the provider floor), runs through the provider chain with fallback, and a transient timeout or error is treated as a passing miss rather than cached as a placeholder for the rest of the day.
- **Assessment wording** — every metric prompt follows one house style (name → place against own baseline → one actionable step; 2–4 sentences; honest, autonomy-supporting, non-diagnostic; says so plainly when the data is too thin). Blood pressure reads systolic and diastolic together; weight tracks the trend while BMI speaks to threshold bands; the mood assessment carries a calm support cue on a sustained low.
- **Insights route slugs are English** — `/insights/` moves to `/insights/` with permanent redirects from the old paths. The dashboard-layout tile ids are English canonically; the previous German ids stay accepted as aliases and are normalised on read and write, so a client that stored the old ids keeps working.
- **Per-metric assessment caches drop on measurement changes** — a fresh, edited, or deleted measurement invalidates the affected category's cached assessment so it regenerates rather than serving a stale reading.
## [1.7.3] — 2026-05-31 — Health-record PDF fixes, multi-time compliance
Two fixes to live behaviour.
### Fixed
- **Health-record export renders in the account language** — the PDF came out in English regardless of the in-app language because the export only consulted the browser's `Accept-Language`. The client now sends the active language and the server falls back to the language cookie before the browser header.
- **Health-record export layout** — charts now label their time axis (start and end date of the plotted window), modules are kept together instead of breaking across a page, a consistent bottom margin is held on every page, and a glyph-encoding issue that stretched a few lines (trend arrows and the `m²` unit) is resolved by sanitising to the font's supported characters.
- **Compliance counts every scheduled time on multi-time plans** — a medication taken twice a day on a weekday plan with two times but no recurrence rule reported 50% even when both doses were logged: the expected-dose count and the taken count expanded the schedule through two different code paths. Both now run through the one canonical recurrence engine, so the rate is correct for every schedule shape.
## [1.7.2] — 2026-05-31 — Coach source parity, first-paint snapshot, medication card cleanup
A focused follow-up to v1.7.0. The unified dashboard snapshot that shipped in v1.7.0 is now the default, so a cold dashboard paints its above-the-fold tiles together instead of letting the mood tile arrive ahead of the rest. The Coach's in-chat data-source rail now reads and writes the same persisted data clusters as the settings sheet — one source of truth that survives a reload, so what the panel shows always matches what reaches the model. The medication overview cards collapse their action row into a single overflow menu, and the detail page is pared back to the intake history under a read-only plan summary. The advanced-settings sheet widens into a two-column layout and offers a medications export beside the existing import.
### Changed
- **Unified dashboard snapshot on by default** — the above-the-fold tiles read from the single `/api/dashboard/snapshot` round-trip introduced in v1.7.0, so they share one completion moment instead of staggering in as independent queries resolve. The build-time toggle `NEXT_PUBLIC_DASHBOARD_SNAPSHOT=false` falls back to the per-tile path if an operator needs it.
- **Coach data-source rail matches the settings sheet** — the in-chat rail now toggles the same persisted data clusters as the gear-icon settings, stored per user rather than reset when the panel closes. The chat request derives its scope from those saved clusters, so the rail always reflects exactly what is sent to the model, and the analysis window persists alongside.
- **Medication card actions in one menu** — the overview cards, standard and GLP-1 alike, move their edit / history / advanced actions into a single overflow menu; the card itself opens the detail page.
- **Medication detail page is history-first** — the redundant header action row and the schedule block are gone; the page leads with the intake history under a read-only summary (name, dose, status, plain-language cadence), and editing happens from the card menu. The estimated active-ingredient curve stays an opt-in disclosure.
- **Advanced-settings sheet widened** — a two-column layout (Data, Reminders, Lifecycle, with the danger zone full-width) so the sheet no longer scrolls on a desktop viewport, and a CSV export of that medication (its row plus intake history) sits beside the per-medication intake import.
### Fixed
- **Medication cards render identically** — the day-streak flame used two different oranges between the standard and GLP-1 cards; both now share one token. The repeated card sections are a single set of shared components, so the two variants stay in lockstep instead of drifting on each edit.
- **Intake import labelled honestly** — the import affordance described CSV/JSON but accepts a JSON intake array; the copy now says JSON.
## [1.7.1] — 2026-05-31 — Overview-card actions
The v1.7.0 medication detail page gained edit / history / advanced-settings actions in its header, but the medication cards in the overview list kept only the edit pencil. This patch brings the **history** (intake history) and **advanced-settings** actions onto the overview cards — both the standard and the GLP-1 variant — so the card and the detail page offer the same three actions. The advanced-settings sheet is mounted once on the list and reused; the history button routes to the same intake-history view.
## [1.7.0] — 2026-05-31 — Health-record export, flexible schedules, first-paint snapshot, full HealthKit coverage
A broad polishing release across the medication, dashboard, export, and HealthKit surfaces. Medications gain PRN (as-needed) and cyclic on/off-week schedules, a server-computed next-due instant, and cadence-canonical compliance so non-daily plans report adherence correctly everywhere — including the detail page, which previously still expanded through the legacy weekday walker. The dashboard now assembles its above-the-fold tiles in a single snapshot round-trip and pre-generates the daily briefing overnight, so a cold load no longer staggers tile-by-tile and never blocks on the model. The doctor handover grows into a selectable health-record export — an enriched PDF and a machine-readable HL7 FHIR R4 bundle, packaged together. Every previously-unplotted HealthKit metric gets a chart, walking speed reads in km/h, and the Coach can be fed any chosen cluster of data instead of a fixed five. The medication detail page restores its history view and gathers every setting into one redesigned advanced sheet.
### Added
- **PRN and cyclic schedules** — a medication schedule can be marked as-needed (`asNeeded`), excluded from the expected-dose count and reminder projection while staying loggable, or cyclic (`cycleWeeksOn` / `cycleWeeksOff` / `cycleAnchor`) for on/off-week dosing. Both are projected by the canonical recurrence engine. Migration `0092` adds the schedule-type enum and cyclic columns.
- **Server-computed next-due instant** — every medication carries a read-only `nextDueAt` (RFC-5545 + rolling math, `null` for PRN), so a client never has to duplicate the recurrence engine.
- **Schedule-aware compliance payload** — the per-day compliance bucket gains `due` / `expectedCount`, so a history view can skip empty marks on non-due days for weekly, bi-weekly, rolling, and cyclic plans.
- **Per-medication reminder flags** — `liveActivityEnabled` and `criticalAlarmEnabled` (both default off) ride the medication contract for clients that surface lock-screen or break-through reminders. A roaming user-level delivery default plus a per-device override let a device honour or locally override the default. Migration `0091`.
- **Health-record export** — a new `POST /api/export/health-record` produces a selectable export: an enriched clinical PDF (patient identity, deterministic summary, native sparklines), an HL7 FHIR R4 document bundle (LOINC-coded `Observation`s, a BP panel, `MedicationStatement`s, a `DiagnosticReport`), or both packaged as one zip. The selection chooses date range and per-domain sections, and the AI summary is an explicit opt-in section clearly marked as not clinically validated. The bundle reuses the same aggregator the PDF consumes, so both describe identical numbers, and matches the iOS client's LOINC/UCUM conventions for interchangeable records.
- **Patient identity on the profile** — optional full name, insurer, and insurance number on Account, for the report cover and the FHIR `Patient`. The insurance number is validated (KVNR mod-10) and encrypted at rest.
- **Unified dashboard snapshot** — `GET /api/dashboard/snapshot` assembles the above-the-fold tile data in one round-trip from the rollup tier plus the cached briefing, so the tiles arrive together. A coverage miss returns the heavy slice as deferred rather than blocking the strip. The snapshot also carries a `metricStates` seed (latest value/timestamp/unit per metric) and the full widget-layout catalogue, so a native client can paint a cold launch from one cached request.
- **Nightly insight pre-generation** — a budget-gated pg-boss job warms the comprehensive insight (and its daily briefing) overnight, so the `/insights` mount is a cache read instead of a synchronous model call. A cache miss now shows a preparing state rather than blocking the request.
- **Charts for every stored HealthKit metric** — the previously-unplotted types (flights climbed, environmental and headphone audio exposure, walking speed, step length, asymmetry, double support, respiratory rate, body-composition family, mobility, daylight, and more) each gain a chart surface. Walking speed displays in km/h and distance in km via a render-time transform; stored and exported values stay canonical SI.
- **Display-unit preference** — a metric/imperial toggle in Settings → Display (migration `0094`), read by the chart display transforms.
- **Coach data clustering** — the Coach accepts a chosen set of data clusters (cardiovascular, body composition, activity, workouts, sleep, mood, glucose, medication, mobility, environment) instead of a fixed set, with a soft budget cap that degrades the lowest-signal clusters first so enabling everything stays within the prompt budget. The default reproduces the prior domains.
- **Offline sync delta feed** — `GET /api/sync/changes` exposes a paginated, opaque-cursor delta feed with tombstones across three domains — measurements, mood entries, and medication intakes — over a single multi-domain cursor; `/api/sync/state` is now part of the documented API and reports the sync window. Measurement, mood, and intake delete routes soft-delete (tombstone) rather than hard-delete, pruned past a retention window keyed to the refresh-token lifetime. A stable `errorCode` on the refresh endpoint distinguishes a revoked family, a token reuse, and a transient failure. Migrations `0096` + `0097` add the keyset indexes and the mood/intake sync columns.
- **Configurable mood-reminder time** — the evening mood-reminder hour is now per-user (it was a fixed 22:00), stored in the roaming notification preferences so a native client and the web share one source of truth.
### Changed
- **Cadence-canonical compliance everywhere** — `calculateCompliance` and the medication detail page route expected-dose computation through the canonical recurrence engine, so `rrule`, `rollingIntervalDays`, weekday subsets, interval weeks, one-shot, and PRN all count correctly. The dashboard tile and the detail page no longer disagree for non-daily plans.
- **Medication detail surface** — the header offers a direct edit, a history view (the clock-with-counterclockwise-arrow icon), and an advanced-settings button. The history view is intake-only, sorted newest-first, with the estimated active-ingredient curve as an opt-in disclosure. The advanced sheet is widened and regrouped into Data, Reminders, Lifecycle, and a danger zone, with CSV/JSON import surfaced as a real action.
- **Dashboard widget layout round-trips the full catalogue** — saving a layout no longer rejects ids the web surface doesn't render; the full widget catalogue (web tiles + client-only tiles) is persisted and returned intact, so a native client keeps its own tiles without a local merge step. A genuinely unknown id is still ignored.
- **Keyboard navigation for segmented controls** — the metric/imperial, export-format, and mood-rating radiogroups gain roving-tabindex arrow-key navigation (Home/End, wrap), via one shared hook.
- **Dashboard charts refresh on an open page** — the snapshot and analytics queries poll on a shared interval so an open dashboard picks up new measurements without a manual reload, served from a warm per-user cache.
- **Profile photo preserves its aspect ratio** — the avatar renders with `object-cover` instead of stretching.
### Fixed
- **The mood-reminder card now delivers on its own** — enabling the reminder previously still required a buried, default-off per-event push preference, so a user who flipped the visible card received nothing; the card alone now drives delivery (an explicit per-event opt-out still suppresses).
- **Multi-time-of-day reminders no longer re-fire** — the reminder ledger delete is scoped to the single dispatched slot rather than wiping every row for a medication, and logged doses are matched to slots by time rather than array order.
- **Soft-deleted measurements stay deleted on direct access** — the single-resource GET and PUT now filter tombstoned rows.
- **Mobile settings navigation no longer auto-scrolls the page** — the section strip positions its active chip horizontally without nudging the document, removing the dizzy scroll on sub-page navigation.
### Tests
- 5628 → 5878 unit; 1 skipped. New coverage spans the recurrence-engine PRN/cyclic cases, cadence-canonical compliance parity per cadence type, the FHIR bundle shape and LOINC/UCUM mapping, the KVNR validator, the export route, the dashboard snapshot envelope and pre-generation cron, the measurement delta feed and soft-delete tombstones, the daily-mean consolidation drain, and the display-unit transforms.
## [1.6.0] — 2026-05-30 — Medication editor overhaul, route of administration, today-tile read-flip
v1.5.6 settled the medication detail page into a pure history view, but the create/edit plan still funnelled every dose through the same flat shape, the injection-site picker only surfaced for GLP-1, and the today-tile on the dashboard and the Erfassen sheet still expanded daily schedules through a legacy walker that silently skipped bi-weekly, rolling, RRULE, and one-time cadences. This release reworks the medication editor and its detail surface, gives route of administration a first-class column so any injection can carry a site, adds a one-time-injection shape, and flips the today-tile onto the canonical recurrence engine the reminder worker already uses — so what the tile shows matches what the worker mints. It also restores the multi-language request snippets on the API-tokens row, adds a profile-photo upload to Account, and finishes an admin-surface polish pass.
### Added
- **Route of administration** — a new `deliveryForm` column (`ORAL` | `INJECTION` | `OTHER`) decoupled from `treatmentClass`. The injection-site picker now surfaces for any `INJECTION` dose rather than only GLP-1, and the editor carries the route through create, update, and the detail snapshot. Migration `0088` adds the enum and backfills `ORAL` onto every existing row via a constant column default (a single non-blocking metadata operation).
- **One-time injection** — a one-off dose modelled as `oneShot = true` + `deliveryForm = INJECTION`, with its own editor path that drops the recurring-schedule step.
- **Profile-photo upload on Account** — Settings → Account gains an avatar card backed by the v1.5.5 upload endpoint (server-side magic-byte sniff, 2 MiB stream-level cap, 2048² dimension cap, per-user rate limit, owner-scoped read).
- **Admin global mood-log toggle** — the admin Services section can suspend mood-log reminders site-wide alongside the existing Web-Push and API toggles.
- **Host-metrics memory detail** — the admin host-metrics memory tooltip now reads `used / total GiB` alongside the percentage.
### Changed
- **Medication editor + detail surface overhaul** — the editor uses the available desktop width while keeping the mobile sheet, restores the edit fields the modal-wizard rework had dropped, and aligns the detail surface with the new route-of-administration and one-time shapes.
- **Measurement note cap raised 25 → 200 characters** — a 25-character note could not hold a meaningful clinical aside; the cap lifts to 200 through a single `MEASUREMENT_NOTES_MAX_LENGTH` constant the client char-counters import. The DB column was already unbounded.
- **Dashboard range colors route through semantic tokens** — the dashboard date-range chips read the shared `success` / `warning` / `info` / `destructive` tokens (with their light-mode contrast overrides) instead of hard-coded hues, and the range inputs gain bound-clamping.
- **Admin surface tidy** — every sidebar section gets a distinct icon, the shared `helpfulRateColour` helper and a `usePublicVersion` hook are consolidated into `_shared`, and the orphaned `status-overview` route is removed.
- **API-tokens row restores multi-language request snippets** — the per-token row again offers copyable cURL / JavaScript / Python examples.
### Fixed
- **Today-tile diverged from the reminder worker for non-daily cadences** — `/api/medications/intake?scope=today` and `/api/dashboard/summary` projected daily schedules through a legacy walker that read only `daysOfWeek` + `windowStart`, silently skipping `intervalWeeks > 1`, rolling, RRULE, and one-time cadences. Both routes now gate every "does this schedule emit today?" decision through the canonical recurrence engine (the same path the worker uses), anchoring the projected instant to `windowStart` so it stays byte-identical to the worker's row and dedupes against the existing unique index. The rolling-cadence baseline fetch mirrors the worker's per-medication `takenAt` query, so projector and worker resolve the same next-due instant.
### Tests
- 5635 → 5628 unit; 1 skipped. The today-tile read-flip carries new coverage on the intake and dashboard-summary projections; the orphaned status-overview route and its test are gone.
## [1.5.6] — 2026-05-29 — Pure-history detail page, legacy step consolidation, egress finish
v1.5.5 gave every retired medication feature a home on the detail page, but the page still carried the create/edit affordances that belong on the list. This release turns `/medications/[id]` into a pure history surface, collapses every setting into one advanced sheet, consolidates the pre-v1.5.0 granular step rows that bloated the per-sample read path, and finishes the outbound-fetch migration the `safeFetch` wrapper started.
### Added
- **Legacy step consolidation** — a pg-boss job (`STEP_CONSOLIDATION_QUEUE`) folds pre-v1.5.0 granular `ACTIVITY_STEPS` rows into one canonical daily-total row per user per day, anchored to the user's timezone, and soft-deletes the originals. A day that already carries a post-v1.5.0 `stats:` total is left untouched (no double-count) while its legacy rows are still tombstoned. Boot-time discovery enqueues only users still holding live legacy rows, so the pass converges to zero work and is idempotent across reboots. Migration `0087` adds a partial index scoped to live step rows to keep the discovery scan cheap.
- **`healthlog/safe-fetch-required` ESLint rule** — bans raw `fetch(` outside the wrapper, exempting only genuine same-origin relative paths. Positioned as the author-time egress floor.
### Changed
- **`/medications/[id]` is now a pure history view** — the Today's-dose card stays on the list page; the detail page reads the medication's past intake, cadence summary, and trends only. Every setting (notifications, phase config, grace window, pause/resume, end, purge) moves into a new `` reached from the header. The edit trigger becomes a two-option picker (plan vs. advanced). The `/medications` list page is unchanged apart from matching the detail-nav card glyph to its destination.
- **Outbound-fetch migration finished** — every remaining server-side fetch routes through `safeFetch`: Withings (token exchange, refresh, measurements, webhook subscribe/unsubscribe, activity + sleep sync), Codex OAuth + SSE client, the bug-report GitHub comment, and the operator-host calls (Umami script/send/test, GlitchTip envelope + store, Loki). Credential-carrying constant hosts inherit the `redirect: "manual"` default so secrets cannot replay onto a redirect hop; operator-supplied hosts additionally pin `requirePublicHost` for the connect-time IP guard.
- **`/api/insights/layout`** is registered in the OpenAPI spec so the contract matches the shipped route.
### Fixed
- **Intake-edit dialog opened empty** — editing a logged intake row now seeds the dialog from the row's real `takenAt` / `skipped` instead of an empty stub.
- **Step consolidation could strand a day** — a minted daily total can collide on the second unique index when a manual step row already sits at the day's canonical-noon instant; the collision rolled back the in-transaction soft-delete and the day re-appeared on every boot. The pass now catches the constraint violation, logs and counts the skip, and steps to the next day.
- **Avatar upload over-buffered on the abort path** — the upload body is now read once through a bounded reader that drops retained bytes past the 2 MiB cap, replacing the clone + parallel parse that left the native parse buffering an oversized body after the cap tripped.
- **Egress lint exemption let absolute URLs through** — the same-origin exemption now rejects protocol-relative (`//host`) and backslash (`/\host`) forms, which both resolve off-origin.
### Removed
- **Dead wizard landing-intent path** — the pure-history rewrite removed the last caller that passed a landing intent into the medication wizard, so the prop, its state-key segment, and the intent branch in `landingStepForEdit` are gone; the helper reduces to the schedule-count heuristic.
### Tests
- 5615 → 5635 unit; 1 skipped. Detail-page history surface, advanced-settings sheet, intake-edit seed, and legacy step consolidation are covered; the pre-tag reconcile sweep keeps the suite green.
## [1.5.5] — 2026-05-29 — Medication detail page, iOS coordination, outbound-fetch hardening
The v1.5.4 modal wizard landed the create + edit plan flow, but it took 16 features down with the retired flat form — Einnahmen bearbeiten / löschen, Medikament pausieren / beenden / löschen, per-Med API tokens, Phasen-Konfiguration, CSV-Import — and the trends-row on Insights had a 34 px overflow that pushed annotation copy on top of the charts. This release lands the medication detail page that gives every retired feature a coherent home, polishes the wizard, closes the iOS audit follow-ups in one go, and hardens every outbound fetch the server makes.
### Added
- **`/medications/[id]/page.tsx`** — a single Server-Component detail page composed of eight sections (Header band → Today's-dose → Cadence summary → Phasen (GLP-1) → Intake history preview → Notifications → Settings → Verwaltung & Gefahrenzone). One-shot medications walk a five-section variant; paused medications keep the structure but the status pill flips to `Pausiert` and the dose-card surface mutes. Restores every one of the 16 features the v1.5.4 flat-form retirement displaced.
- **Per-row + bulk intake history actions** — the preview now ships a per-row kebab (`Bearbeiten` / `Löschen`) and a multi-select toolbar that fires the new `POST /api/medications/{id}/intake/bulk-delete` endpoint.
- **`safeFetch` wrapper** at `src/lib/safe-fetch.ts` — every outbound fetch the server makes for user-supplied hosts now inherits `redirect: "manual"` + `AbortSignal.timeout(15_000)` defaults. Closes #218.
- **DNS-rebinding pinned `undici.Agent`** at `src/lib/safe-fetch-dispatcher.ts` — when `requirePublicHost: true`, the dispatcher resolves the hostname literally inside the connect hook, refuses any address `isPublicIp` would reject, and pins the connection to the first valid public address. The five user-host paths (MoodLog sync + push + test, local-AI client, ntfy) route through it. Closes #217.
- **Self-hosted avatar storage** at `POST/GET/DELETE /api/user/avatar` — multipart-uploaded JPEG/PNG/WebP, 2 MiB / 2048×2048 max, hand-rolled magic-byte sniff + dimension probe so no native dependency lands. Stored on the User row as BYTEA; rides `pg_dump` alongside the rest of the row. The profile response carries an `avatarUrl` with a cache-busting timestamp. `src/lib/gravatar.ts` retires — Automattic no longer sees the email-hash on every authenticated page load.
- **`POST /api/medications/{id}/intake/bulk-delete`** — owner-scoped, capped at 500 event IDs per call, rate-limited per user. Pre-work for the detail page's multi-select.
- **`GET / PUT / DELETE /api/insights/layout`** — mirrors `/api/dashboard/widgets` so insights tile order + visibility persist server-side and sync across devices. Default order: `overview`, `blutdruck`, `puls`, `sauerstoff`, `koerpertemperatur`, `gewicht`, `bmi`, `aktive-energie`, `workouts`, `schlaf`, `ruhepuls`, `hrv`, `stimmung`, `medikamente`. Default-visible: overview, blutdruck, puls, gewicht, bmi, workouts, stimmung, medikamente.
- **Eight new Apple Health quantity-type mappings** — `RESPIRATORY_RATE`, `BODY_MASS_INDEX`, `LEAN_BODY_MASS`, `WALKING_HEART_RATE_AVERAGE`, `WALKING_ASYMMETRY`, `WALKING_DOUBLE_SUPPORT`, `WALKING_STEP_LENGTH`, `WALKING_SPEED`. The convention block on `apple-health-mapping.ts` documents the project rule: raw HK values flow on the wire, the server scales ×100 server-side for percent metrics. Step length and walking speed flow raw in SI (m and m/s) — no scaling.
- **Three new series-kind enum values** — `RESTING_HEART_RATE`, `HEART_RATE_VARIABILITY`, `VO2_MAX`. Detail + trend views on those metrics now respond 200 instead of 422.
### Changed
- **Wizard polish** — dialog widens to `sm:max-w-2xl`; the X-close gets a 44 px target; spacing tokens converge on the existing shadcn cadence (`rounded-md` button, `rounded-lg` dialog, no new radii); the step-progress bar is a width-only `