Commit Graph
10063 Commits
Author SHA1 Message Date
Marc-André Bombeck b970ea7468 perf(dashboard): seed the mood chart's series from the snapshot
The dashboard snapshot carries the mood tile's series from the same
engine `/api/mood/analytics` answers from, and the snapshot already
seeds the widget layout, but nothing seeded the mood analytics cell the
mood chart reads, so every dashboard open with the mood chart asked for
the series again. Both seed points, the server render and the client
snapshot read, now fill that cell too, under the same rule as the
layout: only an empty cell, and only when the tile has readings, the
one case where its summary is the route's.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck ea6e94adcf perf(monitoring): read the monitoring switches in the root layout
Every document load, the sign-in page included, mounted a client
effect that asked `/api/monitoring/settings` for three instance-wide
values (false, null, false on a default install) before it mounted the
analytics script or the error reporter. The root layout is a server
component and now reads them while it renders and passes them in, so
the request and the effect go. The key-mismatch page reads nothing, as
before. The route stays for other callers.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck d2ac3b91bd perf(insights): stop the tab strip reading the briefing on every page
The layout shell around every Insights page read the advisor briefing
(`GET /api/insights/generate`, about half a second on the demo) for the
regenerate button on the tab strip, and once the answer arrived it
loaded the schema chunk that parses it. The strip shows no briefing;
only the overview does, and it reads the same cell itself. On a metric
page both were spent on a button.

The shell's hook now reads the briefing only while its own regenerate
settles. A regenerate from a page that never read the briefing reads
it first, so a timed-out attempt still settles against the briefing it
replaces instead of taking the old one for fresh. The button, its
toasts and the overview are unchanged. The strip's comments that said
only the overview carries the button are corrected.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck e1d143d689 fix(medications): read the dose history's window, not every intake
The history tab asks for a window of at most a year, but the route read
every intake the medication ever had: a medication taken three times a
day for five years read about 5,500 rows on each open. The full read
also served to find the last intake for rolling cadences.

The route now reads the rows anchored or taken in the window, and of
the rows before it only the latest take, which is the anchor a rolling
grid re-starts from and the last intake when none falls in the window.
Earlier takes place no slot in the window, so the ledger is the same.
The read moves into its own function, which shortens the handler.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck 6a1c91d5d4 perf(insights): mount the medication efficacy summary when near
Insights > Medications sent two requests per medication on entry: the
compliance calendar inside each card and the efficacy read behind the
summary card, which sits below every card and shows one line per
medication. The summary now mounts through `DeferUntilNear`, like the
overview's sections, so the efficacy reads go out only when the reader
scrolls toward it. The cards and what the summary shows are unchanged.

The comment that called the summary "never a second computation" goes:
the efficacy route builds the view on every request.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck d310058bc5 fix(admin): read the AI quality summary once on the Coach page
`/admin/coach` mounts the AI quality card and the Coach feedback card,
and both read `GET /api/admin/ai-quality` with the same unwrap, under
two different query keys, so every open sent the request twice. The
feedback card now reads the quality card's cell.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck a6fdd48058 fix(pwa): pause the version check while the tab is hidden
The version poller asked `/api/version` every 60 s from a plain timer,
also in a background tab or a minimised window, for as long as the
browser let timers run. It was the largest standing poll and the only
one that kept going while hidden, although the poll notes describe it
as the same shape as the others.

