Commit Graph
7523 Commits
Author SHA1 Message Date
Marc-André Bombeck bfcf71b7eb fix(dashboard): keep numbers out of the morning push line
The digest line is the body of the morning push and lands on a locked
phone. It took the briefing's first sentence, the top signal's headline
or the deterministic signal sentence verbatim, so a resting heart rate or
a blood pressure average could sit on the lock screen, and its score
floor stated the score.

A candidate with a figure in it is now passed over for the next, and the
floor says the score is ready instead of what it is. The hero's lead in
the app keeps its numbers. Tests that pinned the numeric line are turned
around, and the push builder is checked on a digest whose lead carries a
reading.
2026-10-05 08:34:28 +02:00
Marc-André Bombeck bd8e05e484 fix(dashboard): drop a lead sentence only when it repeats the score as the score
The lead filter dropped any sentence containing the score's digits, so a
score of 52 threw out "You slept 7 h 52 min" and a resting heart rate of
52 bpm. A sentence now repeats the ring only when it names the score (in
any shipped language, or as "out of 100") and carries the number on its
own, not as part of a time, a decimal or a measurement with a unit.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 9e647051d5 fix(dashboard): tell a steady score only when its band held, and say at least at the read's edge
The steady line compared the newest stored score with the number on
screen but not with its band, version or composition, so a score whose
band moved overnight at the same number was still called steady. A run
that reached back past the 120 days read was also told as if it started
there.

The newest stored day now has to match the ring in value, band and,
where the digest carries them, algorithm version and composition. A run
the read cannot see the start of is marked as open (steadyAtLeast on the
digest score) and the hero says "at least" in seven locales.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 13ac27c22d docs(coach): say that older answers may still quote older values
The lookback limit governs what the Coach reads from now on. Answers it
gave earlier in an older conversation stay as written, so the note under
the limit now says they may still quote values from before it.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 87cf32bc67 fix(coach): leave derived scores out when their window does not fit the limit
The derived block shortened each engine's window to the lookback limit,
so a 7-day limit produced a readiness computed over 7 days instead of the
30 the ring on the dashboard uses, under the same name. A block whose own
window does not fit is now left out, as the lookback rule says, and the
coincident-deviation flag follows its 30-day baseline the same way.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 8dc34c7691 fix(coach): keep GLP-1 dose changes before the lookback limit out
The GLP-1 block sent every dose change with its decrypted note whatever
the lookback limit said. Dose changes are entries, so only those inside
the limit reach the prompt; the dose in force stays as the current dose.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 308e154978 fix(coach): move focus to the composer before hiding the jump button
Jumping to the latest message pinned the thread first, which hides the
button behind aria-hidden, and only then moved focus to the composer.
Focus now leaves the button before its state changes.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 3b283507b5 fix(dashboard): lead with the stored lines for a digest cached before the lead
The hero reads only digest.lead. A digest the service worker cached
before the update carries no such field, so the hero lost its lead line
until the next fetch. Such a digest now falls back to the reaction line
or the briefing lead it was built to show; a current digest whose lead
is null stays silent.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 94e0da7953 chore(i18n): drop the orphaned Coach window labels
The insights.coach.window labels lost their last caller with the source
chips. No t() call, literal or templated, reads them in any locale.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck 35edc33871 fix(coach): lock the reasoning select while a change saves
The provider and model selects in the Coach's settings already lock while
a write is in flight; the reasoning select did not, so a second PATCH
could race the first and one change could be lost.
2026-10-05 08:34:27 +02:00
Marc-André Bombeck d0b680c277 fix(dashboard): count the last hour of a 25-hour day in today's doses
buildMedsTodayBlock ended the day at local midnight plus 24 hours. On the
day the clocks go back that stops an hour early, so a dose between 23:00
and midnight was neither projected nor counted in the doses tally. The
exclusive end is now the next local day's start, as the digest loader
already computes it.
2026-10-05 07:48:58 +02:00
Marc-André Bombeck 169c7bdfe6 fix(coach): salvage Coach preferences field by field
parseCoachPrefs parsed the stored blob as a whole and returned the
defaults on any invalid field. A lookback value this version does not
know therefore dropped the metric exclusions with it. Each field now
falls back on its own, and the exclusion and cluster lists keep their
known entries, so drift narrows what the Coach reads instead of widening
it.

