5544 Commits
Author SHA1 Message Date
Marc-André Bombeck a4bea8b0ce Merge pull request #768 from MBombeck/release/v1.37.8
v1.37.8 — the assistant can answer for your visits
v1.37.8
2026-08-10 02:47:30 +02:00
Marc-André Bombeck 4d37ebfc8a chore(release): v1.37.8 — the assistant can answer for your visits 2026-08-10 02:19:59 +02:00
Marc-André Bombeck a38b3bd1df Redact a warning on the way into the wide event
addWarning pushed the raw string while setError, setHttp and
addExternalCall all scrub through redactSecrets. A warning routinely
carries an outbound error string, and for several integrations that
string is the request URL with a credential in it, so a secret could
reach stdout and the log store unredacted. It now scrubs on the way in
like the other entry points, and a test asserts a licence-key-bearing
warning is masked.
2026-08-10 02:18:45 +02:00
Marc-André Bombeck 5933a2dad0 test(links): freeze the link tables to their one gateway
A structural guard asserts that no file outside src/lib/links touches
the five link tables the service owns, with the two backup restore
writers as the only, frozen exemption. The matcher is whitespace
tolerant and proven non-vacuous: it must match every owned table inside
the service, so it can never pass by matching nothing.

This makes provable the property the link module was built for, that the
later absorption of these narrow tables into one join model is a storage
change behind the module rather than a sweep across every call site.
2026-08-10 02:18:45 +02:00
Marc-André Bombeck 448008ac93 feat(coach): give the snapshot a small visits context block
The Coach can now see that a visit is coming and what the last one was
about, without being handed a visit archive. The block is deliberately
small: the appointments booked within the next 14 days plus the single
most recent visit that happened. A full history is a get_visits
question, not a prompt-budget one.

Built as a peer of the illness and labs blocks and registered for
degradation EXPLICITLY against the lowest-priority cluster, so it is
among the first blocks shed when the character cap binds, before any
clinical cluster. Encrypted reason and outcome decrypt fail-soft and
every free-text string routes through the prompt sanitiser first.
2026-08-10 02:18:45 +02:00
Marc-André Bombeck a730ad188b feat(mcp): answer the visit history over the assistant wire
Add get_visits, a bounded read of past visits (default the last 12
months, optional practitioner-name filter) returning date, status,
kind, practitioner, specialty, the visit's own reason and outcome, and
the linked condition labels, newest first. Absence is explicit: an
account that has never recorded a visit reads { present: false }, never
{ present: true, visits: [] }.

