This project is ready for research and pilot-style deployments, not unsupervised clinical production use.
Use the default compose file for local development:
docker compose up --buildService checks:
- API liveness:
GET /api/v1/health - API live probe:
GET /api/v1/health/live - API readiness:
GET /api/v1/health/ready - Web root:
GET /
The compose stack now includes healthchecks for Postgres, the API, and the web app so service startup is gated by readiness rather than timing assumptions.
Use the pilot overlay to remove dev-mode reload behavior and run the web app with next build plus next start:
docker compose -f docker-compose.yml -f docker-compose.pilot.yml up --buildThis keeps the same topology while moving closer to a research-pilot runtime:
- API runs without
--reload - web runs as a built Next.js app
- both app services use
restart: unless-stopped
API:
APP_ENVDATABASE_URLCORS_ORIGINSLOG_LEVELREQUEST_ID_HEADER_NAMEMOCK_AUTH_ENABLEDMOCK_AUTH_DEFAULT_USER_IDMOCK_AUTH_DEFAULT_DISPLAY_NAMEMOCK_AUTH_DEFAULT_ROLEMOCK_AUTH_DEFAULT_SITESAUTH_ROLE_ALIAS_MAPAUTH_PROXY_PROVIDER_PRESETAUTH_PROXY_IDENTITY_HEADER_NAMEAUTH_PROXY_USER_ID_FIELDAUTH_PROXY_DISPLAY_NAME_FIELDAUTH_PROXY_ROLE_FIELDAUTH_PROXY_SITES_FIELDAUTH_PROXY_GROUPS_FIELDAUTH_PROXY_GROUP_ROLE_MAPAUTH_USER_ID_HEADER_NAMEAUTH_USER_NAME_HEADER_NAMEAUTH_USER_ROLE_HEADER_NAMEAUTH_USER_SITES_HEADER_NAMERESEARCH_ID_SALTFHIR_REFERENCE_IDENTIFIER_SOURCE_ORDERFHIR_SOURCE_SYSTEM_SOURCE_ORDERFHIR_ACCESSION_SOURCE_ORDERHL7_PATIENT_IDENTIFIER_FIELD_ORDERHL7_SOURCE_SYSTEM_FIELD_ORDERPANCREATIC_SIGNAL_DATA_DIR
Web:
API_BASE_URLNEXT_PUBLIC_API_BASE_URLPANCREATIC_SIGNAL_API_USER_IDPANCREATIC_SIGNAL_API_USER_NAMEPANCREATIC_SIGNAL_API_USER_ROLEPANCREATIC_SIGNAL_API_USER_SITESPANCREATIC_SIGNAL_API_TRUSTED_IDENTITY_HEADER_NAMEPANCREATIC_SIGNAL_API_TRUSTED_IDENTITYPANCREATIC_SIGNAL_API_TRUSTED_IDENTITY_B64
Checked-in starter env bundles now live under deploy/examples:
local-mock/api.envandlocal-mock/web.envBaseline local development with mock auth enabled.pilot-proxy-demo/api.envandpilot-proxy-demo/web.envTrusted-proxy demo values for the existing fixed-identity overlay.pilot-header-demo/api.envandpilot-header-demo/web.envField-level header auth values for a fixed site-scoped navigator demo.
These files are non-secret examples. You can use them as-is for demo overlays or copy them when preparing a pilot-specific bundle.
Use the proxy demo overlay when you want to simulate a trusted identity provider without standing up a real SSO stack:
make pilot-proxy-demo-upThis overlay:
- disables mock auth on the API
- enables the
keycloakproxy preset with a sample role alias map - loads the sample env bundle in
deploy/examples/pilot-proxy-demo/ - configures the web service to forward a fixed trusted identity envelope for a site-scoped demo navigator at
Demo Hospital
After startup, validate the full flow from the host with:
make pilot-proxy-demo-smokeThe base smoke target waits for API and web readiness, resolves /api/v1/auth/me, verifies /imports renders successfully, imports the bundled demo reports through POST /api/v1/imports/reports, verifies the persisted audit record at GET /api/v1/imports/runs/{run_id}, fetches the visible case queue, and confirms a reviewer-action round-trip using the fixed navigator identity.
Hosted smoke automation is now available in .github/workflows/pilot-smoke.yml. That workflow reuses make pilot-proxy-demo-smoke, make pilot-proxy-demo-fhir-smoke, make pilot-proxy-demo-site-rejection-smoke, make pilot-proxy-demo-adapter-site-rejection-smoke, make pilot-header-demo-smoke, make pilot-header-demo-fhir-smoke, make pilot-header-demo-site-rejection-smoke, and make pilot-header-demo-adapter-site-rejection-smoke on manual dispatch and a weekly schedule. It is intentionally narrower than the full smoke matrix below: base report success-path, attachment-backed FHIR success-path, report-path site-rejection, and structured adapter site-rejection overlay checks are hosted, while the broader HL7 success-path, shared-visibility, audit-denial, and non-site structured failure targets remain manual operator checks. Manual dispatch supports both smoke_scope=fhir-success-only and smoke_scope=hl7-success-only; March 25, 2026 hosted runs proved both scopes operational, and the explicit Phase 6B decision is still to keep HL7 manual-only in the default weekly matrix so recurring hosted runtime and maintenance cost stay bounded. Each hosted job uploads both its raw pilot-smoke.log artifact and generated pilot-smoke-summary.json plus pilot-smoke-summary.md artifacts so run IDs, duration, visible cases, actor scope, and smoke outcome are preserved in a handoff-friendly format.
Research-intel now also has a hosted automation workflow in .github/workflows/research-watchtower.yml. That job restores a cache-backed SQLite state file, runs scripts/run_research_intel_watchtower.py on a six-hour schedule or manual dispatch, and uploads the parent watchtower summary plus generated digest, opportunity, and packet artifacts. The watchtower workflow is intentionally limited to cited discovery automation; it does not mutate triage scores, auto-promote opportunities, or perform paid-source actions.
For the release-facing sequence that turns those artifacts into recorded evidence, use RELEASE_RUNBOOK.md.
Adapter-specific live smoke targets are also available in the proxy demo:
make pilot-proxy-demo-fhir-smoke
make pilot-proxy-demo-hl7-smoke
make pilot-proxy-demo-shared-visibility-smoke
make pilot-proxy-demo-adapter-shared-visibility-smoke
make pilot-proxy-demo-failed-shared-visibility-smoke
make pilot-proxy-demo-adapter-failed-shared-visibility-smoke
make pilot-proxy-demo-site-rejection-smoke
make pilot-proxy-demo-adapter-site-rejection-smoke
make pilot-proxy-demo-adapter-audit-visibility-smoke
make pilot-proxy-demo-parse-validation-smoke
make pilot-proxy-demo-adapter-failure-smoke
make pilot-proxy-demo-audit-visibility-smokeThese targets follow the same readiness and auth-resolution checks, but drive:
POST /api/v1/imports/fhir/diagnostic-reportsPOST /api/v1/imports/hl7/oru
Success-path variants verify the returned run_id, persisted audit detail, visible imported cases, and reviewer-action round-trip in the same trusted-proxy session. Failure-path and visibility-only variants still reach the same web and audit surfaces, but intentionally skip the reviewer round-trip when no run-specific cases should be visible.
The FHIR success-path fixture is attachment-backed: it submits a DiagnosticReport.presentedForm XHTML narrative encoded as base64 UTF-16 so pilot smoke coverage exercises the same supported text-like attachment decoding path covered by the API tests.
The shared-visibility target reuses the successful /api/v1/imports/reports path and then checks the audit endpoints across two actors. It verifies:
- the creating scoped actor can inspect the successful run through
GET /api/v1/imports/runs/{run_id}and sees it inGET /api/v1/imports/runs - a second import-capable actor with the same scoped site can read the same run detail
- that same second actor also sees the successful run in the recent-run list
- the primary actor still sees run-specific cases and completes the usual reviewer round-trip
The adapter shared-visibility target reuses the successful structured adapter paths and then checks the audit endpoints across two actors. It verifies:
- a FHIR
DiagnosticReportimport persists a successful run that both the creating actor and a second same-site actor can inspect - an HL7 ORU import persists a successful run that both the creating actor and a second same-site actor can inspect
- both runs appear in the alternate actor's recent-run list
- the primary actor still sees run-specific cases and completes the usual reviewer round-trip
The failed-run shared-visibility target reuses the generic report import path with a validation-failing payload and then checks the audit endpoints across two actors. It verifies:
- the failed response returns HTTP 400 with
X-Import-Run-ID - the persisted run records the
validation_errorbucket and no imported sites - the creating scoped actor can inspect the failed run through
GET /api/v1/imports/runs/{run_id}and sees it inGET /api/v1/imports/runs - a second import-capable actor with the same scoped site can read the same failed run detail and recent-run list entry
- no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The adapter failed-run shared-visibility target reuses the structured failure paths and then checks the audit endpoints across two actors. It verifies:
- an unsupported FHIR payload persists a failed
unsupported_payloadrun that both the creating actor and a second same-site actor can inspect - a malformed HL7 ORU payload persists a failed
parse_errorrun that both the creating actor and a second same-site actor can inspect - both failed runs appear in the alternate actor's recent-run list even though
imported_sitesis empty - no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The failure-path target drives the bundled report import through a deliberately out-of-scope site value. It verifies:
- the import returns HTTP 403 with
X-Import-Run-ID - the persisted run records the
site_scope_rejectionbucket - the failed items are visible through
GET /api/v1/imports/runs/{run_id} - no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The adapter site-rejection target drives valid structured adapter payloads through the same site-scope boundary. It verifies:
- a FHIR
DiagnosticReportwith an out-of-scope derived site persists a failedsite_scope_rejectionrun - an HL7 ORU payload with an out-of-scope derived site persists a failed
site_scope_rejectionrun - both failed responses return
X-Import-Run-ID - both runs remain visible to the creating actor through the audit routes
- no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The adapter audit-visibility target reuses those persisted structured site-scope rejection runs and then checks the audit endpoints across two actors. It verifies:
- the creating scoped actor can inspect both failed runs through
GET /api/v1/imports/runs/{run_id}and sees them inGET /api/v1/imports/runs - a second import-capable actor with the same in-scope site but a different user ID receives
404on both failed run details - that same second actor does not see either failed run in the recent-run list
- no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The malformed-report target exercises two non-site failure buckets on /api/v1/imports/reports. It verifies:
- a validation-failing JSONL upload persists a failed run with
validation_error - a malformed JSONL upload persists a failed run with
parse_error - both failed responses return
X-Import-Run-ID - no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The adapter-failure target exercises structured import failures directly on the adapter endpoints. It verifies:
- an unsupported FHIR payload persists a failed run with
unsupported_payload - a malformed HL7 ORU payload persists a failed run with
parse_error - both failed responses return
X-Import-Run-ID - no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
The audit-visibility target reuses the persisted site-scope rejection path and then checks the audit endpoints across two actors. It verifies:
- the creating scoped actor can inspect the failed run through
GET /api/v1/imports/runs/{run_id}and sees it inGET /api/v1/imports/runs - a second import-capable actor with the same in-scope site but a different user ID receives
404on the failed run detail - that same second actor does not see the failed run in the recent-run list
- no run-specific cases become visible, so the smoke intentionally skips the reviewer round-trip
When you are done:
make pilot-proxy-demo-downUse the header demo overlay when an upstream gateway can only stamp simple identity headers such as X-User-ID and X-User-Role, rather than a structured trusted-identity envelope:
make pilot-header-demo-upThis overlay:
- disables mock auth on the API
- loads the sample env bundle in
deploy/examples/pilot-header-demo/ - configures the web service to forward fixed
X-User-ID,X-User-Name,X-User-Role, andX-User-Sitesvalues for a site-scoped navigator
After startup, validate the stack with:
make pilot-header-demo-smokeThe base smoke target waits for API and web readiness, confirms /api/v1/auth/me resolves in header-auth mode, verifies /imports renders, imports the bundled demo reports through POST /api/v1/imports/reports, verifies the persisted audit record at GET /api/v1/imports/runs/{run_id}, fetches visible cases, and confirms a reviewer-action round-trip using the fixed navigator identity.
Adapter-specific live smoke targets are also available:
make pilot-header-demo-fhir-smoke
make pilot-header-demo-hl7-smoke
make pilot-header-demo-shared-visibility-smoke
make pilot-header-demo-adapter-shared-visibility-smoke
make pilot-header-demo-failed-shared-visibility-smoke
make pilot-header-demo-adapter-failed-shared-visibility-smoke
make pilot-header-demo-site-rejection-smoke
make pilot-header-demo-adapter-site-rejection-smoke
make pilot-header-demo-adapter-audit-visibility-smoke
make pilot-header-demo-parse-validation-smoke
make pilot-header-demo-adapter-failure-smoke
make pilot-header-demo-audit-visibility-smokeThese targets follow the same readiness and auth-resolution checks, but drive:
POST /api/v1/imports/fhir/diagnostic-reportsPOST /api/v1/imports/hl7/oru
Success-path variants verify the returned run_id, persisted audit detail, visible imported cases, and reviewer-action round-trip in the same scoped session. Failure-path and visibility-only variants still reach the same web and audit surfaces, but intentionally skip the reviewer round-trip when no run-specific cases should be visible.
The FHIR success-path fixture is attachment-backed here as well, using a base64 UTF-16 XHTML presentedForm narrative rather than an Observation result so both pilot auth modes exercise the same supported text-like attachment path.
The header-auth shared-visibility target exercises the same successful import path and then confirms the owning actor plus a second scoped actor can both access the same persisted run detail and recent-run listing.
The header-auth adapter shared-visibility target exercises the successful FHIR and HL7 import paths and then confirms the owning actor plus a second scoped actor can both access those structured run details and recent-run listings.
The header-auth failed-run shared-visibility target exercises a persisted validation_error run and then confirms the owning actor plus a second scoped actor can both access that failed run detail and recent-run listing without exposing any run-specific cases.
The header-auth adapter failed-run shared-visibility target exercises persisted FHIR unsupported_payload and HL7 parse_error runs and then confirms the owning actor plus a second scoped actor can both access those failed run details and recent-run listings without exposing any run-specific cases.
The header-auth failure-path target exercises the same out-of-scope report import and verifies the persisted site_scope_rejection audit record without expecting any visible imported cases.
The header-auth adapter site-rejection target exercises out-of-scope FHIR and HL7 payloads and verifies persisted structured site_scope_rejection audit records without expecting any visible imported cases.
The header-auth adapter audit-visibility target exercises those same structured site_scope_rejection runs and then confirms the owning actor can inspect them while a second scoped actor cannot access the same audit details or recent-run listings.
The header-auth malformed-report target exercises the same persisted validation_error and parse_error audit checks without expecting any visible imported cases.
The header-auth adapter-failure target exercises the same persisted FHIR unsupported_payload and HL7 parse_error audit checks without expecting any visible imported cases.
The header-auth audit-visibility target exercises the same persisted site-scope rejection run and then confirms the owning actor can inspect it while a second scoped actor cannot access the same audit detail or recent-run listing.
When you are done:
make pilot-header-demo-downFor lower-level troubleshooting, the generic helper is still available:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_DEMO=1For live audit-visibility verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_AUDIT_VISIBILITY=1 \
SMOKE_SKIP_REVIEW=1For live failed-run shared-visibility verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_FAILED_SHARED_VISIBILITY=1 \
SMOKE_SKIP_REVIEW=1For live structured adapter failed-run shared-visibility verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_ADAPTER_FAILED_SHARED_VISIBILITY=1 \
SMOKE_SKIP_REVIEW=1For live structured adapter site-rejection verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_ADAPTER_SITE_REJECTION=1 \
SMOKE_REJECTION_SITE="Out of Scope Site" \
SMOKE_SKIP_REVIEW=1For live structured adapter audit-visibility verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_ADAPTER_AUDIT_VISIBILITY=1 \
SMOKE_REJECTION_SITE="Out of Scope Site" \
SMOKE_SKIP_REVIEW=1For live shared-visibility verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_SHARED_VISIBILITY=1For live structured shared-visibility verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_ADAPTER_SHARED_VISIBILITY=1For structured adapter verification in trusted-proxy mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_FHIR_DEMO=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_HL7_DEMO=1For live site-scope rejection verification in either auth mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_DEMO_SITE_REJECTION=1 \
SMOKE_REJECTION_SITE="Out of Scope Site" \
SMOKE_SKIP_REVIEW=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_DEMO_PARSE_VALIDATION_FAILURE=1 \
SMOKE_SKIP_REVIEW=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=proxy \
SMOKE_PROVIDER_PRESET=keycloak \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=pdac-navigator \
SMOKE_BASE64=1 \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_ADAPTER_FAILURES=1 \
SMOKE_SKIP_REVIEW=1For field-level header auth instead of a trusted identity envelope:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=header \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=navigator \
SMOKE_SITES="Demo Hospital" \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_DEMO=1For structured adapter verification in header-auth mode:
make smoke-proxy-auth \
SMOKE_AUTH_MODE=header \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=navigator \
SMOKE_SITES="Demo Hospital" \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_FHIR_DEMO=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=header \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=navigator \
SMOKE_SITES="Demo Hospital" \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_HL7_DEMO=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=header \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=navigator \
SMOKE_SITES="Demo Hospital" \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_DEMO_SITE_REJECTION=1 \
SMOKE_REJECTION_SITE="Out of Scope Site" \
SMOKE_SKIP_REVIEW=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=header \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=navigator \
SMOKE_SITES="Demo Hospital" \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_DEMO_PARSE_VALIDATION_FAILURE=1 \
SMOKE_SKIP_REVIEW=1make smoke-proxy-auth \
SMOKE_AUTH_MODE=header \
SMOKE_USER_ID=pilot-navigator \
SMOKE_DISPLAY_NAME="Pilot Navigator" \
SMOKE_ROLE_VALUE=navigator \
SMOKE_SITES="Demo Hospital" \
SMOKE_CHECK_WEB=1 \
SMOKE_CHECK_IMPORTS_PAGE=1 \
SMOKE_IMPORT_ADAPTER_FAILURES=1 \
SMOKE_SKIP_REVIEW=1The API ships with mock auth enabled by default so reviewer actions, imports, and exports work in local development without an external identity provider.
- Default mock actor:
Demo Reviewer/demo-reviewer/admin - Supported roles:
viewer,reviewer,navigator,analyst,admin - Trusted proxy identity header defaults to
X-Trusted-Identity - Trusted proxy provider presets:
generic,authentik,keycloak,oauth2-proxy - Trusted proxy payload fields default to
sub,name,role,sites, andgroups - Trusted proxy identities may be sent as raw JSON or base64url-encoded JSON
keycloakpreset reads user ID frompreferred_usernameand role candidates fromrealm_access.rolesoauth2-proxypreset reads user ID fromemailauthentikpreset reads user ID frompreferred_usernameAUTH_PROXY_GROUP_ROLE_MAPaccepts comma-separated mappings likepdac-reviewers:reviewer,pdac-navigators:navigatorAUTH_ROLE_ALIAS_MAPaccepts comma-separated mappings likerad_navigator:navigator,site_admin:admin- Header override fields default to
X-User-ID,X-User-Name,X-User-Role, andX-User-Sites - Site scopes are comma-separated and optional. When present, read access, imports, exports, and feedback summary endpoints are restricted to those sites.
The web app consumes /api/v1/auth/me capability flags and hides reviewer and import controls automatically for read-only users, so pilot deployments can rely on the API as the source of truth for both route enforcement and UI gating.
For pilot-style deployments behind a trusted proxy or gateway, disable mock auth and forward either the trusted identity envelope header or the field-level identity headers from that upstream system. The Next.js app can still forward a fixed identity to the API with the PANCREATIC_SIGNAL_API_* environment variables when you want a simple shared reviewer identity for demos, including a fixed site scope or a full trusted identity envelope.
The built Next.js app now exposes a dedicated operator workspace at /imports.
- Allowed roles:
analyst,navigator,admin - Supported submissions: CSV / JSON / JSONL uploads, FHIR
DiagnosticReportJSON, and HL7 ORU text - Immediate operator feedback: the page redirects to the persisted import run and shows processed / created / updated / failed counts plus stable failure buckets
Because the page keys off /api/v1/auth/me capabilities, read-only sessions can still browse the rest of the web app without seeing import controls.
Both pilot demo overlays now validate this route in their smoke targets.
The API now exposes research-safe case surfaces at:
GET /api/v1/cases/researchGET /api/v1/cases/{case_id}/research
These views preserve evidence offsets while masking PHI-like free text, pseudonymizing case/report/reviewer identifiers, and converting report/review timestamps to date-only fields.
For research exports, pass redact=true to:
GET /api/v1/exports/cases.csvGET /api/v1/exports/review-feedback.jsonl
RESEARCH_ID_SALT controls the stable pseudonymization salt for de-identified identifiers. Set it explicitly per deployment if you need deterministic but environment-specific research IDs.
PANCREATIC_SIGNAL_DATA_DIR can point the API at an alternate mounted data directory when the default repo-relative or /data lookup is not appropriate for the deployment layout.
The initial integration adapter now supports POST /api/v1/imports/fhir/diagnostic-reports for:
- single
DiagnosticReportresources - FHIR
Bundlepayloads that includeDiagnosticReportentries plus referencedObservationorOrganizationresources
This adapter is intentionally lightweight and feeds the existing ReportInput triage path instead of introducing a second persistence model. It is best suited for pilot gateways that can already provide JSON DiagnosticReport payloads.
The adapter now also preserves a small explicit provenance surface on each imported report when the payload provides it:
patient_identifierencounter_identifieraccession_numberordering_providersource_systemsource_formatimport_source_id
These values flow through case detail and export surfaces, while research-safe views pseudonymize the identifier-like fields.
Each import request now also persists an import-run audit summary with source format, actor, timestamps, processed / created / updated / failed counts, and stable failure buckets. Recent runs are available through:
GET /api/v1/imports/runsGET /api/v1/imports/runs/{run_id}
When a request fails after an audit run has been recorded, the API also returns X-Import-Run-ID. The web import workspace uses that header to deep-link operators directly into the failed run detail instead of showing a generic error.
Field selection is now configurable with narrow precedence settings so pilots can adapt to upstream variability without code edits. Current knobs:
FHIR_REFERENCE_IDENTIFIER_SOURCE_ORDERValid values:resolved_identifier,resolved_id,reference_tailFHIR_SOURCE_SYSTEM_SOURCE_ORDERValid values:meta_source,performer,results_interpreter,encounter_service_providerFHIR_ACCESSION_SOURCE_ORDERValid values:report_identifier_typed,based_on_identifier_typed,report_identifier,based_on_identifier
The initial HL7 integration adapter now supports POST /api/v1/imports/hl7/oru for raw HL7 v2 ORU result messages.
It currently:
- parses one or more ORU messages from a text payload
- emits one triaged case per
OBRgroup - builds report text from
OBX/NTE - derives site from
PV1-3orMSH-4 - derives modality heuristically from
OBR-24/OBR-4
Like the FHIR path, this adapter deliberately maps into the existing ReportInput triage flow rather than adding a separate hospital-interface persistence layer.
The HL7 path now preserves the same provenance surface from PID, PV1, OBR, and MSH with best-effort fallbacks when optional fields are absent, so pilot feeds can retain patient, encounter, accession, provider, and message-source context without changing triage semantics.
Failed imports are bucketed into stable categories for auditability:
parse_errorvalidation_errorunsupported_payloadsite_scope_rejection
HL7 metadata selection now supports narrow field-preference overrides as well:
HL7_PATIENT_IDENTIFIER_FIELD_ORDERValid values:PID-3,PID-2HL7_SOURCE_SYSTEM_FIELD_ORDERValid values:MSH-3,MSH-4
The API now emits structured JSON request logs with:
eventrequest_idmethodpathstatus_codeduration_msclientenvironment
Clients can provide X-Request-ID and the API will echo it back in the response. If one is not provided, the API generates a request ID automatically.
/api/v1/health/ready performs a database connectivity check with SELECT 1.
200 OKmeans the app is ready to serve requests.503 Service Unavailablemeans a dependency check failed.
- Run
make validate-strict. - Bring up the compose stack.
- Verify
curl http://localhost:8000/api/v1/health/ready. - Verify
curl http://localhost:3000. - Verify
curl http://localhost:3000/importsfor import-enabled pilot identities. - Load demo data or import a study batch.
- If using a pilot overlay, run the matching smoke target.
- Capture request logs and exported benchmark artifacts for the pilot record.