Commit Graph
7289 Commits
Author SHA1 Message Date
BernhardBuckel bb1acfab64 fix(series): follow the source priority ladder in the dense and raw series reads
The rollup tier and sleep resolve every (type, day) to the source the person's
ladder picks before they aggregate. The dense and raw reads of
/api/measurements/series filtered on user and type only, so with a second
provider a day's value blended the two, a reading present in both showed
twice, and the configured ladder had no effect on those charts.

Both reads now take the ladder-winning source per day through the shared
canonicalMeasurementsCte helper, as the other live readers do:

- the day and hour bucket read of the dense kinds (pulse, glucose, HRV, SpO2),
  and the stats strip over its rows;
- the generic raw read (weight, body fat, steps, resting heart rate, ...).

The collapse is per day for the hour grain too, as everywhere else. The helper
gains an optional single-type scope so a series scans one metric, not every row
the person has in the window.

Only a type that has a ladder is collapsed. For a type without one (glucose,
body water, bone mass) the helper breaks the tie by source name, which would
silently hide one of two real sources, so those keep every source as before.
A single-source account reads exactly what is stored.

Tests: a real-database test covering the ladder in both directions, the
per-day fallback, a reading mirrored into two sources, the day-bucket, hour-
bucket and raw reads, a type with no ladder and a single-source account. The
route's unit test file reads its fixtures through a small adapter for the new
raw statements, and the dense-read guard drops the exception for the removed
findMany. The OpenAPI description states the rule.

Refs #1108
2026-10-03 20:18:50 +02:00
Marc-André Bombeck 02b6f06521 Merge pull request #1103 from MBombeck/feat/document-ai-runs
Read documents and lab scans in the background
2026-10-03 19:21:43 +02:00
Marc-André Bombeck d753621bd1 Merge pull request #1105 from MBombeck/fix/lab-list-columns
Keep lab list columns aligned for readings without a range
2026-10-03 19:07:43 +02:00
Marc-André Bombeck dcf3b18c91 Merge pull request #1099 from MBombeck/docs/readme-thanks
Update the thanks section
2026-10-03 18:59:01 +02:00
Marc-André Bombeck 20470d99f4 Merge remote-tracking branch 'origin/main' into feat/document-ai-runs
# Conflicts:
#	docs/api/openapi.yaml
2026-10-03 18:41:55 +02:00
Marc-André Bombeck 5c300a8d9c fix(labs): keep the range bar and trend in their columns when a reading has no range
The range badge renders nothing for a reading without a reference range, so
that row had four cells instead of five and the range bar and the trend
moved one column to the left. The badge now sits in a cell of its own. The
chevron is hidden from lg up, where the row's hover already says it opens;
a phone keeps it.
2026-10-03 18:38:31 +02:00
Marc-André Bombeck d805ccf28b Merge pull request #1100 from MBombeck/ci/trivy-braces
Ignore the braces advisory until a fix is published
2026-10-03 18:28:24 +02:00
Marc-André Bombeck 7b51edcebe Merge pull request #1096 from BernhardBuckel/fix/lab-unit-mismatch
fix(labs): refuse a reading whose unit differs from its marker's
2026-10-03 18:28:20 +02:00
Marc-André Bombeck 89540a999b i18n(documents): say when a read continues in the background
Seven locales for the background-run copy: the calm line while a document
read or a lab scan runs in the worker, the hint when no worker has picked it
up, and the two failures only a background run can produce.
2026-10-03 18:20:59 +02:00
Marc-André Bombeck 0acf3da575 feat(documents): read documents and lab scans in the background
"Read with AI" and the lab scan used to run their model calls inside the
request. At the default response time a read of three calls outlasted the
100 s a proxy in front of the server commonly allows, and the person saw a
cut connection for a read the server was still doing.

Both now run as a background run (`DocumentAiRun`, migration 0366):

- `POST /api/documents/inbound/{id}/index` with `Prefer: respond-async`
  answers 202 with a run id once every quick refusal has passed. Without
  the header it answers exactly as before, which the iPhone app relies on.
  A second request while a read of the same document is queued or running
  attaches to that run.
- `POST /api/labs/ocr/extract` always answers 202. Only the web client
  calls it.
- `GET /api/ai-runs/{id}` serves the run: the same body the synchronous
  route answers with, or the failure code and status it would have given.
  Scoped to the caller; another account's run is a 404.