A tick now asks only while the tab is in view, and a tab that comes
back after a full interval away checks once at once, so a release
still surfaces on return.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck 9b2c68fbe5 perf(insights): keep the health status answer in the analytics cell
`/api/insights/health-status` recomputed the coincident deviation and
the 180-day changepoint scan over every vital on each open of Insights,
the only read on that page that stayed slow when warm, for a body of a
few hundred bytes. It now goes through the same analytics cell its
sibling reads use. Measurement and mood writes sweep the record's
cells; the reader's day and the switched-off modules are part of the
key, so a new day or a module switch never serves the other answer.
2026-11-11 22:05:48 +01:00
Marc-André Bombeck 5ea829df8b docs(api): regenerate the contract
Weight thresholds accept values from 1 kg, the notification preference descriptions name only the events something sends, and the error-code catalogue lists the new codes.
2026-11-11 21:58:18 +01:00
Marc-André Bombeck f6d11e7e37 Merge branch 'fix/v1473-rest' into release/v1.47.3 2026-11-11 21:57:54 +01:00
Marc-André Bombeck 0d41abcda4 fix(settings): list the Coach entry only while the Coach can answer
The settings nav filtered its Coach entry by the raw module map, which
follows the operator's switch and Hide Coach but not a missing provider
or missing consent, so it kept an entry the sidebar and the bottom bar
had already dropped. It now reads useNavModules like them. Settings, AI
decides from the same map whether to list the stored Coach memory, so
the memory always has one place in Settings where it can be read and
deleted.
2026-11-11 21:50:28 +01:00
Marc-André Bombeck 8352b1eef2 fix(onboarding): keep the tour card above the home indicator
On a phone the tour falls back to a card at the bottom edge, 8 px above
it, so in the installed app its Next and Skip row sat inside the home
indicator strip. The card now adds the bottom safe area, as the bottom
bar and the sheets do.
2026-11-11 21:49:56 +01:00
Marc-André Bombeck 54c12473eb fix(ui): give the chip remove buttons a 44 px touch area
The alias chips on a biomarker and the attached-document chips of a
share link had 24 and 20 px remove buttons that act on the first tap,
without a confirm. Aiming for one chip's x on a phone could remove the
one beside it. Both keep their look and take the 44 px touch area the
attachment pills already use.
2026-11-11 21:49:55 +01:00
Marc-André Bombeck dad69c0545 fix(ui): size the practitioner and vaccine pickers to their field
Both dropdowns asked for the trigger's width with the Tailwind 3
shorthand `w-[--radix-popover-trigger-width]`, which Tailwind 4 turns
into a declaration no browser accepts, so they took the width of their
longest row. The v4 form fixes the width, and `max-w-none` lifts the
popover's 20 rem cap, which is shorter than the field on a phone.
2026-11-11 21:48:45 +01:00
Marc-André Bombeck e99180fc7d fix(nav): let the More sheet scroll on a phone held sideways
Held sideways a phone keeps the bottom bar, and the sheet's 90 dvh cap
is about 350 px there, while the hub needs 410 to 540 px. The grid did
not shrink or scroll, so the last rows (documents, timeline, coach,
achievements) sat below the screen edge. It scrolls inside the sheet
now and stops at its end instead of pulling the page.
2026-11-11 21:48:17 +01:00
Marc-André Bombeck 177e85f9c3 test(sheets): give the mocked sheet its dirty check
The capture picker and the mood page import `sheetBodyHasUnsavedInput`
from the sheet module now, and two tests that replace that module with
a stub failed on the missing export.
2026-11-11 21:48:00 +01:00
Marc-André Bombeck ce377ef56d chore: remove leftovers of the deleted onboarding AI offer and badge
The end-to-end setup still seeded the shared-provider offer's account
and captured its session, although the journey that used it is gone.
Three comments still pointed at what was removed: the tour launcher
cited the deleted personal-record badge as its model, the personal
record worker said the badge still renders, and the morning briefing
push said a concerning reading goes out as an anomaly notification,
which nothing sends.
2026-11-11 21:47:54 +01:00
Marc-André Bombeck aacf06b7b3 fix(coach): say when the attachment picker list is cut
The Coach's "Choose from documents" picker reads the newest fifty
documents and dropped the route's nextCursor, so with a longer vault an
older report seemed not to exist unless one already knew to search for
it. A cut list now ends with the same line the document picker shows:
how many are shown and that the search reaches the rest. The document
row moves into its own component, which takes AttachmentPicker under
the function-size cap.
2026-11-11 21:44:42 +01:00
Marc-André Bombeck 20fc03bd61 perf(insights): read the coach strip's pattern scan once per record
Every metric page asked `/api/insights/coach-read`, and every call ran
the full 180-day discovery scan over every channel, plus the emerging,
lab and coincident passes, to pick the one driver whose outcome is the
page's metric. The scan does not depend on the metric, so each page in
a row paid over a second for the same answer and the strip stayed a
skeleton long after the chart had drawn.

