medOS ultra

Lab Data Pipeline

End-to-end lab flow: LabRequest to 8 LAB_* events to orchestrator handlers to Supabase read models to realtime widget.

15 min read diagramsUpdated 2026-05-17docs/architecture/lab-data-pipeline.md

End-to-end flow from backend lab orders (MongoDB) through the encounter-orchestrator (Deno edge function) into Supabase read models, consumed by the LabResultsWidget via realtime subscriptions.

Data Flow Overview

┌─────────────────────────────────────────────────────────────────────────────┐
│  WRITE PATH (Backend — MongoDB)                                            │
│                                                                            │
│  Clinician orders lab  ──▶  LabRequest Service (NestJS/Moleculer)          │
│       OR                        │                                          │
│  Roche LIS auto-result  ────▶  │  services/medication/.../labRequest/      │
│                                 │                                          │
│                                 ▼                                          │
│                          MongoDB write                                     │
│                          + Moleculer event emit                            │
│                          (LAB_ORDER_CREATED, LAB_RESULT_FILED, etc.)       │
└─────────────────────────┬───────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  EVENT BUS                                                                 │
│                                                                            │
│  Moleculer → NATS → webhookDispatcher → POST hospital_events (Supabase)   │
│                                                                            │
│  Legacy event type              Normalized (manifest) type                 │
│  ─────────────────              ──────────────────────────                 │
│  LAB_ORDER_CREATED         →    manifest.lab.order_created                 │
│  LAB_SPECIMEN_COLLECTED    →    manifest.lab.specimen_collected             │
│  LAB_SPECIMEN_RECEIVED     →    manifest.lab.specimen_received              │
│  LAB_SPECIMEN_ANALYZING    →    manifest.lab.specimen_analyzing             │
│  LAB_RESULTED              →    manifest.lab.resulted                      │
│  LAB_VERIFIED              →    manifest.lab.verified                      │
│  LAB_SPECIMEN_REJECTED     →    manifest.lab.specimen_rejected              │
│  LAB_RESULT_FILED          →    manifest.lab.result_filed                  │
└─────────────────────────┬───────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  ENCOUNTER ORCHESTRATOR (Deno Edge Function)                               │
│  infrastructure/medbase/functions/encounter-orchestrator/index.ts           │
│                                                                            │
│  Triggered by: INSERT on hospital_events (Postgres trigger)                │
│                                                                            │
│  Two lab handlers:                                                         │
│                                                                            │
│  ┌─ handleLabSpecimenPipeline() ────────────────────────────────────────┐  │
│  │  Routes: manifest.lab.order_created .. specimen_rejected             │  │
│  │  Writes:                                                             │  │
│  │    • encounter_journey_cache.clinical_context.specimen_pipeline      │  │
│  │    • department_queues (lab worklist rows)                           │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
│                                                                            │
│  ┌─ handleLabResultFiled() ─────────────────────────────────────────────┐  │
│  │  Routes: manifest.lab.result_filed                                   │  │
│  │  Writes:                                                             │  │
│  │    • encounter_journey_cache.clinical_context.latest_labs            │  │
│  │    • observations (unified FHIR R4 Observation)                     │  │
│  │    • acknowledgement_requests (critical value escalation)           │  │
│  └──────────────────────────────────────────────────────────────────────┘  │
│                                                                            │
│  observation trigger → lab_results_ts (TimescaleDB hypertable)             │
└─────────────────────────┬───────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│  READ PATH (Supabase → Frontend)                                           │
│                                                                            │
│  useLaboratoryResultsSupabase hook                                         │
│    • Query: department_queues WHERE patient_id & dept_type='lab'           │
│    • Query: hospital_events WHERE patient_id & event_type IN LAB_*        │
│    • Realtime: postgres_changes on both tables                            │
│    • Projects: LabPanelResult[] (specimen timeline + analytes + flags)     │
│                                                                            │
│  LabResultsWidget                                                          │
│    • Pending / Completed sections                                          │
│    • Specimen stage dots (7 stages)                                        │
│    • Critical value warnings                                               │
│    • Live/offline indicator (Supabase realtime status)                     │
│                                                                            │
│  Widget Rail (macOS-Dock sidebar)                                          │
│    • Lab icon with badge (critical/pending count)                          │
│    • 320px flyout panel                                                    │
│    • "Open full tab" promotes to DynamicContentRenderer slot              │
└─────────────────────────────────────────────────────────────────────────────┘