The worker (`document-ai-run`, a long queue with a one-hour expiry, one
job per run under `lockedPass`) claims the run, asks the capability again
for the record and re-checks the wire for the provider it picks, sets the
run's deadline from the person's AI response time and refuses a read that
cannot fit the job's expiry. Sync route and worker run one body per kind,
so the two paths cannot drift. Budget is reserved at enqueue and settled
once by whoever ends the run.

Input and result are sealed with labelled AES-256-GCM, registered for key
rotation; the input is dropped when the run ends. A five-minute reaper
fails a run no worker took within fifteen minutes (and hands its
reservation back), fails a run past its deadline, and deletes a finished
run after an hour. Runs are outside backups and inside the data wipe.

The web client polls through the query-key factory and says, calmly, that
the read continues in the background, and when no worker has picked it up.
2026-10-03 18:18:56 +02:00
Marc-André Bombeck 87fa2f85fa Merge branch 'main' into ci/trivy-braces 2026-10-03 17:57:51 +02:00
Marc-André Bombeck 3cb98f92e9 Merge branch 'main' into docs/readme-thanks 2026-10-03 17:57:50 +02:00
Marc-André Bombeck ed94e56c5e Merge branch 'main' into fix/lab-unit-mismatch 2026-10-03 17:51:46 +02:00
Marc-André Bombeck b9867caeea Merge pull request #1098 from MBombeck/fix/backup-round-trip-tombstone-age
Keep the backup round-trip tombstone inside the retention window
2026-10-03 17:51:43 +02:00
Marc-André Bombeck 1d86bb8d6d i18n(labs): unit mismatch strings for es, fr, it, ko and pl 2026-10-03 17:45:05 +02:00
Marc-André Bombeck 296ca5d2dc ci(trivy): ignore the braces advisory until a fix is published
braces is reached only by development tooling and never ships in the
production image; no patched version exists yet.
2026-10-03 17:29:51 +02:00
Marc-André Bombeck 1dab2687ab docs(readme): thank contributors, reporters and supporters by what they gave 2026-10-03 17:29:34 +02:00
Marc-André Bombeck 86a6e81fc1 test(backup): keep the tombstone fixture inside the retention window
The round-trip fixture deleted its measurement on a fixed date. The restore
drops a measurement tombstone older than TOMBSTONE_RETENTION_DAYS (75) on
purpose, so on 2026-10-03 the fixture aged out and the suite failed on main
and on every open branch. The deletion is now ten days before the run.
2026-10-03 17:28:50 +02:00
BernhardBuckel d3916036c2 fix(labs): refuse a reading whose unit differs from its marker's
A numeric lab reading was stored with its value as received and the marker's
unit stamped on it, so 5.6 mmol/L against a marker kept in mg/dL became
5.6 mg/dL and read as far below range. Both the manual/API write and the
scan commit did this; nothing compared the two units.

- POST /api/labs returns 422 labs.unit.mismatch, with the marker's unit and
  the reading's unit in meta, and writes nothing. The unit is compared only
  when one was sent: on the biomarkerId path it may be omitted, and the
  marker's unit is then the stated one. A qualitative reading has no unit.
- The scan commit skips such a row with reason unit_mismatch, so the outcome
  reads partial or failed instead of a clean save, and the toast names the
  analytes concerned rather than saying "duplicates".
- The extract response carries the matched marker's unit. The review screen
  shows a calm badge on a row in another unit, starts it unticked like a
  duplicate, and treats it as a blocking error on the unit field if it is
  ticked anyway.
- A small lab-specific normaliser forgives spelling only: spacing, the micro
  prefix, the case of the litre, and a unit printed in capitals for a short
  list where capitals cannot mean another unit. Case is otherwise significant,
  so mIU/L and MIU/L, or g/L and G/L, are never merged.

The document-commit path already refuses a mismatch and is left as it is.
Readings stored wrongly before this cannot be identified, because the
original unit was never kept; they are not touched.

Adds EN and DE strings; the other locales are left to the maintainers. The
OpenAPI spec and the error-code catalogue carry the new code.

