Commit Graph
9450 Commits
Author SHA1 Message Date
Marc-André Bombeck d448e6c9c6 refactor(insights): move the medication card onto the pipeline with its own note codec 2026-10-23 11:14:00 +02:00
Marc-André Bombeck 08403a93b4 Merge branch 'feat/v146-server-tools' into feat/v146-server 2026-10-23 11:13:05 +02:00
Marc-André Bombeck 5b49fa6680 refactor(api): read route bodies through parseJsonBody
132 route handlers spelled out safeJson, safeParse and the 422 refusal by
hand in one of six shapes. Each block becomes one parseJsonBody call with
the same size cap, wide-event action, issue count, error code, message and
status it had, so every response is the one it was. The comments inside
the replaced refusal blocks go with them.

A ratchet (route-body-parse-ratchet) counts the route files that still call
safeJson themselves; the count may only fall.
2026-10-23 11:12:39 +02:00
Marc-André Bombeck 07e1e83ee8 feat(api): read and validate a JSON request body in one call
parseJsonBody composes safeJson, the Zod safeParse, the validation-failed wide-event action and the multi-issue 422 envelope, with the variations routes use (size cap, input derivation, issue count, error code, message, status, an onInvalid hook) as options. The refusals are the existing helpers' bytes, which the unit test compares directly.
2026-10-23 11:12:38 +02:00
Marc-André Bombeck 6f3ad9defa refactor(insights): reduce the mood card to its snapshot and its relation board 2026-10-23 11:11:13 +02:00
Marc-André Bombeck 87a35b670d refactor(insights): reduce the blood-pressure card to its snapshot 2026-10-23 11:08:35 +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 6d3cfbba05 refactor(insights): reduce the overview card to its snapshot 2026-10-23 11:06:42 +02:00
Marc-André Bombeck 49b1888009 refactor(insights): reduce the BMI card to its height check, input gate and snapshot 2026-10-23 11:06:41 +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 b1400a8130 refactor(cycle,medications): break the gate and compliance import cycles
The module gate and the cycle gate imported each other for one pure
predicate. isCycleEnabled moves to lib/cycle/enabled.ts and both gates
import it from there.

The band minter read DOSE_WINDOW_DEFAULTS through the analytics/compliance
re-export barrel, which loads the compliance display and with it the
scheduling modules that import the minter. It now imports the constant from
the module that defines it. Runtime import cycles fall from 6 to 4.
2026-10-23 11:05:08 +02:00
Marc-André Bombeck de866107ad refactor(insights): reduce the weight card to its input gate and snapshot 2026-10-23 11:04:15 +02:00
Marc-André Bombeck ea01610a56 refactor(insights): run the status cards through one staged pipeline, starting with pulse 2026-10-23 11:04:14 +02:00
Marc-André Bombeck 78ac2c5aeb refactor(sharing): read and judge grants from a leaf module
lib/sharing/grants.ts held both the grant transitions and the predicates
every reader asks (state, level, scope, the live grant for a pair). The
transitions reach into the managed-profile lifecycle and the OAuth connect
target, and both of those read a live grant, so grants, lifecycle and
connect-target formed an import cycle.

The predicates, findActiveGrant and touchGrantUsage move verbatim to
lib/sharing/grant-access.ts, which imports only the database client and
the scope vocabulary. Importers and the mocks that replaced those
functions point at the new module. Runtime import cycles fall from 7 to 6.
2026-10-23 11:02:10 +02:00
Marc-André Bombeck e84b80ee3c refactor(boot): register the offline-geo notice from its own helper
Keeps register() under the function-size cap; the test pins that register() still calls it.
2026-10-23 11:01:47 +02:00
Marc-André Bombeck 5a8c736017 chore: keep the v1.45 size baselines for the navigation the topics preview had split
Reverting the topics preview puts sidebar-nav, bottom-nav and the
/api/auth/me handler back to their v1.45 code; their baselines return to
the v1.45 values, nothing grows beyond them.
2026-10-23 11:00:45 +02:00
Marc-André Bombeck 0139c22ae3 refactor(geo): move the offline-geo admin notice out of the resolver
auditLog resolves login locations through lib/geo.ts, and the resolver
imported the notification dispatcher for the offline-database notice. The
dispatcher's collaborators write audit rows, so audit, geo, the dispatcher,
channel state, delivery identity, managed-profile lifecycle, the OAuth
connect target and the grants formed one strongly connected cluster.