Fold upcoming appointments into get_preventive_care as their own named
field rather than minting a second due-list tool, so a question about
what is coming up gets one answer. The Vorsorge arm stays free of
ENCOUNTER-origin reminders; the appointments are read from Encounter
directly. Reason and outcome ride the USER_TEXT wrapping, and every row
is scoped to the caller by construction.
2026-08-10 02:18:45 +02:00
Marc-André Bombeck 79de1181d8 Merge pull request #766 from MBombeck/feat/geolite2-runtime
feat/geolite2-runtime — runtime GeoLite2 fetch (#659)
v1.37.7
2026-08-09 23:22:24 +02:00
Marc-André Bombeck 4757e0588c Allow the POSIX tar the GeoLite2 fetch test uses
The runtime fetch extracts databases in pure Node, but its unit test
builds a MaxMind-shaped tar.gz fixture with the system tar, which the
dead-code gate flagged as an unlisted binary. It is available in CI and
dev, alongside the docker and pg_isready entries already listed.
2026-08-09 22:58:09 +02:00
Marc-André Bombeck 0d496aaf86 chore(release): v1.37.7 — runtime GeoLite2 fetch for self-hosters
A self-hoster on the published image can set MAXMIND_LICENSE_KEY at
runtime and the app fetches the GeoLite2 databases itself, no rebuild or
manual mount. Refs #659.
2026-08-09 22:31:51 +02:00
Marc-André Bombeck 8bad747770 Keep the MaxMind licence key out of the runtime-fetch error path
safeFetch embeds the full target URL in its thrown error, and the
GeoLite2 download URL carries license_key=<secret> in its query string.
The wide-event warning and job-failure paths do not redact it, so a
timeout or network error on the runtime fetch printed the key to stdout,
Loki and GlitchTip in clear. The download timeout is 120s for slow
self-host links, so that path is expected, not rare.

The fetch now re-throws with the edition and the error kind only, never
the URL. As defence in depth, the central query-param redactor also
learns license_key, so any other path that logs such a URL through
redactSecrets is covered too. A regression test asserts the returned
error carries neither the key nor the parameter name.
2026-08-09 22:25:48 +02:00
Marc-André Bombeck 0a2cacb8b3 Document the runtime GeoLite2 fetch path
Cover the new MAXMIND_LICENSE_KEY route in the self-hosting guide as the
first of the three ways to get the offline databases (fetch, mount, bake),
and note that the monthly runtime refresh keeps a keyed host current on its
own. Add the variable to both env examples and to the env-check manifest so
the optional group lists it.

Refs #659
2026-08-09 22:09:12 +02:00
Marc-André Bombeck 869d91c4c2 Fetch the GeoLite2 databases at runtime when a licence key is set
The offline geo tier only reached a self-host two ways: baked into the
image at build time, or mounted from the host. Both need something the
operator of a stock `:latest` pull does not have. Add a third: set
MAXMIND_LICENSE_KEY and the worker downloads GeoLite2-City + GeoLite2-ASN
itself into GEOLITE2_DIR, so the offline tier starts working with no
rebuild and no manual mount.

A worker-only pg-boss queue drives it. Boot discovery fetches once when a
key is set and no database is present yet; a monthly cron (past MaxMind's
first-Tuesday reissue) keeps a keyed host current. The fetch is fail-soft:
each edition extracts to a temp file in the target directory and is
promoted with an atomic rename only after both are in hand, so a bad key
or an upstream error leaves the previous databases exactly as they were
and never writes a partial `.mmdb`. After a successful place the reader
cache resets so a running process picks up the files without a restart.

Extraction is pure Node (zlib + a minimal tar reader), not a shell-out:
this module is reachable from the worker-boot import chain, and a
child_process import there makes Next's file tracer pull the whole project
into the standalone output. zlib keeps the trace bounded and drops the
busybox runtime dependency. The download goes through safeFetch with the
public-host pin and redirect-following, since the MaxMind permalink 302s
to a signed CDN.

The runtime image now owns /opt/geolite2 as the unprivileged app user so
the worker can write it; a read-only bring-your-own mount at that path
still wins, because the fetch cannot write over it and fails soft.

Refs #659
2026-08-09 22:09:02 +02:00
Marc-André Bombeck 9e20b32154 Merge pull request #765 from MBombeck/release/v1.37.6
release/v1.37.6
v1.37.6
2026-08-09 19:05:26 +02:00
Marc-André Bombeck 976c14b223 chore(release): v1.37.6 — the server-side safety pass
Two hardening changes collected on the trunk: a screening reminder can no
longer be marked done through a crafted request, and editing a custom mood
tag, tag group or cycle symptom now leaves an audit entry the way deleting
one already did.
2026-08-09 18:01:26 +02:00
Marc-André Bombeck fe17d06572 Give mood-tag and cycle-symptom edits an audit row
Editing a custom mood tag or group, and editing a custom cycle symptom,
wrote nothing to the audit ledger, while their delete siblings did. Two
of these paths destroy data: a custom mood tag or cycle symptom purged
with ?purge=true cascades its entry links and unpicks historical
entries, so a destruction left no trace of who ran it or when.

Every vocabulary-mutating verb now records an audit row: create, edit,
and delete for both custom tags and custom groups, and edit for a custom
cycle symptom. Encrypted labels are never logged; the row carries only
the field names touched and the resulting active state. The two
display-only writes on this surface, hiding a catalogue tag and
reordering the layout, stay ledger-free by design and say so inline,
since a per-user display preference is not a health-data write and a row
on every drag-to-reorder would be noise rather than a record.
2026-08-09 14:57:12 +02:00
Marc-André Bombeck 311adcfdda Refuse a manual satisfy of a screening reminder server-side
A Vorsorge screening (PHQ-9, GAD-7, WHO-5, SCI) resolves only from the
score row a completed check-in writes, never from a manual done. The web
and iOS clients enforce this by routing a screening to the check-in page
instead of offering the done action, but the satisfy and complete routes
trusted that gate and would stamp any reminder, screening included. A
crafted POST could therefore mark a screening satisfied with no
assessment behind it.

Both routes now reject a screening reminder with 409 before the shared
satisfy primitive runs, mirroring the client rule as defense in depth.
Typed numeric reminders keep their manual satisfy, which is intentional
and covered. The satisfy route gains its first route test alongside the
new guard case on complete.
2026-08-09 14:18:56 +02:00
Marc-André Bombeck 53359d292c Merge pull request #758 from MBombeck/release/v1.37.5
chore(release): v1.37.5 — the app-wide UI and copy polish
v1.37.5
2026-08-09 13:30:55 +02:00
Marc-André Bombeck d711392bea chore(release): v1.37.5 — the app-wide UI and copy polish 2026-08-09 13:15:08 +02:00
Marc-André Bombeck a0200c61a6 Merge pull request #757 from MBombeck/fix/fable-retrofit-headers-spacing
UI: modernize managed-record controls, headers, and spacing
2026-08-09 13:04:25 +02:00
Marc-André Bombeck ca50b92025 docs(changelog): note the managed-record controls and spacing sweep under Unreleased 2026-08-09 11:38:59 +02:00
Marc-André Bombeck 7bb4fe3310 refactor(settings): modernize the managed-record controls onto the primitives
The delegated-caregiver settings forms were the last hand-rolled control
block: native <input>/<select> with a copied border literal, a native
type=date for the birth date (the browser-locale trap closed for the
owner surface in v1.25.4), module checkboxes where the owner section
shows switches, and a save button mid-form with the error line dropped
below it.

Route them through Input, NativeSelect, DateField, and Switch, and give
every form a shared footer that places the error line above the action
row and the action row last (§12). The Switch rows keep a hidden mirror
input so the FormData submit contract is unchanged. Extend the section
tests to hold the DateField primitive and the action-last order in place.
2026-08-09 11:38:17 +02:00
Marc-André Bombeck b24a6eac2c feat(lint): extend spacing-scale to the full off-scale step family on shells
The shell check caught only the bare `5` step on p/px/py/space-y/gap, so
off-scale directional and half-step spacing (`ml-5`, `pl-5`, `gap-3.5`,
`py-2.5`, the `7` step) slipped through on hand-rolled card shells. Widen
it to the whole spacing prefix family (padding, margin, gap, space) and
every banned step — `5`, `7`, and half-steps other than the sanctioned
`1.5`. Height stays out of scope: it is a dimension, not a scale step.

Verified the rule bites by injecting `gap-3.5` onto a real bg-card+border
shell and watching lint flag it, then reverting; the sweep this rule
guards left the tree clean apart from one tooltip shell, now on-scale.
2026-08-09 11:27:29 +02:00
Marc-André Bombeck a959db8679 refactor(ui): route settings shells, actions, and empty states through primitives
Bring the remaining settings and dashboard surfaces onto the shared
anatomy: the API and MCP revoke buttons become the destructive variant
instead of an outline hand-tinted red; the runtime-actions row places
its raw-data setting and status lines above the actions, not under them;
the account section paints its card and header while loading instead of a
lone spinner; the share revoke becomes a right-aligned row action rather
than a full-width alarm strip per link; the recent-workouts tile and the
MCP and notifications empty states compose Card and EmptyState instead of
hand-rolled bg-card shells.
2026-08-09 11:27:18 +02:00
Marc-André Bombeck 7f6eae8425 style(ui): pull off-scale spacing and micro-labels onto the scale
Sweep the spacing rests that sat off the 1/1.5/2/3/4/6/8/10 scale: the
health-score panels and their skeleton, the recommendation ask-Coach
gutter, the onboarding tour footer, the vaccination row, the danger-zone
alert margins, the recovery-codes action wrapper, and the chart-tooltip
tile. Move the last hand-written micro-sizes (coach counters, the
personal-record badge, the score-ring label, the dose-history and
cycle-calendar headers, the mood tag chips) onto the text-2xs token, and
give the documents upload buttons the mobile tap floor their peers set.
2026-08-09 11:27:05 +02:00
Marc-André Bombeck b6332a96d9 refactor(ui): route hand-rolled card headers through the shared header primitives
Move the last hand-rolled tile and section headers onto TileHeader and
SectionHeading so every card speaks one header language: the vorsorge
dashboard tile, the three illness cards, the metric-correlation section,
and the custom-metric list group. The mood-chart title drops to the
primitive semibold weight and its bucket label becomes a TagChip.

Align the in-card sub-headings of the integration cards to the one
sanctioned section-label tier (muted uppercase eyebrow) instead of a
second text-sm font-semibold dialect.

Keep the onboarding and mental-health wizard hero H1s at their
deliberate semibold weight, now marked inline as an intentional
exception so a future heading sweep leaves them alone.
2026-08-09 11:03:43 +02:00
Marc-André Bombeck 844ec396d1 Merge pull request #756 from MBombeck/fix/fable-copy-and-docs
Copy: tighten the metric explainers and settle terminology
2026-08-09 10:54:36 +02:00
Marc-André Bombeck 7aa1fa0174 docs(changelog): note the copy tightenings under Unreleased 2026-08-09 10:32:11 +02:00
Marc-André Bombeck f8197912bb i18n: settle measurement, practitioner, and visit-toast wording
- the single- and bulk-delete flows on the measurement list both say
  Messwert now, instead of switching to Messungen for the bulk case
- the practitioner field on the visit and vaccination forms reads
  Praxis oder Arzt on both, dropping the slash form and the lone feminine
  variant so it matches the practitioner list heading
- the visit saved/updated toasts lose their trailing period to match the
  vaccination toasts they ship beside
2026-08-09 10:31:24 +02:00
Marc-André Bombeck 6ddea30590 i18n: tighten measurements subtitle, mood prognosis, and briefing empty state
- measurements subtitle drops to a single sentence and loses the phrase
  that named Insights twice
- the mood self-rating card takes a short title so it stops wrapping three
  lines and stops sharing the word Einschaetzung with the AI assessment
  beside it; its noPattern and expectation lines move to direct address
- the daily-briefing empty state drops the worked example, leaving the
  setup instruction on its own
2026-08-09 10:28:21 +02:00
Marc-André Bombeck cc36cc501a i18n(insights): trim the six metric explainer bodies to a definition line
The sub-page explainers had grown to four sentences of guideline detail that
pushed the stat strip and chart below the fold; the contract is a one- to
two-sentence definition, with the long form living behind the knowledge
disclosure. Each body now states what the metric is and why it matters.
Also aligns the readiness explainer to the score title across locales
(Bereitschaft / Disposición / Disponibilité), retiring the stray Tagesform.
2026-08-09 10:25:44 +02:00
Marc-André Bombeck 27826ac825 docs: correct stale UI comments on error handling and colour tokens
- mood-insights-sections: the docblock claimed the sections render nothing
  on a failed read; the rest region now surfaces a QueryErrorRow with retry
- band-tokens / coverage-meter: the raw dracula primitives now carry their
  own AA-tuned light overrides, so the no-override claim no longer holds;
  the band still rides the semantic token for concept cohesion
- medication-card-body: drop the pb-2.5 header claim the header no longer sets
- modules-section: the body follows the card gap rhythm, not an mt-4 margin
2026-08-09 10:18:33 +02:00
Marc-André Bombeck 500ed8a7cd Merge pull request #755 from MBombeck/release/v1.37.4
chore(release): v1.37.4 — honest settings copy and doctor-visit polish
v1.37.4
2026-08-09 09:43:40 +02:00
Marc-André Bombeck 3a03732578 chore(release): v1.37.4 — honest settings copy and doctor-visit polish 2026-08-09 09:27:27 +02:00
Marc-André Bombeck dbe2aba7eb Merge pull request #754 from MBombeck/feat/arztbesuch-water-polish
Doctor-visit, link-picker, address-book and water-entry polish
2026-08-09 08:41:37 +02:00
Marc-André Bombeck 089c74cf23 feat(links): searchable, grouped entity link picker for visits and shots
The visit form's document / lab / condition link blocks and the
vaccination form's document link were flat lists capped at 25 rows with
no search: 30 lab values arrived undifferentiated and anything past the
cap was unreachable. Replace them with one shared picker — an inline
summary of removable chips over an add button, and a sheet that searches,
groups and multi-selects. Lab results gather under their panel and sample
date with a select-all per group, documents gather by month, illnesses
stay flat. The picker raises only the fetch limit; the server link cap
stays authoritative.
2026-08-09 07:01:15 +02:00
Marc-André Bombeck 40e53b14d6 feat(practitioners): search and group the address book by specialty
Search now covers the specialty column alongside name and practice, so a
field name like "Zahnmedizin" finds the practice even when the name and
practice hold nothing of the sort. The same OR reaches the visit form's
practitioner picker, which now shows the specialty beside the practice so
a specialty hit is legible. The list groups its rows under their
specialty, sorted alphabetically with the unspecified rows gathered into
one group at the end; when nothing carries a specialty the headings stay
suppressed and the list reads flat as before.
2026-08-09 06:53:57 +02:00
Marc-André Bombeck 9aefce1625 fix(checkups): offer the record-a-visit action only for real visits
The file-a-visit affordance painted on every checkup reminder, including
the self-measurement ones (weigh-in, blood pressure, a screening
check-in) where there is no appointment and 'you went' is never a
reading. Gate both the card and the dense-list branch on the same
free-text predicate the mark-done action already uses: a reminder with a
measurement type is satisfied in the app, a free-text reminder stands for
a practice visit. The server states the same boundary in resolve.ts.
2026-08-09 06:48:46 +02:00
Marc-André Bombeck 4335b38e02 feat(nutrients): remove in-app water logging, keep the sync path
Manual water entry is gone from the app: the water item on the capture
picker, the dashboard quick-add menu and the hydration card's add button
are removed, and the manual quick-add sheet is deleted. Water that syncs
in through Apple Health still arrives and still shows on the hydration
card with its daily total, history and reference line.

The POST /api/nutrients/water endpoint and the batch sync path stay
untouched. The nutrients page's module-enable error, which had borrowed
a water-quick-add string, gets its own key across all six locales.
Input-only water keys (nav.capture.water*, nutrients.hydration.quickAdd*)
are removed.
2026-08-09 06:46:28 +02:00
Marc-André Bombeck 18e9a80ea9 Merge pull request #753 from MBombeck/fix/honest-errors-and-security-copy
Honest read-failure states and MCP scope-note correction
2026-08-09 06:30:12 +02:00
Marc-André Bombeck 9ae42ae33f chore(bundle): raise the aggregate cap for the honest-error-state code
Adding recoverable read-failure handling to thirteen surfaces and the MCP and
managed-record copy rewrites across six locales lifts the whole-build client
total to 3404 KB gz. Every per-route ceiling is still met; the aggregate cap
moves 3352 to 3428 with the same margin.
2026-08-09 06:14:43 +02:00
Marc-André Bombeck 73b82f444e test: use render thunks in the read-error family suites for the jsx-key rule 2026-08-09 04:37:03 +02:00
Marc-André Bombeck edaa5bf424 style: wrap a long assertion in the api-section error test 2026-08-09 04:34:18 +02:00
Marc-André Bombeck c8500dd869 docs(changelog): record the honest-error and MCP scope-note fixes under Unreleased 2026-08-09 04:33:35 +02:00
Marc-André Bombeck ba42354733 fix(admin): replace hand-rolled read-error boxes with the shared primitive
Eight admin surfaces rendered a failed read as their own destructive-
tinted box, and most offered no way to recover. Sweep them onto the shared
QueryErrorRow with retry so the console uses one error form (§13). The
statically renderable sections were watched fail first against the
hand-rolled boxes.
2026-08-09 04:32:54 +02:00
Marc-André Bombeck f6a0ee4623 fix(checkups): use the shared query-error primitive on the Vorsorge list
The section carried a bespoke muted error row with its own retry button —
the exact drift QueryErrorRow was made to absorb. Swap it for the shared
primitive so the failure reads as foreground content. Test watched the
bespoke row first.
2026-08-09 04:28:18 +02:00
Marc-André Bombeck 44582008e9 fix(settings): make settings read failures foreground and recoverable
A family of settings surfaces rendered a failed read as a bare
text-destructive line with no way to recover: the security sessions,
trusted-devices and activity cards, the coach memory and reminders
sections, the about-me section, and the document link/share pickers. Swap
each to the shared QueryErrorRow primitive with a retry control. The suite
covering the statically renderable members was watched fail first against
the bare lines; the two portaled pickers take the same swap.
2026-08-09 04:26:46 +02:00
Marc-André Bombeck f63fdb373e fix(cycle): use the shared query-error primitives on failed cycle reads
The cycle tabs rendered a bespoke error row in muted text, wrapped in a
bare card. Replace it with the shared primitives: QueryErrorCard on the
standalone prediction/insights/settings tabs, and a borderless
QueryErrorRow inside the calendar card so the failure reads as foreground
content with retry rather than muted meta. Test watched the muted row
first.
2026-08-09 04:20:55 +02:00
Marc-André Bombeck 6485aae787 fix(insights): surface mood-insights read failures instead of silent degradation
The mood-insights regions returned nothing on a failed read by documented
intent, degrading to the line chart. Enforce the honest-error standard
here too: on error the main region paints a QueryErrorRow with retry, once,
mirroring how the loading skeleton is scoped to that one region so the
three regions do not stack identical rows. Test watched the silent empty
first.
2026-08-09 04:15:12 +02:00
Marc-André Bombeck 1229d54d30 fix(insights): recoverable read failures on the ECG detail and list page
The ECG detail rendered a read failure as a muted, non-recoverable line;
the routed list page had no error branch at all, leaving the shell
heading over an empty body that read the same as loading. The detail now
paints a QueryErrorCard with retry (an alert is content, not muted meta),
and the page shows the same instead of an empty shell. The overview teaser
still unmounts silently, which is right for the overview. Tests watched
each fail first.
2026-08-09 04:13:46 +02:00
Marc-André Bombeck 22d5c01a68 fix(settings): surface AI settings-read failures instead of a default form
The three AI configuration reads each returned null on a non-OK response,
so a failed read painted the default provider form as if the account were
configured that way. Throw on a non-OK response so the query reports its
error, and render a QueryErrorCard with retry when any read fails. Test
watched the misleading form render first, then pass on the honest error.
2026-08-09 04:11:08 +02:00