diff --git a/CHANGELOG.md b/CHANGELOG.md index 5654e7f10..9048aa07d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,19 +1,83 @@ # Changelog -## [Unreleased] +## [1.40.0] — 2026-10-04 + +Back up the encryption key and refuse a key that cannot open the data; +several courses and custom categories for medications; custom symptoms; +document reads in the background; sick notes; lab unit refusal and charts +that follow the source priority; blood pressure no longer loses its +diastolic value; document previews work again. + +### Added + +- **Encryption key backup step (migration 0365).** Admin step with key id, + fingerprint and per-platform guidance (`HEALTHLOG_PLATFORM`), an optional + "check my copy" that compares and discards the pasted key, a reminder that + returns when the key changes. +- **Boot key check.** A canary per key id, probed oldest values first across + string and Bytes columns; a proven mismatch answers 503 + `encryption.key_mismatch` everywhere except `/api/health` and + `/api/version`, MCP included, and starts no worker; an unproven result + serves, warns and re-checks. `ENCRYPTION_KEY_CHECK=warn` as a last resort. +- **Medication courses and custom categories (#1024, #1041, migration + 0368).** Several courses per medication; compliance, streak, cadence and + dose history count every course and never the gaps; existing dated + medications become one course each. Own categories as `custom:` with + `categoryLabel`; the runtime category table is now in the schema. +- **Custom symptoms (#981, migration 0369).** Up to eight definitions, events + with a 0 to 10 intensity, a capture kind, one correlation channel per + symptom, a Coach block; gated on the Illness module. +- **Document AI as background runs (#1090, migrations 0366 and 0370).** + Index, summary, suggestions and extraction accept `Prefer: respond-async`; + the lab scan always runs in the background; `GET /api/ai-runs/{id}`. +- **`SICK_NOTE` document kind.** +- **Natural durations (#1101)** in the sleep field. +- **Reasoning effort per provider (#1126).** The Local and OpenAI-compatible + forms gain a Reasoning select (Default, Off, Low, Medium, High), stored on + the provider's chain entry; Default sends nothing, Off sends + `reasoning_effort: "none"`. A rejection of that field no longer switches + the endpoint's JSON mode off. ### Fixed -- **Blood pressure saved without its diastolic value.** The iPhone app (up to - 1.0.3, and TestFlight 1.1.0) sends the two halves of a manual reading as two +- **Blood pressure saved without its diastolic value (iOS #15, migration + 0371).** The iPhone app sends the two halves of a manual reading as two requests under one `Idempotency-Key`, and the server answered the second - with the first one's cached response instead of saving it. The replay cache - now stores a SHA-256 fingerprint of the request body (migration 0371, - `idempotency_keys.request_fingerprint`) and replays only the same body; a - different body under the same key is saved normally. Readings saved before - this fix stay incomplete: delete the incomplete reading and enter it again - with both values and the original time. Operators can list affected - readings with the query in `docs/ops/blood-pressure-missing-diastolic.md`. + with the first one's cached response. The replay cache now stores a SHA-256 + fingerprint of method, path and body (JSON canonicalised, a multipart + boundary normalised) and replays only the same request; a different body + under the same key is saved. Readings saved before this stay incomplete; + `docs/ops/blood-pressure-missing-diastolic.md` lists them. +- **Document previews and scanned PDFs (#1124).** The image hoisted an older + `@napi-rs/canvas` binary next to the pinned loader, so every thumbnail + failed; the build now hoists the pinned version and fails on a mismatch. + PDFs render with pdfjs font, cmap and wasm data and a registered fallback + font instead of absent system fonts, and an all-blank render reports + `raster-failed`. The thumbnail job logs why a preview is missing. +- **Portable backups.** Cycle free text, custom mood labels, visit notes + and vaccination notes are carried readable and re-encrypted on restore + instead of as the source server's ciphertext; custom mood tags restore + without mood entries; a round-trip guard fails on any ciphertext in a + portable file. A medication update and its course share one transaction. +- **Lab unit mismatch (#1094).** A reading in another unit than its marker is + refused (`labs.unit.mismatch`) on every write path; a marker's unit is + locked while it has readings (`biomarkers.unit.locked`). +- **Source priority in series (#1108).** Pulse, HRV and SpO2 series read the + ladder's source per day; unranked types keep every source. +- **Sign-in over plain http (#1097).** Refused with + `auth.session.insecure_transport` before a password, code or challenge is + used; the sign-in page says so first. +- **Chart window (#1102).** Ends at the end of today in the profile time zone. +- **Workouts.** The insight cap survives a delete (migration 0367); a changed + `manual:` resubmit updates the workout and its paragraph. +- **Display.** Clinician decimals in the owner's locale, stated dates east of + UTC+12, archive month grouping, lab list columns without a range, icon + picker touch targets and labels, symptom entry load errors. + +### Changed + +- Quick-entry forms load on demand; the four eager route baselines are + lowered to the new measurements. ## [1.39.9] — 2026-10-03 diff --git a/README.md b/README.md index 806f6649d..a757a5c08 100644 --- a/README.md +++ b/README.md @@ -129,9 +129,9 @@ Actively developed — new releases roughly weekly, issue reports and PRs welcom HealthLog started as something I built for myself. That it has turned into a project other people use, care about and help shape still surprises me, and I am grateful for every part of it. -Thank you to everyone who sent code that shipped: [@TimonBed](https://github.com/TimonBed), [@Antiheld86](https://github.com/Antiheld86), [@BernhardBuckel](https://github.com/BernhardBuckel), [@balajiv113](https://github.com/balajiv113), [@aucun6352](https://github.com/aucun6352), [@mathewcsims](https://github.com/mathewcsims) and [@muhdusama](https://github.com/muhdusama). A special thanks to [@BernhardBuckel](https://github.com/BernhardBuckel), who has been working through the lab screens one careful pull request at a time, with tests and a plan before every change. +Thank you to everyone who sent code that shipped: [@TimonBed](https://github.com/TimonBed), [@Antiheld86](https://github.com/Antiheld86), [@BernhardBuckel](https://github.com/BernhardBuckel), [@balajiv113](https://github.com/balajiv113), [@aucun6352](https://github.com/aucun6352), [@mathewcsims](https://github.com/mathewcsims) and [@muhdusama](https://github.com/muhdusama). -Thank you to everyone who opened an issue, asked a question in the discussions or wrote me an email. Most of what got better in the last months started with someone describing what went wrong for them, often with a log, a screenshot and a second try when the first fix was not enough. +Thank you to everyone who opened an issue, asked a question in the discussions or wrote me an email, and especially to [@lutzkind](https://github.com/lutzkind), [@HercoGC](https://github.com/HercoGC), [@mills1975](https://github.com/mills1975), [@sreeramachandramurthy](https://github.com/sreeramachandramurthy), [@CritLoren](https://github.com/CritLoren), [@mbreitkreuz](https://github.com/mbreitkreuz), [@k-nacion](https://github.com/k-nacion), [@NebuPookins](https://github.com/NebuPookins), [@Nazza01](https://github.com/Nazza01), [@Cnote43](https://github.com/Cnote43) and [@juliaanvaneijndt](https://github.com/juliaanvaneijndt), whose reports shaped HealthLog again and again. Most of what got better in the last months started with someone describing what went wrong for them, often with a log, a screenshot and a second try when the first fix was not enough. And thank you to everyone who supported the project with a coffee or by buying the iPhone app. I don't name anyone here, but I read every message that comes with it. It pays for the servers and the Apple developer account, and it tells me that HealthLog is worth the evenings. diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index 82eecc332..bb71a2e8e 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.9 + version: 1.40.0 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 8f2a1b5f0..74f0243db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "healthlog", - "version": "1.39.9", + "version": "1.40.0", "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 12e07572b..32351a83e 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.9"; + /* @sw-version-fallback */ "v1.40.0"; 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/*` diff --git a/src/components/workouts/manual-workout-form.tsx b/src/components/workouts/manual-workout-form.tsx index c849056e4..4455c83ae 100644 --- a/src/components/workouts/manual-workout-form.tsx +++ b/src/components/workouts/manual-workout-form.tsx @@ -116,7 +116,7 @@ export function ManualWorkoutForm({ // a submit after an edit updates the row the first submit stored. const [externalId] = useState(newManualWorkoutExternalId); const [draft, setDraft] = useState(() => - emptyManualWorkoutDraft(new Date(), timezone), + emptyManualWorkoutDraft(), ); const [errors, setErrors] = useState< Partial> diff --git a/src/lib/workouts/__tests__/manual-entry.test.ts b/src/lib/workouts/__tests__/manual-entry.test.ts index 0f728fb10..001142455 100644 --- a/src/lib/workouts/__tests__/manual-entry.test.ts +++ b/src/lib/workouts/__tests__/manual-entry.test.ts @@ -242,7 +242,7 @@ describe("buildManualWorkoutEntry — validation", () => { describe("the form's defaults", () => { it("opens with a blank start; the field caps at the current minute", () => { expect(wallClockNow(NOW, "Europe/Berlin")).toBe("2026-09-15T14:00"); - expect(emptyManualWorkoutDraft(NOW, "Asia/Seoul")).toMatchObject({ + expect(emptyManualWorkoutDraft()).toMatchObject({ sportType: "", start: "", hours: "", diff --git a/src/lib/workouts/manual-entry.ts b/src/lib/workouts/manual-entry.ts index 6be35a294..d672ae8ea 100644 --- a/src/lib/workouts/manual-entry.ts +++ b/src/lib/workouts/manual-entry.ts @@ -118,10 +118,7 @@ export function wallClockNow(now: Date, timezone: string): string { return `${p.year}-${pad(p.month)}-${pad(p.day)}T${pad(p.hour)}:${pad(p.minute)}`; } -export function emptyManualWorkoutDraft( - now: Date, - timezone: string, -): ManualWorkoutDraft { +export function emptyManualWorkoutDraft(): ManualWorkoutDraft { return { sportType: "", // Blank on purpose (#1085): a blank start means "it just ended", so the