Commit Graph
1247 Commits
Author SHA1 Message Date
Marc-André Bombeck 7be3714ac2 Merge pull request #1246 from robdegraaf/feat/i18n-nl 2026-10-20 00:14:49 +02:00
Marc-André Bombeck f05b2f71e7 Merge branch 'fix/v1436-oura' into release/v1.43.6 2026-10-19 20:20:29 +02:00
Marc-André Bombeck 3ff860591e Merge branch 'fix/v1436-ui' into release/v1.43.6 2026-10-19 20:20:28 +02:00
Marc-André Bombeck bf84c0c11e fix(measurements): file sleep sessions by span, not by night label
The SQL page labels a sleep session by the last row of any writer; the
reconstruction labels it by its winning writer. A watch stopping before
midnight beside a phone whose in-bed span ran past it gave two days,
and matching the two by label dropped the night from the list while
the total still counted it. Each reconstructed session is now filed
under the page night whose span holds it and takes that night's label,
which the drill-down also asks by.

A sleep drill-down scans 36 hours either side of its day instead of the
whole history. Both day branches and the sleep list resolve the zone
through the same stored-zone resolver, so a drill-down always cuts the
days its list row summed. The list description states the real page
bounds: a day-sum page takes `limit` as asked, a sleep page caps at
1000 nights.

Refs #1245
2026-10-19 19:49:11 +02:00
Marc-André Bombeck 44d8181cb4 docs(openapi): publish the sleep score value
The derived envelope types `value` as an open record, so the sleep
score's `rhythmNights` and per-sub-score `gap` were on the wire but in
no schema. `SleepScoreValue` joins `WellnessScoreValue` in the forced
components slot, the envelope's `value` description names it, and a
contract test holds the published field set to the one the engine
emits, for a scored rhythm and for one withheld for too few nights.

Refs #1204
2026-10-19 18:56:06 +02:00
Marc-André Bombeck 3a9f619001 chore(release): v1.43.6 2026-10-19 18:18:12 +02:00
Marc-André Bombeck 9d6bfe2624 Merge branch 'fix/v1436-gh' into release/v1.43.6 2026-10-19 17:26:51 +02:00
Marc-André Bombeck f6b09f07cd feat(integrations): say where an imported Google Health history ends
The two status routes publish backfillEnded: each data type whose
backfill stopped short of the start of its history, with the instant it
reaches back to and whether the horizon or a walk that stopped moving
ended it. The integration card lists them in place of an import that
would otherwise read as still loading, in all seven locales.

Refs #1220
2026-10-19 17:25:57 +02:00
Marc-André Bombeck cfd6219981 fix(measurements): page day sums and sleep nights in SQL
The day-grouped list read the newest `limit` raw rows (the web asked
for 5000), folded them into days and returned every day at once: one
page of thousands of rows, and every day older than the 5000th raw row
missing without a word. The sleep list did the same at a year.

Both now pick the page in Postgres. The day mode selects the page's
local days with the distinct-day total from `COUNT(*) OVER ()` in the
same query, then folds only those days into per-source groups in SQL
and lets the source ladder choose among the groups, so no raw row
reaches the process. The sleep mode cuts sessions with the same gap
rule as the reconstruction, pages wake days the same way and reads only
the page's stage rows; the 365-day cut and `meta.windowFrom` are gone.
A sleep page holds at most 1000 nights, echoed in `meta.limit`.

The web list pages both modes at 25 like the per-sample list. The
`partial` day marker is dropped from the contract; no day is cut any
more.

Refs #1245
2026-10-19 16:43:39 +02:00
Rob de Graaf c6f61fad33 feat(i18n): add Dutch as an eighth end-to-end locale
Adds messages/nl.json (AI-initial, informal je/jij register, full key
parity with en.json) and registers nl everywhere the locale list lives:
config, loaders, Intl/date-fns mapping, zod enums, OpenAPI spec, scripts,
the managed-profile picker and the notification locale guards.

Dutch follows the Korean pattern for the AI surfaces (English body plus a
Dutch reply-language footer, no safety-contract matrix) and gets its own
banks in the outbound safety screen (dose, risk, causal, continuation),
the prose-grounding and citation-coverage tables, the reference-range
prose and the no-key fallback copy, each with Dutch test fixtures.