The deploy runbook's rollback section notes the one case this cannot
cover: v1.40.1 and earlier still parse the blob whole, so an account that
saved the new "Last year" lookback loses its exclusions there.
2026-10-05 07:48:37 +02:00
Marc-André Bombeck 1158ae9ffd fix(coach): keep earlier tables beyond the lookback limit out of the context
The earlier-tables block names each stored table with its window and row
count. The list was filtered by module and exclusion only, so a table
stored before the limit was set or narrowed still told the model how much
history it covered. It now answers to the same range test show_result
uses, and the history-reach guard checks both the filter and that the
call site passes the limit.
2026-10-05 07:47:31 +02:00
Marc-André Bombeck 0c1dd1e967 fix(coach): show the operator's central Codex when it answers
The operator's shared Codex is appended to the resolved chain but is never
a chain entry, so the Coach's provider select had no option for it and
showed its first option as the one answering. The answering provider now
appears as a disabled option, with a note that the operator manages the
connection.
2026-10-05 07:46:29 +02:00
Marc-André Bombeck 2a556b47c3 fix(coach): offer only providers that can answer and say when a switch does not take
The provider-chain summary marked every entry available, so the Coach's
provider select offered entries without a key or sign-in. Choosing one
saved it to the front of the chain, the resolver skipped it, and the
select jumped back without a word.

Availability now comes from the presence probe, which names every chain
type whose credential is present. After a switch the picker reads the
chain back and says so when the server still answers with another
provider.
2026-10-05 07:45:05 +02:00
Marc-André Bombeck a2ef2eec24 test(e2e): scan the sources rail with the rest of the Coach frame
The rail's switched-off rows now meet AA contrast, so the exclusion the
frame's accessibility scan carried for them is gone.
2026-10-05 07:32:01 +02:00
Marc-André Bombeck 359203d09a Merge remote-tracking branch 'origin/feat/coach-lookback-limit' into release/v1.40.2 2026-10-05 07:31:34 +02:00
Marc-André Bombeck 929c0ebaa4 Merge remote-tracking branch 'origin/main' into feat/coach-lookback-limit
# Conflicts:
#	src/lib/ai/coach/snapshot.ts
#	src/lib/ai/coach/tools/executor.ts
#	src/lib/insights/features.ts
2026-10-05 07:31:03 +02:00
Marc-André Bombeck 03c43ef70a Merge remote-tracking branch 'origin/feat/documents-procedure-filter' into release/v1.40.2 2026-10-05 07:17:17 +02:00
Marc-André Bombeck 98ac9c3d80 Merge remote-tracking branch 'origin/feat/dashboard-today-overview' into release/v1.40.2 2026-10-05 07:17:17 +02:00
Marc-André Bombeck 0d6bac8c5c Merge remote-tracking branch 'origin/feat/coach-answers' into release/v1.40.2
# Conflicts:
#	messages/de.json
#	messages/en.json
#	messages/es.json
#	messages/fr.json
#	messages/it.json
#	messages/ko.json
#	messages/pl.json
2026-10-05 07:16:55 +02:00
Marc-André Bombeck 57a889ef3c Merge remote-tracking branch 'origin/feat/coach-frame' into release/v1.40.2 2026-10-05 07:16:09 +02:00
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 0219a85e92 feat(documents): filter the archive by procedure or surgery
The vault's filter row gains a procedure facet beside the condition
facet. It lists the record's procedures and surgeries that hold at least
one live document, headed the way the visits page heads them (the
reason, else the practice, else the kind) with the body site and date
beneath, and narrows the list through the existing `encounterId` filter.
Same control, placement and clear behaviour as the condition facet; the
choice rides the URL as `?encounter=`, and an upload dropped into the
filtered view is filed against the procedure.

The usage read publishes the choices as `linkedProcedures` (additive),
decrypting only the label columns, and returns none when a sharing
grant does not cover the visits section.

On a phone an active facet now gives up label width before the search
field does, and the search keeps room for its icon and padding.
2026-10-04 21:59:22 +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 393ac955f6 Make the Coach lookback a limit on every read
The saved Coach window only seeded the snapshot and each tool's default.
A tool could still read back to all time, the availability probe reported
counts and means for history outside the window, and several blocks read
fixed windows of their own (labs 12 months, correlations 180 days, the
coarse tail, the all-time extremes) whatever the setting said.

coachPrefs.defaultWindow is now enforced as a limit through one module,
history-reach.ts: the turn context clamps the client window, the snapshot
clamps its window and gives every fixed-window block either the shorter
window or, when the block's own window cannot fit, leaves it out. Every
retrieval tool clamps its window, an earlier period or a stored table that
reaches past the limit answers outside_reach, and the probe reads only
inside the limit and names older history as outside_reach without any
figure. Follow-up chips that would read past the limit are not offered.
The default (allTime) is no limit and reads as before. MCP clients pass no
limit and keep their own window arguments.