Layer 1 — Backend Lab Services (Write Path)

LabRequest Service

Property Value
Location services/medication/src/api/medication/modules/labRequest/
Data store MongoDB (primary write)
Transport Moleculer via NATS
Controller labRequest.controller.mixin.ts

The LabRequest service handles the full lifecycle of lab orders:

  1. Order creation — clinician requests a lab panel → MongoDB document created
  2. Specimen collection — nurse collects sample → status update + audit log
  3. Result ingestion — manual entry or Roche LIS auto-import → results stored on order document
  4. Result filingupdateLabRequestStatus() or updateLabReportForLisRoche() emits events

Event Emission

When lab results are filed (status = resulted or completed), the service calls EncounterJourneyService.emitLabResultFiled():

Payload shape:
{
  labRequestId: string,
  results: [{
    testCode: string,      // HGB, WBC, ALT, K, etc.
    testName: string,
    value: number,         // parsed from labResult string
    unit: string,
    annotation?: string
  }],
  reportedAt: string       // ISO timestamp
}

Both updateLabRequestStatus and updateLabReportForLisRoche follow the same emit path. Each also creates a LabRequestAuditLog entry with diffString(oldData, newData).

Specimen Pipeline Events

Specimen lifecycle events are emitted at each transition. The backend emits the legacy LAB_* event type; the webhookDispatcher mixin inserts a row into hospital_events (Supabase) which triggers the encounter-orchestrator.

Layer 2 — Event Contract

File: infrastructure/medbase/functions/_shared/event-contract.ts

8 Lab Event Types

Legacy Event Normalized (manifest) Meaning
LAB_ORDER_CREATED manifest.lab.order_created Clinician places lab order
LAB_SPECIMEN_COLLECTED manifest.lab.specimen_collected Nurse draws specimen
LAB_SPECIMEN_RECEIVED manifest.lab.specimen_received Lab department receives tube
LAB_SPECIMEN_ANALYZING manifest.lab.specimen_analyzing Analyzer processing sample
LAB_RESULTED manifest.lab.resulted Raw results available
LAB_VERIFIED manifest.lab.verified Medical technologist signs off
LAB_SPECIMEN_REJECTED manifest.lab.specimen_rejected Sample rejected (hemolyzed, etc.)
LAB_RESULT_FILED manifest.lab.result_filed Final results with analyte values

The LEGACY_TO_NORMALIZED map in event-contract.ts provides bidirectional lookup. All events arrive at the orchestrator already normalized.

Event Payload Contract

Every lab event in hospital_events carries:

{
  "event_type": "LAB_SPECIMEN_COLLECTED",    // legacy type (column)
  "patient_id": "...",
  "encounter_id": "...",
  "payload": {
    "ticket_id": "lab-order-xxx",            // links to department_queues
    "specimen_id": "spec-xxx",               // physical tube/container
    "panel_name_th": "ตรวจเลือด CBC",
    "panel_name_en": "Complete Blood Count",
    "specimen_type_th": "เลือด",
    "specimen_type_en": "Blood",
    "actor": "nurse-001",
    "occurred_at": "2026-05-15T08:30:00Z",
    // result_filed also includes:
    "results": [{ "testCode": "HGB", "value": 12.5, "unit": "g/dL", ... }],
    "alertTier": 0                           // 0=normal, 3=critical
  }
}