The strip now reads the drivers through an analytics cell keyed on the
record, the language and the switched-off modules, the same contract
`/api/insights/correlations` uses: every measurement, mood, medication,
illness, custom-metric and pattern write sweeps the record's cells. A
failed scan is not kept. The band and the latest reading stay per call.
2026-11-11 21:43:58 +01:00
Marc-André Bombeck 3a249bccba fix(settings): keep note and paste fields at 16 px on a phone
On /settings/anamnesis the four note fields were raw textareas styled
by three copies of a class constant with text-sm (14 px), and on
/settings/export the three paste fields used text-xs (12 px) and the
backup passphrase text-sm. iOS zooms the page into such a field on
focus and leaves it zoomed. The notes use the Textarea primitive now
and the export fields keep the small size only under a fine pointer.

The 16 px guard read the five field primitives and could not see a
call site override their size, or a raw field styled from a constant.
It now parses every field call site; four that are still small sit on
a list that only shrinks. The focus guard drops the three records
files, whose fields come from the primitive now.
2026-11-11 21:43:10 +01:00
Marc-André Bombeck 24cca975da fix(charts): draw the BMI of the record on screen
Inside a shared or managed record the BMI chart drew nothing: it only
knew the viewer's height, and dividing someone else's weights by it
would have given a plausible wrong figure, so it divided by nothing.
The chart now takes the record's height from /api/insights/targets,
the cell the BMI page and the reference panel already read, through a
small useBmiDivisor hook; in the viewer's own record it keeps using the
account's height.
2026-11-11 21:43:07 +01:00
Marc-André Bombeck b85989f3df fix(coach): spell the bubble's initials from the display name
The person's chat bubble took the first two letters of the login, so a
managed record read "MA" for its generated handle and everybody else saw
other letters than in the top bar. It now uses nameInitials(accountName)
like the top bar, the sidebar and the settings avatar. The thread test
that pinned the login's two letters now expects the shared spelling.
2026-11-11 21:41:39 +01:00
Marc-André Bombeck b5c97062b8 fix(timeline): close the life event sheet before the timeline refetch
The life event hooks returned the timeline and day refresh from
onSuccess, which a mutation waits for, so saving or removing an event
kept the sheet spinning until the whole timeline had reloaded. The
refresh is now started and not returned, like the other record sheets.
2026-11-11 21:40:02 +01:00
Marc-André Bombeck 2a60f21a8b fix(ui): open hint content on a tap instead of a hover
Seven places kept information in a hover tooltip, which never opens on
a phone: the measurement-diversity lightbulb on the metric pages, the
sleep-source warning on the dashboard tile, the score coverage
breakdown, the target status pill, the range bar's marker, the colour
hint of the dashboard averages and the full token and user names in
the admin token list. The lightbulb showed a button that did nothing,
the warning a sign nobody could read.

They open on a tap now: InfoPopover for the two icon buttons, a
Popover on the meter, pill, marker, averages and cells. Where the
trigger sits in a link (a dashboard tile, a lab row) its tap opens the
popover instead of the page.
2026-11-11 21:39:27 +01:00
Marc-André Bombeck e68041a3af fix(forms): keep one Idempotency-Key for the remaining create forms
A screening, an illness episode, a life event, a symptom, a custom
metric or an address-book entry created on a weak signal could be
stored twice: the POST committed, the answer missed the timeout, and
the second tap went out under a new key. Their routes already honour
the key; the forms now post through useIdempotentPost like the other
create forms, so the retry replays the first answer.

The screening's save moves into a small hook beside MentalWellbeing,
which sat at the function-size cap and now drops below it; the custom
metric form's create-or-update call moves into one as well.
2026-11-11 21:39:19 +01:00
Marc-André Bombeck b89599d66a fix(auth): end the session when a refetch of the account says 401
A failed refetch keeps the last good payload in the query cache, so a
session that expired while the app was open still read as signed in:
the shell kept its pages, and every save failed with "sign in again".
A 401 or 403 now ends the session even with a cached account; a network
failure or a 5xx still holds the last known state.
2026-11-11 21:37:25 +01:00
Marc-André Bombeck 4485dfb3a9 fix(forms): close episode, mood and dose dialogs before the refetch
The illness episode hooks returned the refresh of the illness reads and
the dashboard from onSuccess, so every episode sheet kept spinning until
those had refetched. The mood edit dialog, the mood and measurement bulk
deletes and their Undo toasts did the same with an await, and so did the
dose edit dialog, the log-intake dialog and the ledger's add dialog.
They now answer and close as soon as the write lands and start the
refresh without waiting for it. A scheduled dose lands on its slot, so a
card that is still stale for a moment only repeats it.