Fixes #1094
2026-10-03 15:44:51 +02:00
Marc-André Bombeck 556bd6d6cd Merge pull request #1093 from MBombeck/release/v1.39.9
Release v1.39.9
v1.39.9
2026-10-03 00:57:24 +02:00
Marc-André Bombeck e42b3f331f chore(release): v1.39.9 — log a workout by hand, a page for body fat, and the AI response time everywhere 2026-10-03 00:35:27 +02:00
Marc-André Bombeck a14ae57283 fix(workouts): refuse a manual workout that would end in the future
The form's start defaults to the minute it opened, so a workout logged
right after it finished was stored as ending up to a day ahead. The
duration now fails validation when start + duration lies past now, with
the same one-minute slack the start check uses.
2026-10-03 00:35:26 +02:00
Marc-André Bombeck 2cd8b28a22 Merge pull request #1092 from MBombeck/fix/ai-timeout-everywhere
Run every model call under the AI response-timeout setting
2026-10-03 00:23:26 +02:00
Marc-André Bombeck bd2d43e402 Merge remote-tracking branch 'origin/fix/ai-timeout-everywhere' into fix/ai-timeout-everywhere 2026-10-03 00:03:31 +02:00
Marc-André Bombeck 0f6669847c Merge remote-tracking branch 'origin/main' into fix/ai-timeout-everywhere 2026-10-02 23:59:38 +02:00
Marc-André Bombeck 2473d52f19 Merge pull request #1089 from MBombeck/feat/manual-workout-entry
Log a workout by hand
2026-10-02 23:59:11 +02:00
Marc-André Bombeck e6e5bd0adc Merge pull request #1087 from MBombeck/fix/dashboard-tile-links
Every dashboard tile opens its chart; body fat gets its page
2026-10-02 23:50:43 +02:00
Marc-André Bombeck 3b1d503e87 Merge branch 'main' into fix/ai-timeout-everywhere 2026-10-02 23:20:26 +02:00
Marc-André Bombeck 57a99e6cc8 Merge branch 'main' into feat/manual-workout-entry 2026-10-02 23:20:24 +02:00
Marc-André Bombeck 774dff410c Merge branch 'main' into fix/dashboard-tile-links 2026-10-02 23:20:23 +02:00
Marc-André Bombeck 06f69e4408 Merge pull request #1091 from MBombeck/fix/e2e-lab-review-animations
Wait for the lab review sheet to stop sliding before measuring
2026-10-02 23:20:11 +02:00
Marc-André Bombeck ae0bdd0390 fix(settings): name the real response-timeout defaults
The placeholder said "Default (~120)" while most surfaces fall back to
60 s and the briefing to 180 s. The placeholder and the help line now
interpolate both numbers from the constants the server applies
(PROVIDER_DEFAULT_TIMEOUT_MS, AI_BUDGETS.comprehensive.timeoutMs), in
all seven locales, and a test fails if a locale writes a number of its
own.

Refs #1090
2026-10-02 23:17:37 +02:00
Marc-André Bombeck 6207101006 test(ai): guard that every model call runs under the response-timeout setting
Pins the four ways the setting could be skipped again: a wire client
reading params.timeoutMs itself instead of callTimeoutMs, an exported
resolver in provider.ts returning a provider it did not bind, a client
constructed outside provider.ts (the offline evaluation harness is the
named exception), and a surface ceiling not listed with its reason.
Each matcher asserts a non-zero match count.

Refs #1090
2026-10-02 23:03:45 +02:00
Marc-André Bombeck bf7fb9c401 fix(documents): wait as long as the server may on document reads and lab OCR
The browser gave up on its own: 120 s for the document routes, 90 s for
a lab scan, and the 15 s default on the lab text path, while a slow
self-hosted model with a raised response timeout was still allowed to
answer. A five-page report read by a local model failed on the client
side with the server still working.

Each request now sizes its abort from ai.provider.responseTimeoutMs on
the account payload, times the model calls the route can make, plus
30 s: one call for suggest and summary, three for Read with AI (the
transcription, then the lab staging read and its corrective retry), two
for a lab scan (the read and its retry). With the setting unset that is
90 s, 210 s and 150 s.

Refs #1090
2026-10-02 23:01:41 +02:00
Marc-André Bombeck 5ee1b3e28f fix(ai): run every model call under the response-timeout setting
The response-timeout setting reached the briefing, the status notes and
the Coach, which threaded it by hand. Document reads, document
extraction, lab OCR and lab staging called the provider without a
timeout, so every client fell back to its own 60 s literal and a slow
self-hosted model was cut off however high the setting stood.

The setting now rides on the provider instance. resolveProvider,
resolveProviderChain and resolveProviderForTest stamp the record's
aiResponseTimeoutSeconds on everything they return, and each wire
client reads its ceiling through callTimeoutMs: the setting, else the
surface's own timeoutMs, else 60 s. A call site no longer has to pass
it, so a new one cannot forget it. The connection test runs under the
same ceiling a read does.

