Commit Graph
7413 Commits
Author SHA1 Message Date
Marc-André Bombeck fbd639d9f1 Merge remote-tracking branch 'origin/main' into ready/pr-1131 2026-10-04 14:08:27 +02:00
Marc-André Bombeck c2a3a26537 Merge pull request #1136 from MBombeck/release/v1.40.0
chore(release): v1.40.0
v1.40.0
2026-10-04 14:04:56 +02:00
Marc-André Bombeck dee00613d8 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 12:23:40 +02:00
Marc-André Bombeck 32de7ecf37 Merge pull request #1135 from MBombeck/fix/cycle-backup-and-med-put
fix(backup): carry encrypted free text readable in portable backups
2026-10-04 12:23:35 +02:00
Marc-André Bombeck 257d3ef6a5 docs(changelog): add the portable backup fixes to 1.40.0 2026-10-04 11:32:04 +02:00
Marc-André Bombeck 5d28d05004 docs(changelog): add reasoning effort, blood pressure and previews to 1.40.0 2026-10-04 11:30:20 +02:00
Marc-André Bombeck 4e88243075 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 11:29:47 +02:00
Marc-André Bombeck 24786f6549 Merge pull request #1130 from MBombeck/feat/provider-reasoning-effort
feat(ai): per-provider reasoning effort for Local and gateway models
2026-10-04 11:29:44 +02:00
BernhardBuckel 1763ac85d9 fix(import): honour the origin marker on a blood pressure correlation
A blood pressure reading is a <Correlation> wrapping its systolic and diastolic
records, and the marker can sit on the correlation instead of on each record.
The import only read it from a record's own metadata, so a marked correlation
would have come back in full.

A correlation's records are now held until it closes, then all kept or all left
out together: the marker on the correlation, before or after its records, or on
a record, applies. Tests cover each, the marker not spilling onto the next
record, and a self-closing record.
2026-10-04 11:28:08 +02:00
Marc-André Bombeck ad0f35eadd Merge remote-tracking branch 'origin/main' into release/v1.40.0
# Conflicts:
#	CHANGELOG.md
2026-10-04 11:13:49 +02:00
Marc-André Bombeck ecddcb3fa5 Merge pull request #1128 from MBombeck/fix/pdf-raster-ocrmypdf
fix(documents): hoist the pinned canvas binary and render PDFs with pdfjs font data
2026-10-04 11:13:29 +02:00
Marc-André Bombeck bb36a2afd5 Merge pull request #1127 from MBombeck/fix/idempotency-body-fingerprint
fix(idempotency): replay a key only for the same request body
2026-10-04 11:13:26 +02:00
BernhardBuckel 841d4e559c test(import): add a synthetic export.xml fixture for the origin marker
The fixture is invented, in the shape Apple's DTD documents, and says so in its
header and its file name. It shows the parser reads a file laid out like an
export; it cannot show that Apple writes a custom metadata key verbatim.
2026-10-04 11:10:49 +02:00
BernhardBuckel a5e98c9e71 fix(import): leave out Apple Health samples HealthLog wrote itself
The iOS app writes manual entries into Apple Health, and mirrors Withings and
import rows into it, each stamped with dev.healthlog.app.origin = healthlog.
The server already holds those readings under their own source, so the same
sample arriving through an Apple Health export is a second copy: the series
shows it twice and the rollups count it twice.

The export import now leaves out a record carrying that marker. The marker is a
child <MetadataEntry>, read after the record's open tag, so a record is
committed at its close tag. As a fallback for manual entries mirrored before the
app set the marker, a record is also left out when a MANUAL row of the same
type and value sits within 2 s (same rule and tolerance as the sync path).
Withings and import rows are not matched by value and time: they reach Apple
Health only through the mirror, which always sets the marker.

The import summary says how many records were left out for this reason.

Fixes #1109
2026-10-04 10:53:23 +02:00
Marc-André Bombeck a0fa4783c0 fix(ai): keep JSON mode when an endpoint rejects only reasoning_effort
An endpoint that does not know reasoning_effort answers with an unknown
parameter error, which the JSON-mode probe read as a refusal of
response_format and switched that base URL to plain prompts.
2026-10-04 10:48:35 +02:00
Marc-André Bombeck 39a5862d54 feat(settings): add a Reasoning select to the Local and gateway forms
One select under the model field of the Local and OpenAI-compatible
provider forms: Default, Off, Low, Medium, High, with a one-line hint
that Off leaves the answer budget for the answer itself. It reuses the
NativeSelect, label and hint layout of the field above it, seeds from
the stored entry, and saves with the form. Strings in all seven
locales.