The notice now lives in lib/notifications/offline-geo-notice.ts. The
resolver keeps deciding when it runs (first public-IP lookup of a process,
again after a runtime database fetch) through a slot the notice module
fills at boot from instrumentation.ts. Runtime import cycles fall from 13
to 7.
2026-10-23 10:57:00 +02:00
Marc-André Bombeck df6e182e0b test(integrations): pin what each provider mapper makes of sanitised wire fixtures 2026-10-23 10:56:03 +02:00
Marc-André Bombeck ee1561496b refactor(auth): move the request guards out of api-handler into lib/auth/guards
api-handler.ts held two modules: the route wrapper and thirteen guards. The
guards now live in lib/auth/guards/, one module per family (the caller,
the acting-account carrier, the record modes, the cookie-only helpers, the
fresh-factor and recent-proof gates, the Bearer elevation, MFA management).
api-handler.ts keeps the wrapper and re-exports every guard, so it stays the
one public entry and no route changes its import.

The bodies move verbatim; only the version prefixes of moved comments are
dropped. The structural guards that read the auth layer now find the module
that declares a guard by name (helpers/auth-guards.ts) instead of a fixed
path, and a new guard holds the entry: nothing outside the folder but
api-handler.ts imports a guard module.
2026-10-23 10:54:23 +02:00
Marc-André Bombeck faeebfc63a refactor(coach): run each Coach tool from a module of its own under tools/run 2026-10-23 10:54:19 +02:00
Marc-André Bombeck 4308682c96 test(insights): pin every status card's prompts, results and stored notes in golden files 2026-10-23 10:52:49 +02:00
Marc-André Bombeck e375a0e0ba refactor(mcp): give every read tool a module of its own under mcp/tools 2026-10-23 10:49:40 +02:00
Marc-André Bombeck 0182f91092 Revert "feat(topics): compute topic overviews and topic pages on the server"
This reverts commit cc04e0944d.
2026-10-23 10:46:55 +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 ce0c2a3f3f Revert "test(e2e): walk the Topics preview and sweep its pages at 390 px and with axe"
This reverts commit 957c2f279e.
2026-10-23 10:46:43 +02:00
Marc-André Bombeck b2ebcb198d test(integrations): pin what every OAuth client sends and makes of each token-endpoint answer 2026-10-23 10:46:42 +02:00
Marc-André Bombeck 5c6a1cbb36 test(jobs): pin every queue, schedule and handler binding the worker registers 2026-10-23 10:44:53 +02:00
Marc-André Bombeck b085bb8771 test(mcp,coach): pin the advertised tool lists and the Coach dispatch table 2026-10-23 10:42:58 +02:00
Marc-André Bombeck 9beaa8f640 build(deps): bump the AWS SDK S3 packages to 3.1153.0 and react-query to 5.104.5 2026-10-23 10:37:41 +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 957c2f279e test(e2e): walk the Topics preview and sweep its pages at 390 px and with axe
A journey on its own account, seeded with 40 days of heart and sleep,
weight, blood pressure, a short run of steps and mood entries, and lab
markers with and without a trend: switched off there is no More tile
and no sidebar entry, the pages leave for Insights and the routes
answer 404; switched on in Settings the More sheet leads with the
Topics tile and the sidebar carries the entry below Insights; every
offered topic renders, with the pair that has 14 paired days stated
and the pairs short of it saying so.

The phone sweep opens every topic page with the preview on, and the
axe matrix scans the overview and two topics in both themes. The
Settings sweep gains the Preview features page.
archive/topics-beta-v1.46
2026-10-23 08:19:01 +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 cc04e0944d feat(topics): compute topic overviews and topic pages on the server
GET /api/topics answers one entry per topic the record holds data for:
a headline value, a rule-based sentence and a 30-day sparkline.
GET /api/topics/{topic} answers a topic's signals with their usual
ranges, side-by-side series for a chosen window, the relationships
between its signals, 90-day trends, what it holds nothing for yet, and
its blocks (training load, lab markers with their trend, screener
trends, the cycle day).

Every figure comes from reads the metric pages already make: the local
daily values, the reconstructed nights, the mood score, computeLabTrend,
the training load and the screener trends. No model is involved. A
relationship says nothing below 14 paired days and is only called a
pattern at p < 0.05; a lab trend needs three readings spanning a year.

