Commit Graph
3179 Commits
Author SHA1 Message Date
Marc-André Bombeck eec52534d5 fix(insights): ask the empty-state Coach question in the reader's language
A metric page without data offers to ask the Coach about the metric.
44 of the 49 pages passed that question as an English literal, so a
German, French or Korean reader got an English first message; only 5
read it from the catalog. 31 of the 44 were one sentence with the
metric's name swapped in.

`HealthKitMetricPage` now asks that sentence once, translated, around
the page's own title, and the 31 pages drop the prop. The 13 pages
with a question of their own (steps, HRV, sleep, mood, workouts, blood
pressure and others) read it from the catalog like the 5 did, in all
ten languages.
2026-11-11 22:09:23 +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 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 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 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
Marc-André Bombeck b793be3f84 fix(onboarding): shorten the AI pitch on the last setup screen
Every new account finished setup on a full AI panel: a gradient badge, a
sample briefing, three ways to connect a provider, a one-tap offer for the
operator's key and a disclaimer, the most prominent block on the screen.
It is now one muted line with a link to Settings, AI, shown under the same
rule as the Insights hint (could be set up here, nothing configured yet,
not opted out). That hint and the consent request on Insights and
Settings, AI stay the places that invite.

The sample briefing card, the `onboarding.ai.*` strings in ten locales and
the end-to-end spec for the removed offer go with it. The consent-grant
guard's floor drops to the two callers left.
2026-11-11 20:38:37 +01:00
Marc-André Bombeck 17fd1a0906 chore(insights): remove the "Try a 7-day experiment" placeholder
Every correlation card carried a permanently disabled button with the
tooltip "Coming soon", which on a phone (no hover) was just a dead
control. The feature it stood for was never built; the button and its
two strings go in all ten locales. The Coach hand-off and the dismiss
action stay.
2026-11-11 20:30:24 +01:00
Marc-André Bombeck f519df4433 Merge branch 'fix/v1473-record' into release/v1.47.3 2026-11-11 20:18:59 +01:00
Marc-André Bombeck 6a02cda210 Merge branch 'fix/v1473-writes' into release/v1.47.3 2026-11-11 20:18:58 +01:00
Marc-André Bombeck de8eadbf2f fix(ai): keep setup invitations away from people who opted out
A person who switched both the Coach and AI analysis off under Settings,
Modules still got invited to set AI up: the Insights hint, its copy in
Settings, AI, the checklist row and the onboarding panel all keyed on
`provider.canConfigure`, which only knows the record kind and the
operator switch.

The flag itself stays as it is, because the AI usage card and the consent
request read it too and still serve document reading, lab photos and
medication extraction. The three invitation readers now also check the
module map through `hasOptedOutOfAi`.
2026-11-11 20:18:10 +01:00
Marc-André Bombeck da671f74aa refactor(day): share one window for date-only columns
The day view, the day index and the visit preparation each built the
UTC calendar window for a date-only column by hand (documents had a
fourth copy). `dateOnlyWindow()` beside the other date-only helpers
replaces them. The report name select becomes one spread, the share
page builds its calendar formatter outside the view, and the PDF
section reads it off the context, so the functions these fixes touched
stay within their size budget; `DocumentRecordLinks` drops under it.
2026-11-11 20:12:44 +01:00
Marc-André Bombeck 53f4ecc2c5 fix(documents): say when the document picker list is cut
The picker for linking documents to an illness and for choosing
documents for a share link reads the newest fifty 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 a line that says how many are shown and that the search reaches
the rest. Paging was left out: the coach attachment picker shares the
picker's cache key with a single-page read.
2026-11-11 20:08:24 +01:00
Marc-André Bombeck d204b4344b fix(records): offer Undo after deleting a vaccination, visit or entry
Deleting a vaccination, a doctor visit or an address-book entry closed
the sheet with no way back, although all three are soft deletes with a
documented restore route. The vaccination hook had a restore mutation
nobody called; visits and the address book had none.

