Commit Graph
3015 Commits
Author SHA1 Message Date
Marc-André Bombeck ccd7ee38c9 fix(shell): let the rail take the left inset and mirror its account band
An iPad in a Safari tab laid the app shell out 10 px in from the left edge
in portrait. The shell padded that inset, so a strip of page colour ran
beside the rail from the logo band to the account row, and the rail's band
borders stopped short of the edge.

The desktop shell no longer pads the sides. The rail runs to the window's
edge and takes the left inset inside its own background, widening by it so
its content keeps its 4rem / 16rem; the logo band, the footer group and the
account band reach back over the inset so their lines meet the edge. On the
right the top bar and main already run to the edge. The phone shell keeps
padding both sides. The side insets are read through --safe-area-left and
--safe-area-right, so a test can set one.

The account band takes the logo band's height (4rem) with the avatar
centred in it, instead of 61 to 69 px of padding with more room below the
avatar than above; the home-indicator inset stays under the band.

tablet-shell.spec.ts sets a 10 px side inset and a 20 px home indicator and
checks that the rail owns the window's left column of pixels, that its band
lines start at x = 0, that nothing in it sits inside the inset, and that the
account band mirrors the logo band. shell-viewport.test.ts holds the routing.
2026-10-24 11:00:56 +02:00
Marc-André Bombeck 3d47c1e6b4 test(guards): hold the whole regenerate route to its two model stages
The browser's abort for a forced regenerate waits three calls at the
response timeout, fewer than the five the background generator may make.
That is correct: the route runs its own pipeline, refuses an unparseable
reply or a screened payload instead of retrying, and never reaches the
background generator, so its longest run is the generation, the
grounding retry and the one reasoning repeat.

The guard only read two of the pipeline's files, so a retry stage added
anywhere else on the route would have lengthened the run past the
browser's abort unnoticed. It now sweeps the route and the whole
briefing-request folder for stages and for any call into the background
generator, and the advisor states why its count is the shorter one.

Refs #1274
2026-10-24 09:29:38 +02:00
Marc-André Bombeck 552510c654 fix(insights): keep polling for the briefing while the warm can land
The briefing read polls while the warm it enqueued runs, and it stopped
after ten polls (225 s) whatever the response timeout. A briefing that
landed later never reached the open page, which kept the empty card, or
the previous attempt's "timed out" hint, on screen until a reload.

The read's poll ceiling now follows the published response timeout over
the generation's sequential calls, never less per call than the
briefing's 180 s default, plus one interval for the queue to pick the
warm up. A settling regenerate and the overview body keep their ten
polls. A test walks the poll schedule against the warm's own bound for
every setting, and the client-abort guard holds the read to the derived
ceiling.

Refs #1274
2026-10-24 09:23:47 +02:00
Marc-André Bombeck 685b46fa04 fix(ssr): keep a server render's query cache from outliving the request
Every server render left its request reachable for ten minutes after the
response was sent. Providers builds its QueryClient once per render, and
the app's defaults give queries a ten-minute gcTime. On the server each
entry the render built, disabled observers included, started a timer of
that length, and a Node timer keeps the async context it was created in.
That context is the request's: the response, the React Flight stream and
the compressed body. A heap snapshot after a run of unauthenticated
probes showed one 600 s query-core timer per request and the retained
Flight chunks, ServerResponses and gzip buffers all hanging from them:
about 150 KB of heap and 90 KB of buffers per not-found render.

TanStack Query already defaults gcTime to Infinity on the server for this
reason; an explicit value from the defaults or a hook overrides it.
AppQueryClient folds Infinity back in at defaultQueryOptions, the merge
point every observer, cache build and hydration passes through, so the
server cache goes with the client at the end of the request. The browser
keeps its configured lifetimes.

The retention test reproduces the chain without a server: a response
object held only by an AsyncLocalStorage store must be collectable after
the render's client is dropped, with a positive control on a plain client
that must keep it alive. A guard keeps client modules from building a
QueryClient themselves and allows only option-less clients elsewhere.
2026-10-24 02:12:31 +02:00
Marc-André Bombeck 2357807706 feat(import): bring the account's own mood vocabulary along
A full backup carries the account's own mood tags, rated factors and
tag groups, and the JSON import left them out: a mood entry imported
into another account landed without every link to a tag the person had
made.

