Commit Graph
1291 Commits
Author SHA1 Message Date
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 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 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 fb80b4bc62 fix(settings): say that the nutrient card's totals include the supplements taken 2026-10-16 08:42:13 +02:00
Marc-André Bombeck 69e2f18788 fix(ui): tidy the new record and settings surfaces at phone width
- Composition amounts show only the decimals they carry, so 100 µg no
  longer reads as 100.000 µg; the remove button stays on the amount row.
- AUDIT-C remove controls are icon-only, so they no longer spill out of
  the card or cover the earlier answer.
- Life-circumstance labels keep their colon when they wrap, checkbox
  groups sit in two columns and the weekly-minutes sentence ends.
- Link chips get a 44 px hit area for their remove control.
- The AI usage level reports a failed save with a sentence and the
  error id instead of a bare "Error".
- The Dawarich home-country and admin refresh-gap labels sit above
  their fields.
2026-10-16 08:03:29 +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 f360d221e1 fix(admin): record the pulse rebuild and let a stopped one run again
The rebuild after apply now writes its state beside the dry run
(queued, running, done with counts, failed); a run left queued or
running without a job reads as failed. The card shows it and offers a
new rebuild after a failure, which the route accepts once the method is
applied, without a new dry run.
2026-10-16 05:55:31 +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 8ba4d1b0d0 fix(nutrients): name the form an upper limit covers and phrase it per locale 2026-10-16 05:31:33 +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 73bb13a982 feat(anamnesis): soft-delete AUDIT-C in its route, admit both AUDIT-C verbs at manage level, and settle the guards 2026-10-16 01:24:56 +02:00
Marc-André Bombeck b838ed4403 feat(dawarich): fetch daily movement values from a Dawarich instance
Connect, status, places, settings, test, sync and disconnect replace their
stubs. A nightly job (03:20) and the connect backfill read the timeline,
visits and residency endpoints through safeFetch and store one row of daily
values per local day: distances, places visited, minutes away from the home
place and the sealed country with its travel-day verdict. A private instance
needs its exact origin in DAWARICH_PRIVATE_ORIGINS.

The connection joins the shared ledger and the connection health model,
watched for errors only. The day carries a movement block, the model
projection drops the travel country, the two movement channels join the
correlation matrix under the environment module, travel days become trip
suggestions, and the days travel in the environment backup section.
2026-10-16 01:20:53 +02:00
Marc-André Bombeck 28a8737124 feat(anamnesis): life-circumstances card with AUDIT-C, screening hints and free-text take-over 2026-10-16 01:09:50 +02:00
Marc-André Bombeck 9f17cef8e4 feat(admin): pulse day-mean dry run, apply step and admin card 2026-10-16 00:58:48 +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 1481e38fae feat(links): show related records and file the new pairs on the web
A shared related-records component renders related[] as rows that open the
server-resolved path, with how a derived relation runs. The medication
overview lists them and, in the account's own record, files its condition,
documents and visits; the visit sheet files medications and doses. The
condition page, the document sheet and the dose sheet list what their own
blocks do not already show. A dose's visit named by the form's field counts
as the same relation as the link table.
2026-10-16 00:25:04 +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 ffd46b017b feat(backup): carry the five relation link tables both ways
Lab result to document, visit to medication, visit to dose, medication to
document and medication to condition now ride the full backup in their own
section file and come back with their original filing instant. A link whose
end the restore did not write is dropped and reported under the new
relationLink skip kind. The tables leave the coverage register for the
two-ended list, guarded by relation-links-backup-completeness.test.ts and
seeded in the round trip.
2026-10-15 23:57:27 +02:00
Marc-André Bombeck 01fef691fa feat(web): edit a supplement's composition, read it from a label photo, and show nutrient sources with the upper limit 2026-10-15 23:48:43 +02:00
Marc-André Bombeck 79f7eb199b feat(insights): resolve direction verdicts, chart reference lines and mood patterns on the server 2026-10-15 23:39:11 +02:00
Marc-André Bombeck 9d32a8acb0 feat(medications): let a delegate manage a composition, bypass the proxy for label uploads, and keep the guards current 2026-10-15 23:37:26 +02:00
Marc-André Bombeck 0ebb61343e feat(claim): offer the identity provider on the claim page and name the way in to guardians
The claim page shows the password form, a continue-with-provider button, or
both, as the preview names them, and explains a refused single-sign-on claim
when the callback sends the browser back. The guardian's handover panel says
how the person will sign in.
2026-10-15 23:34:38 +02:00
Marc-André Bombeck 98d64c2052 feat(nutrients): add DHA, EPA, input units and upper limits to the catalogue, and count supplement doses as a nutrient source 2026-10-15 23:19:45 +02:00
Marc-André Bombeck b91328d67e feat(records): publish an age- and sex-aware benchmark beside each personal record 2026-10-15 23:18:58 +02:00
Marc-André Bombeck b562587c45 feat(cycle): name the start a period start folds in and the starts a removal gives back 2026-10-15 23:16:11 +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 74fa4e10b6 Merge branch 'v143/labs' into release/v1.43.0 2026-10-15 23:06:51 +02:00
Marc-André Bombeck e112e05a58 fix(labs): name a merge in the owner's activity feed and toast it through the outcome module
A merge by a record manager now reads as its own sentence in the owner's
activity feed, the merged toast goes through the written-outcome toast,
and the export test doubles carry the alias and merge tables.
2026-10-15 23:03:45 +02:00
Marc-André Bombeck ec67663def feat(settings): choose the AI usage level, and set its bounds as an operator
Settings, AI gains one card with the three levels, each with what it does
and the server's estimate of tokens per day, labelled as one. Levels above
the operator's ceiling are shown disabled with the reason; a capped choice
is named and kept; a stored choice can return to the server default. The
automatic document read says when the frugal level rests it. Admin, Coach
gains the ceiling, the default and the sync gap, and the provider readout
the accounts per level. Strings in all seven languages.

