Commit Graph
9614 Commits
Author SHA1 Message Date
Marc-André Bombeck 3547c4bd2f test: pin the intake history's success badge at the one it now renders
The taken badge moved into one shared component, so the file carries the
token once.
2026-11-03 22:08:05 +01:00
Marc-André Bombeck d1ab195b43 Merge pull request #1330 from FloMaetschke/feature/injection-site-in-intake-history 2026-11-03 22:06:35 +01:00
Marc-André Bombeck a503c8855d Merge branch 'feat/v147-wolfi' into release/v1.47.0 2026-11-03 21:56:23 +01:00
Marc-André Bombeck 5d36afb487 build: run the image on a Wolfi runner
The dependency and build stages stay on node:22-trixie-slim, where pnpm
picks the glibc native packages. The runner moves to wolfi-base, pinned
by its index digest: glibc, about fifty packages, and every one of them
known to the image scan, Node and the PostgreSQL client included.

nodejs-22, tzdata and postgresql-18-client come from apk, so the pgclient
stage and its pin guard go. The builder writes its Node module ABI and
glibc version out and the runner refuses to build on another ABI or an
older glibc. Wolfi's npm is major 12 and runs no install scripts unless
package.json allows them, so the Prisma CLI install names the three
packages that have one, and the build checks that the schema engine file
is there before asking the CLI about it. npm is installed for that step
and removed in the same layer; the check that nothing is left asks a
fresh shell, whose command cache cannot remember the deleted binary.
2026-11-03 21:55:30 +01:00
Marc-André Bombeck 899a054e64 Merge branch 'feat/v147-mcp-reading-id' into release/v1.47.0 2026-11-03 21:33:50 +01:00
Marc-André Bombeck 66709222b2 docs(api): describe the lab reading id next to the analyte id 2026-11-03 21:33:18 +01:00
Marc-André Bombeck 52e6bcd480 test(mcp): prove a report's lab link lands on that report's reading
Two draws of one analyte a year apart and two reports on the same day, against
Postgres: each report lists its own reading id, and fetching it returns that
reading with that report in its links, not the latest one.
2026-11-03 21:33:17 +01:00
Marc-André Bombeck ba27e728cf feat(mcp): fetch one lab reading by its own id
A link to a lab result named its analyte, and fetch lab:<analyte> answers
with the latest reading of that marker. Following a link from a report that
holds an earlier draw therefore landed on a later reading, whose links named
a different report and visit.

fetch labresult:<id> now returns that one reading (value or text result,
unit, reference range, status, date) with its own links, behind the labs
module and narrowed to the caller's live rows. Every link to a lab result
uses this id, each reading in get_labs carries it, and fetch lab:<analyte>
names the reading it quotes in metadata.readingId. The analyte view and
search are unchanged.

Refs #1323
2026-11-03 21:33:16 +01:00
Marc-André Bombeck 691c2e7301 refactor(labs): shape one reading from one shared select
The labs block and a single-reading read now build a reading from the same
select and the same shaping, so one reading fetched by its id renders exactly
like the latest reading of a marker.
2026-11-03 21:33:14 +01:00
Marc-André Bombeck cd344125d5 fix(labs): let a text-only marker take the unit of its first number
A marker minted from a text result ("SEE NOTE") has an empty unit, and a
marker with a reading has its unit locked, so the first number to arrive,
a unitless ratio included, was refused against a unit nothing had chosen.
While every reading is text the unit has nothing to contradict, so the
first number sets it. A marker that already holds a number keeps its unit.

Refs #1313
2026-11-03 21:19:28 +01:00
Marc-André Bombeck 377e0d0d3a build: copy pg_dump and pg_restore from the PostgreSQL image
postgresql-client-18 from the PGDG repository brought Perl and Debian's
client wrapper scripts into the runner for two binaries the app calls by
name. A pgclient stage on postgres:18.6-trixie, pinned by digest, now
hands over pg_dump, pg_restore and the libpq they link; the Debian
libraries libpq needs (Kerberos, LDAP, SASL) still come from apt so the
upgrade and the image scan see them. The runner registers libpq with the
loader and fails the build when either binary has an unresolved library;
the major 18 version checks stay.