Before the entries are written, every key they or the file name is
resolved once. A catalogue tag or one the account owns is used as it
is; an own tag whose label the file states differently is kept, never
overwritten, and named in keptMoodTagKeys (keptMoodCategoryKeys for a
group). A tag the file defines and the account lacks is created in its
group, the group created the same way, within the account's caps of 50
tags and 12 groups. A key another account of the server holds comes
over under a key derived from the account and the file's key, so a
second import of the same file finds the copy instead of minting
another. The links are written through that resolution, own rated
factors included, inside the entry's transaction; anything unresolved
is still dropped and named. The import card names kept keys in all
nine locales.

The field-coverage guard now holds the backup's customMoodTags and
customMoodTagCategories to the import schema. The round trip moves a
backup to another server (the same seeded catalogue, none of the
account's rows), imports it into a fresh account and compares the
re-export, own tags, group and rated factor included; a second case
imports into another account of the same server, and a third keeps a
renamed own tag.
2026-10-23 22:41:36 +02:00
Marc-André Bombeck 7936ab9a57 feat(admin): say what a queued restore is waiting for
A restore never runs beside a backup pass or another large job: the
heavy lane puts its delivery back in the queue for a minute at a time.
The job's own row says queued for the whole wait, and the console said
the restore starts "as soon as the background worker picks it up",
which read as a stuck worker while a backup ran.

The status routes now ask the queue. A delivery the lane put back sits
in created with a start_after ahead of now, and the active row of a
heavy queue is what holds the lane. The job view carries
waiting: "backup" | "busy" | null, and the console says it waits for
the running backup, or for another large job, and since when it has
been queued, in all nine locales. A queue that cannot be asked answers
no wait and is reported at warn.

The job view and its reads move to backup-restore-view.ts, which keeps
the worker module under its size baseline.
2026-10-23 21:43:55 +02:00
Marc-André Bombeck a840e30fac feat(import): carry every field the app's own files write
The JSON import read six measurement fields and six mood fields while
the full backup and the JSON export carry twice that. A sleep night came
back as stageless rows, a range reading as a single value, an ECG alert
without its verdict, and a mood entry without its note, its level-A
values, its zone, its tag and factor links or its day context.

The import now reads valueMin/valueMax, sleepStage,
rhythmClassification and deviceType on a reading, and note, a1 to a5,
tz, factors, structuredTags and context on a mood entry. Each mood entry
is written with its links and context in one transaction, the way the
bulk mood ingest writes one. A link to a tag the importing account does
not have is dropped and named in droppedTagKeys / droppedFactorKeys, and
the import card says which. The schema moves to
src/lib/import/json/schema.ts so the guards can read it.

json-import-field-coverage.test.ts runs the JSON export and both backup
variants on an account with every column filled in: every field they
write is either read by the import or named with the reason it is not
(row ids, source, deletion records, ciphertext, sync bookkeeping), and a
reason for a field no exporter writes fails as stale. The round trip
imports each file into a fresh account and compares the re-export field
for field. The published contract is held to the runtime schema.
2026-10-23 21:33:06 +02:00
Marc-André Bombeck 43002d37e8 Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-ui 2026-10-23 21:12:51 +02:00
Marc-André Bombeck 879473305c Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-ui
# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-10-23 19:54:08 +02:00
Marc-André Bombeck 803ef73a4d Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-tablet 2026-10-23 19:53:46 +02:00
Marc-André Bombeck fd34ee13b9 fix(charts): round y ticks, calendar x ticks and labels beside the plot
The value chart's axes and labels, as drawn on a 90-day blood pressure
view on a tablet:

- Y ticks were whatever Recharts split the padded domain into, so the
  labels read 57, 83, 109. The domain is now widened to a 1/2/5 step
  and every tick is handed to the axis, evenly spaced and all drawn.
- X ticks were every n-th point, so their dates drifted with the data.
  They now fall on a calendar grid (days, Mondays, two weeks, months,
  quarters, years), each on the point nearest the grid line, and the
  step is the smallest whose labels fit the measured plot width.
- The "your average" label sat inside the plot over the readings, and a
  target zone's label was clipped to its first letter. Both now sit
  beside the plot, in a right margin sized to the longest label.

A browser test draws the axes with Recharts and pins the tick values,
the Monday grid, and both labels outside the plot clear of every dot.
2026-10-23 19:53:28 +02:00
Marc-André Bombeck d5ca594bd7 fix(charts): mark only the chart's own point days in the day rug
On an index or band axis the rug placed every day that holds anything
by interpolating it between the two nearest points. A sparse series
beside a daily one squeezed a week of other days into one gap, and the
dots ran together into bars. A day now gets a dot on those axes only
when it is one of the chart's points; a time axis keeps every day of
its span.
2026-10-23 19:53:28 +02:00
Marc-André Bombeck d2be2b181c fix(shell): read the home-indicator inset through one property
The rail, main, the bottom bar and the Coach button each read
env(safe-area-inset-bottom) on their own. They now read
--safe-area-bottom, which is that env() value, so the four always agree
and a test can set an inset that no desktop browser reports.

The app shell's root also states the content colour itself instead of
leaving it to propagate from the body. A WKWebView browser on iPadOS
(Edge, Chrome, Firefox) can keep the strip over the home indicator out
of the page's layout and fill it with the colour it takes from the root;
that strip now meets main in the same colour.
2026-10-23 19:44:52 +02:00
Marc-André Bombeck f55a86a08c Merge remote-tracking branch 'origin/release/v1.46.0' into feat/v146-server
# Conflicts:
#	src/__tests__/baselines/source-function-lines.json
2026-10-23 19:28:58 +02:00
Marc-André Bombeck d3385478a7 feat(settings): switch Apple Health write-back per measurement token
Each measurement token row carries a switch, the mint form an opt-in, and
the card a notice when readings exist that no token reaches. Copy in all
nine locales. The endpoint catalogue moves to its own file and the
revoked list out of the token card.
2026-10-23 19:28:05 +02:00
Marc-André Bombeck 22976337cb fix(tabs): leave the vertical pan to the page
touch-pan-x on the tab strip refused a vertical pan that began on it, so
a swipe up or down over a tab strip scrolled nothing. The strip already
pans sideways through its own overflow.
2026-10-23 18:23:49 +02:00
Marc-André Bombeck 1f4c1bd2f0 fix(rail): keep every destination reachable under a finger
A touch tablet keeps the rail's 44 px rows, so in landscape three to
seven destinations sit below the fold, and the list has no scrollbar at
rest. The edge fade alone did not read as "swipe here", and a swipe that
reached an end chained to the document, which held the next one.

The list region moves into its own module. Under a coarse pointer it
contains its overscroll, keeps a focused row clear of a new scroll cue,
and shows that cue at an edge with more destinations; a tap scrolls the
list most of a screen on. The cue stays out of the focus order and the
accessibility tree: the keyboard and screen readers already reach every
row. The rail takes the home indicator's inset so the account row stays
clear of it. A mouse sees no change.
2026-10-23 18:23:48 +02:00
Marc-André Bombeck 6ad61cdd1f fix(shell): take the window's edges and keep the document still
The authenticated shell was sized by h-dvh. On iPadOS Safari that value
and the area the browser lays out disagree after a rotation and under the
floating toolbars: a short value left a strip of page colour under the
shell with the last card cut off above it, a tall one let the document
scroll by the difference and take the swipes meant for main.

The shell now takes the window's edges (absolute inset-0 against the
initial containing block), and the document under it gets no scroll and
no rubber band (data-scroll="shell": overflow hidden, overscroll none,
height 100% so the box survives a dialog's scroll lock making the body
relative). Not fixed: a fixed box re-rasterises every page in a layer of
its own and traps the bottom bar's z-index.