PHQ-9 / GAD-7 / WHO-5 are not marked validated for nl yet: the Dutch
wording has not been read against the published forms, so Dutch readers
get the English items plus the existing note until it has.
2026-10-19 11:06:55 +02:00
Marc-André Bombeck ade7a252f0 chore(release): v1.43.5 2026-10-18 13:11:47 +02:00
Marc-André Bombeck 5afa22b382 chore(release): v1.43.4 2026-10-17 11:37:20 +02:00
Marc-André Bombeck e8e3cf4cd3 Merge remote-tracking branch 'origin/main' into fix/v1434-illness-symptoms 2026-10-17 11:35:55 +02:00
Marc-André Bombeck c1df60693b fix(symptoms): assign an occurrence to an illness by its link, not its time
The episode detail listed every symptom occurrence that fell inside the
episode's onset-to-recovery span as well as the ones filed against it, so a
long-running condition showed the symptoms of every illness overlapping it,
and picking an illness in the entry sheet changed nothing on the page. The
occurrence edit sheet carried no illness link at all, so once logged the
assignment could be neither seen nor changed.

- The episode card reads only the occurrences filed against the episode
  (`SymptomEvent.episodeId`), with a guard against any other.
- The edit sheet loads the link and edits it like the time and intensity;
  an unchanged link is not re-sent, so a link the list cannot show stays.
- One rule decides which episodes may take an occurrence
  (`isEpisodeLinkableAt`): an open one, or a resolved one whose span holds
  the occurrence's time. The server checks it against the time after the
  edit, and the entry and edit sheets offer exactly what it admits, so a
  backdated occurrence can be filed with an illness that has since ended.
- The illness page names the illness on each occurrence row.
- The day view opens a symptom's illness only while that episode exists.

The stored links were always written correctly, so no data repair is needed.

Refs #1237
2026-10-17 11:00:02 +02:00
Marc-André Bombeck 792fc89a93 fix(auth): count a passkey as the second factor on the managed-profile acts
An account whose only strong credential is a primary passkey was refused
auth.stepup.mfa_not_enrolled on every action behind requireFreshMfa, so it
could not create, edit, hand over or delete a managed profile or manage its
guardians, although a passkey sign-in or re-proof already stamps the
session the gate reads.

requireFreshMfa now defaults to the any-possession proof source: a passkey
makes the gate reachable, and a passkey re-proof through the dialog clears
it. The refusal offers passkey in meta.methods, never the password.

hasSecondFactorEnrolled keeps its narrow meaning on purpose. It decides
whether the conditional gates apply at all (account deletion, the data
reset, the password change, the export, offering manage access, the
recent-proof branches and the native Bearer rules), so none of those
change, and password sign-in still challenges only TOTP and security keys.
The MFA-management routes keep the second-factor source. The decision and
its limits are written beside FreshFactorProofSource.

Copy, the OpenAPI description and the sharing and key-rotation docs say
"second factor or passkey" where this gate applies.

Refs #959
2026-10-17 09:56:30 +02:00
Marc-André Bombeck bb53c7484f chore(release): v1.43.3 2026-10-17 09:17:59 +02:00
Marc-André Bombeck 99f0607a69 fix(auth): let every fresh-factor refusal open the re-proof dialog
A Guardian signed in through single sign-on with a second factor enrolled
could not hand a managed profile over once five minutes had passed since
sign-in: the handover answered 401 auth.stepup.required, and the panel
printed "Confirm your second factor, then try again" with no way to do so.

The same held for every action behind requireFreshMfa: creating, editing
and ending a managed profile, adding and removing a Guardian, disabling
the second factor, rotating recovery codes, removing a security key or a
passkey, changing the password, the data wipe, account deletion and the
key rotation. The recent-proof routes had the dialog; these had text.

- requireFreshMfa names the proofs that clear a stale stamp in
  meta.methods (the possession proofs the account holds, never a
  password, which stamps reproofAt only).
- The re-proof flow treats that refusal like auth.reproof.required:
  open the dialog, post the proof to /api/auth/reproof, send the request
  once more. The flow moves into a React-free module so it can be driven
  in tests; the hook binds it to the dialog.
- The managed-profile mutation hooks take a required proof guard, and
  every caller passes recentProof.run. The other surfaces wrap their
  requests in recentProof.run; disabling the second factor asks before
  its confirmation opens, so the code typed there is not spent twice.
- An account with nothing to re-prove in place (single sign-on only, no
  factor) is told to sign in again when adding a factor, instead of being
  offered a password it does not have.
- step-up-web-caller-guard freezes the gated route methods and fails when
  a web request to one of them runs outside the flow.

An account whose only second factor is the identity provider's is still
asked for an app factor on the managed-profile acts: the app cannot see
the provider's factor. Enrolling right after a sign-in needs no further
proof, and the confirmation stamps the session, so the act follows.

Refs #959
2026-10-17 09:17:03 +02:00
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