The two hooks gain a restore mutation, and each delete now confirms
with an Undo toast, the same pattern the illness episode uses. The Undo
runs after the sheet has closed, so it awaits the restore itself and
the hook's own onSuccess refreshes the lists, instead of relying on
callbacks a closed sheet never receives. Restoring an address-book
entry brings the entry back, not the links the delete cleared on the
visits that named it. The three new success toasts are pinned in the
success-affordance guard.
2026-11-11 20:06:02 +01:00
Marc-André Bombeck 5449933c4f chore: remove three components nothing renders
personal-record-badge.tsx, intake-history-editable.tsx and
intake-history-preview.tsx had no importer outside their own tests and
the preview, so they rendered nowhere while four guard tests kept them
listed as live surfaces. They go with their tests, their guard entries
and the strings only they used.

GET /api/personal-records stays: the native app reads it. The
resolved-field guard now records its benchmark band as a native-only
field instead of naming the deleted badge as its reader.
2026-11-11 20:03:00 +01:00
Marc-André Bombeck f761500247 fix(forms): send one Idempotency-Key per opened create form
A mood, symptom, lab value, custom metric value, unscheduled dose,
allergy, vaccination, visit or measurement saved on a weak signal could
be stored twice: the POST committed, the answer missed the 15 s
timeout, the form said it could not save, and the second tap was a new
call with a new key, so the server ran it again.

useIdempotentPost keeps one key until a save lands and renews it for the
next entry, so the retry replays the first answer. The create forms and
the three create hooks post through it; the comment on the per-call key
in api-fetch now says what that key does and does not cover.

LabForm and MoodForm each gain the one line that mounts the key, so
their function-size baselines go up by one. The intake toast guard now
recognises a dose posted through the hook.
2026-11-11 20:02:40 +01:00
Marc-André Bombeck 3fcfe11694 fix(cycle): date today in the profile zone
The cycle page, its ring and the summary card took "today" and their
read window from the device clock, while the server keys every cycle
day, today included, in the profile zone. With a phone in another zone
the highlighted day and the day "log today" writes to sat one apart
from the rest of the app around midnight.

One `useCycleWindow()` now dates today in the display zone and derives
the shared window from it; it replaces `localYmd` and its two copies.
2026-11-11 20:02:03 +01:00
Marc-André Bombeck af673a63da fix(measurements): offer no edit or delete on a computed score
Recovery, strain and other computed rows in the measurement list carried
a pencil and a bin. The pencil opened an edit sheet labelled "Value ():"
whose save the server then refused, and the bin deleted a score the
engine owns. Both are gone for COMPUTED rows.

Readings from a device or a provider keep both controls: the server takes
a note or time edit on them, and a wrong scale reading has to stay
removable. The server is unchanged.
2026-11-11 19:59:43 +01:00
Marc-André Bombeck 121b23f4a2 fix(forms): show the form's own sentence when a save is refused
The custom metric, allergy, vaccination, booster, visit, address-book,
workout and quick-add dose forms printed the server's error string as
it came: English in every locale, and for a network failure the
browser's own TypeError text. They now resolve the error code through
localizedApiError like the other entry forms, so a known refusal reads
in the person's language and anything else falls back to the form's
"could not save" sentence. A custom metric whose name is taken keeps
its specific message, now translated.

The workout form's browser test expected the server prose and now
expects the form's sentence.
2026-11-11 19:57:06 +01:00
Marc-André Bombeck 7001308636 fix(measurements): mark an implausible edited value on its field
Typing 720 instead of 72.0 in the measurement edit sheet showed the
English banner "Validation failed" in every locale, with no field
marked. The route refuses a changed value outside the type's band with a
422 whose issue list names the field, and the client dropped that list.

The sheet now checks the same band before sending and says, under the
value input and in the unit it shows, which range is accepted. ApiError
carries the field paths of a refusal, so a value the server still
refuses lands on the field too; any other refusal goes through
localizedApiError instead of printing the server's prose.
2026-11-11 19:56:59 +01:00
Marc-André Bombeck 4fa5e6381e fix(insights): take weight and BMI bands from the record on screen
Inside a shared or managed record the weight page shaded the band for
the viewer's own height, and the record's own target never arrived
because `/api/user/thresholds` refuses under a switch: a parent saw
their own healthy range behind a child's weights. The BMI page decided
from the viewer's height whether there was anything to show, and the
target gear opened an editor that could neither load nor save there.

