medOS ultra

Injection Timeline to MAR

How the injection/anesthesia timeline components plug into the MAR.

6 min read diagramsUpdated 2026-05-13docs/architecture/injection-timeline-mar-integration.md

How the new InjectionTimelineLive / AnesthesiaTimelineLive components plug into the existing MAR system, with the canonical write path going through MongoDB and the realtime read path coming through Supabase. Machine indication (anesthesia machine / infusion pump / external HL7v2 ORM/ORU adapters) feeds the same canonical write path — so the timeline reflects whatever the source of truth says, regardless of whether it was a nurse tap or a machine event.


What’s already in place

Frontend — the newest wired MAR

  • MarSystemEnhanced.tsx — the production MAR. The other variants (MarSystem, MarSystemOld, eMar) are older or shells.
  • useSupabaseEmarSync.ts — subscribes to two Supabase realtime channels filtered by encounter:
    • encounter_journey_cache — patient manifest changes
    • medication_administrations — admin events (encounter_ref=eq.${encounterId})
  • MarSystemProvider.tsx — REST backbone:
    • createMedicationAdministrationApi
    • listAllMedicationAdministrationApi
    • updateMedicationAdministrationApi
    • listAllMedicationAdministrationEventStatusApi
    • getMedicationRequestListByEncounter (from ever-medication)

Supabase read model — already supports infusion shape

20260507_medication_administrations.sql creates a table that natively supports continuous infusions. Relevant columns:

Column Type Role for the timeline
id UUID InjectionRun.id
patient_id, encounter_id, medication_request_id, med_order_id UUID Linkage / filter
administered_at TIMESTAMPTZ InjectionRun.startedAt
status enum (administered, held, refused, omitted, self_administered, not_given) Used to decide if the run is closed
is_infusion bool Filter — only true rows become timeline rows
pump_id text Joins to the machine indication source
infusion_rate, rate_unit number / text InjectionRun.rateLabel (e.g. 125 mL/hr)
bag_volume_ml, bag_remaining_ml number Bag tracking (extension point)
route varchar(20) PO / IV / IM / SC / SL / INH / TOP / PR — used to split injection vs discrete

The medication_administrations table needs to gain stopped_at or equivalent (today the timeline derives “stopped” from status != administered + a future end timestamp). See Gap below.

MongoDB — canonical write source

  • MedicationAdministration.ts — Mongo entity. Collection: medication_administration.
  • Range fields: start: Date, end: Date — supports continuous infusions out of the box.
  • Nested infusion?: { rate, hour, bagNumber } — captures rate / hourly volume / bag number.
  • Five-Rights array, witness refs, medicationProviderRef (administered_by).
  • POST endpoint: medicationAdministration.create/medicationAdministrations (Moleculer action in services/medication).

The gap

There is no Mongo → Supabase projection for medication administration events today.

When medicationAdministration.create writes to Mongo, no edge function listens and no NATS-driven trigger upserts into Supabase medication_administrations. The frontend currently fills that table through manual refetches and via encounter_journey_cache side-effects. As a result:

  • If a nurse / machine writes to Mongo, the InjectionTimelineLive bar will not start growing automatically.
  • The same flow is invisible to anyone else watching the encounter unless they refetch.

This is the single integration piece that needs to land before the timeline becomes truly machine-sync ready end-to-end.


Target architecture

   ┌───────────────────────────────────────────────────────────────┐
   │ Source of administration event:                                │
   │   • Nurse tap on MAR (MarSystemEnhanced)                       │
   │   • Anesthesia machine / infusion pump (HL7v2 ORU / ORM)       │
   │   • External adapter (RIS, pharmacy robot)                     │
   └────────────────────────────┬──────────────────────────────────┘
                                │ POST /medicationAdministrations
                                ▼
   ┌──────────────────────────────────────────────┐
   │ MongoDB  (canonical, write truth)            │
   │   collection: medication_administration       │
   │   entity has start + end + infusion{…}        │
   └────────────────────────────┬──────────────────┘
                                │ NATS event:
                                │  medication.administration.{created,updated,completed}
                                ▼
   ┌──────────────────────────────────────────────┐
   │ Edge function: mar-mongo-sync  ← NEW          │
   │   (Deno, infrastructure/medbase/functions/)   │
   │   upsert into Supabase medication_admins       │
   └────────────────────────────┬──────────────────┘
                                │ postgres_changes
                                ▼
   ┌──────────────────────────────────────────────┐
   │ Supabase (read model cache)                  │
   │   table: medication_administrations           │
   └────────────────────────────┬──────────────────┘
                                │ supabase.channel(...).on(...)
                                ▼
   ┌──────────────────────────────────────────────┐
   │ MarSystemEnhanced + InjectionTimelineLive    │
   │   row per medication where is_infusion=true   │
   └──────────────────────────────────────────────┘