Adds the twelve-month option, rewrites the picker text to state what is
read with the numbers the readers use, shows the same options in the
managed-profile settings, counts cluster members with a singular, and
drops the opacity on switched-off rows that put their text below WCAG AA.
2026-10-04 20:45:21 +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
Marc-André Bombeck d07f5796d7 Seed the demo Coach answer with valid metric tokens, steps and token meta 2026-10-04 20:02:49 +02:00
Marc-André Bombeck d1849eed16 Measure the Coach answer layout at 1440 and 390 px 2026-10-04 20:02:49 +02:00
Marc-André Bombeck a91e1bf0a7 Give each Coach answer one action row and keep its chips in its column
Under an answer: copy, read aloud, try again and the time as text on the
left; tokens and model on the right, on one line down to 390 px. The
thumbs leave the web (the feedback route stays for the native client),
and so does the "What I'm looking at" disclosure: the method line and
the tables the answer read move into the steps list, and an older
message without steps lists the areas it drew on, never a raw key.

Remember becomes a bookmark icon in the question's row and confirms in
place with a toast. Follow-up chips render inside the latest answer's
column at its text size. The answer column takes the full width below
`sm` and a fixed 80 % above, so nothing moves sideways as a reply
streams. A round button fades in when the thread is scrolled away from
the end and brings it back.
2026-10-04 20:02:48 +02:00
Marc-André Bombeck 5cfacba16a feat(dashboard): add a Today overview to the hero
The hero led with the first sentence of the cached briefing, which was
often a greeting ("Good morning.") while the sentence with content was
never shown. Without an AI provider it fell back to "Your health score
today is N.", which the hero then removed because the ring already
shows the number, leaving a ring and "Nothing needs your attention".

The daily digest now publishes the lead resolved on the server and a
short list of facts about the day:

- lead: the reaction line or the first briefing sentence that is not a
  greeting and does not repeat the score while AI text is available,
  otherwise a deterministic sentence about the strongest signal (a vital
  outside its personal range, a night an hour off the usual, vitals in
  range, last night). A fact the lead was built from leaves the list.
- today: up to five statements in priority order (Rest Mode, doses taken
  today, an appointment today or tomorrow that is not already on the
  rail, last night's sleep against the usual, vitals against their
  personal range, cycle day). Module-gated on the server, never padded,
  never asking for anything. A phone shows the first four.
- restMode: passed through while an illness episode is active.
- score.steadyWeeks: how long the score has held, read off the stored
  daily scores, shown under the ring in place of a delta.

briefingLead skips greetings and score sentences the same way, so the
morning push improves with it. Visit times are rendered in the profile
timezone and hour-cycle preference; the cycle day comes from the same
verdict the cycle page uses. The ring, the rail and the charts are
unchanged. OpenAPI regenerated; additive fields only.
2026-10-04 20:00:18 +02:00
Marc-André Bombeck b8d31f945d test(e2e): cover the Coach page frame at 1440, 1280 and 390
Measures that the thread and composer keep the same width with the panel
open and closed, that the choice survives a reload, Escape and focus
return for the panel, the sheet and the settings overlay, rename and
delete with undo from the row menu, the settings deep link, and axe on the
panel and the overlay in both themes. Screenshots are attached to the
report.
2026-10-04 20:00:07 +02:00
Marc-André Bombeck 12153fe143 feat(coach): move conversations into a collapsible right panel
The Coach page gets a conversations panel on the right of the thread. From
1280 px it docks beside the thread and slides open and shut; the reading
column keeps its width either way and only its centring moves. It is open
on a first visit and remembers the last choice per device. Below 1280 px it
is a sheet from the right. The toggle sits at the trailing edge of the top
bar, through a small page-owned slot the top bar now renders.

The panel lists every conversation grouped by recency, with a row menu to
rename in place (Enter saves, Escape cancels without closing the panel) and
delete with the existing undo toast. The undo window now survives the sheet
closing. New chat is the round button at the panel's bottom right, and
picking or starting a conversation keeps ?c= in the URL.

The gear in the panel header opens quick settings: a popover from md up, a
bottom sheet on a phone. It switches the answering provider (chain reordered
through the existing route), the model and reasoning level where the person
owns them, and shows what the Coach can see. Keys, the Codex sign-in and the
fallback chain stay in Settings > AI. The drawer's gear opens the same
overlay. The composer's + menu is gone.

Settings > Coach links to /coach?settings=data, which opens the overlay on
what the Coach can see instead of a rail that no longer exists.
2026-10-04 20:00:07 +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