The weight page now reads the record's target and height from
`/api/insights/targets`, the payload the reference panel under the
chart already uses, and paints the same wings as before. The BMI page
reads the record's height there inside somebody else's record. The
gear stays hidden in another person's record, as the BMI chart
already draws nothing there rather than a wrong figure.
2026-11-11 19:56:49 +01:00
Marc-André Bombeck 2c7a0dc3c2 fix(insights): show small lab changes and print them like the labs page
"What changed since your last panel" rounded every delta to two
decimals, so TSH 0.004 to 0.009 read "no change", and it printed values
and deltas through `String()`: a full stop in every language and every
digit of a unit conversion (41.999198717948715 ng/mL).

The summary now reads each reading at the precision it was recorded at
(`recordedPrecision`, with the report's printed figure for a converted
value, which the route now selects) and rounds the delta to the finer
of the two, so its direction agrees with the printed figures. The card
prints both through `useLabNumber()`. 5.9 - 5.6 still comes out as 0.3,
so the existing marker-pairing test stands as it was.
2026-11-11 19:52:13 +01:00
Marc-André Bombeck 9d7c99e60c fix(vaccinations): date a dose by its calendar day on every surface
A dose date is stored at UTC midnight. The health record PDF, the share
page, the related-records list, the document link chips and both dose
pickers formatted it as an instant in the reader's zone, so across the
Americas each of them showed the day before the vaccination list did.
The booster anchor added the interval to the stored instant, which put
the first due day a day early there too.

The report and share page print dose dates through the calendar-date
formatter, the link surfaces through `useCalendarDate()`, and the
booster counts its interval from the dose's date to the start of that
day in the person's zone.
2026-11-11 19:50:34 +01:00
Marc-André Bombeck 4721914cac fix(forms): close the sheet as soon as a save lands
Saving a weight, a mood, a workout, a measurement edit or a quick-add
dose emptied the form and kept the button spinning until every read the
write touches had refetched (snapshot, digest, charts, insights). On a
slow connection that looked like nothing had been saved. The vaccination,
visit, address-book, symptom and check-up hooks did the same through an
onSuccess that returned the refetch, which a mutation waits for.