Refs #1126
2026-10-04 10:43:04 +02:00
Marc-André Bombeck 9cb06ec44d fix(idempotency): read a rebuilt multipart form as the same request
A client that rebuilds a form for a retry picks a fresh random boundary,
so the raw bytes differ and a repeated document upload ran a second time
instead of replaying. The boundary named in Content-Type is replaced by a
fixed token before hashing.
2026-10-04 10:43:00 +02:00
Marc-André Bombeck befac59576 feat(ai): send a per-entry reasoning effort to Local and gateway providers
Reasoning models on Ollama and similar servers think by default and can
spend the whole max_tokens budget before the visible answer starts, so
the call comes back empty or times out. The Local and OpenAI-compatible
chain entries now carry an optional reasoning setting: Default sends
nothing, Off sends reasoning_effort "none", Low / Medium / High send
their own value. It is stored on the entry in aiProviderChain, so a
chain can mix a thinking model with one that does not know the field.

- Every exported resolver binds the entry's value onto the client, the
  connection test included, keyed on the instance type so the operator
  provider, OpenAI, Anthropic and Codex never carry it.
- PATCH /api/user/ai-provider accepts localReasoningEffort and
  compatReasoningEffort and GET reports them. A provider absent from
  the chain holds its value on a disabled entry; restating Default on a
  chain that was never customised writes nothing.
- PUT /api/insights/provider-chain accepts reasoningEffort on local and
  openai-compatible entries, refuses it on any other, and keeps the
  stored value when the body leaves it out. Its schema is now shared
  with the OpenAPI registry.
- A Default request body is byte-identical to the one sent before.

Refs #1126
2026-10-04 10:42:59 +02:00
Marc-André Bombeck 8cf1210bad fix(documents): log why a document got no thumbnail
Refs #1124. The thumbnail job opened no wide event, so every annotation
it made went nowhere: one instance ran it hundreds of times, left sixteen
PDFs without a preview, and the log never said so.

Each run is now a `job.document_thumbnail` background event. A run that
should have produced a preview and did not is raised to warn and names
the reason; a type that never has a preview stays at info.
2026-10-04 10:37:24 +02:00
Marc-André Bombeck 4b73099e75 fix(documents): render PDFs with pdfjs font data and refuse blank pages
Refs #1124. The rasterizer opened pdfjs without its standard font, CMap
and wasm directories and told it to use system fonts. The runtime image
has none, so any text pdfjs could not draw from an embedded program (a
non-embedded standard font, or an embedded one it cannot parse) came out
as nothing, and an image-only provider was sent the page background. A
workstation hides this because its own fonts stand in.

pdfjs now gets the directories from its own package, resolved at runtime
from the working directory, and Liberation Sans from that package is
registered as the canvas' sans-serif, serif and monospace faces for the
text pdfjs draws with a substitute.

A document whose every rendered page is blank now reports render-failed,
so the read records raster-failed instead of indexing a transcript of an
empty page as a success.
2026-10-04 10:37:24 +02:00
Marc-André Bombeck b63fa317f3 fix(docker): hoist the canvas binary of the pinned version
Refs #1124. The store holds two @napi-rs/canvas versions, the pinned one
and an older one that pdf-parse's own pdfjs-dist pulls in. The runner
stage took the musl binary with `find | head -1`, got the old one, and
copied it into the new loader's directory, where the loader looks first.
Rendering still worked, but `loadImage` calls `Image.decode`, which the
old binary lacks, so every preview thumbnail failed with "image.decode is
not a function" on both architectures.

The builder now records the resolved canvas version and the runner hoists
that version by name, as it already does for pdfjs-dist. The build stops
when the binary it copies belongs to another version.
2026-10-04 10:37:23 +02:00
Marc-André Bombeck bf35779db7 Fail the round trip when a portable file carries ciphertext
With every sealed column filled, the column round trip now also builds
the portable file of the same account and fails on any section that
carries a sealed column as stored ciphertext, unless the section is named
with a reason. The disaster-recovery file of the same account is the
floor, so a matcher that finds nothing cannot pass the check.
2026-10-04 10:19:32 +02:00
Marc-André Bombeck b36190b2df Restore custom mood tags without mood entries
The account's own mood categories, tags and hidden set were written only
inside the restore's mood-entry branch, so a vocabulary set up before the
first entry did not come back. They are written whether or not the file
carries an entry.
2026-10-04 10:19:32 +02:00
Marc-André Bombeck 8ac3fddc3b Carry visit and vaccination free text readable in a portable backup
A practitioner's note, a visit's reason, outcome and body site and a
vaccination's note travelled as stored ciphertext in a portable file, the
same defect the cycle and mood sections had. They now go through the
shared sealed-text helpers: readable in a portable file, sealed under the
receiving host's key on restore, ciphertext kept only where this host
opens it and otherwise reported as visitCiphertext or
vaccinationCiphertext. The practitioner address and phone ciphertext of a
disaster-recovery file gets the same check, since the key preflight now
leaves these sections to it.
2026-10-04 10:19:31 +02:00
Marc-André Bombeck 2798cc953f refactor(workouts): drop the unused clock arguments of the blank draft 2026-10-04 10:18:16 +02:00
Marc-André Bombeck 183b7ce2c5 Merge remote-tracking branch 'origin/main' into release/v1.40.0 2026-10-04 10:16:56 +02:00
Marc-André Bombeck b55850967d fix(idempotency): replay a key only for the same request body
The replay cell was keyed on (user, key, method, path). A client that sends
two different writes under one key got the first write's cached response for
the second, and the second was never saved. Native clients up to 1.1.0 post
the two halves of a manual blood pressure exactly that way, so the diastolic
value was silently dropped.

