Commit Graph
738 Commits
Author SHA1 Message Date
Marc-André Bombeck 45c027aee2 Merge branch 'fix/v1437-review' into release/v1.43.7
# Conflicts:
#	src/app/api/analytics/route.ts
2026-10-20 06:20:42 +02:00
Marc-André Bombeck 40cf10ba20 test: bring fixtures in line with the nap rule and the recovery read
Correlation fixtures wrote minute-long midday sleeps, which the nap
rule now reads as naps; they write nights. The analytics test mocks the
user read the empty recovery reason makes, and the control-byte guard
asserts the size of its sweep.
2026-10-20 06:06:25 +02:00
Marc-André Bombeck ea933b91cc Merge branch 'fix/v1437-jobs' into release/v1.43.7 2026-10-20 05:58:02 +02:00
Marc-André Bombeck f6e0d72193 Merge branch 'fix/v1437-reads' into release/v1.43.7 2026-10-20 05:35:06 +02:00
Marc-André Bombeck 5720b70e8e test(guards): drop the removed chip ledger from the course-anchor builders
The cadence chips no longer build a recurrence context of their own; they tally the compliance ledger, whose adapter is already on the list.
2026-10-20 05:33:21 +02:00
Marc-André Bombeck 483c84b42e fix(integrations): report a page walk that stops short instead of ending quietly
The WHOOP, Oura and Withings list walks and the Fitbit exercise walk each
kept a page loop that, at its ceiling or on a cursor that did not advance,
returned what it had read and said nothing, and the backfill around it
stamped the history complete. `walkPages` keeps a ceiling but reports a
walk that stops with a page pending, or on a cursor it already followed,
as truncated. A backfill reads that through one verdict: a failure
retries, a truncated collection leaves the marker unset without a retry,
a clean run stamps it. The Fitbit exercise walk may read a hundred pages
during a backfill instead of twelve. A guard keeps page-counting loops out
of the provider directories, with Google Health and Strava listed until
their next change.
2026-10-20 05:06:21 +02:00
Marc-André Bombeck 9fe6f98fdb refactor(workouts): render workout dates through useFormatters
The workout list, the workout detail header and the dashboard's recent
workouts tile each rebuilt their own date formatter from the shared
cache, with a private copy of the current-year check, and so ignored
the reader's date-order preference. They now use useFormatters(), and
the copies are gone.

The timezone formatter guard now refuses any two-argument
getDateTimeFormat call in src/components, so a component cannot build
its own display formatter again. The medication card's day bucketing,
the one other such call, reads userDayKey instead.
2026-10-20 05:01:37 +02:00
Marc-André Bombeck feb8058233 fix(medications): tally the cadence chips from the compliance ledger
`/api/medications/[id]/cadence` computed its adherence chip from a
private copy of the compliance ledger that ignored pause eras, courses
and auto-missed doses, and read only the chart's window of intakes. A
medication paused for a week read 77 % there and 100 % on the detail
page. The chips now tally `trailingComplianceLedger`, the ledger the
compliance payload builds, over the same window, with the medication's
pauses and courses; the copy is removed and a guard freezes the set of
files that build dose rows.
2026-10-20 04:37:44 +02:00
Marc-André Bombeck e355c6120f fix(workouts): one canonical-workout picker with one source order
Three pickers decided which of two overlapping workouts to keep: the
read-time picker the list, the detail page and the Coach use, a
write-time batch picker with its own 90-second window, and an unwired
field-merge picker. They used different source orders (the `steps`
ladder, or a separate workout default when no order was saved), so the
batch route could keep a row the list then ranked below another.

The batch route now runs the read-time picker over the rows it is about
to insert, with the ladder from `workoutSourceLadder`, and reports what
it hides as duplicates. A session HealthKit hands over twice under two
ids is stored once, as the list already showed it. The write-time and
the unwired picker are removed.
2026-10-20 04:37:39 +02:00
Marc-André Bombeck ac86199808 fix(stats): one standard deviation with its kind named at the call site
About a dozen hand-rolled spreads each picked population or sample
variance without saying so, and the two surfaces a person compares
picked differently: the series API reported the population SD of a
blood-pressure series, the MCP tools and the AI payload the sample SD of
the same readings, about 22 % apart at three readings.

