1230 Commits
Author SHA1 Message Date
Marc-André Bombeck 1a2853395b chore(release): v1.43.2 2026-10-16 17:11:58 +02:00
Marc-André Bombeck e2b7e7350e fix(consent): mint AI consent receipts only on an explicit agreement
The AI settings page posted to /api/consent/ai/web on every mount and the
server minted an ai_full receipt for it, so an account that had agreed to
nothing sent its health data to the operator's provider from background
jobs. Native builds synced a receipt the same way on every launch.

- Both grant routes now require `confirm: true`; anything else writes
  nothing and answers 400 consent.ai.confirmRequired.
- Remove the mount-time heal. A consent card names the operator's service
  and the data that would go there, with one Agree button, in Settings > AI
  and on the Insights overview while AI is closed by the missing consent
  alone.
- Publish the standing consent as `ai.consent` on /api/auth/me (status,
  since, recipients); the settings card renders active-since and the
  withdraw control from it.
- Existing receipts stay valid.
- Guard that no client grants outside a click handler, route tests for the
  confirmation, and an integration test that a fresh operator-key account
  gets no background AI text until it agrees.
2026-10-16 17:11:04 +02:00
Marc-André Bombeck d2fe55bc9d chore(release): v1.43.1 2026-10-16 11:01:44 +02:00
Marc-André Bombeck d06ce36049 chore(release): v1.43.0 2026-10-16 08:46:13 +02:00
Marc-André Bombeck a806676a71 Merge branch 'v143/fixdata' into release/v1.43.0 2026-10-16 06:22:02 +02:00
Marc-André Bombeck f715190703 Merge branch 'release/v1.43.0' into v143/fixdata 2026-10-16 05:57:22 +02:00
Marc-André Bombeck beb7ef4ccc Merge branch 'release/v1.43.0' into v143/fixui 2026-10-16 05:55:14 +02:00
Marc-André Bombeck 120b0966b1 fix(ai-usage): never regenerate more often on frugal than on standard
The frugal sync gap is the larger of a day and the operator's sync gap,
so an instance gap above 24 hours no longer leaves frugal ahead of
standard.
2026-10-16 05:38:36 +02:00
Marc-André Bombeck a8b46b9ac0 docs(api): regenerate the OpenAPI contract 2026-10-16 05:37:18 +02:00
Marc-André Bombeck b99b70127d fix(anamnesis): one newest AUDIT-C entry, scored against the wording offered
The stored language is the wording the read route offers the account;
a request naming another wording, or a takenAt more than five minutes
ahead, is refused. Latest, offerDue, the Coach block and the doctor
report all read the newest entry whose answers open.

The old one-word alcohol pattern gives way to an AUDIT-C entry in the
Coach block, and the alcohol section lists it with a remove action.
2026-10-16 05:36:42 +02:00
Marc-André Bombeck 1734f149d3 docs(openapi): deprecate the legacy integration status fields 2026-10-16 03:57:46 +02:00
Marc-André Bombeck 4daad8f2fb Merge branch 'v143/life' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	src/__tests__/contract-stub-inventory.test.ts
#	tests/integration/sharing-manage-handler-matrix.test.ts
2026-10-16 03:39:40 +02:00
Marc-André Bombeck da860aeb3e Merge branch 'v143/dawarich' into release/v1.43.0
# Conflicts:
#	src/__tests__/contract-stub-inventory.test.ts
2026-10-16 02:51:02 +02:00
Marc-André Bombeck 8f59d1c5ab docs(api): describe the date-only valid-since day 2026-10-16 01:40:16 +02:00
Marc-André Bombeck c03f79afd5 test(dawarich): pin the movement block, the channels and the mocks; document the connection
Covers the day section, the model projection without the travel country and
the two correlation channels; the backup and correlation mocks learn the new
delegate. Adds the operator page for DAWARICH_PRIVATE_ORIGINS and regenerates
the API contract.
2026-10-16 01:38:05 +02:00
Marc-André Bombeck e5860c9f41 docs(api): regenerate the OpenAPI document for the life circumstances 2026-10-16 01:25:35 +02:00
Marc-André Bombeck 17d1c00aab docs(api): list the pulse day-mean maintenance error codes 2026-10-16 01:07:39 +02:00
Marc-André Bombeck 0dd634c9c8 Merge branch 'v143/links' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	src/__tests__/contract-stub-inventory.test.ts
#	src/app/api/medications/[id]/route.ts
#	src/components/medications/detail/medication-detail-tabs.tsx
#	src/lib/export/backup-plan.ts
2026-10-16 00:34:53 +02:00
Marc-André Bombeck 3fc37c9183 Merge branch 'v143/ios' into release/v1.43.0 2026-10-16 00:28:40 +02:00
Marc-André Bombeck 6146082824 Merge branch 'v143/supplements' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
#	src/__tests__/sharing-surface-guard.test.ts
#	src/lib/ai/capabilities/types.ts
#	src/lib/export/backup-plan.ts
2026-10-16 00:28:29 +02:00
Marc-André Bombeck a0f6b89e92 feat(measurements): take hourly sums of summed types into the intraday profile and a backfill trigger that moves no live stamp; fix(backup): restore qualitative lab markers 2026-10-16 00:18:35 +02:00
Marc-André Bombeck 5c039a220b Merge branch 'v143/sso' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	src/__tests__/contract-stub-inventory.test.ts
2026-10-15 23:59:03 +02:00
Marc-André Bombeck b4bc039d16 feat(links): close the relation matrix and publish related[] on six details
The link service gains medication and lab result sources and the five new
tables, each readable from both ends. POST/DELETE /api/medications/{id}/links
and /api/labs/{id}/links replace their 501 stubs; the visit links route also
files medications and doses, the dose links route its visit. A scan commit
with a documentId files each written result against its report.

