Skip to content

feat: import partner-lab reports (report-reader) - #23

Merged
BackStacked merged 3 commits into
mainfrom
claude/project-thread-86xth5
Nov 10, 2026
Merged

BackStacked merged 3 commits into
mainfrom
claude/project-thread-86xth5

Conversation

@BackStacked

@BackStacked BackStacked commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

What

Partner-lab PDFs (Hy Patho Lab, Clinova Pathlabs) are read by @mgpl-diagnostics/report-reader. The import is matched to one of our bookings and applied to our reports as entered results with provenance. Staff verify and release as usual. Full write-up: docs/18-imports.md.

Field Permission
importExternalReport(pdfBase64, fileName, bookingId, branchId) report:import (branch)
externalReport(id), externalReports(branchId, bookingId, status, first, after) report:import
matchExternalReport(id, bookingId) report:import
applyExternalReport(id, mappings, confirmPatientMismatch) report:import + report:enter
rejectExternalReport(id, reason) report:import
externalSignatories(lab) / nameExternalSignature(input) signatory:read / signatory:manage
Report.provenance report:read

New error codes: UNSUPPORTED_REPORT, FILE_TOO_LARGE, BOOKING_PATIENT_MISMATCH, REPORT_ALREADY_APPLIED.

How the rules are kept

  • NABL per test. The NABL line ("NABL accredited laboratory (Certificate No. …)", worded as the partner's) appears only when test.accredited === true. No logo is ever drawn. Hy Patho reports carry no marks, so they never get the line.
  • Signature only with its own PDF. Signature PNGs are stored with the sha256 of the PDF they came from. They are served and printed only through the lookup (orgId, that PDF's sha256, image sha). On pages from a single source, the partner signature prints instead of ours (report-kit's default, the owner's choice). It is never used as our own signatory, and our own reports never print a partner signature (a test checks this).
  • Reported by lists medical signatories only.
  • Staff still verify. Applied reports are entered, and printing still needs verifyReport. Results are copied as printed, with no catalog defaults and no recomputed ranges. Later edits are audited as report.edit_imported (parameter names only).
  • Patient data.
    • The PDF, the JSON and the PNGs are stored per organisation. An unsupported PDF is not kept.
    • Job payloads carry the file id and the actor only. A test checks Redis for the PDF and for tokens.
    • Nothing is logged.
    • Tests draw fictional PDFs at run time (test/partner-pdfs.ts).

Changes from the brief (choices for review)

  • Permission names use the existing resource:action form: report:import, signatory:read / signatory:manage (auth-kit 1.4.0).
  • importExternalReport takes an optional branchId, which defaults to the booking's branch.
  • applyExternalReport takes confirmPatientMismatch, so staff confirm a mismatch explicitly.
  • Signature images are exposed as SignatureImage on ExternalSignatoryOnReport.image, not as a Document.
  • Large uploads: Fastify's limit is raised to fit a base64 PDF of IMPORT_MAX_BYTES, but only for an operation whose single root field is importExternalReport. Every other operation keeps BODY_LIMIT_BYTES (413, FILE_TOO_LARGE).

Dependencies

auth-kit 1.4.0, db-kit 2.1.1 (external_report_files without a foreign key), report-kit 2.1.0, report-reader 0.1.0, client 1.4.0 (dev), and pdf-lib (dev, for the test PDFs). All are published, and package-lock.json and kits.json are updated. The client stays on 1.4.0 so that the lab-switching PR can follow this one.

Tests

CI: 438 passed, 0 failed. It runs on PostgreSQL, through PgBouncer, with replicas, and on MongoDB, with BullMQ on a real Redis. Typecheck, build, schema, API reference and llms-full freshness all pass.

The import checks cover:

  • import and re-import;
  • an unsupported PDF;
  • base64 and size limits;
  • the mismatch warning and its confirmation;
  • apply with provenance (status entered until verify);
  • NABL only when accredited;
  • the signature served only for its own PDF;
  • nameExternalSignature re-extraction;
  • permissions, and NOT_FOUND for another organisation;
  • the worker path.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_018jBohmYiwjQLWmN2duiZcA

importExternalReport / externalReport(s) / matchExternalReport / applyExternalReport /
rejectExternalReport / externalSignatories / nameExternalSignature, and Report.provenance.
- report-reader extracts in the request up to IMPORT_INLINE_MAX_BYTES, else in the worker (imports
  queue; the payload is the stored file's id and the actor). IMPORT_MAX_BYTES caps the PDF (15 MiB);
  only an importExternalReport-only operation may exceed BODY_LIMIT_BYTES.
- Booking match: name without titles, sex, age +-1; differences need confirmPatientMismatch.
- Apply: results as printed into our reports (status entered) with report_provenance; staff verify.
  Edits after import are audited; catalog defaults are not filled in.
- Printing: per-test provenance line; NABL only on tests the partner marked accredited; partner
  signature only from the same PDF (orgId + sourceSha256 + sha), instead of ours on single-source pages.
- Errors: UNSUPPORTED_REPORT, FILE_TOO_LARGE, BOOKING_PATIENT_MISMATCH, REPORT_ALREADY_APPLIED.
- Tests on fictional PDFs drawn at test time; docs/18-imports.md; schema, reference, llms regenerated.
- Kits: auth-kit 1.4.0, db-kit 2.1.0, report-kit 2.1.0, report-reader 0.1.0, client 1.4.0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jBohmYiwjQLWmN2duiZcA

Copy link
Copy Markdown
Contributor Author

CI ci fails at npm ci: ETARGET No matching version found for @mgpl-diagnostics/auth-kit@^1.4.0. This is the blocker listed in the description, not a code failure. auth-kit 1.4.0, db-kit 2.1.0, report-kit 2.1.0, report-reader 0.1.0 and client 1.4.0 are not published to GitHub Packages yet (tags are admin-only). Once they are, I'll update package-lock.json and push. A re-run before that would fail the same way.


Generated by Claude Code

…1.0, report-reader 0.1.0, client 1.4.0, pdf-lib

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jBohmYiwjQLWmN2duiZcA

Copy link
Copy Markdown
Contributor Author

The install now passes. The ci check still fails in [pg] imports, with external_report_files_id_fkey on every import.

The published db-kit 2.1.0 still has a foreign key from external_report_files.id to external_reports. The fix (b30ee5e) reached the branch after db-kit#11 had already merged, so 2.1.0 shipped without it. The api stores the PDF before it creates the report, so the key rejects every import.

The fix is in mgpl-diagnostics/db-kit#12 (2.1.1), which adds a contract check that would have caught this. Once v2.1.1 is published, I'll move this PR to ^2.1.1 and push. Re-running before then would fail the same way.


Generated by Claude Code

@BackStacked
BackStacked marked this pull request as ready for review November 10, 2026 07:42
@BackStacked
BackStacked merged commit bb8664a into main Nov 10, 2026
1 check passed
@BackStacked
BackStacked deleted the claude/project-thread-86xth5 branch November 10, 2026 07:43
BackStacked pushed a commit that referenced this pull request Nov 10, 2026
Conflicts in kits.json, package files, errors.ts and llms-full.txt: kept both
sides (report-reader and db-kit 2.1.1 from main; auth-kit 1.5.0, client 1.5.0
and ORG_UNAVAILABLE from this branch). Generated files regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018jBohk4otGVRjt3Y1cF1N2
BackStacked added a commit that referenced this pull request Nov 10, 2026
…dates) (#26)

<!-- claude-projects-attribution: {"github_login":"BackStacked"} -->
_Requested by **Himanshu** · [project
thread](https://claude.ai/code/project/chan_016JXeBZ2UxSi4N5VU2C26fQ?thread=cmsg_016JXeBZ2UxSi4N5VU2C26fQU57JpX29saJ5azzjf6zEWH)_

## What

**Images per release.** `.github/workflows/release.yml`: a `v*` tag
(admin-only) or a manual run on `main` checks the tag equals
`package.json`, builds `ghcr.io/mgpl-diagnostics/api` for linux/amd64
and arm64 (tags `1.2.3`, `1.2`, `1` from 1.0, `latest`; kits read with
the workflow token as a build secret) and creates the GitHub release
with generated notes.

**Easy hosting.** `docker-compose.yml` now pulls the released api and
dashboard images (`MGPL_API_VERSION` / `MGPL_DASHBOARD_VERSION`, default
`latest`) and adds the dashboard. nginx serves both on one origin:
`/graphql` and `/health` → api replicas, the rest → dashboard, so
`TRUST_PROXY=1` stays right and there is no CORS. Building from a
checkout moved to `docker-compose.build.yml`.

**Update notices.** New query `systemUpdates(dashboardVersion, refresh)`
(permission `settings:manage`: owner and admin) compares this api's
version and the dashboard's with the newest published GitHub releases of
both repos, and returns the changelog in between plus the update steps.
One GET per repo per `UPDATE_CHECK_INTERVAL_MS` (6 h) per process,
"refresh" at most once a minute, last good answer kept on failure;
`UPDATE_CHECK=false` makes no outbound call. Feed data is typed and
capped; non-https links dropped; pre-releases and drafts ignored. New
module `system` (schema, controller, service), no change to existing
modules.

**Fix.** `health.version` read `../../package.json` relative to the
bundled chunk, so production images answered `0.0.0`; `src/version.ts`
now reads the right file in both `tsx` and `dist`.

Docs: new `docs/18-updates.md` (images, hosting, updating, releasing,
the check), config table, `.env.example`, 12/17 updated.
`schema.graphql`, API reference and `llms-full.txt` regenerated.

## Merge order
`@mgpl-diagnostics/client` 1.6.0 (mgpl-diagnostics/client PR) has the
`systemUpdates` operation, which the "every root field has a client
operation" check needs, so this PR bumps the client to `^1.6.0`. Client
main already includes the operations of #23 and #24, so: merge **#24 and
#23**, then **client 1.6.0** (tag it), then this.
`kits.json`/`package.json`/lock will conflict with #24 on the client
line; take `1.6.0`, then `npm install` to refresh the lock and `npm run
schema && npm run docs:reference && npm run llms`.

## Tests
`TEST_BACKENDS=pg npm test` with kits built from local checkouts: 16 new
`[updates]` checks pass (versions, changelog window, cache, refresh
limit, unknown dashboard version, UNAUTHENTICATED, FORBIDDEN for front
desk, 403 feed, `UPDATE_CHECK=false`, repo validation). The one failure
is the client-contract check against today's main, which lacks #23/#24's
fields (goes away in the merge order above). `docker compose config`
passes for both compose files; `nginx -t` passes; nginx routing checked
with stub containers. The image build itself runs for the first time in
CI on the first tag.

## After merging
- First release only: GitHub → org → Packages → `api` → change
visibility to Public (or `docker login ghcr.io` on servers).
- One-click updates are not included (the api never gets the Docker
socket); asked in the project thread.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018jBohmSXdQwAG8EmEBKYkQ)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
BackStacked added a commit that referenced this pull request Nov 10, 2026
…d from the image (#27)

<!-- claude-projects-attribution: {"github_login":"BackStacked"} -->
_Requested by **Himanshu** · [project
thread](https://claude.ai/code/project/chan_016JXeBZ2UxSi4N5VU2C26fQ?thread=cmsg_016JXeBZ2UxSi4N5VU2C26fQU57JpX29saJ5azzjf6zEWH)_

- **docs/20-vps.md**: one Ubuntu VPS with the released images: server
hardening (ufw, SSH keys, Docker), files, the production `.env`, start,
first lab, HTTPS with Caddy, daily `pg_dump` with off-server copies and
a restore, updates, everyday commands, checklist.
- **deploy/production.env.example**: only what production needs,
`CHANGE_ME` where a value is required. `HTTP_PORT=127.0.0.1:8080`
(Docker's ports bypass ufw) and `TRUST_PROXY=2` (Caddy + nginx).
- **dist/seed.js** is now built (tsup entry), so the image can create
the first lab: `docker compose run --rm db-sync node dist/seed.js`
(direct to PostgreSQL; password taken from the shell's environment,
never on the command line). Checked: `node dist/sync.js` then `node
dist/seed.js` against PostgreSQL 17 created the org, branch, owner and
25 ledgers.
- Links fixed: the update guide is `docs/19-updates.md` since #23 took
18 (`.env.example`, compose header, release notes text).

`docker compose --env-file deploy/production.env.example config` binds
nginx to 127.0.0.1:8080 and passes `TRUST_PROXY=2`.

---
_Generated by [Claude
Code](https://claude.ai/code/session_018jBohmSXdQwAG8EmEBKYkQ)_

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants