Commit Graph
9854 Commits
Author SHA1 Message Date
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 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 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 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 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 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 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 1d5236a326 test(ai): settle the ratchets after the empty-chain change
The resolver tests queue their fixture rows through one helper instead of
an `as never` per row, the no-provider status case also proves the
single-provider resolver is never asked, and the size and comment
baselines take the lower numbers the change left behind.
2026-11-11 20:37:48 +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 e098fc882f fix(ai): an empty provider chain means no provider
Eight call sites (Coach turn, status texts, briefing, comprehensive
insights, medication extraction, about-me questions, both lab OCR paths,
the saved-config connection test) answered an empty chain by calling the
single-provider resolver, which ignores the chain's switches. A person who
kept only the local model on and lost its address got the ChatGPT sign-in
or the operator key instead, and every copy tagged that provider as the
operator key, so consent and cost attribution saw the wrong owner.

The rule now lives once in `resolveProviderChain`, and the presence probe
reads the same rule: a saved chain is the person's word, so an empty one
stays empty. Only a chain never saved falls back, to the OpenAI-compatible
gateway picked under Settings, AI, because the default chain has no entry
for it; that entry carries its real type. The callers drop their copies,
and the presence helpers that restated the old fallback go with them.
2026-11-11 20:29:11 +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 493154817d Merge branch 'fix/v1473-switches' into release/v1.47.3 2026-11-11 20:18:57 +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 bdfecfa70e test(e2e): build the mock weight bands with the server's helpers
The dashboard snapshot mock still called `buildWeightBandsFromHeight`,
which went with the weight floor change, and kept the old 30 kg lower
edge. It now builds the bands the way `buildTargetBands` does.
2026-11-11 20:03:46 +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 02856e7f37 fix(targets): accept a child's weight target below 30 kg
A weight target was refused below 30 kg on every save path (the own
targets editor and a managed record's settings parse the same schema),
while a weight reading is accepted from 1 kg. A guardian setting 20 to
23 kg for a child got only "please check your entries". The target
floor is now the measurement floor.

The weight bands on the dashboard and the weight page started their low
red zone at a hard-coded 30 kg, so a target below it lost that zone; the
edge now follows the same floor. Bands are clipped to the chart's own
domain, so an adult's chart looks the same. `buildWeightBandsFromHeight`
had no caller left and goes; the band builder's 30/250 defaults, which
every caller overrode, go with it. The unit-adapter test keeps its
inward-rounding check at the new floor (1 kg is 2.2046 lb, offered as
2.3).
2026-11-11 20:00:39 +01:00
Marc-André Bombeck 46b1cd6f34 refactor(insights): classify the derived BMI through classifyBMI
The derived BMI card kept its own copy of the WHO cut-offs beside
`classifyBMI`, which the targets panel, the status text and the
overview use. It now maps that one classifier onto its four-word
vocabulary, so a later change to the cut-offs has one place to land.
2026-11-11 20:00:38 +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 a28c6b55ab fix(medications): spell the reminder's slot time in the user's clock
The dose reminder printed the slot as the raw stored HH:mm, so it said
"20:00" while the medication card said "08:00 PM". That hit everyone who
set a 12-hour clock, and every en or ko user on the automatic setting,
which resolves to 12 hours for those languages.

The reminder now formats the slot's instant in the owner's zone with the
same formatter the card uses, honouring the owner's hour-cycle choice. A
recipient reading the message in another language gets that language's
clock when the owner left the setting on automatic.
2026-11-11 19:58:15 +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 71fe874ef2 fix(medications): silence a switched-off module and an ended course
With the Medications module switched off, dose reminders, missed-dose
rows and low-stock alerts kept coming, a tap on one landed on
"Medications is switched off", and the Telegram buttons still booked
intakes. The reminder tick now drops the medications of an owner with the
module off, one module read per owner and tick, so neither a reminder nor
a missed-dose placeholder is written while it is off. The low-stock pass
skips such a user, as the mood reminder does for its module, and the bot
offers and books nothing.

A medication whose course had ended still counted as live: the low-stock
pass kept warning about its stock, and the bot's /add list offered it and
booked a dose after the end, taking a unit off the stock. The low-stock
pass and every bot path now skip a course that has ended on the person's
clock. A course that has not started yet keeps its reorder warning and
stays in the bot. A paused medication is already active: false, so the
existing filter keeps it out.

The bot's six copies of the medication lookup go through one helper.
2026-11-11 19:55:09 +01:00
Marc-André Bombeck 8057bb20bc fix(notifications): drop the four switches nothing sends
The notification matrix listed "Unusual measurement", "Low adherence",
"Withings sync failed" and "Personal record" with a switch per channel,
three of them on by default, yet nothing dispatches any of them: a reading
far outside its range or a week of missed doses produced no message.

eventTypesForRole now leaves them out for every role, next to the
admin-only filter, and the preferences read no longer serves rows stored
for them. Their labels in all locales and their tap targets are deleted.
The types stay in EVENT_TYPES, so the preference write and the stored rows
keep parsing; no migration.

The label and tap-target tests walked EVENT_TYPES and so pinned the four
in place; they now walk the event types that are sent.
2026-11-11 19:53:01 +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 0639d28f6a fix(i18n): say "sign in again" or "too many requests" on a refused save
A save after the session ran out, or into a rate limit, showed the form's
generic "could not save". The server already names both refusals in
meta.errorCode (auth.missing for an ended web session, the status codes
http.rate_limited, http.forbidden and http.invalid otherwise), but the
catalogs had no sentence for them, so the lookup fell through to the
fallback. They have one now in all ten locales, and a refusal that
reaches the client without a code (a reverse proxy's 429) is read by its
status the way the server labels it.
2026-11-11 19:48:27 +01:00
Marc-André Bombeck 9afcfd4351 fix(day): read dose and allergy dates as calendar dates
A dose date is stored at UTC midnight and an allergy onset at noon UTC;
neither has a time of day. The day view, the day index, the visit
preparation and the timeline treated both as instants in the person's
zone: west of UTC a dose landed on the previous day, and the day view
printed a clock time nobody entered (01:00 for a dose in Berlin, 13:00
for an onset).

These readers now take the date part, as the vaccination list and the
allergy form already do: the day and the index query the UTC calendar
day (the window documents already use), the events carry no instant,
and the timeline dates both kinds by their key. Correct for midnight
and noon rows in every zone, without touching stored data.
2026-11-11 19:47:53 +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 7f54a820b0 fix(checkups): clear a done check-up from the Today rail at once
Marking a check-up done on the dashboard updated its tile, while the
Today rail above it kept listing it as due until the 120 s poll. The
reminder writes refreshed the two daily reads only through the inactive
pass, which fetches nothing for a query that is on screen. They are now
invalidated the ordinary way too, so a mounted rail refetches, and the
day root joins them because a check-up marked done is listed on its day.

The test fake pinned only the inactive call; a real query client now
shows the mounted rail refetching.
2026-11-11 19:42:17 +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 aae029d94e Merge pull request #1365 from MBombeck/release/v1.47.2
release: v1.47.2
v1.47.2
2026-11-11 07:21:17 +01:00
Marc-André Bombeck 0a0df4946a chore(release): v1.47.2 2026-11-11 07:01:25 +01:00
Marc-André Bombeck 9ef3331f85 test(pwa): give the sw-version script tests time to start node 2026-11-11 01:56:44 +01:00
Marc-André Bombeck a1cfb570c1 test(browser): wait for the file's own iframe before the first command
Every browser test iframe is named "vitest-iframe" and the Playwright
provider looks the frame up by that name. Once a page runs a second
file, the first file's iframe can still be found until its detach event
arrives, so the second file's first click lands in a frame that is going
away and fails with "Frame was detached". With fifteen browser files and
at most twelve pages, three pages now run two files each, and a full
local run failed this way in four of five tries. The setup now waits
until the frame found by name is its own.
2026-11-11 01:47:48 +01:00
Marc-André Bombeck 13fd7edfd8 docs(changelog): do not promise a unit change on a (calc) marker 2026-11-11 01:20:25 +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