Key constraint (from web/CLAUDE.md): the frontend never writes directly to Supabase read-model tables. All mutations flow through the Mongo write path; Supabase is one-way out.


Concrete plan

Step 1 — Mount InjectionTimelineLive inside MarSystemEnhanced

For every medication where the route is IV / SC / IM (or is_infusion=true), render one InjectionTimelineLive strip aligned to the medication row. Reuse the existing useSupabaseEmarSync subscription — do not open a second realtime channel for the same table.

Step 2 — Adapter hook: Supabase rows → InjectionRun[]

New hook useInfusionRunsForMar(encounterId, medRequestId):

function useInfusionRunsForMar(encounterId: string, medRequestId: string): InjectionRun[] {
  const { administrations } = useSupabaseEmarSync(encounterId);
  return useMemo(
    () =>
      administrations
        .filter((a) => a.medication_request_id === medRequestId && a.is_infusion)
        .map<InjectionRun>((a) => ({
          id: a.id,
          startedAt: a.administered_at,
          stoppedAt: a.status === 'administered' ? a.end_time ?? undefined : a.administered_at,
          rateLabel: a.infusion_rate ? `${a.infusion_rate} ${a.rate_unit ?? 'mL/hr'}` : undefined,
          drugLabel: a.medication_name,
        })),
    [administrations, medRequestId],
  );
}

The hook reuses the data useSupabaseEmarSync already populates, so we don’t double-subscribe. InjectionTimelineLive is then driven by passing dataSource={{ kind: 'static', runs }} (the static variant added in InjectionTimelineLive.tsx).

Step 3 — “Done” path: canonical write to Mongo

When (later) a manual stop control is re-added, or when the machine fires a stop event, the path is the same:

await updateMedicationAdministrationApi(adminId, {
  status: 'completed',
  end: new Date().toISOString(),
});

Mongo is updated → NATS event → edge function (Step 4) → Supabase row gets status='completed' → realtime subscription pushes the change → the timeline bar caps and the live pulse stops.

The frontend never touches Supabase for this write.

Step 4 — Close the gap: mar-mongo-sync edge function

A new Deno edge function at infrastructure/medbase/functions/mar-mongo-sync/index.ts. Listens to the hospital_events topic for medication.administration.* events and upserts into medication_administrations keyed by id. ~80 lines, mirrors the encounter orchestrator pattern documented at docs/architecture/encounter-orchestrator-triggers.md.

Required Supabase schema additions

The existing medication_administrations table needs one column for the range close:

ALTER TABLE medication_administrations ADD COLUMN end_time TIMESTAMPTZ;
COMMENT ON COLUMN medication_administrations.end_time IS
  'Range end for continuous infusions. Null while infusion is running.';

Optional but useful for the InjectionTimelineLive drugLabel:

ALTER TABLE medication_administrations ADD COLUMN medication_name TEXT;

(or join to medication_request on read — adapter hook can resolve either way.)


Machine indication ingestion

Two sources to wire over time, both reduce to “produce an AdministrationEvent and POST to /medicationAdministrations”:

Source Adapter Status
Anesthesia machine (continuous gas / drip) HL7v2 ORU R01 via services/interoperability/.../modules/hl7v2/oru-to-medos.mapper.ts (already exists for lab — extend for med admin) Open
Infusion pump MLLP listener on port 2575 (already exists), new mapper for pump.run.{started,completed} events Open
External vendor (e.g. BD Alaris, Hospira) Vendor-specific adapter writing into the same shape Open

All three converge on the same Mongo write → so the timeline doesn’t have to know who initiated the event.


Frontend surfaces today (verified)

  • DynamicContentRenderer cases registered:
    • modules.InjectionTimelineLive → single-drug panel
    • modules.AnesthesiaTimelineLive → 6-drug anesthesia stack
  • Sandbox targets:
    • http://localhost:5189/?target=InjectionTimelineLive
    • http://localhost:5189/?target=AnesthesiaTimelineLive
  • Component file map:

Open questions

  1. Do we want end_time as a new column, or model the range as two rows (start event + completion event) keyed by a run_id? Two-row model is more FHIR-faithful but more code. Single-row with end_time is simpler and matches the Mongo entity shape (start, end).
  2. Should mar-mongo-sync also project to encounter_journey_cache.medications to keep the patient manifest fresh, or leave that to the existing encounter orchestrator?
  3. For the in-renderer wiring inside MarSystemEnhanced — is the right place inline under each medication row, or a side-panel drawer that opens for the currently selected medication? The first is more glanceable, the second saves vertical space.

References

Ask Anything