Each cell now stores a SHA-256 fingerprint of the request body (JSON with
recursively sorted keys, otherwise the raw bytes; scoped by method and path;
at most the first MiB is read). Lookup replays, or refuses as in flight, only
when the fingerprint matches. A different body, or a row written before the
column existed, runs the handler without claiming or caching and leaves the
existing cell for its own retries; a lost claim race re-checks the winner's
fingerprint the same way. The body itself is never stored.

Migration 0371 adds the nullable idempotency_keys.request_fingerprint column.
The OpenAPI parameter description, the client notes and the Home Assistant
guide say what a replay now requires. docs/ops gains a read-only query that
lists systolic readings left without their diastolic half, held to the
schema by an integration test.
2026-10-04 10:14:22 +02:00
Marc-André Bombeck 808b2672cd Merge pull request #1125 from MBombeck/fix/manual-workout-blank-start
Read a blank workout start as now minus the duration
2026-10-04 09:30:20 +02:00
Marc-André Bombeck 17b653b819 feat(workouts): read a blank start as now minus the duration
Refs #1085. Logging a workout right after finishing it meant changing the
start first, or the form refused it as ending in the future. The start field
now opens blank; left blank, the workout is saved as starting the duration
before now. A start the person enters is validated as before.
2026-10-04 09:07:39 +02:00
Marc-André Bombeck c7a2eec078 Drop the column fallback around the medication update
The update retried without pausedAt and notificationsEnabled for a
database that lacked the columns. Migrations guarantee both, and inside
the transaction a failed statement aborts it, so the retry could not
work anyway.
2026-10-04 08:31:11 +02:00
Marc-André Bombeck 43a53255c9 Carry custom mood labels readable in a portable backup
The labels of the mood tags and mood categories an account created
travelled as the stored ciphertext in a portable file, so a restore onto
a host with another key refused the file or wrote labels nobody could
open. They now follow the cycle note: readable in a portable file, sealed
under the receiving host's key on restore, ciphertext kept only where
this host opens it and otherwise reported as moodLabelCiphertext.

The sealing helpers move to one module both sections share.
2026-10-04 08:31:11 +02:00
Marc-André Bombeck 715df7d5d3 docs(readme): keep the contributor list without a separate line for one person 2026-10-04 08:29:43 +02:00
Marc-André Bombeck b3e18739fb docs(readme): name the people whose reports shaped HealthLog 2026-10-04 08:24:07 +02:00
Marc-André Bombeck 6bc764fa28 Write a medication PUT and its course in one transaction
The row update (name, schedule, oneShot) committed before the course
write. When a concurrent course made the course write refuse, the client
got the refusal while the rest was already written. The schedule replace,
the row update, the pause era and the course write now commit together,
so a refusal leaves the medication unchanged. The rollup recompute runs
after the commit.
2026-10-04 08:17:34 +02:00
Marc-André Bombeck 8121935642 Carry cycle free text readable in a portable backup
A portable file carried the day-log note, the sensitive-category envelope
and custom symptom labels as the stored ciphertext, so a restore onto a
host with another key either refused the file or wrote values nobody
could open. The portable file now carries them readable and the restore
seals them under the receiving host's key; a disaster-recovery file keeps
the ciphertext.

A file written before this still restores: each ciphertext value is
opened with this host's keys first, and one that does not open is kept
out of the row and reported in the skip summary as cycleCiphertext. The
key preflight leaves the cycle sections to that per-value check.
2026-10-04 08:17:34 +02:00
Marc-André Bombeck 964a3d050c Merge pull request #1123 from MBombeck/fix/key-check-newest
Never refuse a key that opens the newest data
2026-10-04 02:22:13 +02:00
Marc-André Bombeck 2f3da10317 Ask the newest values before the key check refuses
The probe decided on the eight oldest values alone. An install
reinstalled under a fresh key long ago holds old rows sealed under a key
that is gone and all current data under the key it runs with; it works
today, and the check would refuse it with 503 on every request.