Layer 3 — Encounter Orchestrator (Projection)

File: infrastructure/medbase/functions/encounter-orchestrator/index.ts

The orchestrator is a Deno edge function triggered by a Postgres trigger on hospital_events INSERT. It routes events by normalized type to handler functions.

handleLabSpecimenPipeline()

Routes: All manifest.lab.* events except result_filed

Stage mapping:

Event Stage Queue Status
manifest.lab.order_created ordered WAITING (creates row)
manifest.lab.specimen_collected collected IN_PROGRESS
manifest.lab.specimen_received received IN_PROGRESS
manifest.lab.specimen_analyzing in-analysis IN_PROGRESS
manifest.lab.resulted resulted IN_PROGRESS
manifest.lab.verified verified COMPLETED
manifest.lab.specimen_rejected rejected CANCELLED

Writes to encounter_journey_cache:

// clinical_context.specimen_pipeline[specimenId]
{
  "currentStage": "received",
  "orderedAt": "2026-05-15T07:00:00Z",
  "orderedBy": "dr-smith",
  "collectedAt": "2026-05-15T07:15:00Z",
  "collectedBy": "nurse-jane",
  "receivedAt": "2026-05-15T07:30:00Z",
  "receivedBy": "mt-tech-01",
  // rejected specimens also get:
  "rejectionReason": "Hemolyzed sample"
}

Writes to department_queues:

  • On ordered: INSERT new row with dept_type='LABORATORY', status='WAITING', ticket_id from payload
  • On subsequent stages: UPDATE existing row’s status + patch metadata JSONB with specimen_[stage]_at timestamps

handleLabResultFiled()

Routes: manifest.lab.result_filed

Writes to encounter_journey_cache:

// clinical_context.latest_labs[testCode]
{
  "HGB": { "value": 12.5, "unit": "g/dL", "refLow": 12.0, "refHigh": 17.5, "critical": false },
  "K":   { "value": 6.7,  "unit": "mmol/L", "refLow": 3.5, "refHigh": 5.0, "critical": true }
}

Writes to observations (via projectLabResultFinalized):

Each analyte becomes a row in the unified observations table with source_kind='lab', category='biomarker', LOINC code mapping, and interpretation flag.

Writes to acknowledgement_requests (critical values only):

When alertTier === 3, creates an acknowledgement request with:

  • 30-minute deadline
  • 2-tier escalation: 15 min → charge_nurse, 30 min → attending_physician
  • Links back to the lab result for one-click review

Observation → lab_results_ts Trigger

A Postgres trigger on observations INSERT (where category = 'biomarker') copies the row into lab_results_ts, a TimescaleDB hypertable optimized for time-series queries (trend charts, analytics dashboards).

Retention policy: 2yr full, 5yr hourly aggregates, 10yr daily aggregates. Compression starts after 30 days.

Layer 4 — Supabase Read Model Tables

department_queues (Lab Worklist)

One row per lab order. Serves both the lab department worklist and per-patient widget views.

-- Key columns for lab rows
id              UUID PRIMARY KEY
tenant_id       TEXT
encounter_id    TEXT
patient_id      TEXT
dept_type       TEXT          -- 'LABORATORY'
ticket_id       TEXT          -- lab order reference
status          TEXT          -- WAITING | IN_PROGRESS | COMPLETED | CANCELLED
priority        INTEGER
metadata        JSONB         -- specimen stage, panel name, analytes, timestamps
created_at      TIMESTAMPTZ
updated_at      TIMESTAMPTZ

metadata JSONB for lab rows:

{
  "panel_name_th": "CBC",
  "panel_name_en": "Complete Blood Count",
  "specimen_type_th": "เลือด",
  "specimen_type_en": "Blood",
  "specimen_ordered_at": "...",
  "specimen_collected_at": "...",
  "specimen_received_at": "...",
  "specimen_analyzing_at": "...",
  "specimen_resulted_at": "...",
  "specimen_verified_at": "...",
  "analytes": [
    { "code": "HGB", "value": 12.5, "unit": "g/dL", "flag": "normal" },
    { "code": "K",   "value": 6.7,  "unit": "mmol/L", "flag": "critical-high" }
  ],
  "has_critical": true
}
// NOTE: critical_acknowledged is NOT stored here — it is derived at
// query time from the acknowledgement_requests bounded context.
// See "Acknowledgement as a Separate Bounded Context" below.

hospital_events (Timeline)

Append-only event log. Each lab lifecycle transition creates a row.

id              UUID PRIMARY KEY
event_type      TEXT          -- LAB_ORDER_CREATED, LAB_SPECIMEN_COLLECTED, ...
patient_id      TEXT
encounter_id    TEXT
payload         JSONB         -- ticket_id, specimen_id, actor, results, etc.
occurred_at     TIMESTAMPTZ
created_at      TIMESTAMPTZ

observations (Unified FHIR R4)

One row per analyte result. Used for cross-encounter trending and FHIR export.

id              UUID PRIMARY KEY
patient_id      TEXT
encounter_id    TEXT
category        TEXT          -- 'biomarker' for lab results
code_system     TEXT          -- 'http://loinc.org'
code            TEXT          -- LOINC code
code_display    TEXT          -- human-readable name
value_numeric   DOUBLE PRECISION
unit            TEXT
reference_low   DOUBLE PRECISION
reference_high  DOUBLE PRECISION
interpretation  TEXT          -- normal, low, high, critical-low, critical-high
effective_at    TIMESTAMPTZ
source_kind     TEXT          -- 'lab'
source_id       TEXT          -- lab order reference
status          TEXT          -- final, amended, corrected
metadata        JSONB

lab_results_ts (TimescaleDB Hypertable)

Time-series copy of lab observations for analytics and trend charts. Auto-populated by Postgres trigger on observations INSERT where category = 'biomarker'.

time            TIMESTAMPTZ   -- effective_at
patient_id      TEXT
encounter_id    TEXT
order_id        TEXT
test_code       TEXT
test_name       TEXT
value_numeric   DOUBLE PRECISION
value_text      TEXT
unit            TEXT
reference_low   DOUBLE PRECISION
reference_high  DOUBLE PRECISION

encounter_journey_cache (Patient Manifest)

JSONB document per encounter. Lab data lives in clinical_context:

{
  "clinical_context": {
    "specimen_pipeline": {
      "spec-001": {
        "currentStage": "verified",
        "orderedAt": "...", "orderedBy": "...",
        "collectedAt": "...", "collectedBy": "...",
        "receivedAt": "...", "receivedBy": "...",
        "analyzingAt": "...",
        "resultedAt": "...",
        "verifiedAt": "...", "verifiedBy": "..."
      }
    },
    "latest_labs": {
      "HGB": { "value": 12.5, "unit": "g/dL", "refLow": 12.0, "refHigh": 17.5, "critical": false },
      "WBC": { "value": 8.2, "unit": "x10^3/uL", "refLow": 4.0, "refHigh": 11.0, "critical": false },
      "K":   { "value": 6.7, "unit": "mmol/L", "refLow": 3.5, "refHigh": 5.0, "critical": true }
    }
  }
}

Layer 5 — Frontend Consumption

useLaboratoryResultsSupabase Hook

File: web/packages/miniapps/laboratory-results/useLaboratoryResultsSupabase.ts

Two modes:

Mode When Scoping
Encounter-scoped encounterId provided Only labs for this admission/visit
Patient-wide No encounterId Recent labs across encounters, bounded by withinDays (default 90)

Encounter-scoped mode is designed for long-stay IPD patients (months-long admissions) — it prevents pulling thousands of historical rows. Patient-wide mode suits OPD workflows and the Widget Rail badge.

