7493 Commits
Author SHA1 Message Date
Marc-André Bombeck 324e59914e Merge pull request #1161 from MBombeck/release/v1.40.1
chore(release): v1.40.1
v1.40.1
2026-10-05 00:06:19 +02:00
Marc-André Bombeck 4769502a4b fix(backup): check the weekly copy's preview against the schema section by section
Taking the weekly preview from the writer dropped the schema check the
old scanner ran at the end, so a copy that violates the schema was
described as restorable. The collector now checks each section in its
JSON form as the writer shows it, an array section and a bulk table one
element at a time, plus the schema's one whole-document rule (a
canonical copy names every measurement by id). A copy that fails gets
no stored preview, and the preview route reads the copy itself, as it
did before. Nothing is kept beyond one element.

Measured on the preview memory test under --max-old-space-size=524:
held after the producer 3 MB with and without the check, raw peak
heapUsed 293 MB with it (335 MB without). The test now loads the
backup schema before its baseline: a module loaded once per process,
not memory that grows with the record.
2026-10-04 22:17:27 +02:00
Marc-André Bombeck 48ed149ef0 fix(import): leave out a marked Apple Health sample only when this account holds it
A sample carrying the HealthLog origin marker was always dropped from an
export import. After a move to a new instance without a backup, the
export is how the mirrored values come back, and every one of them was
silently left out. A marked sample now counts as written by HealthLog
only when its HKExternalUUID names a row of this account of the same
kind, deleted rows included; a marked cycle sample only when the
account holds a day-log on its day. Every other marked sample is
imported, and still meets the manual-value match. A correlation's own
HKExternalUUID now covers records that carry none.
2026-10-04 22:15:48 +02:00
Marc-André Bombeck 090e3d9a7e perf(measurements): weigh pulse days with window functions over one scan
The day weight joined its source to an hour grouping of itself. With
stale or missing planner statistics, as after a new account imports a
year of pulse below the autoanalyze threshold, the planner ran that
join as a nested loop and the all-time pulse aggregate ran into the
60 s statement timeout. The weight is now two window passes over one
scan: the readings in a row's hour, and one marker per hour summed over
the day for the hour count. The weights are unchanged.