The client minor follows the bundled database's: a guard holds the two
pins together, and the Dockerfile's Dependabot watch now ignores a
PostgreSQL major the same way the compose watch does.
2026-11-03 21:18:44 +01:00
Marc-André Bombeck cf7905dc92 refactor(medications): hold recurrence.ts at its size baseline 2026-11-03 20:58:46 +01:00
Marc-André Bombeck 270c6ca619 refactor(medications): keep the rrule start alignment in its own module
recurrence.ts sits on the source-size ratchet; the helper and its reasoning
read on their own.
2026-11-03 20:57:46 +01:00
Marc-André Bombeck d8743e9111 chore: drop the trivyignore entries for npm's bundled packages
Both covered copies inside the npm CLI of the Node base image. The
runner removes npm, so neither CVE matches anything in the image; a scan
of the built image with an empty ignore file reports neither.
2026-11-03 20:53:29 +01:00
Marc-André Bombeck 3d0ae05192 docs: say the published image renders documents on a Raspberry Pi 4
The image runs the glibc canvas build now, which needs no ARMv8.2
features, so the Pi 4 gets thumbnails and scanned-PDF rendering back
without a code change: the gate only ever closed for musl on arm64. The
gate's comment, the env example and the getting-started prerequisites
say so, and keep NATIVE_CANVAS as the override. The x64 AVX2 gate stays:
the glibc x64 build rendered a PDF page under an emulated core without
AVX2, but it still carries AVX2 code and that one run does not cover
every path.
2026-11-03 20:53:28 +01:00
Marc-André Bombeck de7858100f build: run the image on Debian trixie instead of Alpine
Every stage now starts from node:22-trixie-slim, pinned by its multi-arch
index digest, so the native modules load their glibc builds. The arm64
musl Skia build needs ARMv8.2 (LSE atomics, RCpc loads) and dies with
SIGILL on an ARMv8.0 core such as the Raspberry Pi 4's Cortex-A72; the
glibc build runs there (#1311).

The runner keeps what it had: tzdata, TZ, the build-time OS upgrade, the
nextjs user (uid 1001, now in group nodejs gid 1001), the backup and
GeoLite2 directories and every build-time assertion. pg_dump and
pg_restore of major 18 come from the PGDG apt repository, whose signing
key is fetched by node and checked against a pinned SHA-256 before apt
trusts it; PATH names the major 18 directory so the binary does not
depend on Debian's version-picking wrapper. The canvas hoist takes the
-gnu binary package by the pinned version and still fails the build on a
mismatch. bookworm-slim was the other candidate; on the day of the
switch it carried a CRITICAL zlib finding that trixie-slim does not.
2026-11-03 20:53:20 +01:00
Marc-André Bombeck 65e35761ec build: probe container health with Node's fetch instead of wget
The probe now needs nothing but the node binary the image already runs,
which frees the base image from carrying wget. The image HEALTHCHECK and
the compose healthcheck use the same one-liner: a non-2xx answer, a
refused connection or the 4 s abort exit 1, inside Docker's 5 s timeout.
The boot-storm guard reads the abort from the fetch and refuses a probe
that names wget or curl.
2026-11-03 20:53:20 +01:00
Marc-André Bombeck 775933b7d5 Merge branch 'feat/v147-dbroles' into release/v1.47.0 2026-11-03 20:52:17 +01:00
Marc-André Bombeck 3d1a6224ba test: drop vi imports the removed schema-mirror mocks left unused 2026-11-03 20:49:31 +01:00
Marc-André Bombeck df1e19f1c2 docs: describe the app's database role, its limits and how to opt out 2026-11-03 20:38:46 +01:00
Marc-André Bombeck c7ff22ab00 chore(env): whitelist DB_APP_ROLE and document it beside the database settings 2026-11-03 20:37:48 +01:00
Marc-André Bombeck 485161a657 feat(db): run the app as a role that cannot change the schema
After the migrations the entrypoint ensures healthlog_app, a login role
with row access to the app's schema, MAINTAIN on measurements and
ownership of the pgboss schema, and starts the app with a DATABASE_URL
for it in place of the owner's. The role cannot alter or drop tables,
create roles, schemas or functions in the app's schema, read server
files or run programs, or write the migration history.

The password is an HMAC of a fixed label under API_TOKEN_HMAC_KEY, so a
separate worker container derives the same one, and it is sent as a
SCRAM verifier. Every boot re-applies the grants in one transaction
under an advisory lock, and drops as the owner the indexes the
maintenance job may no longer drop. When the owner cannot create roles,
the server is older than Postgres 17, or the role cannot log in, the
boot says why and the app stays on the owner as before. DB_APP_ROLE=off
skips it.
2026-11-03 20:36:37 +01:00
Marc-André Bombeck 95525a0259 fix(jobs): let pg-boss create its schema only when it is missing
Postgres checks the database's CREATE privilege before it checks
whether the schema exists, so CREATE SCHEMA IF NOT EXISTS fails for a
role that may not create schemas even when the schema is there. The
worker now asks first and passes createSchema: false when it exists.
2026-11-03 20:32:53 +01:00
Marc-André Bombeck f05a112c61 feat(jobs): leave index drops to the owner when maintenance runs without it
DROP INDEX belongs to the table's owner; the MAINTAIN privilege covers
VACUUM and REINDEX but not the drops of retired indexes and invalid
rebuild copies. A run that does not own measurements now skips those
drops and names what is left, for the boot to drop as the owner. The
retired-index list moves to a plain module the boot script can read.
2026-11-03 20:32:53 +01:00
Marc-André Bombeck b6da360856 refactor(db): drop the runtime schema mirror
ensureDbCompatibility ran ALTER TABLE / CREATE TABLE IF NOT EXISTS on the
login, register, token and session paths, mirroring columns every
migration under prisma/migrations already creates. The entrypoint applies
those migrations before the server listens and stops the boot when one
fails, so the mirror never had work to do, and DDL on a request path
needs the table's owner: a connection that may only read and write rows
fails every login on it. Drop the module, its call sites and the mocks
that stubbed it out.
2026-11-03 20:32:35 +01:00
Marc-André Bombeck 381726f471 perf(medications): start the rrule walk just before the window
between() iterates from DTSTART, so every compliance read walked a daily
schedule from the day the medication was created: about 4 ms per rate,
growing with the medication's age, and enough under a loaded test run to
push a compliance property past its timeout. DAILY and WEEKLY rules repeat
with a fixed period, so the start moves forward by whole periods to just
before the window. A property test holds the occurrences in the window
equal to the walk from the real anchor.
2026-11-03 20:23:31 +01:00
Florian Maetschke 8643a1ab8e Note the injection-site chip in the changelog 2026-11-03 12:03:33 +01:00
Florian Maetschke a3516160c0 Share the taken/skipped chip between the two history layouts
The desktop table and the mobile cards carried the same status badge
twice. Pulling it into one component makes room for the injection-site
chip without growing the list function past its size baseline, and
the baseline tightens to the new, lower count.
2026-11-03 11:49:41 +01:00
Florian Maetschke 5aac158b12 Show the injection site in the intake history
An injected dose already stores where it went, but the per-medication
history only showed when it was taken and where the entry came from.
The list now renders the site as a chip next to the status, on the
desktop table and the mobile cards, so the rotation can be read back
from the history. Rows without a site are unchanged.
2026-11-03 11:46:07 +01:00
dependabot[bot] 540f4ff84f build(deps): bump the production-weekly group across 1 directory with 3 updates (#1329)
Bumps the production-weekly group with 3 updates in the / directory: [@aws-sdk/client-s3](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/clients/client-s3), [@aws-sdk/lib-storage](https://github.com/aws/aws-sdk-js-v3/tree/HEAD/lib/lib-storage) and [@napi-rs/canvas](https://github.com/Brooooooklyn/canvas).


Updates `@aws-sdk/client-s3` from 3.1159.0 to 3.1161.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/clients/client-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1161.0/clients/client-s3)

Updates `@aws-sdk/lib-storage` from 3.1159.0 to 3.1161.0
- [Release notes](https://github.com/aws/aws-sdk-js-v3/releases)
- [Changelog](https://github.com/aws/aws-sdk-js-v3/blob/main/lib/lib-storage/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-js-v3/commits/v3.1161.0/lib/lib-storage)

Updates `@napi-rs/canvas` from 1.0.13 to 1.1.0
- [Release notes](https://github.com/Brooooooklyn/canvas/releases)
- [Changelog](https://github.com/Brooooooklyn/canvas/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Brooooooklyn/canvas/compare/v1.0.13...v1.1.0)

---
updated-dependencies:
- dependency-name: "@aws-sdk/client-s3"
  dependency-version: 3.1161.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-weekly
- dependency-name: "@aws-sdk/lib-storage"
  dependency-version: 3.1161.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-weekly
- dependency-name: "@napi-rs/canvas"
  dependency-version: 1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-weekly
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-11-02 21:12:37 +00:00
Marc-André Bombeck 2c8140a502 Merge pull request #1328 from MBombeck/release/v1.46.6
release: v1.46.6
v1.46.6
2026-11-02 21:39:10 +01:00
Marc-André Bombeck 1bde3fc618 chore: drop exports nothing imports
knip 6.44 follows the members a dynamic import reads, and the Insights
page reads two of the derived barrel's: the barrel now exports those two,
whose building blocks are imported from their own modules. BAND_VAR had
no reader, and the drawer re-exported useResettableValue for a test that
only imports nextResettableValue. The size and version-stamp baselines
take the lower numbers.
2026-11-02 21:22:16 +01:00
Marc-André Bombeck 94da0bc352 test: tighten the version-stamp baseline for the review row 2026-11-02 20:58:15 +01:00
Marc-André Bombeck 165120e845 chore(release): v1.46.6 2026-11-02 20:52:22 +01:00
Marc-André Bombeck 49a0eb92ff fix(labs): word a value without a unit the same on every remaining surface
The briefing's lab input passed the stored "1" to the model, the unit
mismatch refusal read "is in 1", and the trend card left a gap where a
language puts the unit before a particle. get_labs says that a value
without a unit has an empty unit, and a lab link id is scrubbed like its
label. The review row is back at its baseline length.
2026-11-02 20:52:18 +01:00
Marc-André Bombeck 6a8ac98613 fix(labs): let a unit typed key by key pass through "1"
The no-unit state was read from the value, so the first keystroke of
10^9/L or 1/min ticked the box and locked the field. Only the box makes
the choice now; a typed 1 stays text.
2026-11-02 20:52:17 +01:00
Marc-André Bombeck 70cc753c49 Merge branch 'fix/pdf-unicode-font' into release/v1.46.6 2026-11-02 20:38:27 +01:00
Marc-André Bombeck ee0adae99c test(doctor-report): read the font guard's files through readSource 2026-11-02 20:36:16 +01:00
Marc-André Bombeck 0115a02d22 fix(doctor-report): draw the PDF in an embedded Unicode font
Helvetica's WinAnsi encoding turned every glyph outside Latin-1 into an
empty box: a Polish report lost its diacritics, a Korean report was
unreadable, and free text in any other script broke in every locale.

The renderer now registers the embedded ReportSans family on each
document, names it in every setFont, and makes it the autoTable default.
The Hangul variant is read only when the report is Korean or its data
contains Hangul. Font files are cached per process; streams are
compressed so the subset fonts add about 30 KB to a report.

The arrows, superscripts and comparison signs now print as themselves,
so the WinAnsi sanitiser and its ASCII fallbacks are gone. Tests render
Polish, Russian and Korean strings and arrows and read them back out of
the text layer.
2026-11-02 20:32:17 +01:00
Marc-André Bombeck 80a62afbbf build(pdf): add Unicode fonts for the doctor report
The report's built-in Helvetica is WinAnsi-only. These fonts cover what
the report can carry: ReportSans (Noto Sans plus the arrows and
mathematical operators of Noto Sans Math; Latin incl. Extended-A/B,
Greek, Cyrillic) in regular, bold and italic, and ReportSansKR, the same
plus Hangul from Noto Sans KR, in regular and bold.

scripts/build-pdf-fonts.py rebuilds them from the upstream sources. All
are SIL OFL 1.1; the licence sits beside the files. The directory is
named in outputFileTracingIncludes because the renderer reads the files
by a path the tracer cannot follow.
2026-11-02 20:32:11 +01:00
Marc-André Bombeck a822aa5a0e fix(documents): send a read that states no unit itself to review
A unit of "1" is the explicit no-unit choice a person makes. When a
document read returns it on its own, the value is staged for review
instead of passing straight through, so a unit the read dropped never
lands as a dimensionless value.
2026-11-02 20:31:33 +01:00
Marc-André Bombeck 2c6fb82326 Merge branch 'feat/unitless' into release/v1.46.6 2026-11-02 20:30:41 +01:00
Marc-André Bombeck e605fe0ae8 docs(api): describe the dimensionless lab unit on every lab unit field
The unit on lab results, biomarkers, the scanned-report commit and the
document fact edit now states that "1" means the value has no unit, that
it matches only itself, and that an empty unit stays refused. The lab CSV
import description names the 1 in the unit column.

Refs #1313
2026-11-02 20:28:35 +01:00
Marc-André Bombeck f9431da64f feat(labs): show a value without a unit as the number alone
Every place a lab unit is printed beside a value now goes through one
helper, so a reading stored in "1" reads "3.6" and not "3.6 1": the
list, history and detail, the chart and its tooltips, the reference bar
and range notes, the trend card, the timeline, the day view, the
clinician page, the doctor-report PDF and the changes card. Where the
unit stands alone (a marker's subtitle, a picker row, the merge preview)
it reads "No unit".

The Coach's lab block, the get_labs history and the visit-prep summary
hand the model an empty unit for such a value, so it never quotes the
"1" either.

Refs #1313
2026-11-02 20:28:34 +01:00
Marc-André Bombeck 5405ce053d feat(fhir): code a lab value without a unit as the UCUM unity
A value stored with unit "1" carries system and code on its quantity
even when the analyte map knows no coding for it: the unity is UCUM by
definition.

Refs #1313
2026-11-02 20:28:34 +01:00
Marc-André Bombeck 79ac5670c7 feat(labs): offer an explicit no-unit choice for ratios and indices
A ratio (cholesterol/HDL, albumin/globulin) or an index such as INR has
no unit, and every lab write path refuses a numeric value without one.
That refusal stays: an empty unit usually means the extraction dropped
it. Instead the value is stored as the UCUM unity "1", chosen on purpose.

The unit field when a marker is defined, on a scanned report's row and
in the document review edit now carries a "No unit" checkbox that sets
"1". The CSV import takes 1 in the unit column, as the template and the
import sheet say; an empty cell is still refused, and other spellings
such as "ratio" or "-" stay the literal unit they always were.

"1" compares as a unit of its own: it matches only itself, so it is
refused under a marker kept in mg/dL and the other way round.

Refs #1313
2026-11-02 20:28:23 +01:00
Marc-André Bombeck cae6edec64 Merge branch 'feat/mcp-links' into release/v1.46.6 2026-11-02 20:25:04 +01:00
Marc-André Bombeck 5f9c8461a3 test(mcp): pin record links against Postgres
Source system and id without the address, the Documents switch, the empty
answer, the cap, document links to labs and medications, row ids, and that
another account's document never appears, not even through a link row that
points across accounts.
2026-11-02 20:24:28 +01:00
Marc-André Bombeck 34302867aa feat(mcp): list links on lab and medication reads, ids on visit and condition rows
get_labs readings (the latest, earlier values in full, trajectory readings)
carry the fetch ids of what they link to, with linksTruncated; each linked
record is described once per answer in linkedRecords, because one report
usually holds a whole panel and would otherwise repeat on every reading.
fetch lab: and fetch med: list their links like the other record kinds, and
get_medication_schedule rows carry their id and links inline. get_visits and
get_illness_recovery rows carry visit: and condition: ids, so a row's links
are one fetch away. The labs and illness blocks gain an opt-in to carry row
ids; the Coach never asks for them.
2026-11-02 20:24:28 +01:00
Marc-André Bombeck ea7c6c860e feat(mcp): share one record-link builder and list labs and medications on a document
The links fetch lists beside a visit, condition, document or vaccination move
out of record-search into record-links, so the lab and medication reads can
list theirs through the same builder. A linked document imported from
Paperless-ngx or Papra now names its source system and its id there, never
the instance address. fetch document: also lists the lab results read from
it and the medications filed against it, each behind its own module.
2026-11-02 20:24:27 +01:00