Options:

interface UseLaboratoryResultsOptions {
  encounterId?: string | null;  // scope to single encounter
  withinDays?: number;          // default 90 for patient-wide mode
  limit?: number;               // default 200 queue rows
}

Two initial queries:

  1. department_queues — filtered by patient_id + dept_type='lab'
    • optional encounter_id or created_at >= cutoff, capped by limit
  2. hospital_events — filtered by patient_id + 7 LAB_* event types
    • same encounter/time scoping, capped at limit * 10

Realtime subscriptions (debounced incremental):

Channel: lab-results-{patientId}[-{encounterId}]

  postgres_changes on department_queues
    filter: patient_id=eq.{patientId}
    event: * (INSERT, UPDATE, DELETE)
    → client-side filter: dept_type='lab', optional encounter match
    → debounced reload (300ms coalesce window)

  postgres_changes on hospital_events
    filter: patient_id=eq.{patientId}
    event: INSERT
    → client-side filter: LAB_* event types, optional encounter match
    → debounced reload (300ms coalesce window)

Debouncing coalesces burst updates (e.g. a batch of specimen-stage events from LIS auto-import) into a single re-fetch instead of N sequential reloads.

Projection logic:

  1. Each department_queues row becomes a LabPanelResult
  2. Panel names, specimen type, analytes extracted from metadata JSONB
  3. hospital_events matched to queue rows via payload.ticket_id or payload.orderId
  4. Events sorted by occurred_at → build SpecimenTracking.timeline[]
  5. Latest event determines currentStage
  6. Critical flags derived from analyte flags in metadata

Returns:

{
  panels: LabPanelResult[] | null,  // null = not yet loaded
  loading: boolean,
  error: Error | null,
  realtimeConnected: boolean
}

LabResultsWidget

File: web/packages/miniapps/laboratory-results/LabResultsWidget.tsx

Compact widget rendered inside the Widget Rail flyout (320px wide):

  • Summary chips — pending count (orange), completed count (green), critical count (red)
  • Live indicator — CloudDone/CloudOff icon showing Supabase realtime status
  • Panel list — “In Progress” and “Completed” sections
    • Each row: colored stage dot, Thai+English name, order number, stage chip
    • Warning icon for unacknowledged critical values
  • Footer — “Open full lab results” button to promote to DynamicContentRenderer tab
  • States — loading spinner, Supabase error display, empty state message

Props:

interface LabResultsWidgetProps {
  encounterId: string;
  patientId: string;
  locale?: string;
  onClose: () => void;
  onOpenTab?: () => void;
  panels?: LabPanelResult[];      // pre-fetched bypass
  disableLive?: boolean;          // force sample data
}

Type System

File: web/packages/miniapps/laboratory-results/types.ts

Type Purpose
SpecimenStage 7 stages: ordered → collected → received → in-analysis → resulted → verified / rejected
LabFlag Result interpretation: normal, low, high, critical-low, critical-high, abnormal
LabPanelCategory 13 categories: hematology, chemistry, liver, renal, coagulation, etc.
LabValueKind Value representation: numeric, positive-negative, text, titer
LabAnalyte Individual test within a panel (code, value, unit, reference, flag, trend)
LabPanelResult Complete panel: analytes + specimen tracking + status + bilingual names
SpecimenTracking Physical tube: currentStage + timeline entries + container ID + ETA
LabReferenceRange Numeric bounds + critical bounds + qualitative text
LabTrendPoint Historical value for sparkline charts

Demo Seed Data

File: infrastructure/medbase/migrations/081_laboratory_demo_seed.sql

Seeds 8 lab orders for demo patient pt-demo-lab-001 (HN 00012345):

Order Panel Stage Notable
1 CBC verified Normal results
2 Electrolytes verified K=6.7 critical-high
3 Liver Function resulted Pending verification
4 Renal Function in-analysis Analyzer processing
5 Coagulation received Lab received tube
6 Lipid Panel collected Nurse drew specimen
7 Thyroid Function ordered Just placed
8 Urinalysis rejected Contaminated sample

