Technical manual
Rosetta re-represents patient observations sourced from gateway.pdhc into
three clinical-data standards — FHIR R5, openEHR, and OMOP CDM —
and serves them over a web UI and a JSON REST API. It is an analysis-phase
service: SSO-gated, org-scoped, consent-enforced (fail-closed), and
audited on every read.
requests for upstream calls, Jinja2 views.127.0.0.1, behind the reverse proxy at https://rosetta.pdhc.se).SSO user ─Refresh─► gateway.pdhc GET /api/v1/observations?organization=<org>
│ (FHIR R5 bundle)
▼
observation_cache ──► fhir_converter ─► fhir_representations
├─► openehr_converter ─► openehr_representations
└─► omop_converter ─► omop_measurements
▼
web UI (3 side-by-side cards) / REST /api/v1/patient/<guid>/{fhir|openehr|omop}
Concept resolution is delegated to plan.pdhc: observations arrive from
gateway already FHIR-coded; Rosetta passes concept_guid through and records
concept_name, linking back via
https://plan.pdhc.se/api/v1/concepts/<concept_guid>. No local terminology.
All under the /api/v1 prefix (blueprint api); SSO bearer via session.
| Method | Path | Returns |
|---|---|---|
| GET | /patient/<guid>/fhir |
FHIR searchset Bundle {resourceType, type:"searchset", total, entry:[{resource}]} |
| GET | /patient/<guid>/openehr |
{patient_guid, total, compositions:[…]} |
| GET | /patient/<guid>/omop |
{patient_guid, total, measurements:[{measurement_concept_id, measurement_date, measurement_datetime, value_as_number, unit_source_value, measurement_source_value}]} |
HTML views (blueprint views): GET / (patient list, org-scoped),
GET /patient/<guid> (3-card detail; auto-converts if missing),
POST /patient/<guid>/convert (idempotent re-convert),
POST /refresh (sync cache from gateway).
Auth (blueprint auth, /auth): /login, /callback, /logout,
/logged-out.
Public: GET /healthz → {status:"ok"|"degraded", service:"rosetta.pdhc",
database:"connected"|"unavailable", auth_mode} (200/503, CORS for
www.pdhc.se); GET /metadata → FHIR R5 CapabilityStatement (fhirVersion
5.0.0).
app/services/)FHIR R5 — fhir_converter.to_fhir_r5(obs): prefers the rich FHIR from
gateway's raw JSON (basedOn / performer / referenceRange / extensions
preserved); normalises status=final, subject=Patient/<guid>,
category=laboratory, meta.profile includes the IPS profile; code system
https://plan.pdhc.se/api/v1/concepts/<concept_guid> + concept_name
display; valueQuantity with UCUM unit. → fhir_representations.
openEHR — openehr_converter.to_openehr_composition(obs): root
openEHR-EHR-COMPOSITION.report-result.v1 wrapping
openEHR-EHR-OBSERVATION.laboratory_test_result.v1; DV_QUANTITY
(magnitude+unit), POINT_EVENT time. → openehr_representations.
OMOP CDM — omop_converter.to_omop_measurement(obs): measurement domain —
person_id←patient_guid, measurement_concept_id←concept_guid,
measurement_date/datetime←observed_at, value_as_number←value (categorical
→ value_as_concept_id), unit_source_value←unit,
measurement_source_value←concept_name, measurement_source_url←plan.pdhc
concept URL. → omop_measurements.
Conversion is idempotent: re-convert deletes and rebuilds all three per
patient; runs are tracked in conversion_log.
app/models/__init__.py, PostgreSQL)| Table | Purpose |
|---|---|
users |
Local user mirror (FK + audit, Rule 24) |
observation_cache |
Raw gateway observations: source_obs_guid (unique), patient_guid, org_guid, concept_guid, concept_name, value, unit, observed_at, raw (JSONB) |
fhir_representations |
Validated FHIR R5 Observation (resource_json, FK → cache) |
openehr_representations |
openEHR Composition (archetype_id, composition_json) |
omop_measurements |
OMOP measurement row (person_id, measurement_*, measurement_source_url) |
conversion_log |
Per-patient conversion runs (status, fhir/openehr/omop counts) |
refresh_log |
Gateway sync runs (rows_fetched, status) |
audit_log |
X1 (#407) read audit tuple (see §6) |
Migrations: ad445ccc0480 initial schema; b7f2a1c3d901 adds
omop_measurements.measurement_source_url.
app/auth.py)effective_phases=["analysis"].Authorization: Bearer; validated againstsso.pdhc.se/api/auth/me/service with SSO_CLIENT_ID/SSO_CLIENT_SECRET.has_analysis_access(): user_type=="professional" andsession_phases (legacy fallback effective_phases); SUaffiliations[].care_unit_guid (legacy fallback organization_ids) viascope_to_user_orgs(); admins unscoped and may seed refresh fromDEFAULT_ORG_GUIDS.analysis_consent.check_patient_allowed()POST /api/v1/patients/analysis-filter (contract inplans/pdhc_data_shapes.md §5). Purpose is derived from the active role:research (+ research_project_guids); quality/registry →quality_registry; other clinical → statistics; SU-admin-no-affiliation →administration. No verdict → HTTP 503, no data. Applied to everyx1_audit writes an audit_log row per read:person_guid, role_guid, purpose, access_basis, route, n_rows, session_id.session_headers addsX-Operator-Session-Id (from forwarded header, else SSO blob session_id /sid) to every onward gateway/ips call, for end-to-end kontrollör audit..env)APP_PORT=9092
DB_HOST / DB_PORT=9091 / DB_NAME=rosetta_pdhc_db / DB_USER / DB_PASSWORD
DATABASE_URL=postgresql+psycopg2://…:9091/rosetta_pdhc_db
AUTH_MODE=off|sso
SSO_BASE_URL=https://sso.pdhc.se SSO_CLIENT_ID / SSO_CLIENT_SECRET
SSO_CALLBACK_URL=https://rosetta.pdhc.se/auth/callback
GATEWAY_BASE_URL=https://gateway.pdhc.se
IPS_BASE_URL=https://ips.pdhc.se # #422 consent; fail-closed if unset
SECRET_KEY / DEFAULT_ORG_GUIDS
Single db service via docker-compose (postgres:16); app runs under gunicorn
via ./start.sh:
.shared/), ensures Docker/DBflask db upgrade.127.0.0.1:9092; logs under .shared/logs/./healthz smoke (10×1 s) before declaring up.127.0.0.1:9092 atrosetta.pdhc.se.app/tests/)test_scaffold (/healthz, /metadata), test_converters (all three
converters), test_analysis_consent (purpose derivation + ips filter),
test_reform_scope (Zone-1 org scoping), test_session_propagation
(X-Operator-Session-Id), test_x1_audit (audit tuple).