GET on a lab result, visit, document, condition, medication and dose now
carries related[]: stored links, the condition a result shares a visit with,
and the medication whose efficacy target is the result's marker, each with
its web path. A kind is listed only when its module is on and the caller's
grant covers its section. relation-matrix.test.ts holds every one of the
fifteen pairs to a table, a reader or a written exception.
2026-10-15 23:57:34 +02:00
Marc-André Bombeck 8ac19e422a docs(api): regenerate the contract for the backup's medication composition 2026-10-15 23:54:09 +02:00
Marc-André Bombeck eb3cc4237f feat(medications): store a supplement's composition, read its label with the document AI, and carry it through backup and restore 2026-10-15 23:29:40 +02:00
Marc-André Bombeck 9eae352bb1 feat(claim): hand over a managed profile through single sign-on
POST /api/auth/claim/sso checks the handover link and the username and seals
the link id, the username and a per-start binding into an hl_claim cookie.
GET /api/auth/oidc/login?intent=claim carries the binding into the encrypted
OIDC state, and the callback binds the provider subject to the profile in the
claim transaction, refusing a subject or verified email another account
holds, a link that is no longer usable, and a live session.

The handover status and the claim preview name the redemption methods; a
single-sign-on-only server hands over through the identity provider, and only
the password claim stays refused there.
2026-10-15 23:12:59 +02:00
Marc-André Bombeck fd44a8381b Merge branch 'v143/ai' into release/v1.43.0
# Conflicts:
#	docs/api/openapi.yaml
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
#	src/__tests__/contract-stub-inventory.test.ts
2026-10-15 23:12:51 +02:00
Marc-André Bombeck 6f673a7b84 feat(ai): resolve an AI usage level per record and publish it as ai.usage
Three levels (frugal, standard, full) with an operator ceiling and default
and a per-account choice within them, resolved in the capability layer for
the record and published on GET /api/auth/me as ai.usage: the level in
effect, the stored choice, the bounds, why (user, default, admin_cap), the
three options with whether each is allowed and an estimate of tokens per
day scaled from the record's last seven full days of coach_usage, and the
measured mean. A delegate sees the level, never the owner's spend.

PUT /api/auth/me/ai-usage stores the account's own choice or null for the
default, refusing a level above the ceiling with 422 ai.usage.aboveCeiling
and meta.maxLevel. The new supplementLabel capability needs standard or
above; below it the capability is unavailable with the new usage_level
reason, refused with 403 ai.usage.levelTooLow and meta.requiredLevel.

Refs #1217
2026-10-15 22:49:01 +02:00
Marc-André Bombeck 81be73fca8 feat(labs): merge markers, keep aliases and read lab series per marker
POST /api/biomarkers/{id}/merge folds markers into one in a transaction,
converting readings where the registry knows the units and refusing the
whole merge where it does not; a dry run answers the same result and
writes nothing. Aliases appear on the marker and are edited through the
marker's PUT. Scan and document review rows name the matched marker or
offer up to three, and a chosen marker is remembered as an alias. The
doctor report, FHIR and MCP group readings by marker. Aliases and the
merge ledger travel through backup and restore.
2026-10-15 22:22:40 +02:00
Marc-André Bombeck 3786deb503 feat(api): publish the v1.43 contract with routes that answer 501 until filled
Every new endpoint of the contract exists, wrapped in apiHandler, with its
authentication, module gate and request validation, and answers
501 not_implemented until its behaviour lands. The OpenAPI document carries
the new operations and schemas and the additive response fields on the
changed routes. The four server feature flags are published as false.