All orders include full bilingual metadata, analyte values where applicable, and corresponding hospital_events timeline entries.

Critical Value Escalation Flow

Lab result with alertTier=3
  │
  ▼
handleLabResultFiled()
  │
  ├─▶ encounter_journey_cache.latest_labs[code].critical = true
  │
  ├─▶ observations row with interpretation = 'critical-high' or 'critical-low'
  │
  └─▶ acknowledgement_requests INSERT
        ├── deadline: now() + 30 min
        ├── escalation[0]: 15 min → charge_nurse
        └── escalation[1]: 30 min → attending_physician
              │
              ▼
        AcknowledgementInbox FAB (global, mounted in App.tsx)
        + push notification via messaging service

The LabResultsWidget shows a red WarningAmberIcon on any panel row where hasCritical && !criticalAcknowledged. The full lab miniapp (promoted via “Open full tab”) provides the acknowledge action.

Acknowledgement as a Separate Bounded Context (DDD)

The acknowledgement_requests table is a shared kernel — owned by the ack domain, consumed by every feature that needs “has this been seen?” semantics. The lab pipeline does not denormalize ack state into department_queues metadata. Instead, the widget queries the ack domain directly:

useLaboratoryResultsSupabase.load()
  │
  ├─▶ department_queues  (lab panels, specimens, statuses)
  ├─▶ hospital_events    (specimen timeline events)
  └─▶ acknowledgement_requests
        WHERE subject_order_type = 'lab'
          AND patient_ref = $patientId
          AND status = 'acknowledged'
          AND active = true
        → Set<subject_order_id>   (ackedOrderIds)

toPanel(queue, events, ackedOrderIds)
  → criticalAcknowledged = ackedOrderIds.has(ticket_id) || ackedOrderIds.has(encounter_id)

A third realtime subscription on acknowledgement_requests (filtered by patient_ref and subject_order_type='lab') triggers a debounced reload so the badge clears within ~300ms of an ack response — no trigger, no denormalization, single source of truth.

Sequence Diagram — Happy Path

Clinician          LabRequest Service    MongoDB    Supabase             Orchestrator            Frontend
   │                     │                  │          │                      │                     │
   │─── order lab ──────▶│                  │          │                      │                     │
   │                     │── write ────────▶│          │                      │                     │
   │                     │── emit LAB_ORDER_CREATED ──▶│ hospital_events      │                     │
   │                     │                  │          │── trigger ──────────▶│                     │
   │                     │                  │          │                      │── INSERT dept_queues │
   │                     │                  │          │                      │── PATCH ejc specimen │
   │                     │                  │          │◀─────────────────────│                     │
   │                     │                  │          │── realtime ─────────────────────────────────▶│
   │                     │                  │          │                      │                     │── re-render
   │                     │                  │          │                      │                     │
   ┊  (nurse collects, lab receives, analyzer runs — same pattern per stage) ┊                     │
   │                     │                  │          │                      │                     │
   │                     │── results ──────▶│          │                      │                     │
   │                     │── emit LAB_RESULT_FILED ───▶│ hospital_events      │                     │
   │                     │                  │          │── trigger ──────────▶│                     │
   │                     │                  │          │                      │── PATCH ejc labs     │
   │                     │                  │          │                      │── INSERT observations│
   │                     │                  │          │                      │── (if critical) ack  │
   │                     │                  │          │◀─────────────────────│                     │
   │                     │                  │          │── realtime ─────────────────────────────────▶│
   │                     │                  │          │                      │                     │── badge + alert

Backend Event Emitters

The labRequest.controller.mixin.ts emits specimen-stage events at each lifecycle transition via EncounterJourneyService.emitLabSpecimenEvent():