The take and skip buttons on a medication card keep waiting: nothing
marks the dose in the cache meanwhile, so releasing them early would
offer a Take for a dose already recorded.
2026-11-11 21:36:37 +01:00
Marc-André Bombeck a03782f93f docs(sharing): say what the Vary header still guards
The service worker stores no API answers any more, so the comment no longer describes a disk cache that serves one record's answer to another context.
2026-11-11 21:36:27 +01:00
Marc-André Bombeck 18e8990638 fix(demo): keep a visitor's language off the shared account
The session read copies the language cookie into the account row when
the two differ, so on a demo every visitor's language still landed on
the one account all visitors share, although the locale route already
left the row alone there. Under DEMO_MODE the session read now answers
in the visitor's language and writes nothing.

The route's unit test mocked the database and the handler wholesale;
both paths are now proved against Postgres and read back from the row.
2026-11-11 21:36:27 +01:00
Marc-André Bombeck 6f9b42c35e fix(sheets): let Back close the open sheet
Back on a phone (the Android button, the edge swipe of the installed
app) with a sheet open went back a page under it: the sheet and what
was typed in it were gone, in the edit sheets too. Only the account
form guarded Back, behind an option.

Opening a ResponsiveSheet now pushes one history entry with the page's
URL. Back lands below it and closes the sheet, asking first when it
holds unsaved input or when its owner keeps it open; a sheet closed any
other way takes its entry off again, so the next Back still leaves the
page. A history write made while that Back is under way waits for it, a
link followed from a sheet replaces the sheet's entry, and the page
entry keeps the URL the page has now, so a stripped one-shot `?add=`
does not come back. The vault's document sheet already owns `?doc=` and
opts out.
2026-11-11 21:33:48 +01:00
Marc-André Bombeck 0278b6045e fix(pwa): keep the Medications home-screen shortcut
The shortcut is the quickest way to a dose for everyone who tracks
medications. With the module switched off it opens the page that says so
and offers to turn it back on, which is a sensible landing, so the
shortcut stays. The unread `source=shortcut` parameter stays removed.
2026-11-11 21:33:07 +01:00
Marc-André Bombeck 212ff9ee55 Merge branch 'fix/v1474-auth' into release/v1.47.4
# Conflicts:
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-11 21:32:28 +01:00
Marc-André Bombeck 85240ab858 fix(records): give the visit, vaccination and workout refusals a sentence
The visit, vaccination, allergy, address-book and workout forms read a
refusal's code through the catalogue, but nineteen of the codes their
routes send had no sentence: a stale doctor or check-up, a planned visit
that would close a check-up, a dose without a name, a booster that
cannot be planned, an invalid body, a workout batch that is too large.
Each of them read as the form's generic "could not be saved", which
hides what the person can change. They now have their own sentence in
all ten languages.
2026-11-11 21:32:23 +01:00
Marc-André Bombeck 188d21d6ac Merge branch 'fix/v1474-offline' into release/v1.47.4
# Conflicts:
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-11 21:32:11 +01:00
Marc-André Bombeck ffde57869b fix(sheets): ask before a dismissal drops typed input
The eight-step medication wizard, the add sheets on /measurements,
/mood and /labs, and the visit, vaccination, practitioner and checkup
forms closed on a stray backdrop tap or swipe and lost what was typed.
The dashboard quick entry and the capture menu asked first, through two
hand-copied dialogs and a DOM check that read the first sheet body in
the document and counted the date field's prefilled "now" as input.

ResponsiveSheet now takes a dirty flag, or a check it hands its own
body, and asks with the existing discard dialog. Forms that keep a seed
pass whether they differ from it; the wizard compares what a save would
send. Plain add forms pass the DOM check, which skips the date and time
fields again. The three copies of the dialog are gone.
2026-11-11 21:32:07 +01:00
Marc-André Bombeck 59f94683ae Merge branch 'fix/v1474-push' into release/v1.47.4 2026-11-11 21:32:03 +01:00
Marc-André Bombeck 0b7afd8460 fix(errors): show the catalogue sentence, not the server's text
About forty refusal messages still printed the error string the server
sent: English in every locale, and for a dropped connection the
browser's own TypeError text. Mood tags, targets and thresholds, family
history, medication side effects, phases, API tokens and dose imports,
the Coach model picker, the AI provider, fallback chain, timeout and
Codex cards, the notification channel cards, the avatar, passkeys,
security keys, the authenticator card, the re-proof dialog and the
admin pages now resolve the refusal's code through localizedApiError
and fall back to the surface's own sentence.