`lib/stats/spread` holds `variance` and `standardDeviation` with an
explicit `population | sample` kind and `READING_SPREAD`, the kind every
surface uses to report the spread of a person's readings. The series
route and the feature payload both report it; the spreads that feed a
verdict (the normal-swing checks, the changepoint threshold) and the
clinical glucose SD stay the sample estimate, now said out loud. A guard
fails on a new hand-rolled spread and on the two surfaces drifting apart.
2026-10-20 04:36:54 +02:00
Marc-André Bombeck 7535235b82 test(guards): give the new guards a floor on what they find
The zone-shift and single-Pearson guards now assert that their matchers
find the correct shapes before concluding nothing else matches, and the
dense-read guard lists the streak's per-day existence probe with the
reason it stays bounded.
2026-10-20 04:36:25 +02:00
Marc-André Bombeck b4333d65ff test: refuse raw control bytes in source files
Three files carried invisible control bytes in string literals, one of
them a production join key. They are escapes now, the same strings, and
a guard fails on any raw byte below 0x20 other than tab, newline and
carriage return.
2026-10-20 04:23:46 +02:00
Marc-André Bombeck a22bf91311 fix(insights): report every correlation through the significance gate
The /insights envelope (weight x BP, mood x BP, mood x weight, mood x
pulse, BP medication continuity) and the AI feature payload computed
their coefficients with an ungated Pearson that banded |r| above five
pairs, so five days of noise sloping the right way read as a strong
correlation there while every other surface refused the same points.
Both now use `significantPearsonCorrelation` (n >= 20 and p < 0.05,
through the engine's exact p-value), and the ungated function is gone.
The allowlist guard that tracked its remaining callers becomes a guard
that there is one Pearson implementation.
2026-10-20 03:41:54 +02:00
Marc-André Bombeck 3cf67d05f9 fix(ui): let the read-failure primitives decide by the error
About forty compact rows and seventy cards still offered a retry on a
spent read budget and drew an error card on a refusal, while a third
primitive (ReadFailureRow) did the right thing at eight places.

QueryErrorRow and QueryErrorCard now take the query's `error` and read
it through readFailureKind, which moves to src/lib/api next to
ApiError: a refusal (403) draws the surface's refused state or nothing,
a spent budget (429) says so without a retry, an ended session (401)
says so and offers sign-in instead of going blank, and only a real
failure offers the retry. ReadFailureRow is gone and every site passes
its error; a guard test fails on a site that does not.

The coach read strip keeps its card in the error state and holds one
body height across loading, failure and a one-line read.
2026-10-20 03:38:57 +02:00
Marc-André Bombeck 2b61762962 fix(integrations): round imported provider values through one normaliser
Fitbit, WHOOP, Oura, Polar and Withings each kept a private
`parseFloat(n.toFixed(2))`, which mis-rounds halves on the double below
the decimal. `importValue` keeps the same two places, rounds half away
from zero on the printed decimal and returns the nearest double, so a
value prints cleanly. A guard keeps `toFixed` and private `round2`
helpers out of the provider directories; the Google Health mapper is the
one listed exception until its next change.
2026-10-20 03:35:41 +02:00
Marc-André Bombeck 1ee73908ae fix(jobs): run one heavy job at a time per process
Web and worker share one process and one heap limit, and the jobs that
hold a large working set (the Apple Health and Health Connect imports,
restore, the weekly and off-host backups, the restore drill, the rollup
backfill, the nightly status batch, the pre-generation walk and the
narrative walk) ran side by side whenever their schedules or an upload
met, so the peak was their sum. `heavyLane` queues them in the process,
first come first served. A waiter leaves the line when pg-boss expires it
and its retry takes the place; a one-shot whose input lives outside
pg-boss keeps its place. The runtime guard binds every heavy queue in the
lane, keeps `lockedPass` outside it, and allows the one-shot rule only
where the send options forbid a retry.
2026-10-20 03:35:23 +02:00
Marc-André Bombeck 6026a28fe4 fix(dashboard): key streak and compliance days in the person's own zone
`measured_at` and `scheduled_for` are naive UTC timestamps. The dashboard
streak and the medication-compliance backfill keyed them with a single
`AT TIME ZONE tz`, which reads the value as local time and shifts it the
wrong way: in Berlin a reading at 23:30 UTC counted for the day before
instead of the day after, so a streak could break or stretch near
midnight and the backfill recomputed the wrong day.

The streak's measurement days now come from `readActivityDayKeys`: one
existence probe per local day instead of a DISTINCT over a year of
readings. The DAY rollups cannot answer this because their buckets are
UTC days. The two compliance-rollup readers had no caller left and are
removed with the coverage probe that carried the same shift. A guard
fails on any single shift of a naive column into a bound zone.
2026-10-20 03:35:19 +02:00
Marc-André Bombeck d9e56482d9 perf(jobs): poll a queue only its cron feeds every 30 seconds
pg-boss polls each idle worker slot every two seconds, so the hundred-odd
bindings asked the database for work about fifty times a second while
most of them wait for one tick a day. `createAndWork` now gives a queue
that a registrar schedules and nothing else sends to a 30-second poll.
Scheduled queues that also take sends keep the default, because with one
job per fetch a slow poll would drain a burst at one job per interval;
the runtime guard holds that list to the send sites in the tree,
including sends through a variable.
2026-10-20 03:35:09 +02:00
Marc-André Bombeck 9c51dd0d30 perf(analytics): read the latest value per type with one probe per type
The latest canonical reading per type ran as `DISTINCT ON (type)` over
every live row of the account in five places (summaries slice warm and
cold path, comprehensive aggregator warm and cold path, targets). With no
skip scan in Postgres 16 that sorted the whole account to keep one row
per type. `readLatestCanonical` drives the read from the type list
instead: one index probe for each type's latest instant, then a closed
one-day window ranked by the source ladder. The closed window also keeps
the plan under `jit_above_cost`. The targets read reuses `readLatestEver`
with a floor.

The summaries slice was built inside three cache cells (the tile-strip
route, the thick analytics body, the dashboard snapshot). It now has one
cell, read through `readSummariesSlice`; composing callers read it
fresh-only so a refresh after a write cannot cache the pre-write slice
again, and the snapshot folds the water tile into a copy instead of the
cached object.
2026-10-20 03:35:03 +02:00
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 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 0e6e50fc7b Merge branch 'fix/v1436-ui' into release/v1.43.6 2026-10-19 17:04:31 +02:00
Marc-André Bombeck 38c106e358 fix(ui): draw no empty frame and say when a read failed
"Since the last visit" was wrapped in a card by the visits section, so
the card stayed on screen empty while the window loaded, when it held
nothing and when the read failed. The component now owns its card and
draws it only with a row; a failed read shows a compact error row.

The same shapes elsewhere: the caffeine card and the mood forecast no
longer open a loading card that most often collapses, and a failed read
on them, the mood tag catalogue, the usual-range strip, the cycle ring
and the measurement drill-down now says so with a retry instead of
passing for no data. The scores strip keeps the ring's cell on a failed
calendar read. The mental-health card no longer draws an empty action
slot for a viewer without the right to start.

The frame guard now also flags a frame whose only content sits behind a
condition.

Refs #1241
2026-10-19 16:43:52 +02:00
Marc-André Bombeck d00281f35a test: count neither a comment nor a zero offset as a pager
The client fetch-limit guard took any `offset` within three lines as a
pager. The day-grouped measurement list passed it with `limit` 5000
between a comment about pagination and `offset` pinned to "0", while it
drew a whole history on one page. Comment lines, trailing comments and
inline block comments no longer count, nor does an offset pinned to a
literal zero in any of the three spellings. The old list shape is a
test case and fails the guard.

Refs #1245
2026-10-19 16:43:50 +02:00
Marc-André Bombeck 146fcac079 chore: write NUL as an escape instead of a raw byte
Raw NUL bytes in source files make grep and ripgrep treat the file as
binary and skip it, so sweeps silently miss it. The affected string and
regex literals use the escape now; behaviour is unchanged.
2026-10-19 14:57:00 +02:00
Marc-André Bombeck ff3559c169 fix(workouts): render workout dates in the profile timezone
The workout list, the workout header and the dashboard tile formatted
instants through the shared formatter cache without a timeZone, so they
followed the browser's zone. The year comparison in the list used the
browser's calendar as well. All three now take the profile zone, and the
timezone guard also matches getDateTimeFormat(locale, options) calls that
leave the zone out.
2026-10-19 14:56:53 +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 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 0ee7e156d3 fix(report): print lab values with the precision they were recorded at
The doctor report PDF printed lab values and reference ranges with one
decimal and the share page with two at most, so a PSA of 0.034 read 0.0
or 0.03 and a TSH window of 0.27 read 0.3, while the labs page kept more.
The labs page itself cut values above 1 to two decimals.

One rule now decides every recorded number (src/lib/format/recorded-number):
a stored value keeps every digit it holds, minus binary float noise, and a
value converted from the unit its report printed keeps the precision of the
printed figure. The report payload carries sourceValue for that. The labs
page, its chart and history, the PDF lab table and footnote, the share
page, the day view and the OCR review all print through it, reference
bounds included. Doses on the PDF and the share page follow the same rule.

Measurement statistics in the PDF and on the share page now share one
per-row precision: up to three significant digits but never more decimals
than recorded, so a waist-to-height ratio of 0.52 no longer reads 0.5;
glucose keeps its unit's precision.

Refs #1228
2026-10-16 11:00:27 +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 6776decaa0 test(guard): discover the resolved fields and demand a reader or a stated reason for each 2026-10-16 05:35:48 +02:00
Marc-André Bombeck adf0c7f4c2 refactor(validation): route every ISO instant through isoDateTime 2026-10-16 03:57:46 +02:00
Marc-André Bombeck 92b361c3c6 feat(validation): add isoDateTime with a refusal counter for instants without seconds 2026-10-16 03:57:45 +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 f2be772eaf feat(anamnesis): serve the life circumstances, record AUDIT-C, and keep both off the closed legacy lists 2026-10-16 00:54:38 +02:00
Marc-André Bombeck 877920f5f7 feat(pulse): weigh sparse hours in the pulse day mean by the stored method 2026-10-16 00:49:41 +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 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