The preview is the viewer's own opt-in: with it off both routes answer
404 topics.preview_off. Topics, signals and blocks follow the record's
module switches and the sections the viewer's grant opens; the routes
read across sections, so a scoped grant is refused like the timeline.
/api/auth/me publishes the caller's opt-ins as previewFeatures.
2026-10-23 08:18:43 +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 39092aa936 test(e2e): take the check-in in the reader's language
For German, Dutch, Korean and Brazilian Portuguese, the note and the
switch to the English original appear on an unvalidated translation and
nowhere else, the question carries the language it is written in, and
the result of an unvalidated translation says so. The switch shows the
English original and the result then carries no note, and a Brazilian
PHQ-9 with item 9 above zero leads with the crisis card. Each screen is
scanned with axe, at desktop and phone width.
2026-10-23 08:14:32 +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 02cc0631fa feat(mental-health): record which wording a check-in was answered in
An administration now stores `wording`: `validated` (the published
validated version of the reader's language, the original for English),
`unvalidated_translation` (HealthLog's own translation) or `original_en`
(the English original, chosen by a reader of another language). The
client sends only `englishOriginal`; the server resolves the rest from
the wording registry, so a client cannot claim a validated wording the
registry does not hold. `locale` keeps the reader's language, which
still selects the crisis resources.

Additive migration 0475: a nullable column with no backfill, since which
text earlier rows showed depended on releases since replaced. Every read
returns the field, the OpenAPI contract carries it and `englishOriginal`,
and the disaster-recovery backup carries and restores it (the parity
goldens gain the column; their pinned timestamps move by one step
because the fill writes one more column).

The PHQ-9 item-9 crisis signpost is answered from the answers alone and
is proven in every wording through the real route.
2026-10-23 08:14:15 +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 cf61bd1c85 Merge pull request #1264 from MBombeck/release/v1.45.0
Release v1.45.0
v1.45.0
2026-10-23 03:26:55 +02:00
Marc-André Bombeck 8c43ff7cf9 fix(bench): move the heap snapshot and read the commit without a shell 2026-10-23 02:50:02 +02:00
Marc-André Bombeck 3b2ebc1081 docs(changelog): filters keep the address they were given 2026-10-23 02:06:12 +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
Marc-André Bombeck 333dfe8746 fix(backup): give every export read a total order
Several export reads sorted only by a column two rows can share (a
creation time, a day, a name), and a few to-many reads (medication
schedules, mood tag links, cycle and illness symptom links) had no order
at all. Tied rows then came back in whatever order they sat on disk, so
two exports of one account could differ, and the parity goldens flapped
once the fixture's child rows shared their pinned timestamps.

Each read now ends its order in the model's primary key: the existing
sort key plus `id`, or the remaining key columns of a compound key
(nutrient `source`, hidden tag `moodTagId`, link `symptomId` /
`moodTagId` / `documentId`). Schedules sort by window start, then id.
The restore re-export reads through the same modules, so it inherits
the order.

The parity goldens are unchanged: they already hold the tie-broken
order, so no regeneration was needed and they still pin the output of
the pre-split exporter.

A new integration test runs both purposes, built and streamed, through
a client that records every findMany and every nested to-many read, and
fails on any whose order does not cover the primary key (keys pinned by
an equality in `where`, or by the foreign key to the parent, count as
covered). A second case writes the same tied rows in forward and
reverse physical order and requires byte-identical files.
2026-10-23 00:28:38 +02:00
Marc-André Bombeck b09a6d2df8 test: fail a test whose code caught and logged a missing mock export
A module mock that leaves out an export the code under test reaches throws
at the call, and where that code catches and logs (a best-effort tail, a
fail-soft read) the test went on green while the path it meant to cover
never ran. The Apple Health import tests mocked the rollup module without
its fold window, so the import tail's rollup fold and cache invalidation
never ran in any of them; four route test files mocked the logging context
or the rollups without what the measurement tail reaches.

Both setup files now watch the console and fail the test in which such a
line was logged. The ECG import test keeps the real rollup module and
stubs only the fold, and the four unit mocks gain what they lacked.
2026-10-22 23:15:43 +02:00
Marc-André Bombeck 2844088251 test(integration): seed the large pulse account a month at a time
The heap test for the pulse day-mean rebuild wrote its million readings in
one INSERT ... SELECT, which outran the 60-second statement timeout on CI's
shared Postgres. The fixture is now written a month per statement, each in
its own transaction with the timeout lifted for that transaction only, and
ANALYZE the same way; the code under test keeps the timeout, and the
production-heap assertions are unchanged. With the timeout at 4 seconds the
old seed is cancelled with 57014 and the new one passes.
2026-10-22 22:56:05 +02:00
Marc-André Bombeck 294be53883 fix(admin): keep a sideways-scrolling table from widening the page on a phone
The login overview's IP button gained screen-reader text, which is
absolutely positioned. Inside a scroll container that is not positioned,
its containing block lies outside the container, so the span escaped the
container's clipping and every row panned past the edge widened the page:
the admin page scrolled sideways at 390 px. Every overflow-x-auto wrapper
around a table is now relative (the admin sections, the settings token
tables and the Coach result table), and a guard holds that.
2026-10-22 22:52:48 +02:00
Marc-André Bombeck dda4c57f5a docs(changelog): the maintenance job and today's mood dimensions 2026-10-22 22:05:24 +02:00
Marc-André Bombeck c10446f0d7 Merge branch 'release/v1.45.0' into fix/v145-review-data 2026-10-22 21:48:34 +02:00
Marc-André Bombeck a787cad7f9 chore(deps): take @tanstack/react-query 5.104.4, knip 6.42 and shadcn 4.22.1
The three updates the newest dependency pull requests carry, pinned the way each already was: exact for the query client, a caret floor for the two tools (knip ^6.42.0 resolves to 6.42.1). The dead-code gate is clean under the new knip.
2026-10-22 21:48:27 +02:00