Two refusals carried something a person needs and had no code: a
private AI base URL (the AI_PRIVATE_ORIGINS hint) and removing the last
passkey. Both now send a code with a sentence of their own. A throttled
re-proof says so in the reader's language. Raw fetch sites throw the
same ApiError as the verb helpers through the exported throwApiError,
which replaces the admin's getApiErrorMessage; the authenticator card's
copy of describeStepUp is gone, and describeStepUp no longer passes a
non-step-up refusal's prose through (its test pinned that).

The admin password reset keeps the server's verdict on a 422: the
server words the strength and breach verdict in the account's language.
2026-11-11 21:29:33 +01:00
Marc-André Bombeck 5029b30b2b fix(i18n): drop the translation notice from the sign-in pages
Eight of ten languages opened the sign-in, registration and profile
take-over pages with the "machine translated" strip above the form, the
first thing a visitor read before having an account. The strip stays in
the app shell, where the language can be changed and a correction
reported; its text is unchanged.
2026-11-11 21:27:54 +01:00
Marc-André Bombeck 3385163fd6 fix(demo): stop refusing a visitor's own sign-out, language and timings
On the demo every page logged its own failures. The sign-out answered
403, so a visitor could not sign out at all; the language write answered
403 on every page load, and because its once-per-tab marker is only set
on success it was sent again on the next one; the page-timing beacon was
refused the same way. Five password sign-ins per 15 minutes per address
locked out a group trying the published demo account together.

Sign-out, the language write and the timing beacon now pass the demo
block: each touches only the visitor. The language route still answers
and sets the visitor's cookie, but under DEMO_MODE it no longer writes
the shared account's row, so one visitor's choice is not the next one's
starting state. A demo allows 30 password sign-ins per 15 minutes per
address; other instances keep 5.
2026-11-11 21:27:03 +01:00
Marc-André Bombeck 62b9e404ef fix(shell): return to the list's scroll position on Back
Back from a detail page (a biomarker, a medication, a workout) opened
the list at the top. The shell scrolls inside `<main>`, which stays
mounted across routes, and the browser's own restoration only moves
the document. Nothing kept the list's offset, so `<main>` stayed where
the detail page had left it; and the one-shot skip on `popstate` was
cleared by a timer that could run before the restored page rendered,
in which case the forward-navigation reset fired anyway.

The shell now records `<main>`'s offset per address while scrolling,
reads it back on `popstate` and applies it once the page has rendered,
retrying for up to a second while a list is still shorter than the
offset. A forward navigation still starts at the top.
2026-11-11 21:23:17 +01:00
Marc-André Bombeck ecb219f346 fix(auth): say a refused sign-in in the reader's language
The sign-in page (password and passkey), the second-factor step and
registration printed the server's English error text, so a German or
French page said "Invalid credentials" or "Invalid code". They now
resolve the error code through localizedApiError like the other forms
and fall back to the page's own sentence. Registration still shows a
422 as sent: that is the password feedback, which the server already
words in the reader's language.

A 429 carries the generic `http.rate_limited` code, which had no
sentence, so a person locked out by the limiter would have read only
"Login failed". The catalog now has one, reusing the wording of the
profile take-over's rate-limit message.
2026-11-11 21:21:20 +01:00
Marc-André Bombeck a4766824ef fix(auth): keep the page when a sign-out from an auth page is refused
The sign-out buttons on the registration and profile take-over pages
took every HTTP answer as success. A refused sign-out (the demo's 403,
a 429, a 5xx) wiped the client caches and reloaded the page while the
session stayed valid; on the take-over page a network failure was an
unhandled rejection with no message at all.

Both go through apiFetch now, which throws on a non-2xx answer, and say
that the person is still signed in: inline on the registration page, as
a toast on the take-over page. The message is the one the account
menu's sign-out uses.
2026-11-11 21:18:46 +01:00
Marc-André Bombeck 8fdc238981 fix(auth): replace the sign-in page in the history after signing in
Back after signing in, registering or taking over a profile showed the
form that had just been submitted, because each pushed the next page
on top of it. Password, passkey and second-factor sign-in, registration
and the password take-over now replace the page instead.
2026-11-11 21:15:05 +01:00
Marc-André Bombeck 6c5a6eacbb fix(auth): bring a deep link back to its page after signing in
A home-screen shortcut, a notification tap or a bookmark opened without
a session landed on the sign-in page with a bare URL, and after signing
in the person was on the dashboard instead of the page they asked for.
The sign-in page already honoured a sanitised `next`; neither redirect
set one.