A new integration test seeds over 100k pulse rows with autovacuum off
and no ANALYZE, bounds the all-time reader and a zoned per-source mean
well under the timeout, and compares every row's weight against the
earlier self-join after ANALYZE. The ANALYZE the metric-series snapshot
test needed for the same trap is gone.
2026-10-04 22:14:14 +02:00
Marc-André Bombeck 3681b23d30 test(e2e): mount every overview section before asserting none paints
The AI-optional overview and account journeys asserted zero error cards,
Coach hand-offs and briefings without scrolling the deferred sections
into view, so the sections below the first three never mounted and the
zero counts held whatever they would paint. Both journeys now reveal
every section and wait for the reads those sections fire to land;
waiting on networkidle again resolves at once, because the page reached
that state before.
2026-10-04 21:36:25 +02:00
Marc-André Bombeck 2bbcce73e4 docs: name the memory cap on small hosts and the shared connection pool
Getting started names the APP_MEMORY_LIMIT default and how to lower it
on a 2 GB host; the scaling guide says that web and worker share one
pool in the bundled single-container setup. The rollup comment names
migration 0372, which carries the hourly-mean statistic.
2026-10-04 21:25:28 +02:00
Marc-André Bombeck da56177290 fix(backup): take an interrupted account last for one run only
An account whose last backup attempt never came back was kept behind
every other account for as long as its attempt stayed open. A pass that
regularly stops on its time budget never reached it, so it never got a
copy again. It is now held back only until a later run has started on
another account; if its record kills the process again, its attempt is
the newest once more and it goes last again. A crash loop still leaves
the other accounts their copies, and the account itself is attempted at
least every second run.
2026-10-04 21:21:32 +02:00
Marc-André Bombeck 4261139dfa fix(backup): run the restore drill as a locked long pass
The drill now reads every account's newest off-host object but still ran
on the default fifteen-minute expiry, without a lock and with two
retries, so a large instance could see it killed, run beside itself, and
page every failed account three times. It now carries a four-hour expiry,
stops between accounts on its budget and reports the objects it did not
reach, holds a pass lock at the binding, and is not retried.
2026-10-04 21:19:32 +02:00
Marc-André Bombeck 4be7109a2b fix(medications): refetch an invalidated prefetch on mount
A hover prefetch excused the mount refetch even after a dose logged
elsewhere had invalidated the medication reads, so the page showed the
compliance from before the dose. An invalidated entry now always
refetches.
2026-10-04 21:13:28 +02:00
Marc-André Bombeck 5e5e387fab chore(release): v1.40.1 2026-10-04 20:55:14 +02:00
Marc-André Bombeck dc7061634e Merge pull request #1152 from MBombeck/dependabot/npm_and_yarn/p-limit-7.3.3
chore(deps): bump p-limit from 7.3.1 to 7.3.3
2026-10-04 20:52:41 +02:00
Marc-André Bombeck 92b4a9c103 Merge pull request #1149 from MBombeck/dependabot/npm_and_yarn/testcontainers-12.2.0
chore(deps-dev): bump testcontainers from 12.1.0 to 12.2.0
2026-10-04 20:52:26 +02:00
Marc-André Bombeck 31b7fd0a45 Merge pull request #1148 from MBombeck/dependabot/npm_and_yarn/zod-openapi-6.0.2
chore(deps): bump zod-openapi from 6.0.1 to 6.0.2
2026-10-04 20:52:11 +02:00
Marc-André Bombeck 10e34bcab6 Merge pull request #1150 from MBombeck/dependabot/npm_and_yarn/eslint-config-next-16.3.8
chore(deps-dev): bump eslint-config-next from 16.3.6 to 16.3.8
2026-10-04 20:51:56 +02:00
Marc-André Bombeck 5e267e2ddf Merge pull request #1151 from MBombeck/dependabot/npm_and_yarn/next-16.3.8
chore(deps): bump next from 16.3.6 to 16.3.8
2026-10-04 20:51:40 +02:00
Marc-André Bombeck d23595f924 Merge pull request #1153 from MBombeck/perf/mcp-metrics-slim
perf: single-metric MCP reads from the metric's own rows; load the backup schema on first use
2026-10-04 20:51:28 +02:00
Marc-André Bombeck 59311fe401 Merge remote-tracking branch 'origin/main' into perf/mcp-metrics-slim 2026-10-04 20:08:25 +02:00
dependabot[bot] 8fd58e9824 chore(deps): bump next from 16.3.6 to 16.3.8
Bumps [next](https://github.com/vercel/next.js) from 16.3.6 to 16.3.8.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/compare/v16.3.6...v16.3.8)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-04 17:33:27 +00:00
dependabot[bot] c064210a34 chore(deps): bump p-limit from 7.3.1 to 7.3.3
Bumps [p-limit](https://github.com/sindresorhus/p-limit) from 7.3.1 to 7.3.3.
- [Release notes](https://github.com/sindresorhus/p-limit/releases)
- [Commits](https://github.com/sindresorhus/p-limit/compare/v7.3.1...v7.3.3)

---
updated-dependencies:
- dependency-name: p-limit
  dependency-version: 7.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-04 17:33:00 +00:00
dependabot[bot] ddb5c6462a chore(deps-dev): bump eslint-config-next from 16.3.6 to 16.3.8
Bumps [eslint-config-next](https://github.com/vercel/next.js/tree/HEAD/packages/eslint-config-next) from 16.3.6 to 16.3.8.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Commits](https://github.com/vercel/next.js/commits/v16.3.8/packages/eslint-config-next)

---
updated-dependencies:
- dependency-name: eslint-config-next
  dependency-version: 16.3.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-04 17:32:08 +00:00
dependabot[bot] 10d5b7334e chore(deps): bump zod-openapi from 6.0.1 to 6.0.2
Bumps [zod-openapi](https://github.com/samchungy/zod-openapi) from 6.0.1 to 6.0.2.
- [Release notes](https://github.com/samchungy/zod-openapi/releases)
- [Changelog](https://github.com/samchungy/zod-openapi/blob/master/CHANGELOG.md)
- [Commits](https://github.com/samchungy/zod-openapi/compare/zod-openapi@6.0.1...zod-openapi@6.0.2)

---
updated-dependencies:
- dependency-name: zod-openapi
  dependency-version: 6.0.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-04 17:31:50 +00:00
dependabot[bot] d64cbbaebf chore(deps-dev): bump testcontainers from 12.1.0 to 12.2.0
Bumps [testcontainers](https://github.com/testcontainers/testcontainers-node) from 12.1.0 to 12.2.0.
- [Release notes](https://github.com/testcontainers/testcontainers-node/releases)
- [Commits](https://github.com/testcontainers/testcontainers-node/compare/v12.1.0...v12.2.0)

---
updated-dependencies:
- dependency-name: testcontainers
  dependency-version: 12.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-04 17:31:43 +00:00
Marc-André Bombeck 3be83eee22 Merge pull request #1155 from MBombeck/test/zone-offset-reference-cache
test(tz): reuse one reference formatter per zone in the offset parity test
2026-10-04 19:29:19 +02:00
Marc-André Bombeck 016278ba24 test(mcp): analyze the seeded rows before the metric-series golden test
The first case spent 14 s per full-snapshot build in the all-time pulse
aggregate, planned against a table with no statistics, until autovacuum
analyzed it; on a loaded runner that took it past its 60 s budget. Analyze
measurements and rollups after seeding, as a production table would be.
The cases and the comparison they make are unchanged.
2026-10-04 19:21:01 +02:00
Marc-André Bombeck 528ecfbcdc test(tz): reuse one reference formatter per zone in the offset parity test
The parity test built a fresh Intl.DateTimeFormat for each of ~36k
comparisons and crossed the 5 s timeout on CI runners. One formatter per
zone halves its time; it also gets an explicit 20 s budget, since it is
meant to be exhaustive.
2026-10-04 19:07:54 +02:00
Marc-André Bombeck 13ea849188 perf(backup): load the backup payload schema on first use
backupPayloadSchema is built when validations/backup.ts is evaluated and
holds about 12 MB of heap. The restore job, the stored-backup readers and
the export writer import that module, and they sit on the boot path, so
every server process built the schema at start whether or not it ever
touched a backup.

The schema version, the compatibility check and the per-section summary
move to validations/backup-summary.ts, which never builds the schema. The
call sites that validate a file (upload, restore, the summary and download
routes, and the streamed reader's element check) load backup.ts with a
dynamic import at that moment; everything else takes only types. The
OpenAPI registry keeps its static import, since only the spec generator
loads it.

After warm-up on a production build under --max-old-space-size=524, the
used heap drops by about 29 MB, and the schema no longer appears in a heap
snapshot. A structural test refuses a new static value import of the
module outside the registry.
2026-10-04 18:23:14 +02:00
Marc-André Bombeck 546e67475f perf(mcp): build single-metric reads from the metric's own rows
get_metric_series (and get_metrics, the metric resources and the review
prompts, which fan out over it) answered with one section of a Coach
snapshot, but built the whole snapshot to get it: the all-type feature
extraction, the derived wellness layer, trajectory, correlations, memory,
GLP-1, cycle, illness, labs and visits. On a seeded account with about
215k measurements a resting heart rate read ran 208 SQL statements and
blocked the event loop for 175 ms to return under 2 KB.

The MCP surface now asks the executor for a source snapshot. It runs the
same prelude (prefs, module gates, admitted sources, cutoffs, now shared in
snapshot-prelude.ts) and the same block builders over the requested
source's reads only. The core clinical aggregates come from extractFeatures
scoped to the one block, whose bulk read keeps its all-type cap so the rows
a block sees do not change. A section large enough for the full builder's
prompt-budget pass to trim, and the environment cluster, go to the full
builder, so their results are the old ones by construction.

The integration test compares the tool's serialised result with the full
builder's section and grounding for nine sources over four windows, the
get_metrics batch and the metric resource, on a seeded account dense enough
to put the feature row cap into play.
2026-10-04 18:17:24 +02:00
Marc-André Bombeck 1cb50ed21d Merge pull request #1144 from MBombeck/fix/restore-key-probe
fix(backup): restore opens legacy values, drill checks every account, backups hold less memory
2026-10-04 17:44:57 +02:00
Marc-André Bombeck 72ca843fd5 Merge pull request #1147 from MBombeck/perf/dashboard-server
perf(server): one Prisma client, indexed coverage probe, memoised timezone offsets
2026-10-04 17:33:57 +02:00
Marc-André Bombeck 77fece0fa5 Merge pull request #1146 from MBombeck/chore/app-memory-default
chore(compose): default the app to 1536m so Node gets a 792 MB heap
2026-10-04 17:33:54 +02:00
Marc-André Bombeck 006679ed9e Merge pull request #1145 from MBombeck/perf/dashboard-client
perf: faster charts and insights, no sidebar scrollbar flash, Umami events delivered
2026-10-04 17:33:51 +02:00
Marc-André Bombeck 92f17c8ca4 Merge pull request #1140 from MBombeck/ready/pr-1131
fix(import): leave out Apple Health samples HealthLog wrote itself
2026-10-04 17:33:47 +02:00
Marc-André Bombeck 45fafdf096 Merge pull request #1138 from MBombeck/feat/pulse-hourly-mean-complete
fix(pulse): a day of pulse is the mean of its hours' means, everywhere
2026-10-04 17:33:43 +02:00
Marc-André Bombeck 6920694912 chore(compose): give the app 1536m by default so Node gets a 792 MB heap
Node sizes its heap from the container's memory cap. The 1g default gave
524 MB, which a long-used account fills in normal operation: on a live
instance 490 to 507 MB stayed live after a full GC, garbage collection
stalled requests for up to 8.5 s, and the process ran out of heap 26 times
in a week, taking the weekly backup with it. 1536m gives 792 MB. The
example env and the backup runbook say how to lower it on a small host.
2026-10-04 16:38:10 +02:00
Marc-André Bombeck 6c0a606bf3 fix(a11y): give the loading score and vitals grids a role to name
Both grids carry an aria-label while their data loads, on a plain div,
which axe reports as aria-prohibited-attr: the label has no role to
name. A scan that lands while the scores strip is still loading fails on
it. The loading grids now take role=status, the role a loading region
has and which already implies the polite live region; the loaded vitals
grid carries neither.
2026-10-04 16:24:37 +02:00
Marc-André Bombeck 4916a29bbe perf(insights): mount below-the-fold overview sections near the viewport
Entering the Insights overview fired every section's reads at once, about
two dozen requests on a seeded account, and the server worked through
them in a queue: the hero and the first sections waited behind trends,
the period review, cycle, signals, rhythm events, health status,
breathing, labs and ECG, all far below the fold.

The first three sections in the account's own order still mount with
the page. Every later one renders a 1 px sentinel until it comes within
400 px of the visible area of the shell's scroll container, then mounts
and fetches; once mounted it stays. Measured on a seeded account under a
4x CPU slowdown: 24 -> 13 requests on entry, the entry network span
1.3-1.8 s -> 0.5-0.9 s, LCP about 1.35 s -> 0.9 s, blocking time about
550 ms -> 200 ms. Scrolling to the end still loads all 25 and renders
all ten sections.

Specs that assert on, measure or scan the lower sections scroll them in
first through a shared helper, so their coverage is unchanged.
2026-10-04 16:24:37 +02:00
Marc-André Bombeck fa48110875 fix(nav): mark the scroll edge when the sidebar list overflows
On a short window (around 785 px and below) the nav list is taller than
the room between the logo band and the footer group, so it scrolls. With
nothing marking the edge, the last visible entry was cut off right at
the Collapse row and read as the footer covering it; macOS hides the
scrollbar that would have said otherwise.

While the list overflows, the footer group draws the same hairline the
user section does. A ResizeObserver on the nav and its list keeps the
state current as the window or the entries change; with room to spare
the rail is unchanged. The e2e spec checks the edge, that the last entry
scrolls fully clear of the footer, and that a tall window draws no edge.
2026-10-04 16:24:37 +02:00
Marc-André Bombeck 194712c916 Merge pull request #1143 from MBombeck/fix/rest-mode-date
fix(insights): show the Rest Mode onset as a date
2026-10-04 16:15:03 +02:00
Marc-André Bombeck ef14f539de Merge pull request #1142 from MBombeck/ready/pr-1134
fix(series): follow the source priority ladder in the blood-pressure series
2026-10-04 16:15:00 +02:00
Marc-André Bombeck 63a5304e43 Merge pull request #1141 from MBombeck/ready/pr-1132
feat(labs): let the lab list's range bar use a wide card
2026-10-04 16:14:56 +02:00
Marc-André Bombeck dc9b0e3936 test(import): read the note's text without a tag-stripping replace 2026-10-04 16:14:47 +02:00
Marc-André Bombeck 28046f05f7 test(measurements): group the hourly-mean fake once instead of per day
The in-memory day-aggregate fake re-filtered every row for every day,
converting each timestamp to the profile timezone again, so a 1000-day
series took about five seconds and the graded pulse-status test hit its
timeout. It now groups by day and segment in one pass (4.8 s to 85 ms).
2026-10-04 16:14:10 +02:00
Marc-André Bombeck 23961d2280 perf(medications): stop re-sending the read a nav prefetch just made
The medications list and the compliance summary refetch on every mount,
so a take or skip logged on another device shows on return (#316). The
nav link's hover / touch / focus intent and the route-commit preload
prefetch both reads too, so every visit through a link sent each of
them twice: the prefetch, then the identical request from the mount a
few hundred milliseconds after the first answer landed.

The mount now skips its refetch only when the cached data answers a
prefetch that went to the network for this visit: within the last 10 s,
and since the route last left /medications. A direct load, a return
with an older entry, or a quick return whose prefetch found the entry
still fresh keep the refetch on mount.
2026-10-04 16:12:04 +02:00
Marc-André Bombeck c08e049a05 perf(rollups): share an in-flight coverage probe between concurrent readers
A Coach snapshot, a derived-metric batch or the Insights page fans out
into a dozen readers that each probe the same account's rollup coverage
at once. In production one MCP get_metrics call held 59 of these probes
open on the database together, each taking about 320 ms under that
contention, and the same burst repeated every 15 minutes.

Concurrent probes for one account now join the query already running.
Nothing is kept after it settles, each caller gets its own map, and a
measurement write drops the in-flight entry so a probe that starts after
a write always reads afresh. The registry lives on globalThis so every
server bundle shares it.
2026-10-04 16:04:04 +02:00
Marc-André Bombeck 3e08fd0e68 fix(documents): stop queueing a thumbnail that cannot be rendered on every boot
A document whose preview render fails for good (a PDF whose first page
renders blank) was queued by the boot-time backfill, refused, and queued
again at the next boot, without end.

The thumbnail job now keeps the failure with the document
(document_thumbnail_failures, migration 0374): the reason, the attempts
since the last success and when the newest failed. The backfill, both the
boot discovery and the per-account pass, leaves a document out after one
deterministic refusal (raster-failed, empty-render, pixel-cap) or three of
any other kind, until the document changes. A direct enqueue still renders
it, and a preview that is made clears the record.
2026-10-04 15:56:56 +02:00
Marc-André Bombeck f18e6ed34b fix(backup): notice a backup run the process died under, and take its account last
A weekly or nightly backup job whose process died (a restart, a
container out of memory) stayed active until its two- or four-hour
expiry, and its retry walked the accounts in the same order: the account
that killed it has the oldest copy, so it led again, and every account
after it went without a copy for as long as that record stayed too
large.

Both passes now send with a five-minute pg-boss heartbeat, so a dead run
fails within minutes and its retry picks it up. Each pass records when it
started and finished every account (backup_pass_attempts, migration
0373); an account whose last attempt never finished goes behind all the
others. The backups page shows when the run in progress started and names
an account a run died under, for the weekly and the off-host card, and a
heartbeat timeout reads as what it means.
2026-10-04 15:56:56 +02:00
Marc-André Bombeck d55e329c48 perf(backup): take the weekly copy's preview from the writer, not a second parse
Since v1.39.6 the preview scanner parsed the JSON on its way into the
pieces, kept every section but the three bulk tables, and validated the
whole document at the end: a second copy of the record beside the
writer's own, inside the weekly job. On an account with a document vault
that is most of the record.

The writer now shows each section and each bulk row to an observer just
before writing it. The preview collects key uses from that and takes its
counts from the writer's own FullBackupCounts; nothing is parsed or kept.
An upload hands over the summary and key uses it has already validated.
The round trip pins the writer's counts and key uses to a full read of
the copy on an account with a row in every model, and a memory test holds
what the pass keeps once the producer is done.

Measured on 70,000 measurements, 3,000 mood entries and 40 one-megabyte
documents under --max-old-space-size=524: held after the producer
56 MB before, 3 MB after; peak heapUsed 398 MB before, 334 MB after.
2026-10-04 15:56:56 +02:00
Marc-André Bombeck 5345c8be1e fix(backup): check every account of the newest date in the restore drill
The drill read only the alphabetically last object of the newest date, so
one account stood in for all of them. It now checks every object of that
date one at a time, records a failure against the account it belongs to,
keeps going, and pages once per failing object with its key and how many
accounts were checked.
2026-10-04 15:56:55 +02:00
Marc-André Bombeck f67006783c fix(backup): open legacy text ciphertext as a string and probe only counted sections
A text column written before key ids existed holds the bare base64 of
nonce, tag and ciphertext. The key check read such a value as an encoded
Bytes column, decoded it once too often, and accused the v1 key of not
opening a copy it opens. The instance's stored AI key has exactly that
shape and is the shortest v1 value in every disaster-recovery copy, so it
was picked as the probe and refused every restore of an account with v1
values, even with the instance settings left out of the verdict.

A legacy value now reads as a string unless its decoded head is base64
text. Samples are kept per section and only the sections the verdict
counts are probed; a key is unreadable when none of its probes opens.
Binary values open with the label their column is sealed under, taken
from the encrypted-column registry, and are not probed when no label is
known. A preview stored by an earlier release is still read, and its
sample's layout is derived again from the value.
2026-10-04 15:56:55 +02:00
Marc-André Bombeck 76e621b067 perf(tz): memoise zone offsets per hour for wall-clock reads
Day keys and wall-clock fields are computed per row in the analytics,
digest and personal-record loops, and each call ran
Intl.DateTimeFormat#formatToParts plus a resolvedOptions() allocation
to re-validate the zone. In a CPU profile of an Insights page load the
zone helpers were 17-19% of the busy time on the request thread.

Probe the offset once per UTC hour at both ends of the hour and derive
the wall clock arithmetically; an hour that contains a transition, an
instant outside 1971-2199 or an invalid date keeps the formatter path.
Zone validity is memoised per name. userDayKey drops from 9.3 to 1.5 us
per call, wallClockInTz from 6.4 to 0.5 us; a parity test checks the
result against Intl around every 2026 transition in ten zones.
2026-10-04 15:53:48 +02:00