contract-stub-inventory.test.ts registers the stubbed routes; each one
leaves the register with the change that fills it. The route, sharing,
session, security, module-gate and outcome inventories name the new routes
deliberately, and the error-code catalogue lists the codes the routes emit
today.
2026-10-15 21:49:02 +02:00
Marc-André Bombeck b1c59b908c chore(release): v1.42.4 2026-10-15 16:43:39 +02:00
Marc-André Bombeck 03b24461ad feat(healthkit): record Apple Health dose arrivals and the last background arrival per type
Migration 0401 turns healthkit_type_syncs.type into text so the ledger can
hold MEDICATION_DOSE, and adds last_background_at (seeded from rows whose
latest arrival was a background one). POST /api/medications/intake/bulk
takes syncTrigger and records APPLE_HEALTH dose arrivals; the Apple Health
status read serves a MEDICATION_DOSE freshness entry and lastBackgroundAt
on every entry with arrival facts.
2026-10-15 15:33:30 +02:00
Marc-André Bombeck 6313c0174a Merge branch 'v1424/readiness' into release/v1.42.4 2026-10-15 15:24:29 +02:00
Marc-André Bombeck b17a250894 feat(record-settings): publish connection health for a managed record's integrations
The guardian's read of a managed record's connections now carries the same
server-resolved health the owner's own envelope does, loaded for the record
and never for the guardian. The legacy state stays on the wire for one
transition release.
2026-10-15 15:06:02 +02:00
Marc-André Bombeck 7c905249fb fix(medications): page the side-effect list and name every GLP-1 medication in the timeline 2026-10-15 15:05:51 +02:00
Marc-André Bombeck 53e60f43f1 feat(labs): list every marker from a server-side per-biomarker overview 2026-10-15 15:03:06 +02:00
Marc-André Bombeck de40db7ad0 Merge branch 'v1424/fixes' into release/v1.42.4 2026-10-15 12:23:55 +02:00
Marc-André Bombeck 73c4dda224 fix(visits): reach every lab result, condition, document and visit from the link pickers
The visit form's lab picker offered the newest 200 results, the condition
picker the newest 100 conditions, the document pickers the newest thousand
documents and the document sheet's visit picker the newest 100 visits.
On a long record everything older could not be linked.

Each picker now opens on the records around the date of the thing being
linked, pages older and newer from there, and searches through the list
route itself. Records already linked always show, from the labels the
record's own read carries and from what was picked in the sheet.

The lab list takes a q filter over analyte and panel, the condition list
from/to/q/offset/sortDir, and the visit list an offset.
2026-10-15 11:24:40 +02:00
Marc-André Bombeck 8162afc0e1 docs(api): publish the connection health state 2026-10-15 11:09:50 +02:00
Marc-André Bombeck 9d167069db chore(release): v1.42.3 2026-10-14 19:35:25 +02:00
Marc-André Bombeck 42b66b2228 Merge branch 'v1423/labs' into release/v1.42.3 2026-10-14 19:34:44 +02:00
Marc-André Bombeck 035ae37ee4 Merge branch 'v1423/integrations' into release/v1.42.3 2026-10-14 19:20:16 +02:00
Marc-André Bombeck 07f09e1b61 fix(labs): store the catalog slug as a marker's analyte key
The matcher stored the registry entry's key, so LDL and HDL markers were
recorded as total-cholesterol. The key now names what the marker is (the
catalog slug, or a registry-only key for an analyte with no seed), and the
registry resolves ldl and hdl to the cholesterol entry for the conversion as
before. A key already written in the old form is rewritten by the same fill
step that brings a stale key in line with its name, the first time a reading
needs it. The OpenAPI descriptions say what the key is and that it follows
the marker's name and unit.
2026-10-14 19:18:49 +02:00
Marc-André Bombeck 47a91d6922 fix(labs): never present a converted reading's printed range in the wrong unit
A converted reading's bounds move with its value, but the printed range text
stays in the unit the report used, and every reader preferred that text: the
list showed "Report range 3.9 to 5.5" beside 100.9 mg/dL, and the doctor
report and the FHIR referenceRange.text did the same. The text is still
stored verbatim. The range resolver now takes the reading's sourceUnit (as a
required field, so no reader can forget to select it) and, for a converted
reading, leaves the window's text empty so every surface states the converted
bounds in the marker's unit, and hands the printed text over separately with
its unit. The lab DTO carries it as printedReferenceText, and the note beside
a reading shows it as "as printed in mmol/L: ...".
2026-10-14 19:10:49 +02:00
Marc-André Bombeck 044f9008ba fix(oura): ask for the spo2 and heart_health scopes, explain a missing scope calmly
Oura's current authorization server enforces scopes per collection.
daily_spo2 needs spo2; vO2_max, daily_cardiovascular_age and
daily_resilience need heart_health. The authorize request now asks for
both.