The proxy now passes the request's own path and query as `next` (the
bare start page goes without one). The shell does the same when the
account answer ends a session on a protected page, except after a
deliberate sign-out: the marker a confirmed session leaves is gone by
then, so the next person on the device starts from a bare sign-in page
rather than the page the previous one left.
2026-11-11 21:14:04 +01:00
Marc-André Bombeck 75b879279f fix(auth): leave nothing of the last account behind on any sign-in
Only the password and passkey success path cleared the offline copies
on this device. A session that ended by expiry reached the sign-in page
without any wipe, and single sign-on and both profile take-over paths
started the next account on top of the previous one's IndexedDB
snapshot and worker caches.

The shell now clears those copies on every /auth/* page once
/api/auth/me answers 401 or 403: once per signed-out stretch, never on
an answer without a verdict. Single sign-on and the take-over by single
sign-on clear them before leaving for the identity provider, and
registration and the password take-over start from an empty query cache
like the sign-in page does.
2026-11-11 21:10:38 +01:00
Marc-André Bombeck 9227844f42 Merge branch 'fix/v1473-ai' into release/v1.47.3
# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-11-11 21:07:27 +01:00
Marc-André Bombeck aa6e617da0 fix(insights): grade blood pressure and pulse against the own target
Someone whose doctor agreed a target of 140/90 entered it on /targets;
the chart and the dashboard tile followed it, but the BP and pulse status
texts, the general status, the comprehensive overview, the insight-card
alert and `extractFeatures` (and through it the comprehensive text, the
daily briefing and the Coach) still graded every reading against the age
default. The text under the chart could contradict the chart.

All six places now resolve the band the way the charts do since #1261:
`resolveEffectiveBpTargets` for blood pressure, the PULSE override for
resting pulse, the default only where no target is set. The resolved band
rides in each snapshot's targets block, which is part of the input hash,
so the next generation follows a changed target. The Health Score keeps
its clinical targets on purpose.
2026-11-11 21:00:10 +01:00
Marc-André Bombeck ee04c5bcf4 test: register the push unsubscribe DELETE by hand
The DELETE behind "Disable push" moved into the shared
unsubscribeWebPush helper under lib/, which the destructive-control
detectors do not scan. The card's entry stays and says where the
request goes out now.
2026-11-11 20:55:08 +01:00
Marc-André Bombeck af623d12db fix(push): send the test push to every device, in the user's language
The Web Push test went to the newest subscription only. With a phone
and a laptop subscribed, Test pressed on the laptop buzzed the phone
and the laptop looked broken. The text was English in every locale,
and the push was dialled through the library's own transport, past the
pinned safeFetch that real sends use.

The route now calls sendViaWebPush, so the test reaches every
subscription, reaps endpoints the push service reports gone, and goes
out through the same dial as a reminder. Title and body come from the
account's locale. The response keeps ok and latencyMs and gains reason;
sent and perEndpoint are gone, nothing read them.
2026-11-11 20:52:36 +01:00
Marc-André Bombeck 926c427907 fix(coach): stop computing the plan review verdict nobody sees
The nightly plan review read a before/after window of the plan's metric
and stored an English verdict in `outcomeEncrypted` for every plan. No
screen and no route returned it; only the Coach snapshot read it, behind
the `COACH_EXPERIMENT_VERDICT` flag that is off by default. The worker now
only moves an active plan whose review fired to `reviewed`, which is what
takes it out of the standing group. The flag, its snapshot block, the
three eval cases for that block and the compose and env entries go with
it. The Coach prompt keeps its experiment rules and now ties the
read-back to the plan's progress line instead of the removed block.
Existing stored outcomes stay in backups untouched.

The plans page and the Coach reminders card printed the metric as a raw
key in capitals (WEIGHT, BLOOD_PRESSURE) in every language. Both now show
the measurement's name from the catalogue, through the plan metric
resolver moved to `plan-metric.ts`; a key HealthLog does not read shows
as written. The unused `coach.plans.proposalTitle` string goes.
2026-11-11 20:51:18 +01:00