The admin card's save confirms with a success toast like its neighbour
assistant-section; it is a settings write without counts, so it is pinned
in the success-affordance guard at one.

Refs #1217
2026-10-15 22:55:40 +02:00
Marc-André Bombeck e65ada2961 feat(labs): merge markers, edit their aliases and pick a marker on review
The marker list offers a merge with the server's preview: what moves, what
is converted, which names become aliases, and a refusal when a unit cannot
be converted; it says a merge cannot be undone. The marker page shows and
edits the other spellings and follows a merged marker to its target. Scan
and document review name the matched marker or offer its suggestions and a
new marker; the choice is remembered. The biomarkerMerge feature flag is
on.
2026-10-15 22:55:08 +02:00
Marc-André Bombeck f7386742b4 feat(web): show the error id on server failures and add the support info and report surfaces 2026-10-15 22:31:51 +02:00
Marc-André Bombeck a3ce6af9a3 feat(labs): resolve marker names through aliases and catalog shorthand
A printed name now resolves to one of the account's markers by its own
name, then by an alias of the account, then by a catalog name for the same
analyte when the reading's unit fits. The catalog gains seeds for alkaline
phosphatase and total bilirubin and a checked list of common lab shorthand
in seven languages; names that mean two analytes stay out.
2026-10-15 22:22:29 +02:00
Marc-André Bombeck e16c9fb717 feat(links): search a year or a month in the link pickers by its date window
A picker term that names a period ("2019", "03.2019", "2019-03",
"März 2019", "2019년 3월") is sent as the list route's from/to window
instead of q, newest first, on every family including the visit list,
which has no text search. Month names come from Intl for the UI locale,
folded for case, accents and umlaut spellings. Instant routes get the
period's bounds in the person's timezone; the document route gets the
period's calendar days. The sheet names the period it shows.
2026-10-15 22:11:49 +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 f95c2bfd41 Merge branch 'v1424/cards' into release/v1.42.4 2026-10-15 15:22:12 +02:00
Marc-André Bombeck 73922c039e fix(insights): say what fills the recovery page when it is empty
The empty note told a connected Oura account to connect Oura. The page
charts the WHOOP and Polar recharge, strain and load readings and the
Oura resilience level; Oura's readiness is the recovery score linked
above. The note now says so.
2026-10-15 15:10:18 +02:00
Marc-André Bombeck 1a5b09a73c feat(settings): one integration card for every connection
Every connection in Settings renders the same card from the server's health
field: a badge with one word, one sentence per reason, the newest data as a
meta line, a collapsed details block whose text is exactly what the copy
button copies, and an action row with the rare actions in a menu and at most
one primary button. The per-provider banners, the raw provider error line,
the token expiry, the failure counter and the button rows are gone; every
capability they carried is reachable from the sentence, the one button, the
menu or the details. The managed-profile view renders the same card,
read-only. A structural guard keeps the cards off the retired fields.
2026-10-15 15:06:03 +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 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 42b66b2228 Merge branch 'v1423/labs' into release/v1.42.3 2026-10-14 19:34:44 +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
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
Marc-André Bombeck 765dbe1e7f Merge branch 'v1422/caption' into release/v1.42.2 2026-10-11 14:41:28 +02:00
Marc-André Bombeck 2f780df0b0 Merge branch 'v1422/autotaken' into release/v1.42.2 2026-10-11 14:41:28 +02:00