A grant issued before keeps answering 401 on those four collections
while every other one succeeds. Such a 401 next to a success is now
recorded as a scope gap rather than a failure: it no longer counts
toward the reconnect threshold, it no longer forces a token refresh once
known, and the Oura card asks for one reconnect instead. The Withings
card names the readings the account's API plan withholds.
2026-10-14 19:01:02 +02:00
Marc-André Bombeck 9aa60fb2ff fix(illness): keep chronic conditions out of rest mode
A chronic ongoing condition has no end date, so counting it as rest mode
kept the account in it for good: Day N counted from an onset years back,
cadence nudges never resumed and every streak gap since the onset was
bridged. Only active episodes that are not CHRONIC_ONGOING (acute,
recurring, flare) now trigger rest mode, and Day N counts from the
earliest of those. The achievements streak freeze applies the same
filter to its own episode query.

Chronic conditions stay context: a coincident vital deviation is still
framed as explained by any active episode, and the Coach block and the
briefing still name them, with restMode false.

Refs #1216
2026-10-14 18:57:01 +02:00
BernhardBuckel 747e0964ad feat(labs): show a reading's conversion in the scan review and beside the value
Slice 3 of phase 3 of #1095. The scan review no longer treats every reading in
another unit as unwritable. The server tells it which analyte the matched marker
is (markerAnalyteKey on each extracted row); a row whose unit differs but can be
converted starts selected and carries a badge saying what Save will store and
what the report stated ("Saved as 100.9 mg/dL, converted from 5.6 mmol/L"). A
row that cannot be converted keeps the existing blocked badge and error. Save
still sends what the report printed; the server converts, as in slice 2.

The review's duplicate flag also recognises a re-scan of a reading that was
stored converted, by what the report printed, the same rule the commit applies.

A converted reading carries a quiet "converted from 5.6 mmol/L" on the lab list
(row and tile) and the history list (phone and table), so a number nobody typed
can always be traced back to what the report printed.

Strings in all seven locales.

Refs #1095
2026-10-11 20:41:08 +02:00
BernhardBuckel caa7a937fd feat(labs): convert a reading into its marker's unit when the pair is known
Slice 2 of phase 3 of #1095. A reading that stated a unit other than its
marker's was refused. When the marker's analyte is known and the registry has the
pair, the three write paths now convert it instead: POST /api/labs, the OCR
commit and the document commit.

The converted value is stored at full precision in the marker's unit, with what
the report printed in the existing source_value and source_unit columns. The
report's own reference window is in the printed unit, so it is converted with the
value; otherwise the verdict would compare a converted value with an unconverted
window. A pair the registry does not have is still refused as before (the 422
now says why), as is a marker whose analyte is unknown.

The marker's key is filled the first time a reading needs it. The same scan
committed again is still a duplicate: a converted row holds the number in the
marker's unit, so the duplicate check also matches on what the report printed.
Editing the value of a converted reading clears the conversion record, because
the number is then the person's own.

The lab result DTO and the OpenAPI schemas (labs and the OCR commit) carry
converted, sourceValue and sourceUnit.

Refs #1095
2026-10-11 20:28:00 +02:00
BernhardBuckel 52ea3b9c2f feat(labs): link a marker to its analyte for the unit conversion
Slice 1 of phase 3 of #1095. A Biomarker row keeps only its name, so nothing
said which analyte it was. This adds the explicit alias list in the catalog the
issue settled on, including the registry-only keys (urea, blood urea nitrogen,
uric acid, total calcium, total bilirubin), and sets biomarkers.analyte_key
(from migration 0385) from it.

matchAnalyteKey equals a marker's folded name against the localised names of the
catalog seeds the registry knows and against the alias list. It matches the
whole name only, so "Calcium (ionisiert)" is not "Calcium"; a name two analytes
claim matches neither; and the marker's own unit must be one the analyte reaches,
so a marker called "Glucose" in a unit the registry cannot convert stays
unlinked.

The key is set when a marker is created (the biomarker route, the mint used by
lab writes, the legacy backfill) and, for an existing marker, by
ensureAnalyteKey the first time a reading needs it. It never overwrites a key
that is set. There is no boot job for the existing markers: the conversion that
uses the key fills it on first use, and a sweep can be added later if wanted.

The key is returned on the biomarker API and in the OpenAPI schema.

Refs #1095
2026-10-11 20:04:43 +02:00
Marc-André Bombeck 24f06884b3 fix(reminders): an ended course reads as not reminding
The worker expands no slot past endsOn, so it never reminds for a
course that has ended, but remindersActive still read true for it. The
reminder gate now includes the course end on the person's clock, so the
published value, the worker and the admin check make the same decision.
2026-10-11 15:16:54 +02:00