// Fixture routes for invitations, patients, shared data, records, reading and checks. import { randomUUID } from "node:crypto"; import type * as T from "../../src/api/types"; import { modelProvider, transcribe } from "../../server/model"; import { exportLines, printableHtml, readRecords, runCheckRun, toSources, type StoredExtraction } from "./clinical"; import { connectMariam, iso, nextId, type StoredPatient, type StoredRecord } from "./data"; import { clinicianName, HttpError, invalid, need, route, text, type Ctx, type Route } from "./http"; import { logActivity, patientDetail, recordsFor, renamedDeep, type Store } from "./store"; import { fingerprint } from "../../server/context"; import { wireCheck, wireDetail, wireExtraction, wireInvitation, wireItem, wireRecord, wireTile } from "./wire"; const MB = 1024 * 1024; const ACCEPT: Record = { "text/plain": "TXT", "application/pdf": "PDF", "image/jpeg": "JPG", "image/png": "PNG", "image/heic": "HEIC", }; const patientOf = (ctx: Ctx): StoredPatient => need(ctx.store.patients.get(decodeURIComponent(ctx.params[0])), "That patient"); const isStale = (store: Store, ex: StoredExtraction) => fingerprint(toSources(recordsFor(store, ex.patientId))) !== ex.fingerprint; const publicExtraction = (store: Store, ex: StoredExtraction) => wireExtraction(ex, isStale(store, ex)); const lastCheckAt = (store: Store, p: StoredPatient) => (p.base.lastCheckId ? store.checks.get(p.base.lastCheckId)?.ranAt ?? null : null); const latestCheck = (store: Store, check: T.CheckRun) => (store.patientChecks.get(check.patientId) ?? []).at(-1) === check.id; // The backend refuses (409 PATIENT_NOT_VISIBLE / 403) shared reads for a patient that is not connected to this clinician. const sharedPatient = (ctx: Ctx) => { const p = patientOf(ctx); if (p.base.kind !== "connected") throw new HttpError(404, "PATIENT_NOT_VISIBLE", "This patient is not shared with you."); return p; }; function shared(ctx: Ctx, section: T.SharedSection | null) { const p = sharedPatient(ctx); const s = p.base.sharing; if (!s || s.status === "stopped" || !p.shared) throw new HttpError(403, "SHARING_STOPPED", "This patient stopped sharing with you."); if (s.status === "paused") throw new HttpError(403, "SHARING_PAUSED", "This patient paused sharing. New information is hidden until they resume."); if (section && !s.scopes.includes(section)) throw new HttpError(403, "SCOPE_NOT_SHARED", "This patient does not share this with you."); // As the backend: the previous look at this section, then this one is recorded. const lastLookedAt = section ? p.lastLooked[`shared.${section}`] ?? null : null; if (section) p.lastLooked[`shared.${section}`] = new Date().toISOString(); return { p, asOf: new Date().toISOString(), lastLookedAt }; } function addRecord(store: Store, p: StoredPatient, fields: Omit): StoredRecord { const rec: StoredRecord = { ...fields, id: nextId("rec"), patientId: p.base.id, addedAt: new Date().toISOString(), addedBy: clinicianName(store) }; store.records.set(rec.id, rec); return rec; } export const patientRoutes: Route[] = [ route("GET", "/invitations", "patients", ({ store }) => ({ invitations: [...store.invitations.values()].map(({ token: _t, connect: _c, ...inv }) => wireInvitation(inv)), })), route("POST", "/invitations/:id/accept", "patients", (ctx) => { const inv = need(ctx.store.invitations.get(ctx.params[0]), "That invitation"); if (ctx.body?.confirmedMyPatient !== true) throw invalid("Check the highlighted fields.", [{ path: "confirmedMyPatient", message: "Invalid input: expected true" }]); const link = typeof ctx.body.linkToPatientId === "string" ? ctx.body.linkToPatientId : null; let patient: StoredPatient; if (inv.id === "inv-mariam") { const existing = ctx.store.patients.get("p-mariam")!; if (link === "p-mariam") patient = existing; else { patient = { ...existing, base: { ...existing.base, id: "p-mariam-app" }, activity: [] }; ctx.store.patients.set(patient.base.id, patient); } const appRecord = renamedDeep(connectMariam(patient, Date.now())); appRecord.id = `rec-${patient.base.id}-app`; appRecord.patientId = patient.base.id; ctx.store.records.set(appRecord.id, appRecord); patient.newSinceLastCheck = true; patient.base.newInformation = { since: new Date().toISOString(), items: ["Medicines, allergies and doses from MARMAR Health"] }; } else { patient = inv.connect(); ctx.store.patients.set(patient.base.id, patient); } const by = inv.withPerson ? `${inv.patientName.split(" ")[0]} and ${inv.withPerson.name}` : inv.patientName.split(" ")[0]; logActivity(patient, by, "patient", `${by} invited Dr Okafor from MARMAR Health.`); logActivity(patient, clinicianName(ctx.store), "clinician", `Dr Okafor accepted and confirmed ${inv.patientName} is his patient${link ? ", linked to the clinic record" : ""}.`); ctx.store.invitations.delete(inv.id); return { patientId: patient.base.id, shareId: inv.id }; }), route("POST", "/invitations/:id/decline", "patients", (ctx) => { const inv = need(ctx.store.invitations.get(ctx.params[0]), "That invitation"); const reason = ctx.body?.reason ?? null; if (reason !== null && !["not_my_patient", "not_for_context", "not_using_context", "other"].includes(reason)) throw invalid("Check the highlighted fields.", [{ path: "reason", message: "Invalid option" }]); ctx.store.invitations.delete(inv.id); return { declined: true }; }), route("GET", "/patients", "patients", ({ store }) => ({ items: [...store.patients.values()].map((p) => wireItem(patientDetail(store, p), lastCheckAt(store, p))), asOf: new Date().toISOString(), })), route("POST", "/patients/search", "patients", (ctx) => { const q = text(ctx.body?.query, 120).toLowerCase(); if (!q) throw invalid("Check the highlighted fields.", [{ path: "query", message: "Too small" }]); return { items: [...ctx.store.patients.values()].filter((p) => p.base.name.toLowerCase().includes(q)).map((p) => wireItem(patientDetail(ctx.store, p), lastCheckAt(ctx.store, p))), asOf: new Date().toISOString(), }; }), route("POST", "/patients", "patients", (ctx) => { const name = text(ctx.body?.displayName, 120); if (name.length < 2) throw invalid("Check the highlighted fields.", [{ path: "displayName", message: "Too small" }]); const a = ctx.body?.attestation; if (a?.careRelationship !== true || a?.patientAgreement !== true || a?.version !== "2026-10-30") throw invalid("Check the highlighted fields.", [{ path: "attestation", message: "Confirm the care relationship and the patient's agreement" }]); const dob = typeof ctx.body.dateOfBirth === "string" ? ctx.body.dateOfBirth : null; if (!dob === (ctx.body.ageYears === undefined)) throw invalid("Check the highlighted fields.", [{ path: "dateOfBirth", message: "Give a date of birth or an age" }]); const id = `p-${randomUUID().slice(0, 8)}`; const p: StoredPatient = { base: { id, kind: "clinic_added", name, firstName: name.split(" ")[0], age: dob ? Math.floor((Date.now() - Date.parse(dob)) / (365.25 * 86_400_000)) : Number(ctx.body.ageYears), sharing: null, lastActivityAt: new Date().toISOString(), dateOfBirth: dob, birthYear: dob ? Number(dob.slice(0, 4)) : null, sex: ctx.body.sex === "female" || ctx.body.sex === "male" ? ctx.body.sex : null, phone: null, pronoun: null, preferredTitle: null, language: "en", hasOwnAccount: false, identityOnFile: dob ? "date_of_birth" : "none", timeZone: "Africa/Lagos", quietHours: { start: "22:00", end: "07:00" }, blockedTimes: [], lastCheckId: null, newInformation: null, }, shared: null, lastDaily: null, lastLooked: {}, newSinceLastCheck: false, usedThisWeekBefore: 0, variant: "film", activity: [], }; logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor added ${name} and confirmed a care relationship and the patient's agreement.`); ctx.store.patients.set(id, p); // As the backend: 201 with the bare patient row, not the detail. const { attention: _a, lastCheckId: _l, newInformation: _n, sharing: _s, medicines: _m, doses: _d, lastCheckIn: _c, name: _name, age: _age, ...row } = wireItem(patientDetail(ctx.store, p)); return { patient: row }; }, 201), route("GET", "/patients/:id", "patients", (ctx) => { const p = patientOf(ctx); return { patient: wireDetail(patientDetail(ctx.store, p), lastCheckAt(ctx.store, p)) }; }), route("GET", "/patients/:id/shared", "patients", (ctx) => wireTile(patientDetail(ctx.store, shared(ctx, null).p))), route("POST", "/patients/:id/viewed", "patients", (ctx) => { const p = patientOf(ctx); const section = text(ctx.body?.section, 20); if (!["medicines", "doses", "check_ins", "records", "overview"].includes(section)) throw invalid("Check the highlighted fields.", [{ path: "section", message: "Invalid option" }]); const lastLookedAt = p.lastLooked[`viewed.${section}`] ?? null; p.lastLooked[`viewed.${section}`] = new Date().toISOString(); return { lastLookedAt }; }), route("GET", "/patients/:id/activity", "patients", (ctx) => ({ events: [...patientOf(ctx).activity].reverse() })), route("GET", "/patients/:id/shared/medicines", "patients", (ctx) => { const { p, asOf, lastLookedAt } = shared(ctx, "medicines"); return { ...p.shared!.medicines, asOf, lastLookedAt }; }), route("GET", "/patients/:id/shared/doses", "patients", (ctx) => { const { p, asOf, lastLookedAt } = shared(ctx, "doses"); return { ...p.shared!.doses, asOf, lastLookedAt, complete: true }; }), route("GET", "/patients/:id/shared/check-ins", "patients", (ctx) => { const { p, asOf, lastLookedAt } = shared(ctx, "check_ins"); return { daily: p.shared!.daily, requested: [], asOf, lastLookedAt }; }), route("GET", "/patients/:id/shared/records", "patients", (ctx) => { const { p, asOf, lastLookedAt } = shared(ctx, "records"); return { ...p.shared!.records, asOf, lastLookedAt }; }), route("GET", "/patients/:id/records", "patients", (ctx) => ({ records: recordsFor(ctx.store, patientOf(ctx).base.id).map(wireRecord), })), route("GET", "/records/:id/lines", "patients", (ctx) => { const rec = need(ctx.store.records.get(ctx.params[0]), "That record"); return { recordId: rec.id, lines: (rec.text ?? "").split("\n").map((line, i) => ({ number: i + 1, text: line, page: null, box: null, confidence: null, hardToRead: false, handwritten: false })), }; }), route("PATCH", "/records/:id", "patients", (ctx) => { const rec = need(ctx.store.records.get(ctx.params[0]), "That record"); if (rec.origin === "app" || rec.origin === "check_in") throw new HttpError(403, "READ_ONLY", "Records from MARMAR Health and check-ins cannot be edited."); const title = text(ctx.body?.title, 120); if (ctx.body?.title !== undefined && !title) throw invalid("A record needs a title."); if (title) rec.title = title; if (ctx.body?.type) rec.type = ctx.body.type; if (ctx.body?.place !== undefined) rec.place = text(ctx.body.place, 120) || null; if (ctx.body?.recordDate !== undefined) rec.recordDate = text(ctx.body.recordDate, 10) || null; return wireRecord(rec); }), route("POST", "/records/:id/confirm-patient", "patients", (ctx) => { const rec = need(ctx.store.records.get(ctx.params[0]), "That record"); if (!rec.heldBack) throw new HttpError(409, "NOT_HELD_BACK", "This record is not held back."); rec.heldBack = null; return wireRecord(rec); }), route("POST", "/records/:id/read-again", "patients", (ctx) => { const rec = need(ctx.store.records.get(ctx.params[0]), "That record"); if (rec.reading !== "failed") throw new HttpError(409, "NOT_FAILED", "Only a record that could not be read is read again."); return wireRecord(rec); }), route("DELETE", "/records/:id", "patients", (ctx) => { const rec = need(ctx.store.records.get(ctx.params[0]), "That record"); if (rec.origin === "app") throw new HttpError(403, "READ_ONLY", "Records from MARMAR Health are removed in the app."); ctx.store.records.delete(rec.id); return { deleted: true, removedAt: new Date().toISOString(), reading: "unchanged" }; }), route("POST", "/patients/:id/records/uploads", "patients", (ctx) => { const p = patientOf(ctx); const fileName = text(ctx.body?.fileName, 200); const size = Number(ctx.body?.sizeBytes); const type = text(ctx.body?.contentType, 100); if (!ACCEPT[type]) throw invalid(`${fileName} is not a file Context reads. Use TXT, PDF, JPG, PNG or HEIC.`); if (size > 15 * MB) throw new HttpError(400, "FILE_TOO_LARGE", `${fileName} is ${(size / MB).toFixed(1)} MB, ${((size - 15 * MB) / MB).toFixed(1)} MB over the 15 MB limit per file.`); const uploadId = randomUUID(); ctx.store.uploads.set(uploadId, { patientId: p.base.id, fileName, contentType: type, sizeBytes: size, data: null }); return { uploadId, uploadUrl: `${ctx.url.origin}/__fixture/uploads/${uploadId}`, method: "PUT", headers: { "Content-Type": type }, expiresAt: iso(Date.now() + 15 * 60_000), maxBytes: 15 * MB }; }, 201), route("POST", "/patients/:id/records", "patients", async (ctx) => { const p = patientOf(ctx); if (recordsFor(ctx.store, p.base.id).length >= 20) throw new HttpError(400, "TOO_MANY_RECORDS", "A check can use up to 20 records. Remove one before adding another."); const title = text(ctx.body?.title, 120) || "Untitled record"; const type = ["prescription", "lab_result", "discharge_summary", "clinic_note", "other"].includes(ctx.body?.type) ? ctx.body.type : "other"; const common = { title, type, place: text(ctx.body?.place, 120) || null, recordDate: text(ctx.body?.recordDate, 10) || null, pages: null }; let rec: StoredRecord; if (ctx.body?.uploadId) { const up = need(ctx.store.uploads.get(ctx.body.uploadId), "That upload"); if (!up.data) throw invalid("The file has not finished uploading. Try again."); const isText = up.contentType === "text/plain"; // Photos and PDFs: Meta Muse Spark transcribes them when it is the configured model. const byModel = !isText && modelProvider() === "meta"; const body = isText ? up.data.toString("utf8").replace(/\r\n/g, "\n").trim() : byModel ? await transcribe(up.data, up.contentType) : null; const read = isText || byModel; rec = addRecord(ctx.store, p, { ...common, origin: ctx.body.origin === "camera" ? "camera" : "upload", fileName: up.fileName, contentType: up.contentType, sizeBytes: up.sizeBytes, reading: read ? "read" : "failed", readingError: read ? null : "The fixture reads text files only. Scans and photos are read on MARMAR's servers.", readingErrorCode: read ? null : "OCR_NOT_CONFIGURED", heldBack: null, clarification: null, lineCount: body ? body.split("\n").length : 0, text: body, }); ctx.store.uploads.delete(ctx.body.uploadId); } else { const body = typeof ctx.body?.text === "string" ? ctx.body.text.replace(/\r\n/g, "\n").trim() : ""; if (!body) throw invalid("Paste or type the record's text."); if (body.length > 12000) throw invalid("This note is longer than 12,000 characters. Split it into smaller records."); rec = addRecord(ctx.store, p, { ...common, origin: "pasted", fileName: null, contentType: "text/plain", sizeBytes: body.length, reading: "read", readingError: null, readingErrorCode: null, heldBack: null, clarification: null, lineCount: body.split("\n").length, text: body }); } logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor added the record "${rec.title}".`); return wireRecord(rec); }, 201), route("POST", "/patients/:id/extractions", "patients", async (ctx) => { const p = patientOf(ctx); const records = recordsFor(ctx.store, p.base.id); if (!records.some((r) => r.reading === "read")) throw new HttpError(400, "NO_READABLE_RECORDS", "Add at least one readable record first."); const ex = await readRecords(p.base.id, records); const previous = ctx.store.extractions.get(ctx.store.latestExtraction.get(p.base.id) ?? ""); if (previous) // Keep decisions for facts that read exactly the same. for (const f of ex.facts) { const old = previous.facts.find((o) => o.id === f.id && o.label === f.label && o.detail === f.detail && o.quote === f.quote); if (old && previous.decisions[old.id]) ex.decisions[f.id] = previous.decisions[old.id]; } ctx.store.extractions.set(ex.id, ex); ctx.store.latestExtraction.set(p.base.id, ex.id); return publicExtraction(ctx.store, ex); }, 201), route("GET", "/patients/:id/extractions/latest", "patients", (ctx) => { const ex = ctx.store.extractions.get(ctx.store.latestExtraction.get(patientOf(ctx).base.id) ?? ""); return { extraction: ex ? publicExtraction(ctx.store, ex) : null }; }), route("GET", "/extractions/:id", "patients", (ctx) => publicExtraction(ctx.store, need(ctx.store.extractions.get(ctx.params[0]), "That reading"))), route("PUT", "/extractions/:id/decisions", "patients", (ctx) => { const ex = need(ctx.store.extractions.get(ctx.params[0]), "That reading"); if (ctx.body?.revision !== ex.revision) throw new HttpError(409, "STALE_REVISION", "This review was changed in another tab. Load the latest version before you go on."); const confirmed: string[] = Array.isArray(ctx.body?.confirmedHardToRead) ? ctx.body.confirmedHardToRead : []; const decisions = ctx.body?.decisions ?? {}; if (confirmed.some((id) => !ex.facts.some((f) => f.id === id && f.hardToRead) || decisions[id] !== "include")) throw invalid("Only an included, hard-to-read fact can be confirmed."); if (Object.entries(decisions).some(([id, d]) => !ex.facts.some((f) => f.id === id) || (d !== "include" && d !== "exclude"))) throw invalid("A decision names a fact outside this reading."); ex.decisions = decisions; ex.coverageReview = Object.fromEntries( Object.entries(ctx.body?.coverageReview ?? {}).map(([id, v]) => [id, { ...(v as object), confirmedBy: clinicianName(ctx.store) }]), ) as T.Extraction["coverageReview"]; ex.acknowledgedOriginals = ctx.body?.acknowledgedOriginals === true; ex.confirmedHardToRead = confirmed; ex.revision += 1; return { revision: ex.revision, savedAt: new Date().toISOString() }; }), route("POST", "/extractions/:id/clarifications", "patients", async (ctx) => { const ex = need(ctx.store.extractions.get(ctx.params[0]), "That reading"); const p = need(ctx.store.patients.get(ex.patientId), "That patient"); const original = need(ctx.store.records.get(text(ctx.body?.recordId, 80)), "That record"); const line = Number(ctx.body?.line); const quote = (original.text ?? "").split("\n")[line - 1]?.trim(); const value = text(ctx.body?.text, 500); const reason = text(ctx.body?.reason, 500); if (!quote || quote !== text(ctx.body?.quote, 12000)) throw invalid("The original line changed. Read the records again."); if (!value) throw invalid("Write the fact you are adding."); if (reason.length < 3) throw invalid("Say how you confirmed it."); addRecord(ctx.store, p, { title: `Clarification ยท line ${line} of ${original.title}`, type: "clinic_note", place: null, recordDate: original.recordDate, pages: null, origin: "pasted", fileName: null, contentType: "text/plain", sizeBytes: value.length, reading: "read", readingError: null, readingErrorCode: null, heldBack: null, clarification: { clarificationId: nextId("clar") }, lineCount: 1, text: value, clarifies: { sourceId: original.id, line, quote, reason, confirmedBy: clinicianName(ctx.store) }, }); logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor added a clarification to "${original.title}".`); const next = await readRecords(p.base.id, recordsFor(ctx.store, p.base.id)); for (const f of next.facts) if (ex.decisions[f.id] && ex.facts.some((o) => o.id === f.id && o.label === f.label && o.detail === f.detail)) next.decisions[f.id] = ex.decisions[f.id]; ctx.store.extractions.set(next.id, next); ctx.store.latestExtraction.set(p.base.id, next.id); return publicExtraction(ctx.store, next); }, 201), route("POST", "/patients/:id/checks", "patients", (ctx) => { const p = patientOf(ctx); const ex = need(ctx.store.extractions.get(text(ctx.body?.extractionId, 80)), "That reading"); if (ex.recordsNotReady.length) throw new HttpError(409, "RECORDS_NOT_READY", `Some records are not ready for the check: ${ex.recordsNotReady.map((r) => `${r.title} (${r.status.replace("_", " ")})`).join(", ")}.`); if (isStale(ctx.store, ex)) throw new HttpError(409, "STALE_EXTRACTION", "The records changed after they were read. Read them again."); if (ex.facts.some((f) => f.hardToRead && ex.decisions[f.id] === "include" && !ex.confirmedHardToRead.includes(f.id))) throw new HttpError(400, "HARD_TO_READ_NOT_CONFIRMED", "Check each included Hard to read value against the original first."); if (ctx.body?.revision !== ex.revision) throw new HttpError(409, "STALE_REVISION", "This review was changed in another tab. Load the latest version first."); let check: T.CheckRun; const newMeds = (p.shared?.medicines.medicines ?? []).filter((m) => m.startDate && Date.now() - Date.parse(m.startDate) < 21 * 86_400_000).map((m) => m.name.toLowerCase()); try { const reactions = Object.fromEntries((p.shared?.medicines.allergies ?? []).map((a) => [a.substance.toLowerCase(), { reaction: a.reaction, onset: a.onset, year: a.year }])); check = runCheckRun(ex, recordsFor(ctx.store, p.base.id), clinicianName(ctx.store), !!p.base.sharing?.withheldSome, newMeds, reactions); } catch (error) { throw invalid(error instanceof Error ? error.message : "The check could not run."); } ctx.store.checks.set(check.id, check); ctx.store.patientChecks.set(p.base.id, [...(ctx.store.patientChecks.get(p.base.id) ?? []), check.id]); p.base.lastCheckId = check.id; p.newSinceLastCheck = false; p.base.newInformation = null; logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor ran the check: ${check.assessment.contextual.findings.length} findings for review.`); return wireCheck(check, true); }, 201), route("GET", "/patients/:id/checks", "patients", (ctx) => ({ checks: (ctx.store.patientChecks.get(patientOf(ctx).base.id) ?? []) .map((id) => ctx.store.checks.get(id)!) .map(({ id, ranAt, ranBy, assessment }) => ({ id, ranAt, ranBy, findings: assessment.contextual.findings.length, added: assessment.addedRuleIds.length })) .reverse(), })), route("GET", "/checks/:id", "patients", (ctx) => { const check = need(ctx.store.checks.get(ctx.params[0]), "That check"); return wireCheck(check, latestCheck(ctx.store, check)); }), route("POST", "/checks/:id/findings/:finding/mark", "patients", (ctx) => { const check = need(ctx.store.checks.get(ctx.params[0]), "That check"); const findingId = decodeURIComponent(ctx.params[1]); if (!check.assessment.contextual.findings.some((f) => f.id === findingId)) throw new HttpError(404, "FINDING_NOT_FOUND", "That finding is not part of this check."); const kind = ctx.body?.kind; if (!["reviewed", "not_relevant", "problem"].includes(kind)) throw invalid("Choose Reviewed, Not relevant or Report a problem."); const reason = text(ctx.body?.reason, 500) || null; const note = text(ctx.body?.note, 1000) || null; if (kind === "not_relevant" && (!reason || reason.length < 3)) throw new HttpError(400, "REASON_REQUIRED", "Say why this finding is not relevant."); if (kind === "problem" && !note && !reason) throw new HttpError(400, "DESCRIPTION_REQUIRED", "Describe the problem for MARMAR's clinical team."); const mark: T.FindingMark = { findingId, kind, note, reason, by: clinicianName(ctx.store), at: new Date().toISOString() }; check.marks.push(mark); const p = ctx.store.patients.get(check.patientId); if (p) logActivity(p, mark.by, "clinician", `Dr Okafor marked a finding ${kind.replace("_", " ")}.`); return mark; }, 201), route("GET", "/checks/:id/export", "patients", (ctx) => { const check = need(ctx.store.checks.get(ctx.params[0]), "That check"); const p = need(ctx.store.patients.get(check.patientId), "That patient"); const format = ctx.url.searchParams.get("format") ?? "json"; if (!["json", "html", "pdf"].includes(format)) throw invalid("Check the highlighted fields.", [{ path: "format", message: "Invalid option" }]); logActivity(p, clinicianName(ctx.store), "clinician", `Dr Okafor exported the check as ${format.toUpperCase()}.`); const name = `marmar-context-${p.base.name.toLowerCase().replace(/\s+/g, "-")}-${check.ranAt.slice(0, 10)}`; // As the backend: pdf and html both answer the printable page. if (format !== "json") { const html = printableHtml("MARMAR Context: check evidence", exportLines(check, p.base.name, check.ranBy, recordsFor(ctx.store, p.base.id))); ctx.res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Content-Disposition": `inline; filename="${name}.html"`, "X-Export-Format": "html", "Content-Security-Policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'" }); ctx.res.end(html); return undefined; } ctx.res.writeHead(200, { "Content-Type": "application/json", "Content-Disposition": `attachment; filename="${name}.json"` }); ctx.res.end(JSON.stringify({ product: "MARMAR Context", schemaVersion: 1, exportedAt: new Date().toISOString(), exportedBy: clinicianName(ctx.store), patient: { name: p.base.name, age: p.base.age, dateOfBirth: p.base.dateOfBirth }, clinic: { name: ctx.store.clinic.name }, check: wireCheck(check, latestCheck(ctx.store, check)), informationTool: check.informationTool }, null, 2)); return undefined; }), ];