Commit Graph
4589 Commits
Author SHA1 Message Date
Marc-André Bombeck a15a148667 fix(insights): hold the scores strip until the cycle ring is decided
For a cycle-tracking account the strip painted Strain first and swapped
it for the cycle ring once the calendar read landed, moving the row
under the reader. The ring's decision now reports while it is pending,
and the strip keeps its skeleton until then.
2026-09-24 12:22:52 +02:00
Marc-André Bombeck e49997bf97 fix(settings): offer Coach links only while the Coach can open
Settings → Coach's sources pointer and the About-me follow-up questions
linked to /coach whatever the Coach's state. Without a provider or
consent the Coach page sends the visitor straight back to Insights, so
the link only bounced. Both now follow the Coach capability; the
questions themselves stay readable.
2026-09-24 12:21:21 +02:00
Marc-André Bombeck fbe2cd5833 fix(settings): say precisely what withdrawing AI consent stops and deletes
The confirmation said the AI features stop straight away, and the
withdrawn line that no health data goes to an AI provider. The Coach and
Insights need the receipt only when they use the AI access the server
provides, so on an own key or a local model they keep working, and the
model-written notes are kept while another consent still covers
Insights. Both strings now say so in every locale, and a guard pins the
copy next to the resolver rule it describes.
2026-09-24 12:20:14 +02:00
Marc-André Bombeck 8d60690b4a fix(insights): give the setup hint's dismiss button a 44 px target on phones 2026-09-24 12:18:24 +02:00
Marc-André Bombeck a2282a498c fix(encounters): give the visit form's link pickers their own query keys
The visit form's lab and episode pickers fetched with their own page
sizes under the same keys as the Labs list and the illness pages, which
fetch with others. Whichever read landed first served the other: the
picker could see 50 episodes instead of 100, or the Labs list a 200-row
page as the whole list. The picker reads now have keys of their own,
under the lab and illness roots so writes still evict them.
2026-09-24 12:17:56 +02:00
Marc-André Bombeck 15cce33a28 fix(documents): keep the link pickers from deleting a link made elsewhere
A dose or a visit can link documents from its own form, and a
document's sheet can link doses and visits; both ends write the same
table, and the sheet writes the whole set on every tap. Dose and visit
writes left the document reads alone, and the sheet seeded its pickers
from a detail cached for five minutes, so the first tap there after
linking from the dose's form wrote a set without the new link and
removed it.

Dose and visit writes now evict the document root, the sheet reads the
document again on every open, and its pickers wait for that read before
they offer a change.
2026-09-24 12:15:36 +02:00
Marc-André Bombeck 28d491174e fix(onboarding): keep the tour from bouncing between the Coach and Insights
With no provider set up, a fresh self-host's default, the Coach module
is on but /coach redirects to /insights because the Coach is not
available. The tour read the raw module map, kept its Coach stop, pushed
/coach, was sent back to /insights and pushed /coach again on the
pathname change: a loop.

The tour now reads the same capability-folded map as the navigation, so
the Coach stop is not in the list. And a stop whose page redirects is
dropped after one navigation, in the direction the person was going,
instead of being pushed again.
2026-09-24 12:13:15 +02:00
Marc-André Bombeck 676bb2eff7 fix(ai): refresh the account's AI answer after every change to its inputs
Every AI surface reads its capability from the ai block on
/api/auth/me, but setting up or removing a provider, signing in to or out
of Codex, saving the fallback chain, the central-Codex opt-in, the
local-OCR switch, a minted consent receipt, the admin's server key and
central Codex, and a guardian's save inside a managed record evicted only
their own reads. The surfaces stayed on the old answer until a reload.

