From 45bae0013c26b553bdf391d411fbcf3fb4125719 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Sun, 27 Sep 2026 02:12:27 +0200 Subject: [PATCH] =?UTF-8?q?chore(release):=20v1.39.3=20=E2=80=94=20documen?= =?UTF-8?q?ts=20from=20Paperless=20and=20Papra,=20confirmations=20that=20h?= =?UTF-8?q?old,=20the=20right=20day=20everywhere,=20and=20backups=20that?= =?UTF-8?q?=20delete=20what=20they=20should?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 347 ++++++++++++++++++++++++++++++++++++++++++ docs/api/openapi.yaml | 2 +- package.json | 2 +- public/sw.js | 2 +- 4 files changed, 350 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc3f840dc..764d85206 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,352 @@ # Changelog +## [1.39.3] — 2026-09-27 + +Documents can be picked straight from Paperless-ngx and Papra. Exports, +share links, tokens, invitations and new second factors ask for a recent +confirmation, and sign out everywhere reaches every way into the record. +Medication days west of UTC, long-range figures and synced daily totals +land on the right day. Deleting an account removes its off-site copies, +restores check the keys they need, and the privacy wording says what is +and is not encrypted. + +### Added + +- **Document picker for Paperless-ngx and Papra.** Settings, Integrations + has a Document archives card (address, API token, for Papra the + organization id); the connection is tested before it is saved, the + token is stored encrypted and never shown again, and it is sent only to + the origin it was saved for. The Documents page and the document link + dialogs on visits, vaccinations and conditions get an Import button: + search by name, filter by tag or date, tick documents, and each is + copied with its title and date and linked where the picker was opened. + Documents already imported are marked and linked instead of stored + twice; documents deleted in HealthLog are marked and not brought back. + Picked documents follow the owner's automatic AI reading setting. The + archive is contacted only on search or import, never in the background. + Off until the operator sets `DOCUMENT_SOURCE_ORIGINS`. Paperless-ngx + 2.16 or later; a Papra key needs read access to documents and tags. + Seven cookie-only routes under `/api/documents/sources` (a Bearer token + gets `403 documents.sources.browserOnly`). Thanks to @AntonPalmqvist for + #1038 and to the self-hoster behind #938. +- **Source instance for imports.** `POST /api/documents/inbound` and + `GET /api/documents/inbound/source` take an optional `sourceInstance` + (the archive's address, normalised to its origin), so the same id in two + archives is two documents. Keys stored without it match any instance, as + before. The import script sends it and skips files larger than the + server accepts once it has learned the limit. `sourceId` refuses `.` and + `..`. +- **Body site in the FHIR export.** A condition's site goes out as + `Condition.bodySite.text` ("knee (left)"); a recorded side also goes out + as a contained `BodyStructure` with an active SNOMED CT laterality + qualifier (7771000 left, 24028007 right, 51440002 both). Applies to the + health-record download, `Patient/$everything` and the clinician share + link. Thanks to @Cnote43 for #1025. +- **MCP search and fetch cover visits, conditions, documents and + vaccinations.** Ids `visit:`, `condition:`, `document:` and + `vaccination:`; `fetch` returns fields in `metadata` (unrecorded + values `null`) and linked records in `metadata.links`. Matching covers + visit type, reason, outcome, body site and side, practitioner and + specialty, condition name, document title, file name and type, and + vaccine name, with German and English body-part names treated alike. + Documents are searchable by their indexed text and `fetch` returns a + short excerpt, never the file, a link or the AI summary. A document held + back from AI reading answers `metadata.reason: "ai_read_deferred"` with + no excerpt. Condition and vaccination notes are never sent. The consent + page says all of this. Thanks to @Cnote43 for #1025. +- **Low-supply notice for as-needed medications.** Sent once when two + doses or fewer are left, re-armed by a refill, naming the units left + rather than a date. Push metadata carries `asNeeded: true`, + `runwayDays: null`, `triggerDays: null`. +- **Switches for the remaining outbound requests.** + `PASSWORD_BREACH_CHECK_DISABLED` stops the HaveIBeenPwned range check + (known leaked passwords are then no longer refused); + `UPDATE_CHECK_DISABLED` stops the daily GitHub update check, and + `/api/version/check-updates` answers `status: "unknown"` with + `reason: "disabled"`. Both default to on and are on the compose + whitelist. +- **Admin, Encryption lists the keys backups still need.** Per key, the + stored backups and off-site copies written under it; the "safe to drop" + badge appears only when backups are clear too. Backups written before + this release show as unrecorded. +- **`BACKUP_ENCRYPTION_PREVIOUS_KEYS`** keeps retired off-site backup keys + readable, so `BACKUP_ENCRYPTION_KEY` can be rotated. +- **`POST /api/auth/reproof`** (cookie only) takes the step-up body and + counts as a recent confirmation for five minutes. + +### Changed + +- **Sign out everywhere ends everything that can open the record.** Besides + browser and phone sign-ins it now revokes connected AI assistants, API + tokens (except the caller's own access token), clinician share links + (kept with `?keepShareLinks=1`, a checkbox in the dialog) and invitations + nobody has accepted. Accepted access is kept and listed on the sessions + card with End access. `DELETE /api/auth/me/sessions` adds + `accessTokensRevoked`, `connectorsRevoked`, `shareLinksRevoked`, + `pendingInvitesRevoked` and `grantsKept`. +- **A password change revokes connected assistants and share links too.** +- **Turning on the first second factor signs out other devices.** A second + key added later does not. +- **Password sign-in is limited per account without locking it.** From a + place the account has not signed in from, five wrong passwords a day are + free, then each doubles the wait from 30 seconds up to 15 minutes. A + remembered browser, a phone whose `X-Device-Id` a login of this account + was issued to, and an address used by this account in the last 30 days + are never held back. The per-address limit of five in fifteen minutes is + unchanged. Answers are `429` with `Retry-After`. +- **Request bodies are capped at 1 MB before authentication** (was 512 MB). + Apple Health archives, backup uploads, documents, CSV and dose history + imports, lab scans, profile pictures and the app's measurement, workout, + ECG, mood, medication and cycle batches read their own bodies after + authenticating, with their own limits enforced while reading (`413`). +- **`X-Real-IP` is read only with `TRUST_PROXY_HOPS=1`** (the default) and + only when `X-Forwarded-For` is absent. With `0`, or `2` and more, it is + ignored. The log says once when a proxy header arrives that the setting + does not allow reading, naming the header and the setting. +- **CSV re-import.** An unchanged row with an `externalId` is left alone and + reported `skipped` with reason `duplicate` (was `updated`); lookups and + writes run in small batches. +- **`GET /api/analytics/range`.** For step-like totals `mean` is the + average daily total and `count` the number of days; for + `SLEEP_DURATION` it is the average night asleep, `count` the number of + nights, `granularity` `live`. +- **Synced daily totals from Withings, Fitbit, Google Health, Polar and + Oura** are stored at noon in the user's zone (was noon UTC). The + `externalId` is unchanged; rows the next sync rereads move and reach the + change feed as updates. Fitbit weigh-ins without an offset are read in + the user's zone. +- **Check-up `anchorDate`** accepts a bare `YYYY-MM-DD`, read as that day in + the profile zone; the web form sends it. Responses keep the date-time. +- **"Send raw data" is now "Send detailed history"** and says what it sends: + daily averages for 90 days, weekly for the year before, monthly up to + five years, each with its date and reading count. +- **Encryption wording.** The app, the Coach and the README said all health + data is encrypted at rest. Notes and other free text, documents, + questionnaire answers, AI-written text, the Coach conversation and every + access token are; measurement values, lab values, medication names and + mood scores are not. The wording now says so. Storage did not change. +- **Local lab OCR is served from the instance.** The engine and its German + and English data (about 17 MB) come from the server instead of a public + CDN, which the page policy blocked; `/labs`, `/documents` and the OCR + worker may run WebAssembly. +- **The image ships `scripts/reset-password.mjs`** and the intake repair + script from the operator guide. Key rotation is documented as Admin, + Encryption, Rotate now. +- `IP_GEO_LOOKUP_DISABLED`, like every on/off setting, accepts `1`, `true`, + `yes` and `on`. +- `.env.production.example` is the complete settings list; `.env.example` + is a starter and no longer lists the unused `WITHINGS_CLIENT_ID` and + `WITHINGS_CLIENT_SECRET`. + +### Deprecated + +- **`ALLOW_LOCAL_AI_PRIVATE_HOSTS=true`.** It now covers only the admin AI + key and AI settings saved on an admin account, over a pinned connection + without redirects, and will be removed in a later release. Other + accounts' private endpoints need `AI_PRIVATE_ORIGINS`. The host-list form + (`ollama.lan,10.0.0.5`) keeps working with the same pinning. The boot + summary names the setting, and the admin AI settings page shows the + `AI_PRIVATE_ORIGINS` line for the endpoints saved on the instance. + +### Fixed + +- **Medication days west of UTC.** A Monday plan reminded on Sunday, a + course starting on the 14th had a dose on the 13th, and an evening dose + on the last day could be dropped; monthly and one-time doses were a day + early too. Start and end dates are read as calendar days and the times + placed on them in the user's zone, for the list, today list, reminders, + history and adherence. History for affected weeks can show the wrongly + named day as an off-schedule entry beside the planned day. +- **Weeks on and weeks off count whole weeks from the start day** instead + of Sunday-based UTC weeks; the pause of a plan that did not start on a + Sunday can move. +- **A snooze holds only the dose it answered.** Skipping the morning dose + from Telegram no longer silences the evening dose. A snooze set while no + dose is open holds the next one. +- **Check-up reminders with several times of day are sent at each.** The + evening half of the morning-and-evening blood pressure course waited + until the next morning. +- **Supply estimate.** Honours `INTERVAL` on daily, monthly and yearly + rules, several days of the month or months of the year, every weekday a + monthly rule names (every Monday counts four or five times; the first + Monday once), the pause of an on/off plan, and as-needed schedules as + not consuming. An injection every third month with two doses left reads + about 180 days, not 60. Whole-dose counts no longer lose one to rounding + (2.4 mL at 0.8 mL is three doses). +- **The doctor report keeps same-named medications apart**, in the PDF, the + FHIR export, the clinician link and the AI summary. +- **A doctor report for a past period stays inside it** and compares course + dates by day in the report's zone, so west of UTC it no longer drops a + course ending on the first day or lists one starting after the last. +- **Weekly, monthly and yearly figures pick the source per day** instead of + once per bucket, so mixed manual and synced readings and a device switch + mid-month count every day. Affects year and All ranges, baselines, the + one-year change and assessments. +- **"vs. last year"** is the average of the 30 days starting a year and a + month ago, not whichever month began inside that window. +- **All-time average, minimum and maximum** count one source per day for + histories older than five years, and so do the all-time figures the + assessments and the Coach see for weight, blood pressure and pulse. +- **The logging streak** was one day short west of UTC in the evening. +- **The activity part of the health score** uses the 28 completed days up + to yesterday, so it no longer dips every morning. +- **Heatmaps, digest milestones, assessments and mood and cycle + comparisons** use the user's day instead of the server's or Berlin's. +- **Days that start at a skipped midnight** (Chile, Cuba) or on a clock + change in New Zealand and eastern Australia start at their first real + minute. +- **Synced daily totals from UTC+12 and beyond** showed a day late. The next + sync fixes the recent days, about the last month for most services; + reconnecting Fitbit or Google Health rereads the whole history. Oura + cycle phases are fetched over the user's days as well. +- **Apple Health export import** split a day on which the phone or watch + updated and kept only the larger half of its steps. Importing the export + again fixes stored days. +- **Step and sleep period comparisons** compare average days and nights + instead of single readings, and end with yesterday. +- **Long ranges showed no data** when all readings fell in the first, + partial year; they fall back to monthly figures. +- **Assessments refresh after readings from every source** (assistant, + Telegram, CSV, backup import, Apple Health export), not only in-app entry. +- **CSV re-import** of a few thousand rows timed out. A re-import that moves + a reading recalculates the day it left, and a failed import recalculates + the days it wrote; uploading the same file again finishes it. +- **A queued Apple Health import lost its upload** when it waited longer + than seven hours; the cleanup keeps files a queued or running import + needs. +- **Empty settings from the compose file count as not set**: weather and + daylight lookups, the login-location lookup, `TRUST_PROXY_HOPS`, and the + Withings, Fitbit and Google Health redirect URIs. +- **The first due date of a check-up** no longer moves a day when the + browser is in another zone than the profile. +- **The password reset command failed** with "Cannot find module" in the + published image. +- **FHIR export codes.** Body water and bone mass use the body-composition + codes; step length, walking speed, walking asymmetry and VO2 max go out + under their Apple Health names; mood (now outside vital signs), estimated + A1C and average cycle and period length go out as named values without a + code; LDL, vitamin D and eGFR use the method-less codes; mean glucose is + coded (in mg/dL) only from a continuous monitor; allergy and injury use + current SNOMED CT concepts; a medication code no longer carries the + user's name as its display; resting heart rate and oxygen saturation add + the required vital-signs codes; an export without vital signs has no + empty vital-signs report. No longer sent: 73704-9, 73708-0, 41955-6, + 41957-2, 91557-1, 96402-2, 76542-6, 41995-2, 64700-8, 64698-4, 18262-6, + 1989-3, 33914-3, SNOMED CT 106190000. +- **MCP tools follow the module switches.** With Medications, Labs, + Conditions, Documents or Vaccinations off, search and fetch skip them and + `get_labs`, `get_medication_schedule`, `get_medication_compliance` and + the lab and medication resources answer `present: false`, + `reason: "module_disabled"`; `get_visits` sets + `conditionsReason: "module_disabled"`. + +### Security + +- **Recent confirmation for actions that outlast a session.** Whole-record + exports (full backup, encrypted backup, JSON and CSV of everything, the + doctor report, FHIR `$everything`), share links, API and + MCP tokens, invitations, connecting an AI assistant, and the admin + backup download, upload, restore, delete all data and reset password + need a sign-in or confirmation within five minutes, with the second + factor on an account that has one. Otherwise `401 auth.reproof.required` + with `meta.methods`, and the app shows a confirm dialog and continues. + The encrypted backup now asks accounts without a second factor too. On a + token, `GET /api/export/full-backup` and `GET /api/export?type=all` need + an `X-Step-Up` elevation and `POST /api/export/encrypted` a second-factor + one on accounts that have a second factor; the doctor report + (`POST /api/export/health-record`) and `$everything` keep taking the + token. Migration `0359`. +- **Adding a second factor needs a recent confirmation.** Without a second + factor: a sign-in within five minutes or the password. With one: that + factor or a passkey, never the password alone, also for passkeys and on + the token path. An authenticator never confirmed with a code no longer + counts as a factor anywhere. +- **Confirmation attempts share one limit** of five per fifteen minutes per + account, including password change and email change; each wrong attempt + is in the security activity and a correct one is not counted. Second + factor attempts at sign-in are counted before they are checked. +- **Single sign-on links an existing account only after its own sign-in** + (password and second factor, or passkey) within ten minutes, also on + SSO-only instances. The app receives `oidc_link_required`. Existing links + stay. +- **Changing the email address** needs the current password or a sign-in + within five minutes; on an account with a second factor, that factor or + a passkey. The rest of the save still lands; the address is reported in + `rejectedFields`. +- **Private AI endpoints by exact origin.** New `AI_PRIVATE_ORIGINS` + (comma-separated `scheme://host[:port]`). Granted endpoints are called + over a pinned connection without redirects; metadata, link-local and + unspecified addresses stay refused. A failed lab scan no longer logs the + endpoint's answer. +- **Coach conversation titles and custom-metric notes are encrypted at + rest.** Existing rows are encrypted in the background after start; + reads fall back to the old column until then. Migration `0357`. +- **Deleting an account, a managed profile or all data removes its + off-site backup copies**, right after and again the next night; refused + deletions are retried and shown on Admin, Backups, Off-host, which also + warns when the bucket has no lifecycle rule. Admin Wipe all data leaves + the bucket alone. Migration `0358`. +- **Restores check the keys a backup needs.** Preview, restore, upload and + the monthly drill refuse a backup whose encrypted content needs a key the + server lacks (`backup.key.missing` with `keyIds`), before changing + anything. A restore that would reference another account is rolled back + (`backup.foreign_reference`). +- **Off-site copies are bound to their account and date** (envelope + version 4 with a key id); older copies still restore. +- **Apple Health imports.** An archive may inflate to 200 times its size on + disk (64 MiB to 8 GiB); one import runs per account, and a second one + answers `409` `import.apple_health.busy` with `meta.jobId`; the upload + is removed on every exit and swept every fifteen minutes; a job that + cannot be queued answers `503` and is marked failed. The admin backup + upload stops at 100 times its compressed size (256 MB to 4 GB) with + `413`. +- Every credential the server issues, invite links and the sign-in handoff + code included, is kept out of logs and the replay cache. +- A passkey or security-key challenge is used up when checked. +- A device-bound refresh token is refused without `X-Device-Id`. +- Upload routes that read their own body send the same security headers + as the rest. + +### Upgrade notes + +- Take a database backup before upgrading. +- Migrations: `0356` adds document archive connections and a nullable + `source_instance` to the document import tables, `0357` adds encrypted + columns for Coach titles and custom-metric notes, `0358` records the + keys each backup needs and adds off-site purge requests, `0359` adds + `reproof_at` to sessions. None deletes anything. +- Going back to 1.39.2 is not supported once the server has started: it + cannot read Coach titles stored only encrypted. Restore the database + backup from before the upgrade instead. +- Off-site backup: the bucket credential needs `DeleteObject` (an R2 Object + Read & Write token has it). To rotate `BACKUP_ENCRYPTION_KEY`, move the + old key into `BACKUP_ENCRYPTION_PREVIOUS_KEYS`. +- Key rotation does not reach inside earlier backups. Keep a retired + `ENCRYPTION_KEYS` entry until Admin, Encryption lists nothing for it. +- `ALLOW_LOCAL_AI_PRIVATE_HOSTS=true`: nothing changes for the admin key + or an admin account. If other accounts use an AI endpoint on your + network, copy the `AI_PRIVATE_ORIGINS` line from the admin AI settings + page, restart and remove the old setting. +- `TRUST_PROXY_HOPS`: with a proxy that sends only `X-Real-IP`, set `1` + (unset means `1`); a host reachable directly wants `0`. Check the log for + a line starting `[getClientIp] requests carry`. +- Request bodies over 1 MB are refused outside the upload routes. Keep any + upload size limit in your reverse proxy as it is. +- New settings, all on the compose whitelist: `DOCUMENT_SOURCE_ORIGINS` + (empty, picker off), `AI_PRIVATE_ORIGINS` (empty), + `BACKUP_ENCRYPTION_PREVIOUS_KEYS` (empty), + `PASSWORD_BREACH_CHECK_DISABLED` and `UPDATE_CHECK_DISABLED` (unset, + checks on). +- Users are asked to confirm before exports, share links, tokens, + invitations and connecting an assistant when their last sign-in is older + than five minutes. After a password change or sign out everywhere, share + links have to be made again. +- The image is about 17 MB larger for the local OCR engine. +- Past medication history west of UTC is not rewritten; a wrongly named day + stays as an off-schedule entry and can be deleted by hand. +- iPhone app 1.0.3 and 1.0.4 keep working. Reminders, supply figures, + aggregates and synced rows change value without a schema change. + ## [1.39.2] — 2026-09-26 Documents can be brought over from Paperless-ngx and Papra, conditions diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index ad42df0c6..824b25a94 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.2 + version: 1.39.3 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 48e77f94d..cbb99830e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.39.2", + "version": "1.39.3", "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 15ecbd8d2..6d754253d 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.2"; + /* @sw-version-fallback */ "v1.39.3"; 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/*`