feat(fhir): serve Observations as a time series, code RMSSD and workouts

GET /api/fhir/Observation keeps its snapshot answer when no date is
sent. A date range (FHIR prefixes eq/gt/ge/lt/le, UTC when no offset)
switches it to the time series: every reading in the range plus
workouts, newest first, paged in the database through one UNION id
query and a count, so a page costs what it holds. Series ids are the
row's own (m-<id>, w-<id>) and read back through /Observation/{id}.
The series replays the same saved report selection as the snapshot,
pairs systolic and diastolic recorded at one instant into the
blood-pressure panel, leaves sleep stage segments out, and reads an
hourly heart-rate bucket as a mean over its hour. A code parameter
narrows either answer; a malformed date is a 400 OperationOutcome.

A workout is an activity Observation after the Physical Activity IG:
LOINC 73985-4 with the sport as its value and duration (55411-3) and
energy (41981-2) as components. RMSSD gets its own registry coding,
text-only because LOINC has no RMSSD term, so it never borrows the SDNN
code. The capability statement lists the two new parameters.
This commit is contained in:
Marc-André Bombeck committed 2026-10-21 21:56:16 +02:00
1 parent 9e2a0681b2
commit 77619f8aa5
12 files changed
+1935 -26

No files matched your search

+25 -6
View File
@@ -1245,8 +1245,9 @@ paths:
summary: FHIR R4 CapabilityStatement (v1.11.0)
description: "Read-only FHIR R4 capability statement for the REST face. Declares the served resource types (Patient,
Coverage, Observation, MedicationStatement, MedicationAdministration), their `read` and `search-type`
interactions, the `Patient/$everything` operation, and the `application/fhir+json` format. Every advertised
interaction has a route. Auth: `fhir:read` scope (cookie sessions also pass)."
interactions, the search parameters (`_count`, `_offset`; `date` and `code` on Observation), the
`Patient/$everything` operation, and the `application/fhir+json` format. Every advertised interaction has a
route. Auth: `fhir:read` scope (cookie sessions also pass)."
responses:
"200":
description: CapabilityStatement (application/fhir+json).
@@ -1291,7 +1292,16 @@ paths:
- FHIR
summary: FHIR R4 Observation search (v1.11.0)
description: "Read-only `searchset` Bundle of the caller's own Observations (vitals / activity / lab / survey). Auth:
`fhir:read` scope. Offset paging via `_count` (clamped ≤200) / `_offset`."
`fhir:read` scope. Offset paging via `_count` (clamped ≤200) / `_offset`. Without `date` the answer is the
snapshot it has always been: one latest reading per type plus the derived panels, ids `obs-N`. With `date`
(v1.44, repeatable, FHIR prefixes eq/gt/ge/lt/le, a value without an offset read as UTC) the answer is the time
series instead: every reading in the range plus workouts, newest first, paged in the database, ids `m-<id>` /
`w-<id>` that read back through `/api/fhir/Observation/{id}`. The series follows the same saved report selection
as the snapshot, leaves sleep out (stored as stage segments), pairs a systolic and diastolic recorded at the
same instant into the blood-pressure panel, and codes a workout as an `activity` Observation (LOINC 73985-4
Exercise activity, duration 55411-3 and energy 41981-2 as components). `code` (repeatable, `system|code` or a
bare code, comma-separated) narrows either answer; an unknown code matches nothing. A malformed `date` is a 400
OperationOutcome."
parameters:
- in: query
name: _count
@@ -1301,6 +1311,14 @@ paths:
name: _offset
schema:
type: number
- in: query
name: date
schema:
type: string
- in: query
name: code
schema:
type: string
responses:
"200":
description: searchset Bundle (application/fhir+json).
@@ -1463,9 +1481,10 @@ paths:
tags:
- FHIR
summary: FHIR R4 Observation read
description: "Read-only `Observation` by id, out of the caller's own record. An id belonging to someone else is
indistinguishable from one that never existed — both answer a `not-found` OperationOutcome. Auth: `fhir:read`
scope; `userId` is narrowed from auth."
description: "Read-only `Observation` by id, out of the caller's own record: a snapshot id (`obs-N`) or a series id
(`m-<id>` / `w-<id>`, v1.44), the latter read inside the same saved selection the series search applies. An id
belonging to someone else is indistinguishable from one that never existed — both answer a `not-found`
OperationOutcome. Auth: `fhir:read` scope; `userId` is narrowed from auth."
parameters:
- in: path
name: id