The forms now report and close first and start the refresh without
waiting for it; the hooks no longer return it. The quick-add loses a
second sweep of the inline compliance charts that the medication bundle
already covers, and its source pin follows.
2026-11-11 19:46:17 +01:00
Marc-André Bombeck 1d0a2a8128 fix(doctor-report): print the date of birth on its own day west of UTC
The date of birth is a calendar date stored at UTC midnight. The PDF
cover and the share page formatted it as an instant in the owner's
zone, so everyone in the Americas read the day before; settings and
the FHIR export, which take the date part, already showed the right
day. Both now print it through a calendar-date formatter (read in UTC,
the owner's date order), which is right in every zone, unlike a noon
anchor that moves the day east of UTC+12.
2026-11-11 19:45:12 +01:00
Marc-André Bombeck 971d5d32cc fix(documents): refresh what confirmed facts and a take-over create
Confirming a document's facts creates medications, conditions and lab
values, and the profile take-over creates a condition or an allergy.
Both evicted only the roots of those lists, so the dashboard and the
Today digest showed the old medication set until the 120 s poll, the day
and timeline kept the old state for five minutes, and adherence and
nutrient reads were not evicted at all.

Both now spread the bundles of what they create, and the condition paths
pair them with the inactive refetch of the daily reads.
2026-11-11 19:44:13 +01:00
Marc-André Bombeck 9a02dfff2e fix(settings): build the avatar initials from the display name
The settings avatar took its fallback letters and its alt text from the
login, while the top bar and the sidebar spell the display name, so the
same account showed different initials on two screens. The three
copies of the initials function become one `nameInitials()` beside
`accountName()`, and the settings avatar reads the same name.
2026-11-11 19:43:21 +01:00
Marc-André Bombeck 5cf4a7d213 fix(doctor-report): name a managed record by its display name
The report cover, the PDF author field, the FHIR Patient and the share
page printed `fullName ?? username`, and the collector never read the
display name. A managed record has no way to get a full name, so its
generated `managed-<hex>` login landed on every clinical document made
for it; an own account printed the login instead of its display name.

The collector now resolves the name once (`patientName()`: full name,
display name, login, never a managed record's generated handle) and the
five readers print that one field. `patient.username` and
`patient.fullName` leave the report payload; nothing else read them.
2026-11-11 19:42:17 +01:00
Marc-André Bombeck 2e73b0ffec fix(query-keys): refresh the day view and timeline after every write
Symptoms, workouts, lab results, allergies, documents and questionnaires
evicted only their own lists. An open day, its calendar dots and the
timeline kept the pre-write answer for up to five minutes, so "Capture
for this day" saved a symptom or a workout and the day showed the old
list, and a deleted row stayed on it.

Workouts, labs, allergies and documents get a bundle beside the seven
that already carry the day and timeline roots; the six lab lists become
one (the OCR dialog's own copy of the commit hook's list goes with it).
Symptoms and questionnaires add the two roots to their list. Symptoms
stay off the illness bundle on purpose: nothing on the dashboard or the
Today digest reads them. Custom metrics need nothing: neither the day
nor the timeline reads them. invalidateKeys now drops a key listed twice,
since spread bundles share the two roots.
2026-11-11 19:41:14 +01:00
Marc-André Bombeck 3745a0207d test(gki): check the chart's series colour without a browser
In the full run the browser version made a neighbouring browser file
fail with a detached frame, while each passed alone. Render the chart in
Node with Recharts stubbed, as the coach result chart tests do.
2026-11-11 01:11:27 +01:00
Marc-André Bombeck 8b607ab501 test: drive the revoke race against Postgres and type the new fixtures
The revoke audit is now asserted in the integration race test where a
rotation commits while the turn-off waits, instead of a route test that
mocked the handler and the client. The other new tests type their
fixtures rather than casting them away.
2026-11-11 01:00:23 +01:00
Marc-André Bombeck 8aa8379861 fix(gki): draw the index chart in a chart token
The custom metric chart drew its line in --primary, which put the index
in the action colour right under the ketone series drawn in a chart
token. Give the chart an optional series colour, default primary so a
custom metric looks as before, and draw the index in --chart-4.
2026-11-11 00:52:41 +01:00
Marc-André Bombeck 0b3c013af7 fix(i18n): name the period in the forecast style hints
Only the selected style's hint is shown, so "it may start" and "they
may fall" had no noun on screen to point back to. Name the period in
every locale that left it implied, and put the Korean hints in the same
register as the rest of the card.
2026-11-11 00:49:02 +01:00
Marc-André Bombeck b4a5442fb5 fix(labs): leave "/" to the browser when no search field is on screen
The shortcut called preventDefault before checking for the field, so on
a labs or documents page still loading, in error, or with nothing
recorded, "/" did nothing and the browser lost its own use of the key.
2026-11-11 00:48:16 +01:00
Marc-André Bombeck ee1eb921a9 fix(ui): announce a filter bar's result count
The count beside every filter bar was a plain span, so a screen reader
heard nothing while a list narrowed, only the empty state once nothing
was left. Make it a polite status region.
2026-11-11 00:47:27 +01:00
Marc-André Bombeck 128cf55ab6 fix(day): fit the GKI tile's pair line in its column
The single-reading line spelled out both readings with their names and
units and was cut off after the glucose in every layout. Show the two
numbers with one unit, which fits the tile, and keep the worded pair for
assistive tech.
2026-11-11 00:46:30 +01:00
Marc-André Bombeck 5ebfac1259 fix(gki): build the index from the glucose shown beside it
A reading entered in mmol/L is stored as whole mg/dL, and the index
divided the unrounded conversion, so a 5.2 / 0.3 pair printed beside a
17.4 rather than 17.3. Round the glucose to the decimal it is shown with
first and divide that. The tiles also printed a two-decimal ketone
reading to one decimal; show it as recorded.
2026-11-11 00:44:29 +01:00
Marc-André Bombeck ba3ee968c7 style(labs): format the filter rail files 2026-11-11 00:22:26 +01:00
Marc-André Bombeck cc9cbc4640 fix(labs): read odd filter links kindly and match names without accents
A link with the date bounds reversed showed an empty list; it now reads
as the range it means. The search ignores accents as well as case, so
"hamoglobin" finds "Hämoglobin". The category pill only appears when
there are categories to choose between, or when a link already filters
by the only one, so the filter can still be cleared where it shows.
2026-11-11 00:18:17 +01:00
Marc-André Bombeck a40c9e4ea4 fix(labs): keep keyboard focus where it belongs around the search field
Clearing the search with its button removed the button under the
focus and dropped focus onto the page; it now stays in the field. The
/ shortcut no longer pulls focus out of an open dialog, menu or list,
and ignores a key another handler already took or one typed while an
input method is composing.
2026-11-11 00:16:39 +01:00
Marc-André Bombeck 74464184c5 fix(labs): keep what is typed in the search field when the URL catches up
The labs search writes its trimmed query to the URL after a short pause,
and every change of the URL query replaced the field's text. The
field's own write came back as well: a trailing space vanished, so
"vitamin " followed by "d" searched for "vitamind", and a letter
typed while the write landed was lost. A query the field wrote itself
now leaves the field alone; one from back/forward, a link or Reset
still replaces it.
2026-11-11 00:15:24 +01:00
Marc-André Bombeck 04a4353e50 fix(labs): put the search field beside the filter rail, not in it
Inside the rail, the full-width search field wrapped on phones and left
the filter icon alone on a line above it, and the shared FilterBar grew
a class override and a second layout rule to make room. The search
field now sits next to the unchanged rail: above it on phones, where
the pills keep the usual scroll row, and in one line with it from sm
up. FilterBar and docs/ui-guidelines.md are back as they were, so the
filter-bar grammar stays the one ui-standards.md defers to.
2026-11-11 00:13:41 +01:00
Marc-André Bombeck 2c63bf7a3a chore(labs): tidy the filter rail follow-ups
Drop a local-only ignore line, end the search placeholder with the
ellipsis character the other placeholders use, and put the
UrlFilterSyncOptions interface above the hook's doc comment so that
comment documents the hook.
2026-11-11 00:12:04 +01:00
Marc-André Bombeck 82ad1e2b45 fix(labs): call the date filter "Last measured"
The filter compares only the day of a marker's newest reading, so a
marker measured inside the range and again later drops out. "Date
range" promised more than that. The pill now reads "Last measured" in
every locale, and the from/to semantics are written down where the
filter is defined.
2026-11-11 00:10:44 +01:00
Marc-André Bombeck 2e2a0ac4a1 fix(labs): keep groups with a match open while filtering
With groups opening only when flagged, the open state was worked out
from the filtered rows. Filtering to in-range markers, or searching for
an unremarkable one, closed the header while the counter reported
matches. A group's default now follows the server's status for the
whole category again, and while any filter is active every group with
a match is open. A group closed by hand during filtering stays closed
while the filter changes, is not stored, and the saved state returns
once the filter is cleared.
2026-11-11 00:09:54 +01:00
Marc-André Bombeck 2f4da62c46 refactor(labs): drop the sort pill from the filter rail
Settings > Labs already holds the list's sort order. The pill added a
second, URL-only one that was lost on the next visit, and Reset cleared
it while the rail counted it as no filter at all. The list now sorts
the way Settings says, and a ?sort param is ignored.
2026-11-11 00:07:58 +01:00
Marc-André Bombeck a486c3c8a3 Merge remote-tracking branch 'origin/release/v1.47.2' into takeover/1370
# Conflicts:
#	CHANGELOG.md
#	src/lib/module-list-prefs.ts
2026-11-11 00:04:51 +01:00
Konrad Garbal b4ac67ee7c feat(labs): add biomarker filtering and sorting 2026-11-10 20:46:57 +01:00
Marc-André Bombeck 4dcb041612 fix(labs): review a unit that was only a calc note, keep unseen category places
A document unit that held only "(calc)" read as no unit and went straight to the record; it now goes to review. Saving a category order keeps the saved keys not on screen after the shown ones. The changelog says what each fix does and no more.
2026-11-10 19:53:47 +01:00