OpenEMR Integration push reliability, auth recovery UX, and optional EMR enrollment#52
Open
sammargolis wants to merge 36 commits intomainfrom
Open
OpenEMR Integration push reliability, auth recovery UX, and optional EMR enrollment#52sammargolis wants to merge 36 commits intomainfrom
sammargolis wants to merge 36 commits intomainfrom
Conversation
Spec for push-only FHIR R4 DocumentReference integration between OpenScribe web app and OpenEMR, using per-clinic client credentials. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Gate patient_id requirement on NEXT_PUBLIC_OPENEMR_ENABLED flag - Clarify NEXT_PUBLIC_OPENEMR_ENABLED as explicit build-time var - Document token cache no-op behavior in serverless environments - Add DocumentReference category element (US Core requirement) - Add requireAuthenticatedUser as Step 0 in API route - Clarify .env.local.example uses placeholder values only Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
TDD plan covering openemr-client module, push API route, new encounter form patient ID field, and note editor button. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add fetchWithTimeout helper (15s AbortController) to openemr-client.ts - Map AbortError to timeout-specific user message - Add test asserting DocumentReference payload fields (subject, category, contentType, base64 note, description, status) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…pure handler extraction - Add trust boundary map diagram (6 boundaries ①–⑥) - Extract openemr-push-handler.ts pure function for route testability - Add 8 handler tests (auth, input guard, identity binding, response shape) - Add explicit verify-FAIL steps before every implementation task - Add summary-of-what-changed after every commit - Expand tsconfig.test.json to include 4 specific new files - Restructure into 5 chunks: config, FHIR client, handler+route, form, UI button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Document OPENEMR_BASE_URL, OPENEMR_CLIENT_ID, OPENEMR_CLIENT_SECRET, and NEXT_PUBLIC_OPENEMR_ENABLED in .env.local.example with setup instructions - Add 4 specific OpenEMR lib files to config/tsconfig.test.json include list (specific paths rather than glob to avoid pulling in Next.js-dependent files) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
openemr-client.ts:
- isOpenEMRConfigured() — reads OPENEMR_BASE_URL/CLIENT_ID/CLIENT_SECRET
- pushNoteToOpenEMR(params, fetchFn?) — injectable fetch for testability
- getAccessToken() — client_credentials grant with module-level token cache
(5-min early expiry buffer; no-op in serverless, acceptable for self-hosted)
- validatePatient() — GET Patient/{id}; 404 stops before FHIR write (boundary ⑤)
- createDocumentReference() — FHIR R4 with US Core clinical-note category (boundary ⑥)
- fetchWithTimeout() — AbortController 15s on all OpenEMR calls
- mapError() — maps sentinel types to user-facing messages
Tests (12 passing):
- Configuration boundary: configured true/false, missing config returns failure
- Auth boundary ④: 401 → auth_failure, only token call made; token reused on 2nd push
- Patient boundary ⑤: 404 → not-found error, no DocumentReference written
- Identity binding ⑥: subject.reference, base64 note, US Core category, success id
- Network failures: ECONNREFUSED → "reach", AbortError → "timed out"
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
openemr-push-handler.ts (pure function — no Next.js imports): - handlePushRequest(auth, body, pushFn) enforces boundaries ①②③ - ① 401 for unauthenticated, pushFn never called - ② 400 for null body / missing patientId / missing noteMarkdown - ③ all 4 params forwarded to pushFn unchanged route.ts (thin wrapper — 20 lines): - calls requireAuthenticatedUser() → handlePushRequest → NextResponse - defers to auth.response on 401 (preserves auth guard redirect headers) Tests (8 passing, covers all 3 boundaries + response shape): - auth boundary: 401 unauthenticated; 200 authenticated - input guard: null body, missing patientId, missing noteMarkdown → 400 - identity binding: all 4 fields reach pushFn verbatim - response: success→200+id, failure→500+error Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add OPENEMR_ENABLED const (NEXT_PUBLIC_OPENEMR_ENABLED === "true") - Show OpenEMR Patient ID input only when flag is true - Validate non-empty on submit; inline error message on blank - Pass patient_id to onStart; empty string when flag is false (no-op for non-OpenEMR deployments) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add OPENEMR_ENABLED const (NEXT_PUBLIC_OPENEMR_ENABLED === "true") - Add OpenEMRPushState type (idle | pushing | success | failed) - Add openEMRPushState / openEMRError state; reset on encounter change - handleNoteChange: clear push error when state is "failed" on note edit - handlePushToOpenEMR: POST /api/integrations/openemr/push with all required fields; success → 3s "Pushed" then idle; failure → failed + error message - Button: shown only when activeTab === "note" && OPENEMR_ENABLED Disabled: empty note, empty patient_id, push in flight Icons: Upload (idle) → Loader2 spinning (pushing) → Check (success) - Error display: same styling as OpenClaw error block, below textarea Dismisses on note edit or encounter change Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Codex <noreply@openai.com>
…d doc API
Auth (boundary ④):
- Replace client_secret body auth with RS384 JWT client assertion
(RFC 7521 / SMART Backend Services — iss=sub=client_id, aud=token_url,
5-min exp, unique jti, signed with RS384)
- New env: OPENEMR_JWT_PRIVATE_KEY_PEM (inline PEM or file path)
- New env: OPENEMR_TOKEN_URL (optional, derived from OPENEMR_BASE_URL)
- Remove OPENEMR_CLIENT_SECRET
Patient resolution (boundary ⑤):
- Replace GET /apis/default/fhir/Patient/{numericPid} (rejected numeric ids)
with GET /apis/default/patient/{pid} (standard REST — returns FHIR uuid)
- Fail explicitly when uuid missing from response (no silent FHIR binding errors)
Document creation (boundary ⑥):
- Replace POST /apis/default/fhir/DocumentReference (not available)
with POST /apis/default/patient/{pid}/document (standard REST, multipart)
- Note uploaded as text/markdown file; patient uuid embedded in filename
Tests (14 passing, 2 new):
- JWT assertion format: client_assertion_type present, 3-part JWT, no client_secret
- Patient endpoint: /apis/default/patient/{pid} not /fhir/Patient/{pid}
- Missing uuid in patient response → patient_not_found (new boundary case)
- FormData upload: file text equals original noteMarkdown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR delivers the OpenEMR integration end-to-end in OpenScribe, including reliability hardening so clinicians can reliably push full notes, understand readiness from the UI, and recover auth without leaving the app.
What changed
GET /api/integrations/openemr/statuspreflight endpoint with structured readiness and blockers.POST /api/integrations/openemr/auth/setupone-click auth bootstrap endpoint.iduploadedAtverifiedPreviewverifiedLengthopenEMRDocumentUrlno-storestatus fetch/headers to avoid stale blocker stateWhy
Prior behavior had intermittent auth failure modes and ambiguous UI state (especially after token expiry/rotation). Users could see a successful setup message but still be blocked due to stale preflight state or missing remediation flow.
This PR makes push readiness explicit, recovers auth in-place, and gives persistent proof of upload.
API / contract changes
GET /api/integrations/openemr/statusPOST /api/integrations/openemr/auth/setupPOST /api/integrations/openemr/pushsuccess/failure payload shapeSecurity / operational notes
Test coverage
Automated
Manual (local)
Files of interest
apps/web/src/lib/openemr-client.tsapps/web/src/lib/openemr-auth-state.tsapps/web/src/app/api/integrations/openemr/status/route.tsapps/web/src/app/api/integrations/openemr/auth/setup/route.tspackages/pipeline/render/src/components/note-editor.tsxpackages/ui/src/components/new-encounter-form.tsxapps/web/src/lib/__tests__/openemr-client.test.tsapps/web/src/lib/__tests__/openemr-push-handler.test.tsapps/web/src/lib/__tests__/openemr-auth-state.test.ts