When every oldest value fails, the newest values of the same columns are
now sampled in reverse order. One that opens makes the verdict
inconclusive (mixed): the process serves, warns and probes again at the
next start. Only old and newest values that all fail are a mismatch. A
newest-only open never seals; sealing stays with the oldest-first rule.

Every probe query now runs in its own transaction under a
transaction-local statement_timeout (2 s), so a large or locked table
ends that column with an error and the probe as incomplete instead of
holding up the boot. The newest order is the exact mirror of the oldest,
so an index on the timestamp serves both directions.

The module comment lists the three conditions that still yield a
mismatch.
2026-10-04 01:58:04 +02:00
Marc-André Bombeck f685cbe460 chore(release): v1.40.0 — back up the encryption key, medication courses and categories, your own symptoms, document reads in the background 2026-10-04 01:26:37 +02:00
Marc-André Bombeck 2c2e3fcc72 Merge pull request #1122 from MBombeck/feat/medication-categories-courses
Custom medication categories and several courses per medication
2026-10-04 01:25:33 +02:00
Marc-André Bombeck db9618ee7b Merge remote-tracking branch 'origin/main' into wip/v140-H 2026-10-04 00:43:10 +02:00
Marc-André Bombeck 4317eb91ca Make the rolling course anchor opt-in
The rule that keeps an earlier course's intake from anchoring a rolling
cadence applied to every caller unless one opted out, which is how a
booster reminder picked it up. It now applies only when the context
declares rollingAnchor: "courseStart"; the four medication context
builders declare it, and every other caller keeps the previous
behaviour, where the last intake always anchors. A guard enumerates
every RecurrenceContext builder in src, requires each to be listed as a
medication or a non-medication builder, and checks the declaration on
both sides.
2026-10-04 00:42:54 +02:00
Marc-André Bombeck df85fd62c9 Drop the unused built-in category list export 2026-10-04 00:33:34 +02:00
Marc-André Bombeck b930ab80ff Keep booster and measurement reminders anchored on the last satisfaction
The course rule that stops an old intake from anchoring a rolling
cadence read a reminder's anchor date as a course start, so a booster
fourteen years past on a ten-year cadence came due later today instead
of staying overdue. Measurement and booster reminders now opt out with
rollingAnchor: "lastIntake"; the rule stays on medications.
2026-10-04 00:33:34 +02:00
Marc-André Bombeck bc41dfb1b3 Merge pull request #1121 from MBombeck/fix/key-check-hardening
Make the encryption key check unable to lock out a correct key
2026-10-04 00:30:28 +02:00
Marc-André Bombeck 363070540b Merge remote-tracking branch 'origin/main' into fix/key-check-hardening 2026-10-04 00:01:07 +02:00
Marc-André Bombeck e04daed904 Merge pull request #1120 from MBombeck/fix/v140-review-ui
Keep the glucose card in mg/dL and fix pre-release review findings
2026-10-04 00:00:53 +02:00
Marc-André Bombeck 69bae8bdb4 Add ENCRYPTION_KEY_CHECK=warn as an escape hatch for the boot key check
At a first start there is no recorded key yet, so an operator facing a
refusal they are certain is false has no row to delete. With
ENCRYPTION_KEY_CHECK=warn (default enforce) the check still runs and logs
its full outcome, /api/health names a mismatch as a warning field without
failing, requests are served, and no key is recorded unless every key id
passes.

The variable is on the compose whitelist, in both example files, the env
manifest, the Unraid and Portainer templates and the published-env guard.
The log block, the mismatch page and the self-hosting pages offer it as the
last resort after the three real fixes, and say that it disables a safety
check.
2026-10-03 23:46:40 +02:00
Marc-André Bombeck 2c2151ae70 Keep the rolling anchor when a start only moved forward
An intake before startsOn stopped anchoring a rolling cadence, which
also moved the next dose of a single-course medication whose start had
merely been pushed forward a few days. The intake is now ignored only
when the dose it implies would fall before the course starts (a new
course after a gap); otherwise it anchors as before.
2026-10-03 23:42:29 +02:00
Marc-André Bombeck 17bced4fdf docs(self-hosting): an Apple Health import from a web-only container is queued, not refused 2026-10-03 23:38:13 +02:00
Marc-André Bombeck b74c6de7ce fix(cycle): give the custom-symptom popover the shared symptom icon picker 2026-10-03 23:38:12 +02:00