All of these writes, and the ones that already refreshed the payload
(consent, modules, Hide Coach, the operator's switches), now evict one
bundle, aiInputDependentKeys. A structural test holds every client write
to those endpoints to it.
2026-09-24 12:11:14 +02:00
Marc-André Bombeck 03ecb5bd52 fix(coach): keep stored plans and conversations reachable with the Coach off
The page gate matched /coach/plans and /coach/conversations to the Coach
module by the nav prefix, so with the Coach switched off by the operator
or hidden by the person both pages were replaced by the module notice and
never reached their read-only mode. Both are now stored-record pages the
gate leaves alone.

With the Coach off nothing in the navigation led there either. Settings
now carries a plans card, shown when plans exist, and the conversations
card links to the full list.
2026-09-24 12:05:52 +02:00
Marc-André Bombeck 6e45acfbc6 feat(coach): keep stored reminders readable and erasable with the Coach unavailable
Reminders are the person's own record, like the stored facts,
conversations and plans. The list and delete routes no longer ask the
Coach module; creating, confirming, resolving or rescheduling one stays
Coach use and is refused by POST and PATCH as before.

Settings -> Coach now always mounts the reminders card (shown with Hide
Coach on only when reminders exist), and Settings -> AI lists them through
the stored-memory block while Settings -> Coach is out of reach. With the
Coach unavailable the card is read-only: every reminder can be read and
deleted, while keep / done / dismiss are not offered.
2026-09-24 11:57:42 +02:00
Marc-André Bombeck 5e5d33ecf8 feat(coach): keep stored plans readable and erasable with the Coach unavailable
Plans are the person's own record, like the stored facts and
conversations. The list and delete routes no longer ask the Coach
module, and the plans page turns read-only instead of redirecting to
Insights: every plan can be read and removed, while confirming or moving
one stays Coach use and is refused by the PATCH route as before.
2026-09-24 11:48:15 +02:00
Marc-André Bombeck e0ece41e21 Merge branch 'feat/ai-web-surfaces' into release/v1.39.0 2026-09-24 11:37:10 +02:00
Marc-André Bombeck e72441743f fix(settings): never report a failed consent read as a withdrawal
The AI consent card turned any failed read of the receipt (a rate limit, a
network drop) into "Withdrawn" and offered a grant. It now says the read
failed, with a retry. The Coach history note is one sentence.
2026-09-24 11:36:29 +02:00
Marc-André Bombeck a6c65bd37e fix(insights): give the cycle ring a score cell only when it has a dial
The scores strip reserved a cell for the cycle ring, and dropped Strain to
make room, whenever the cycle module was on. Without an active cycle the
ring renders nothing, so the row showed a gap where Strain used to be. The
overview now asks the same calendar read whether there is a dial today.
2026-09-24 11:10:48 +02:00
Marc-André Bombeck 5f6a0e42b4 refactor(web): retire the feature-flag reader for the capability map
The web no longer reads GET /api/feature-flags: useFeatureFlags and its
query key are gone, and use-ai-capability gains the two readers the Coach
pages and the operator notice need. A structural guard fails when web code
reads the retired projection, and when a module requests an AI route
without being registered with the capability that gates it.

Adds the copy for every new state in all seven locales and drops the
per-card no-provider strings.
2026-09-24 11:05:59 +02:00
Marc-André Bombeck 3d85445e50 feat(settings): explain operator switches and keep the Coach memory reachable
Settings, AI says which AI features the server's operator switched off and
shows the one setup hint while no provider is configured. The consent
withdrawal says what it deletes and what stays, and a document-reading
consent on file gets its own withdrawal. Regenerate follows the briefing
capability and the connection test names an operator refusal.

The Coach memory (facts and stored conversations) is never gated on the
Coach: Settings, Coach always shows it, and Settings, AI shows it while
the Coach page is out of reach, whenever rows exist.

Onboarding offers AI only where it can be set up here: the done-screen
panel and the getting-started row go away while the operator has AI off
or inside somebody else's record.
2026-09-24 11:05:54 +02:00
Marc-André Bombeck 4449b34ac7 feat(documents): gate document, lab and medication reading on their capabilities
Reading a document, scanning a lab report and turning a medication
description into a schedule follow documentAi, labsOcr and
medicationExtract. Unavailable, the action is not offered. With only the
document-reading consent missing, the place that reads asks for it and
grants exactly that (ai_extraction), nothing wider.

Refusals are read by meta.errorCode: a 403 is no longer always called a
missing consent, and a spent daily budget no longer shows the rate-limit
sentence.
2026-09-24 11:05:49 +02:00
Marc-André Bombeck 48d97ef70d feat(insights): render AI parts from the capability map, never as errors
The daily briefing, the hero's briefing paragraph, the trend sentences and
the status notes follow the briefing and statusText capabilities: hidden
while unavailable, never an error card or a per-card "connect a provider"
state. The advisor and status reads only fire while their capability is
available; a 403 settles as an outcome instead of throwing.

A failed comprehensive read no longer replaces the overview, and every
overview section is fenced so a section that throws becomes one error row.
The period review always renders its composed narrative. Cycle, breathing
and lab-change sections go through the surface map. One calm setup hint
appears on the overview while no provider is configured and one could be.

Signals, rhythm events and the mood patterns card say a failed read with a
retry, the score anatomy no longer calls a failed read insufficient data,
the catalog marks only the rows whose read failed, and workouts are not
probed while the module is off.
2026-09-24 11:05:44 +02:00
Marc-André Bombeck 61d912627d feat(coach): follow the coach capability at every entry point
The floating button, the drawer, the launch pills, the nav entry and the
Coach pages read the coach capability from /api/auth/me instead of
combining the operator switch with Hide Coach on their own. A missing
provider or consent now hides the Coach too, instead of opening one that
can only answer with an error.

Stored conversations stay reachable: while the Coach is unavailable the
conversation list turns read-only, opens a transcript in place and keeps
delete. Refusals before the stream opens are read by meta.errorCode and
each code has its own copy.
2026-09-24 11:05:38 +02:00
Marc-André Bombeck 114ff4b4eb test(sharing): read loadDocument as a headerless navigation sink
The connect and sign-in hand-offs moved from window.location writes onto
the loadDocument helper, and the guard stopped seeing them. Its reference
floor caught the drop. Watched red with a fenced thumbnail route handed to
loadDocument.
2026-09-24 10:57:45 +02:00
Marc-André Bombeck f893dae004 Merge branch 'test/v139-flag-mocks' into release/v1.39.0 2026-09-24 10:52:30 +02:00
Marc-André Bombeck 00f6eb6dcb test(measurements): mock the status re-warm where the mutation tail imports it
The bulk-delete, restore and by-external-ids suites mocked
`invalidateStatusInsightsForTypes` on the comprehensive-generate barrel,
while the shared post-mutation tail imports it from status-invalidation.
The mock was never called, the real re-warm ran the capability loader
against a stub database, and the "no invalidate" assertion could not
fail. The mock now sits on the imported module; the positive cases
assert the re-warm with the distinct types, the zero-row cases assert
its absence.
2026-09-24 10:52:04 +02:00
Marc-André Bombeck 863d32af5a test(ai): refuse feature-flags mocks that leave the loader's reader out
A structural check over src/ and tests/: every `vi.mock` / `vi.doMock`
of `@/lib/feature-flags` either uses the shared switches factory or
names `loadAssistantSwitches`. The matcher is whitespace-tolerant,
asserts a non-zero match count, and checks itself against planted
one-line, split, automock and `import()` forms.
2026-09-24 10:52:04 +02:00
Marc-André Bombeck 2c4dec08f1 test(ai): answer both switch readers from one source in flag mocks
Eight files mocked `@/lib/feature-flags` with `getAssistantFlags` alone.
The capability loader reads `loadAssistantSwitches`, so wherever it ran
it threw and every capability resolved to `check_failed`. A shared
factory now points both readers at the function the test drives.

Only the config-change chain reached the loader. Its fixture means AI
switched off by the operator; it now supplies the loader's other inputs
and asserts `operator_disabled` on the digest, so a starved loader turns
it red instead of passing on the failure path.
2026-09-24 10:52:03 +02:00
Marc-André Bombeck 48bfc1f987 refactor(navigation): name the full-document loads the client router cannot serve
Nine places assigned window.location.href directly: OAuth connects and
sign-in hand-offs that land on API routes answering with a redirect, and
the reload after account deletion. The newer Next lint rule reads each as
an in-app page change. They now go through one helper that says why a
document load is the right tool there.
2026-09-24 10:37:21 +02:00
Marc-André Bombeck 9c7ddc13b3 Merge branch 'feat/ai-routes-insights-coach' into release/v1.39.0
# Conflicts:
#	src/app/api/insights/correlations/__tests__/route.test.ts
#	src/app/api/insights/correlations/route.ts
2026-09-24 10:26:56 +02:00
Marc-André Bombeck 3662d5139e chore(ai): drop the switch type import the retired gate left behind 2026-09-24 10:25:45 +02:00
Marc-André Bombeck d8dc6ca8a7 fix(settings): answer a switched-off layout subpage with the module notice
/settings/layout/<module> is a URL a person can land on, so with its
module off it now shows the module notice in place, like every module
page, instead of bouncing to the Layout hub without saying why.
2026-09-24 10:18:04 +02:00
Marc-André Bombeck 5bd19573ea fix(insights): pass the record's module map to every correlation scan
The Coach correlations tool, the per-metric card and the period
narrative scanned every channel, so a switched-off module still fed
their statistics and the text a model was grounded on. The matrix's
modules option is now required, each caller resolves the record's map,
and the Coach skips the lab pass with labs off. A registry guard holds
every caller to resolving the map itself.

The mask's doc comment now states what it does to the false discovery
correction: fewer channels is a smaller family, so q-values of the
remaining pairs move and a borderline pair can cross the threshold.
2026-09-24 10:17:14 +02:00
Marc-André Bombeck 2143445da2 feat(ai): show stored model text inside a shared record as the owner would
Serving stored text and starting model work are separate questions. A
delegate may not start model work on somebody else's record, but reads
the owner's stored briefing, period narrative, status notes, workout
notes and Coach unread signal exactly when the owner would: those
readers now resolve the record's own capability state, not the
viewer's. Generation, warms and enqueues still ask as the viewer, so a
delegate's read of a stale narrative queues nothing. A hidden
model-written narrative is no longer overwritten by a deterministic
warm while AI is unavailable.

The sharing front door reads the digest's briefing lead again, and an
integration case pins both halves.

Refs #975
2026-09-24 10:17:00 +02:00
Marc-André Bombeck 7df260d62e fix(daily): send the morning briefing push with AI analysis off
The push refused outright while the `insights` module was off, which
now means only "AI analysis". The digest it reads already drops the
model-written lead and top signal in that case, so the push goes out
with the deterministic line (the score, or the day's items) and a
digest with nothing to say is still skipped. The `module-off` outcome
and its counter are gone.

Refs #975
2026-09-24 10:17:00 +02:00
Marc-André Bombeck c509afeb2e Merge release/v1.39.0 into feat/ai-routes-insights-coach
Brings in the jobs, stored-text and consent package and the extraction
routes. Conflicts resolved:

- The digest keeps its capability-driven reaction line and check-in,
  now through the shared `aiCapabilityForRecord`; the local copy of it
  is removed.
- The route inventories classify the extraction routes (documents,
  labs OCR, medication text, fenced chat and attachments) as actions;
  a capability call counts as the gate for the module inventory.
- With every caller moved, `requireAssistantSurface` and
  `AssistantDisabledError` are deleted. The `assistant.disabled.*`
  codes stay on the capability refusal, and the guard now asserts the
  retired gate is gone.
- `aiCapabilityToServe` answers whether stored model text may be shown
  from the record's own state, apart from who is reading.

Refs #975
2026-09-24 10:16:53 +02:00
Marc-André Bombeck ab0d1b4b71 Merge branch 'feat/module-surfaces' into release/v1.39.0 2026-09-24 10:10:11 +02:00
Marc-André Bombeck 1f7a52cd7a test(nav): name the surface map in the cycle entry's comment 2026-09-24 10:09:35 +02:00
Marc-André Bombeck 72f0eccf27 test(weight): read the target cases through the status note table 2026-09-24 10:00:05 +02:00
Marc-André Bombeck 347e63d653 fix(dashboard): keep the derived score rings with AI analysis off
The hero rings read their owner from the surface map instead of their
own map, and the readiness, recovery and sleep score rings no longer
wait on the AI analysis switch: they are computed scores, not model
text. The dose ring still follows the medications module.
2026-09-24 09:58:27 +02:00
Marc-André Bombeck 2594045bef test(modules): hold every surface list against the surface map
Each family of ids must name a surface that exists (nav hrefs, sub-page
directories, section ids, widget ids, derived ids, Settings slugs,
correlation channels). Sub-pages, trend slots and correlation channels
must follow the owner of the types they plot, joined against the
dashboard's summary owners, and a pinned table covers the owners no
join can derive. The replaced per-surface keys and the redirecting page
guard are searched for in source, and the derived route's gate is
compared with the map entry by entry.

Broken on purpose before committing: dropping the sleep sub-page entry,
the mood trend entry or a nav entry, drifting a derived owner, and
reintroducing a per-destination key each fail by name.
2026-09-24 09:56:50 +02:00
Marc-André Bombeck 8ae0f75ecd fix(insights): leave switched-off modules out of the correlation statistics
The correlation scan still paired mood, sleep, glucose, adherence,
symptoms and weather with everything else after their modules were
switched off. The discovery matrix now drops every channel whose module
is off, read through the surface map, and the correlations route passes
the record's module map, skips the lab draws with labs off, and keys its
cache on the switched-off set.
2026-09-24 09:54:25 +02:00
Marc-André Bombeck e89f41710a Merge branch 'feat/v139-targets-and-links' into release/v1.39.0 2026-09-24 09:53:41 +02:00
Marc-André Bombeck 9f98636a43 fix(settings): read section and overview-block owners from the surface map
Settings sections and Layout groups no longer declare their own module
keys; they ask the surface map. The Insights overview arrange list marks
the breathing, labs changes and cycle blocks unavailable when their
modules are off, as the overview itself would not show them. The rows
stay in the list so a save keeps each block's place.
2026-09-24 09:51:55 +02:00
Marc-André Bombeck 81cb6aa63d fix(insights): let the trends row fill a switched-off module's slot from the next metric
selectTrendCharts takes the record's module map and skips every slot
whose module is off, on the briefing path and on the fallback row,
through the surface map. Sleep was never covered before, and the
fallback row shrank to two charts with mood off; it now reaches for
pulse. The row reads the module map itself.
2026-09-24 09:50:12 +02:00
Marc-André Bombeck 74154460fd fix(insights): hide the Medications and recovery metric pills with their modules off
The tab strip's slug map is gone; every pill asks the surface map by its
slug, the same entry the page notice reads. That adds the Medications
pill and the cardio recovery, six-minute walk and stair speed pills,
which stayed visible with their modules off.
2026-09-24 09:48:48 +02:00
Marc-André Bombeck 39ae034b8c fix(modules): route nav entries and module pages through the surface map
Navigation destinations, the bottom bar's fixed slots and the tour stops
no longer carry their own module keys; each asks the surface map by its
href. Insights has no owner there, so switching AI analysis off leaves
the Insights entry, slot and tour stop in place.

The shell's page gate now resolves Insights sub-pages by slug as well
as top-level pages, so /insights/mood, /insights/medications and the
recovery metric pages show the module notice when their module is off,
worded from the access reason, with the Settings link only for the
record's own switch. The per-page redirect guard, the achievements
redirect and the nutrients page's own notice are gone; nothing
redirects any more.
2026-09-24 09:48:12 +02:00
Marc-André Bombeck a4f8e6db27 fix(capture): leave Mood and Medication out of the add menu with their modules off 2026-09-24 09:48:05 +02:00
Marc-André Bombeck 15d6251ad2 feat(vaccinations): show the planned booster's renewal state on the log
Whether a vaccination is still current was known only on the checkups
page. The vaccination log now shows it on each antigen's heading:
the booster the person planned when they logged a dose, marked as
overdue, due soon (within 30 days) or scheduled, tinted only in the
first two cases.

The state is resolved on the server from the person's own confirmed
booster reminder and published on the list response as renewals, so
the web page and the native client read the same answer. Nothing is
worked out from age, history or the catalogue: an antigen without a
planned booster shows no state. Renewals are null for a grant that does
not reach the measurements section the reminders live in.

Refs #1005
2026-09-24 09:44:51 +02:00
Marc-André Bombeck 94e07d0d90 fix(dashboard): hide a switched-off module's widgets on every feed
The dashboard widget and summary maps are now views of the surface map.
The legacy dashboard feed painted the stored layout as saved, so the
medications card stayed with the module off; the mask the snapshot
applies now runs on that feed too, in the browser, from the same map.
The widgets route keeps returning the stored layout unmasked, because
Settings saves that value back.

The Today digest is the day's data and no longer waits on the AI
analysis switch, on the server prefetch or in the browser.
2026-09-24 09:42:17 +02:00
Marc-André Bombeck 91be6b2967 feat(modules): add one map from every surface to the module that owns it
SURFACE_MODULE names each surface a toggleable module owns (nav entries,
Insights pages, overview blocks, trend slots, add-menu entries, dashboard
widgets and summaries, derived scores, Settings sections, correlation
channels) as <kind>:<id>. isSurfaceVisible answers from it on the server
and in the browser, useSurfaceVisible reads it against the account.

The insights key owns no surface: it means AI analysis, and the Insights
area is data.
2026-09-24 09:39:58 +02:00
Marc-André Bombeck b480ac4543 feat(documents): link a document to visits and vaccinations from its own sheet
A document could show its visits read-only and its vaccinations not at
all, so a childhood record covering a dozen doses had to be attached
from a dozen dose forms. The document's detail sheet now lists the
visits and vaccinations it is filed against and, for someone who may
manage the vault, links and unlinks them in one place, with the records
dated near the document on top. Both ends write the same link tables
through the link service, so the dose's form and the document's sheet
cannot disagree about what is filed where.

The document detail response carries the linked doses, named by
catalogue slug or the person's own wording. It is null rather than
empty for a grant that opens the vault but not the health background,
so a shared page never claims to record no doses. The update route
takes a replace-set of vaccination ids, up to 100 so one record can
cover a whole primary schedule; ownership is re-checked, a foreign id
answers 404 and the change lands in the audit log beside the other link
fields. Linking from the dose suggestion now refreshes the sheet.

Refs #1024, #1005
2026-09-24 09:39:27 +02:00
Marc-André Bombeck 1f6a13e2f7 feat(ai): answer the status notes with a null note when AI is unavailable
The status family (the five metric cards, medication compliance, the
generic metric status and the biomarker assessment) asked the retired
switch and the `insights` module and refused with 403. The note is model
text and the card around it is data, so they are mixed reads now: while
the `statusText` capability is available the note is served and warmed
as before, with `ai` beside it; otherwise the answer is 200 with the
note null, `preparing: false`, `hasProvider` as provider presence only
and `ai` naming the reason, and neither the cache nor the warm queue is
touched. The generic metric status still reports `insufficient` from the
data. The mood card keeps its mood module gate.

Kept apart so it can be held back while the shipped iPhone app's
rendering of a 200 without a note is confirmed.

Refs #975
2026-09-24 09:30:23 +02:00
Marc-André Bombeck c9534b343e Merge branch 'feat/ai-jobs-stored-text' into release/v1.39.0 2026-09-24 09:29:54 +02:00