From a46d0b41c8c39ea74855ace874b3975368cca902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Fri, 25 Sep 2026 13:12:34 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v1.39.1=20=E2=80=94=20large?= =?UTF-8?q?=20accounts,=20restores=20in=20the=20background,=20and=20medica?= =?UTF-8?q?tions=20kept=20as=20a=20record?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 206 ++++++++++++++++++++++++++++++++++++++++++ docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- 4 files changed, 209 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 62190effa..c717fbf8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,211 @@ # Changelog +## [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 + +- **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 diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 45e68d01d..8e942b3e6 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -1,7 +1,7 @@ openapi: 3.1.0 info: title: HealthLog API - version: 1.39.0 + version: 1.39.1 description: >- Self-hosted personal-health-tracking PWA — public API surface for the iOS native client and external ingest. diff --git a/package.json b/package.json index 796fdf300..a76cc2bb0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.39.0", + "version": "1.39.1", "description": "Self-hosted personal-health-tracking PWA with Withings integration, AI insights, and doctor-report PDF export.", "license": "PolyForm-Noncommercial-1.0.0", "homepage": "https://healthlog.dev", diff --git a/public/sw.js b/public/sw.js index b7f0d39e6..864db5747 100644 --- a/public/sw.js +++ b/public/sw.js @@ -36,7 +36,7 @@ try { // v1.4.38.4 → v1.4.42. Do not hand-edit; bump `package.json` and rebuild. const CACHE_VERSION = (typeof self !== "undefined" && self.__APP_VERSION__) || - /* @sw-version-fallback */ "v1.39.0"; + /* @sw-version-fallback */ "v1.39.1"; const STATIC_CACHE = `healthlog-static-${CACHE_VERSION}`; const PAGE_CACHE = `healthlog-pages-${CACHE_VERSION}`; // v1.18.6 — read-only data cache for a curated allowlist of safe GET `/api/*`