The desktop shell's main now clears the home indicator of an installed
app, and the page frame clears the Coach button there with pb-24: the
button sits 5.5rem up, so pb-20 left the last 8 px under it. The
sub-shell grid floor follows the new padding.
2026-10-23 18:23:48 +02:00
Marc-André Bombeck c0d387025d test(guards): strip comments the way the compiler reads them, in every guard
Forty-two guard files carried their own comment stripper, block comments
first in one non-greedy pass, so a /* inside a line comment opened a
block that hid the code after it, and the stricter copies hid code
behind a /* inside a string, which every e2e route glob has. They all
read through the shared stripComments now, and it scans the way the
parser does: strings, template literals and regular expressions keep
every character, a comment inside a template expression goes, and line
numbers are kept. A parity test strips every source file both ways,
with the scanner and with the TypeScript parser, and fails on the first
that differs.

No guard reports anything new. Each one fails when the stripper returns
nothing; three of them passed on an empty file and assert that the code
they ban is read at all now.
2026-10-23 18:09:10 +02:00
Marc-André Bombeck 8a11b7bfc6 fix(charts): measure axis labels at the size they are drawn under reduced motion
The reduced-motion backstop gives every element a near-zero transition,
and the default transition property is all, so the font size Recharts
writes into its measurement span started a transition and the read in
the same task returned the previous size. Labels were sized at whatever
the last one had, or at the inherited 16 px: the last date label moved
in from the edge and value labels that fit were dropped as colliding,
differently from one visit to the next. Nothing in the measurement host
is ever seen, so nothing in it transitions now. A browser test pins the
read under reduced motion and without it.
2026-10-23 17:21:06 +02:00
Marc-André Bombeck a60111cc9d Merge branch 'feat/v146-ui-browser' into feat/v146-ui
# Conflicts:
#	src/__tests__/baselines/test-source-reads.json
#	src/__tests__/baselines/version-stamp-comments.json
2026-10-23 14:20:08 +02:00
Marc-André Bombeck 53bc663cbf test(security): follow a fresh-proof request into the hook that sends it
The backups section now hands its flow to the download, restore and
upload hooks. Each caller entry may name the file that sends the
request; the component still owns the flow and the dialog and must
import that hook and hand it the flow.
2026-10-23 14:04:03 +02:00
Marc-André Bombeck ad9e0d8732 test(day): read a chart and its same-named folder as one
The HealthChart split moved its layers, tooltip and day footer into
health-chart/, so the guard read the composer as a date-axis chart with
no doors. A component and the folder of its name are now one entry, a
tooltip action handed whole to the tooltip counts as wired, and the mount
floor counts the dashboard's one builder mount for its seven charts.
Drop two return-type exports nothing imports.
2026-10-23 14:01:17 +02:00
Marc-André Bombeck 5eafed8dd5 refactor(admin): split the backups section
The section becomes a composer over backups/: the restore dialog with its
preview, the refusal, skip and schedule notices, the restore jobs and
their request, the upload flow, the run and download actions, and the
two row layouts sharing one action pair. The upload total is a pure
function and tested as one. The read-failure guard's 250-character
window had stopped reaching the restore preview's own failure lines; the
preview now sits in a smaller component, the guard sees it, and it is
listed as a deliberate exception with its reason.
2026-10-23 13:20:48 +02:00
Marc-André Bombeck 392b2c47a9 test(mental-health): drive the check-in wizard in a real browser
One question at a time in the source order, Back keeping the answer,
the number keys and arrows, the review with a way back to every answer,
the optional PHQ-9 follow-up, a failed submit, and the note over an
unvalidated translation staying inside the viewport on every step. The
switch to the English original is carried in the answers, and the full
path from the overview posts the instrument, the reader's language and
the item scores. Questionnaire text loads through the production chunk
loader.

The wizard's markup cases are removed from the surface suite; the
loading-frame case stays there, since a browser loads the chunk before
a test can look. The browser project inlines an empty process.env for
Next's client modules.
2026-10-23 13:16:02 +02:00
Marc-André Bombeck c9917d2de0 refactor(settings): split the managed record settings forms
Each family form moves into managed-record/ beside a small kit (the
coercion helpers, the form footer and the switch row); the section keeps
the record-scoped read, the save and the family switch.
2026-10-23 13:13:55 +02:00
Marc-André Bombeck 7c1ed25915 refactor(documents): split the document detail sheet
The sheet becomes a composer over detail/: the original-file preview and
download controls, the detail read with its writes, the AI layer with its
gate and review-first apply, and the field, meta and footer sections.
Guards that read the sheet's source now read the detail folder.
2026-10-23 13:11:20 +02:00
Marc-André Bombeck b161ed3bd2 test(workouts): drive the manual workout form in a real browser
Pick a sport, type a duration and distance, press Save and read the
batch body: miles asked of an imperial reader arrive in metres, a blank
start counts back from now, a retry after a failed save reuses the
form's external id, and an edit re-posts the stored instants and the
fields the form does not show. Field errors, the pending state and the
phone layout are measured in the page.

The markup and edit-render cases are deleted from the old suite, which
keeps the save and delete helper checks as manual-workout-save.test.ts.
2026-10-23 13:09:01 +02:00
Marc-André Bombeck 3ab85f85f9 test(measurements): drive the measurement form in a real browser
Type into the form, press Save and read the request body off the fetch
boundary: blood pressure as one batch, a weight typed in lb stored in
kg, a decimal comma, glucose typed in mmol/L stored in mg/dL with its
meal context, a sleep typed as 7:30 stored as 450 minutes. Native
required validation, the pending state, a refused write's localised
message, the module-off type list, the last-used type and the menu's
Reset are covered too.

The unit-label and glucose-unit markup tests are deleted; their source
pins are replaced by the wire assertions. The module-types file keeps
its pure ownership checks and drops its render.

The browser helper gains a Date-only clock pin, clears localStorage
between tests, carries an envelope's meta, and answers the providers'
locale mirror without counting it as the component's request.
2026-10-23 13:07:29 +02:00
Marc-André Bombeck ac77e52e77 refactor(measurements): hand both row layouts one row context
The table and the card list each took nine props per row and passed
them on to the shared row actions. One MeasurementRowContext carries
them, and rowState derives a row's grouping, selection and drill-down
state in one place for both layouts.
2026-10-23 13:03:39 +02:00
Marc-André Bombeck cc36f46f10 test(filters): drive the URL filter rail in a real browser
A FilterBar over useUrlFilterSync now proves the contract end to end:
a facet picked from the open menu pushes a history entry, typed input
replaces the current one, a pill's clear and Reset leave a bare URL,
two writes in a row both land, Back and an edited address re-seed the
pills, and the open menu rests inside the viewport under its pill.

The markup tests for FilterBar and useUrlFilterSync are deleted; the
popper-placement source pin is replaced by the measured menu position.
The browser project re-bundles its dependencies on every run so a
cache warmed before a new file existed cannot load a second React.
2026-10-23 13:00:19 +02:00
Marc-André Bombeck d7f0d88d59 refactor(checkups): split the checkups section into form, card and postpone sheet
The checkups file held two components of about 620 lines each. Their
parts move to components/measurement-reminders/checkups/:

- reminder-form: the type groups, cadence presets, the form state and
  its mapping to and from a reminder (pure)
- reminder-form-sheet: the create / edit sheet and its cadence fields
- checkup-card: the card in both views, its menu, details, skip and
  snooze lines, next / last lines and action row
- postpone-sheet: the postpone chips and date, the honest skip and its
  confirmation, and the calendar-day offsets

vorsorge-section.tsx keeps the list, the value capture and the visit
filing. Markup and behaviour are unchanged; the guards name the modules
that now hold what they check.
2026-10-23 12:59:49 +02:00
Marc-André Bombeck ca1cceb3a9 test(segmented): drive the control in a real browser
Roving focus, Home/End, wrap, toggle mode, disabled and the chosen
segment's fill are now asserted through key presses and computed styles
instead of markup greps. The static render test is deleted; its two
source sweeps move to segmented-single-implementation.test.ts.

The browser setup sets the default theme class so colour tokens
resolve, and pre-bundles the browser files' dependencies so a cold
cache does not reload the page mid-run.
2026-10-23 12:54:24 +02:00
Marc-André Bombeck 25b5833b11 refactor(settings): split the dashboard layout section into draft, lists and hero settings
DashboardLayoutSection was a 620-line component. Its parts move to
components/settings/dashboard-layout/:

- layout-model: the reorder and hero-kind helpers and the label maps
- use-dashboard-layout-draft: the tokened read, the draft, save and
  reset (with the snapshot and digest refresh) and the draft edits
- hero-settings-fieldset: the top card's switch, lead and highlights
- widget-lists: the sortable web list, the native-only group and rows

The section composes them. Markup and behaviour are unchanged.
2026-10-23 12:52:36 +02:00
Marc-André Bombeck 3d1692204b test: add a browser Vitest project for interactive components
Run *.browser.test.tsx in headless Chromium through the Playwright
provider. The shared helper in tests/browser/ mounts the app's own
providers, stubs only the fetch boundary, and shims next/navigation
onto the page's real location and history.
2026-10-23 12:50:42 +02:00
Marc-André Bombeck 58215f3338 refactor(medications): split the dose-history ledger into its hook, row and dialogs
The ledger file held two components of 404 and 314 lines. It now reads:

- dose-history/use-dose-history-ledger: the window read, summary, day
  groups and the optimistic mark, plus the owner's edits (toggle, pin,
  release, delete) and one reconcile step after every write
- dose-history/ledger-row-item: the row and, apart from it, the owner's
  row menu
- dose-history/ledger-summary-chart and ledger-confirms

dose-history-ledger.tsx keeps the ledger layout. Markup and behaviour
are unchanged; the guards name the modules that now hold what they
check.
2026-10-23 12:41:46 +02:00
Marc-André Bombeck 3eb5aa9364 refactor(documents): split the vault page into filters, library, selection and detail
DocumentsView carried 970 lines of state before its first element. The
state moves to components/documents/vault/:

- selection-history: the `?doc=` history entry helpers (pure, tested)
- use-vault-filters: URL filters, the debounced search and the `/` key
- use-vault-library: usage, the paged list and its processing poll,
  layout, uploads and their intake paths, the corpus-index affordance
- use-vault-filter-choices: conditions, procedures, visits, years
- use-vault-selection and use-vault-bulk: the selection with ranges and
  Escape, the confirmed delete, re-filing, linking and the bulk share
- use-vault-detail: the detail sheet driven by `?doc=`, its close and
  the check that the close took
- vault-page-actions, vault-drop-overlay

The page composes them in about 300 lines; markup and behaviour are
unchanged, and the guards follow the code they watch.
2026-10-23 12:35:46 +02:00
Marc-André Bombeck 3c0063edb2 refactor(measurements): split the measurement list into view state, data, rows and sheet
MeasurementList was a 1,586-line component with fourteen pieces of
state. Its parts move to components/measurements/list/:

- list-model: the row type, page size, labels and formatting helpers
- use-measurement-list-filters: URL-owned filters, the value-range
  draft, page, sort and the page-scoped selection
- use-measurement-list-data: the list read, the soft deletes with undo
  and the selection operations
- use-measurement-edit: the edit draft, its unit conversion both ways,
  the save and the discard guard
- use-measurement-row-display: how a row shows its reading, as a pure
  rule (rowDisplayFor) bound to the account
- measurement-filter-rail, measurement-table (one row component),
  measurement-card-list (one card component), row-actions shared by
  both layouts, measurement-edit-sheet, day-drill-down

measurement-list.tsx lays these out in about 330 lines. Markup and
behaviour are unchanged. The glucose edit checks call the conversion
functions instead of matching source, and the guards that named the
old file name the module that now holds what they check.
2026-10-23 11:42:43 +02:00
Marc-André Bombeck 5075536f7f refactor(charts): split HealthChart into its read, view, layers and header
HealthChart was a 1,929-line component. Its parts move to
components/charts/health-chart/:

- series-math: series names, day keys, moving average, personal
  baseline, annotation and marker placement (pure)
- use-health-chart-series: the fetch window, the batched-slice decision,
  the daily read and the data-ready latch
- use-health-chart-view: the visible slice, bucketing, the average and
  trend lines, the prior period, the visible-range stats and the y domain
- use-health-chart-overlays: value bands, target zones, baselines, the
  trend read-out, the spoken summary and the table columns
- chart-header, band-overlay, chart-tooltip-content
- chart-layers: the reference layers and series as plain functions that
  return the Recharts elements, so the plot's child list is unchanged

health-chart.tsx keeps the component and its path (chart-runtime still
imports it from there) and shrinks to 488 lines. Output is unchanged.
The guards pinned to the file follow the code into the folder, and the
baseline-gating check now calls the layer instead of matching source.
2026-10-23 11:25:16 +02:00
Marc-André Bombeck a812a902dd refactor(dashboard): build tiles and charts from shared card builders
Most strip tiles differ only in their series, label, unit and icon, and
most chart cards share the reveal report, the batched slice, the time
zone and the day doors. One metric-card and one chart builder carry the
shared props, so each registry entry states only what is its own. The
dashboard route's client code shrinks by 0.2 KB gz; rendering is
unchanged. The tile-destination guard now builds every strip tile from
the registry instead of reading ids out of the source.
2026-10-23 11:14:47 +02:00
Marc-André Bombeck 0a88155aec Merge branch 'release/v1.46.0' into feat/v146-ui
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
2026-10-23 11:08:24 +02:00
Marc-André Bombeck e25b6365fd refactor(dashboard): compose the page from a tile registry
The dashboard page held every tile and chart card in one 2,100-line
function: a 1,279-line inline block pushed 24 cards behind 36 show flags.
The cards now live in components/dashboard/tiles/, each with its layout
widget, its own visibility rule (data floor, module, resolved layout) and
its render; the layout toggle and order are applied once by the registry.

- use-dashboard-data: the snapshot or legacy reads, the layout, mood,
  thresholds, the batched chart series and the loading gate
- tile-context: summaries, display units, comparison deltas, freshness,
  directions and target bands, derived once per render
- strip-tiles / chart-tiles: the two registries and their builders
- dashboard-tiles: the strip, the chart row, their skeletons and the
  empty state

The page is a composer of about 170 lines. Rendering is unchanged. The
guards that read page-client.tsx read the whole registry through one
helper; the weight-direction, HRV freshness, BP-in-target comparison and
chart-reveal checks now build the real tiles instead of matching text.
2026-10-23 11:06:32 +02:00
Marc-André Bombeck c1911e71e3 Revert "feat(topics): add the Topics preview area, its navigation entry and its switch"
This reverts commit c68e833dcc.
2026-10-23 10:46:54 +02:00
Marc-André Bombeck 9de64cae3e Merge branch 'release/v1.46.0' into feat/v146-questionnaires
# Conflicts:
#	src/__tests__/baselines/source-file-lines.json
#	src/__tests__/baselines/source-function-lines.json
2026-10-23 10:20:34 +02:00
Marc-André Bombeck e94b7f828c fix(mental-health): keep the unvalidated-translation note on screen with every step
On a phone, arriving on a step scrolled to the question and left the note
and the switch to the English original at the top of the page, out of
view. They now sit at the top of every step's card, above the stem, and
the step scrolls its top block (note, stem, question) into view as one,
so a block already on screen does not move and focusing the question
finds it visible. The review step carries the note the same way.

The phone e2e run asserts the note is in the viewport on the first and a
later step.
2026-10-23 08:26:24 +02:00
Marc-André Bombeck c68e833dcc feat(topics): add the Topics preview area, its navigation entry and its switch
/topics shows a tile per topic the record holds data for, and names the
empty topics and the module-hidden ones once each. /topics/<topic>
shows the topic today, its signals side by side over 7, 30, 90 days or
a year against their usual range, how they move together, their trends,
its blocks, the single-metric pages behind it and what it is missing.
Every topic links into the metric pages, which stay as they are, and
each page ends on a neutral note that this is a preview.

Settings > System > Preview features is listed again and switches the
area on for the person reading. While it is on, the sidebar carries a
Topics entry with a Beta badge directly below Insights and the phone's
More sheet leads with a Topics tile; the bottom bar does not change.
The pages leave for Insights when the switch is off.

The charts load through the shared chart runtime. The sidebar rows move
into their own module and the More sheet tile into its own component,
so neither shell file grows; the lab trend wording is shared between the
marker page and the metabolism topic. Copy in all nine locales.
2026-10-23 08:18:53 +02:00
Marc-André Bombeck 14d0184b63 fix(ui): give the neutral score pill a label colour that passes AA
The neutral tone set muted text on the secondary fill, which measures
3.6:1 in the dark theme. The secondary surface's own foreground reads
the same in light and passes in dark.
2026-10-23 08:18:33 +02:00
Marc-André Bombeck 14273051db feat(doctor-report): mark screener totals answered in an unvalidated translation
The report data counts, per screener score type, how many of the
window's results were answered in a translation that has not been
validated (`screenerUnvalidatedTranslations`, read only when a screener
total is in the report). The PDF marks such a row with an asterisk and
says under the vitals table how many of how many read as orientation;
the clinician share view says the same under the row.

To keep the aggregator and the PDF section from growing, the wellness
summary and the per-context glucose rows move into helpers; the size
ratchets only fall.
2026-10-23 08:14:32 +02:00
Marc-André Bombeck 3843705296 feat(mental-health): keep questionnaire wording in code, one cited file per language
The PHQ-9, GAD-7, WHO-5 and SCI now have text in every app language
instead of falling back to the English original. Each instrument and
language is one file under src/lib/mental-health/wording/ that declares
what it is: a published validated translation reproduced word for word,
with its source and terms in `source`, or a translation written for
HealthLog, faithful to the English and on the same scale.

Validated versions added: the Pfizer PHQ-9 for France, the Netherlands,
Korea and Brazil, the Pfizer GAD-7 for the Netherlands, Korea and
Brazil, the PRU Dutch WHO-5, the Korean WHO-5 validation (J Korean Geriatr Psychiatry 2010) and
the Polish SCI validation (Sci Rep 2026). The SCI in the other
languages and the WHO-5 in Brazilian Portuguese carry HealthLog's own
translation; each file names any validated version found and why it is
not used.

Where the text is unvalidated, the check-in shows a note that the result
is orientation and a one-tap switch to the English original. Each
language loads as its own chunk when a questionnaire surface needs it.

The item text, answers, stems and the PHQ-9 follow-up leave every message
bundle, so a translator never sees them; the i18n guard now requires that
no bundle carries them, and the registry test requires every instrument
in every language with the item count and anchor scores the scoring
expects. CONTRIBUTING explains where the wording lives and how to propose
a validated version.
2026-10-23 08:13:56 +02:00
Marc-André Bombeck d8e4cf6000 fix(url): write page state into the query string through one serialised helper
Next 16.4 adopts a native history write a render later. A second write
inside that gap was undone when the router adopted the first, and a
write computed from useSearchParams started from the URL before the
last one. Clearing the vault's filters could therefore leave
?encounter= behind: the debounced search wrote the filters it had
rendered back over the clear.

@/lib/url-state is now the one way a page writes its own query: each
write starts from the live address bar, at most one waits for the
router at a time, and later updates queue on top of it and go together
once the router has it (or after a short grace). useUrlSearch reads the
same live value and reports each adoption. The filter hook (documents,
measurements, mood), the open document, the day layer, the timeline's
zoom and range, the medication tabs, the AI provider choice, the
Coach's ?c= and the one-shot parameter strips all go through it; the
filter changes merge into the live filters. A guard refuses raw
history writes and router writes built from the rendered params
outside the helper.

The timeline spec now waits for the values menu to hand focus back
before scrolling to the last row; it measured mid-animation and the
focus return scrolled the page back.
2026-10-23 02:04:21 +02:00