The Coach nudge tick and the reaction line keep their short ceilings
(timeoutPolicy: "surface-ceiling"): the tick runs accounts one after
another under a shared budget, the reaction line's claim lease is
shorter than the largest setting, and both have deterministic copy
that stands in.

/api/auth/me publishes the effective value as
ai.provider.responseTimeoutMs, so a client can size its own request
timeout from it instead of guessing.

Refs #1090
2026-10-02 23:01:36 +02:00
Marc-André Bombeck c08dfc618f test(e2e): call a lab review field beside the value when it shares its row
The spec compared field tops within 8 px. A field label that wraps onto a
second line, which depends on the runner's font metrics, moves that
field's input down by one line while it stays in the same row, and the
560 px case failed on it. A field is now beside the value when it sits to
its right and starts above the value's bottom edge.
2026-10-02 22:56:27 +02:00
Marc-André Bombeck 4125d08fb4 test(e2e): wait for the lab review sheet to stop sliding before measuring
On a phone and a small tablet the review sheet slides up from the bottom;
its width settles before the slide ends, so waiting on width alone still
compared fields from different frames. The spec now waits for every
running animation to finish and for the surface to hold its position and
width across two frames.
2026-10-02 22:33:17 +02:00
Marc-André Bombeck b079e23a83 Merge remote-tracking branch 'origin/feat/manual-workout-entry' into feat/manual-workout-entry 2026-10-02 22:30:19 +02:00
Marc-André Bombeck 9e239dd6c5 build: re-measure the aggregate client budget for the manual workout entry
Main measured 3718 KB gz after v1.39.7 and v1.39.8, almost all of it new
copy in the seven lazy locale catalogs; the workout form, its sheet, the
delete action and their copy add about 7 KB (3725). The cap moves from
3724 to 3749, the same 24 KB headroom as before; the eager routes stay
inside their drift allowances.

Refs #1085
2026-10-02 22:30:09 +02:00
Marc-André Bombeck 11936ed93c Merge remote-tracking branch 'origin/main' into feat/manual-workout-entry 2026-10-02 22:28:44 +02:00
Marc-André Bombeck 1a403b78f7 Merge pull request #1086 from MBombeck/fix/audit-node-forge
Ignore the node-forge advisory until a fix is published
2026-10-02 22:16:59 +02:00
Marc-André Bombeck e6e1b51918 Merge branch 'main' into feat/manual-workout-entry 2026-10-02 21:49:45 +02:00
Marc-André Bombeck f0abe984b0 Merge branch 'main' into fix/dashboard-tile-links 2026-10-02 21:49:44 +02:00
Marc-André Bombeck 5dde6f785c Merge branch 'main' into fix/audit-node-forge 2026-10-02 21:49:42 +02:00
Marc-André Bombeck 1dd0609535 Merge pull request #1088 from MBombeck/fix/e2e-lab-review-settle
Measure the lab review rows once the dialog has settled
2026-10-02 21:49:31 +02:00
Marc-André Bombeck d9dfa7e5db test(dashboard): type the module cases of the add-menu parity check
Refs #1085
2026-10-02 21:44:37 +02:00
Marc-André Bombeck 69f19ec232 copy(workouts): mention manual entry in the empty workouts list
Refs #1085
2026-10-02 21:38:22 +02:00
Marc-André Bombeck cf678f6ec4 fix(dashboard): gate every add-menu entry on its module, as the capture picker does
The dashboard add menu asked only whether the record's section was
writable, so a switched-off mood or medications module still offered its
entry here while the bottom bar's picker hid it. The menu now renders from
the picker's own visibleCaptureKinds, so the two cannot disagree.

Refs #1085
2026-10-02 21:38:06 +02:00
Marc-André Bombeck 074cccd920 docs(api): regenerate the contract for the workout delete error code
Refs #1085
2026-10-02 21:36:24 +02:00
Marc-André Bombeck 2979b5c4ef test(e2e): measure the lab review rows once the dialog has stopped widening
The review stage widens the sheet and the row grid re-flows from the
container width; on a slow runner the 560 px case measured a half-laid-out
row and failed on the first attempt. The spec now waits until the surface
keeps its width across two frames before it measures.
2026-10-02 21:33:31 +02:00