LabRequestStatus Emitted Event Orchestrator Handler
(created via createCache) LAB_ORDER_CREATED handleLabSpecimenPipeline
collected LAB_SPECIMEN_COLLECTED handleLabSpecimenPipeline
sent-specimens LAB_SPECIMEN_COLLECTED handleLabSpecimenPipeline
accepted-specimens LAB_SPECIMEN_RECEIVED handleLabSpecimenPipeline
resulted LAB_RESULTED + LAB_RESULT_FILED both handlers
completed LAB_VERIFIED + LAB_RESULT_FILED both handlers
(via rejectLabRequestStatus) LAB_SPECIMEN_REJECTED handleLabSpecimenPipeline

All events are inserted into hospital_events (Supabase), which triggers the encounter-orchestrator via pg_net.http_post (migration 002_orchestrator_trigger_hardening).

File Inventory

File Layer Purpose
services/medication/.../labRequest/labRequest.controller.mixin.ts Backend LabRequest CRUD, result filing, specimen-stage + LAB_RESULT_FILED emission
services/medication/.../encounterJourney/encounterJourney.service.ts Backend emitLabSpecimenEvent() + emitLabResultFiled() — inserts into hospital_events
infrastructure/medbase/functions/_shared/event-contract.ts Event bus Legacy ↔ normalized event type mappings
infrastructure/medbase/functions/encounter-orchestrator/index.ts Orchestrator handleLabSpecimenPipeline, handleLabResultFiled
infrastructure/medbase/migrations/20260512_observation_unification.sql Schema observations table + lab→hypertable trigger
infrastructure/medbase/migrations/021_timescale_vitals.sql Schema lab_results_ts TimescaleDB hypertable
infrastructure/medbase/migrations/081_laboratory_demo_seed.sql Seed 8 demo lab orders with full analytes
web/packages/miniapps/laboratory-results/types.ts Frontend LabPanelResult, SpecimenStage, LabAnalyte, etc.
web/packages/miniapps/laboratory-results/useLaboratoryResultsSupabase.ts Frontend Supabase queries + realtime subscription hook
web/packages/miniapps/laboratory-results/LabResultsWidget.tsx Frontend Compact widget for Widget Rail flyout
web/packages/miniapps/laboratory-results/index.ts Frontend Barrel + widgetSurface export
web/packages/ui-kit/src/components/widget-rail/WidgetRail.tsx Frontend Dock + flyout chrome (hosts the widget)
web/sandbox/targets/WidgetRailTarget.tsx Dev Sandbox demo with mock patient profile

Extending the Pipeline

Adding a new analyte/panel type

No code changes needed. The pipeline is panel-agnostic — any lab order that emits the standard 8 event types will flow through automatically. The panel name, specimen type, and analyte codes are carried in event payloads, not hardcoded.

Adding trend charts

Query lab_results_ts for historical values:

SELECT time, value_numeric, unit
FROM lab_results_ts
WHERE patient_id = $1 AND test_code = $2
ORDER BY time DESC
LIMIT 20;

The LabAnalyte.trend field already supports LabTrendPoint[] — populate it from the hypertable query and the widget will render sparklines.

Adding new result sources (HL7v2 ORU, FHIR write)

Any source that can emit LAB_RESULT_FILED / LAB_SPECIMEN_* events into hospital_events will be picked up by the orchestrator. Current sources:

  1. Manual entry via LabRequest service
  2. Roche LIS auto-import (updateLabReportForLisRoche)
  3. HL7v2 ORU mapper (services/interoperability/.../oru-to-medos.mapper.ts)
  4. FHIR write API (services/public-api/.../fhir-write.controller.ts)

Connecting to a new LIS

See docs/architecture/ris-adapter-design.md for the bidirectional adapter pattern. Lab instruments follow the same ORM/ORU flow — the adapter translates LIS-specific HL7v2 segments into the standard event payloads.

Ask Anything