mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-11 05:42:19 +00:00
Merge branch 'main' into feat/mood-level-a
This commit is contained in:
86 files changed
+4346
-206
No files matched your search
@@ -980,6 +980,9 @@ const handler = apiHandler(
|
||||
unit: lab.unit,
|
||||
referenceLow: lab.referenceLow ?? null,
|
||||
referenceHigh: lab.referenceHigh ?? null,
|
||||
sourceReferenceLow: lab.sourceReferenceLow ?? null,
|
||||
sourceReferenceHigh: lab.sourceReferenceHigh ?? null,
|
||||
sourceReferenceText: lab.sourceReferenceText ?? null,
|
||||
takenAt: new Date(lab.takenAt),
|
||||
source: lab.source,
|
||||
noteEncrypted:
|
||||
|
||||
@@ -57,6 +57,7 @@ function editToFactData(edit: InboundFactEdit): FactData {
|
||||
unit: edit.unit ?? null,
|
||||
referenceLow: edit.referenceLow ?? null,
|
||||
referenceHigh: edit.referenceHigh ?? null,
|
||||
referenceText: edit.referenceText ?? null,
|
||||
effectiveDate: edit.effectiveDate ?? null,
|
||||
};
|
||||
return data;
|
||||
|
||||
@@ -46,6 +46,9 @@ export const GET = apiHandler(async () => {
|
||||
value: true,
|
||||
referenceLow: true,
|
||||
referenceHigh: true,
|
||||
sourceReferenceLow: true,
|
||||
sourceReferenceHigh: true,
|
||||
sourceReferenceText: true,
|
||||
takenAt: true,
|
||||
},
|
||||
orderBy: { takenAt: "desc" },
|
||||
@@ -58,6 +61,9 @@ export const GET = apiHandler(async () => {
|
||||
value: r.value as number,
|
||||
referenceLow: r.referenceLow,
|
||||
referenceHigh: r.referenceHigh,
|
||||
sourceReferenceLow: r.sourceReferenceLow,
|
||||
sourceReferenceHigh: r.sourceReferenceHigh,
|
||||
sourceReferenceText: r.sourceReferenceText,
|
||||
takenAt: r.takenAt,
|
||||
})),
|
||||
);
|
||||
|
||||
@@ -143,6 +143,12 @@ export const PUT = apiHandler(
|
||||
// historical columns the serialiser ignores. Reject the attempt with a
|
||||
// 422 rather than silently no-op, so the client never believes an edit
|
||||
// landed. Edit such fields on the Biomarker itself instead.
|
||||
//
|
||||
// `sourceReference*` is deliberately NOT on that list. The catalog band is
|
||||
// a property of the marker; the source window is a property of THIS
|
||||
// reading — it is what one report printed beside one value — so it is
|
||||
// editable on a linked row and there is nothing on the biomarker to edit
|
||||
// it on instead.
|
||||
const isLinked = existing.biomarkerId !== null;
|
||||
if (
|
||||
isLinked &&
|
||||
@@ -194,6 +200,15 @@ export const PUT = apiHandler(
|
||||
if (d.unit !== undefined) data.unit = d.unit;
|
||||
if (d.referenceLow !== undefined) data.referenceLow = d.referenceLow;
|
||||
if (d.referenceHigh !== undefined) data.referenceHigh = d.referenceHigh;
|
||||
if (d.sourceReferenceLow !== undefined) {
|
||||
data.sourceReferenceLow = d.sourceReferenceLow;
|
||||
}
|
||||
if (d.sourceReferenceHigh !== undefined) {
|
||||
data.sourceReferenceHigh = d.sourceReferenceHigh;
|
||||
}
|
||||
if (d.sourceReferenceText !== undefined) {
|
||||
data.sourceReferenceText = d.sourceReferenceText;
|
||||
}
|
||||
if (d.takenAt !== undefined) data.takenAt = d.takenAt;
|
||||
|
||||
// Inverted-range guard for a PARTIAL bound update. The schema-level
|
||||
@@ -212,6 +227,20 @@ export const PUT = apiHandler(
|
||||
errorCode: "labs.update.referenceRangeInvalid",
|
||||
});
|
||||
}
|
||||
// The same partial-update hole exists on the source window, and closes the
|
||||
// same way.
|
||||
if (
|
||||
isInvertedRange(
|
||||
effectiveBound(d.sourceReferenceLow, existing.sourceReferenceLow),
|
||||
effectiveBound(d.sourceReferenceHigh, existing.sourceReferenceHigh),
|
||||
)
|
||||
) {
|
||||
return apiError(
|
||||
"sourceReferenceLow must not exceed sourceReferenceHigh",
|
||||
422,
|
||||
{ errorCode: "labs.update.sourceReferenceRangeInvalid" },
|
||||
);
|
||||
}
|
||||
if (d.note !== undefined) {
|
||||
data.noteEncrypted = d.note ? encryptNoteToBytes(d.note) : null;
|
||||
}
|
||||
@@ -239,6 +268,9 @@ export const PUT = apiHandler(
|
||||
panel: existing.panel,
|
||||
referenceLow: existing.referenceLow,
|
||||
referenceHigh: existing.referenceHigh,
|
||||
sourceReferenceLow: existing.sourceReferenceLow,
|
||||
sourceReferenceHigh: existing.sourceReferenceHigh,
|
||||
sourceReferenceText: existing.sourceReferenceText,
|
||||
takenAt: existing.takenAt,
|
||||
},
|
||||
after: {
|
||||
@@ -249,6 +281,9 @@ export const PUT = apiHandler(
|
||||
panel: updated.panel,
|
||||
referenceLow: updated.referenceLow,
|
||||
referenceHigh: updated.referenceHigh,
|
||||
sourceReferenceLow: updated.sourceReferenceLow,
|
||||
sourceReferenceHigh: updated.sourceReferenceHigh,
|
||||
sourceReferenceText: updated.sourceReferenceText,
|
||||
takenAt: updated.takenAt,
|
||||
},
|
||||
redacted: d.note !== undefined ? ["note"] : [],
|
||||
|
||||
@@ -33,6 +33,7 @@ import { withIdempotency } from "@/lib/idempotency";
|
||||
import { enqueueReminderSatisfy } from "@/lib/jobs/reminder-satisfy";
|
||||
import { emitDataArrival } from "@/lib/arrivals/emit-shared";
|
||||
import { resolveOrMintBiomarker } from "@/lib/labs/biomarker-store";
|
||||
import { parseReferenceRange } from "@/lib/labs/parse-reference-range";
|
||||
import { serialiseLabResult } from "@/lib/labs/serialise";
|
||||
import {
|
||||
linkOcrLabsToVaultDocument,
|
||||
@@ -151,12 +152,19 @@ async function commitOcrRows(request: NextRequest) {
|
||||
}
|
||||
|
||||
const isQualitative = row.valueText !== undefined;
|
||||
// The window the scan read off the report. The confirmed numeric bounds
|
||||
// win over a re-parse of the string (the human may have corrected them on
|
||||
// the review screen); the string rides along either way.
|
||||
const printed = parseReferenceRange(row.referenceText, row.unit);
|
||||
const sourceLow = row.referenceLow ?? printed?.low ?? null;
|
||||
const sourceHigh = row.referenceHigh ?? printed?.high ?? null;
|
||||
const sourceText = printed?.text ?? null;
|
||||
const biomarker = await resolveOrMintBiomarker(user.id, {
|
||||
analyte: row.analyte,
|
||||
// A qualitative reading has no numeric unit / range.
|
||||
unit: isQualitative ? (row.unit ?? "") : (row.unit as string),
|
||||
referenceLow: isQualitative ? null : (row.referenceLow ?? null),
|
||||
referenceHigh: isQualitative ? null : (row.referenceHigh ?? null),
|
||||
referenceLow: isQualitative ? null : sourceLow,
|
||||
referenceHigh: isQualitative ? null : sourceHigh,
|
||||
panel: row.panel ?? null,
|
||||
});
|
||||
|
||||
@@ -173,6 +181,9 @@ async function commitOcrRows(request: NextRequest) {
|
||||
unit: biomarker.unit,
|
||||
referenceLow: biomarker.lowerBound,
|
||||
referenceHigh: biomarker.upperBound,
|
||||
sourceReferenceLow: isQualitative ? null : sourceLow,
|
||||
sourceReferenceHigh: isQualitative ? null : sourceHigh,
|
||||
sourceReferenceText: isQualitative ? null : sourceText,
|
||||
takenAt: row.takenAt,
|
||||
source: "OCR",
|
||||
noteEncrypted: null,
|
||||
|
||||
@@ -184,6 +184,9 @@ async function postLabResult(request: NextRequest) {
|
||||
unit,
|
||||
referenceLow,
|
||||
referenceHigh,
|
||||
sourceReferenceLow,
|
||||
sourceReferenceHigh,
|
||||
sourceReferenceText,
|
||||
takenAt,
|
||||
note,
|
||||
source,
|
||||
@@ -248,6 +251,13 @@ async function postLabResult(request: NextRequest) {
|
||||
unit: biomarker.unit,
|
||||
referenceLow: biomarker.lowerBound,
|
||||
referenceHigh: biomarker.upperBound,
|
||||
// The window this reading's own report printed, kept beside the catalog
|
||||
// stamp rather than folded into it: it governs THIS reading's verdict,
|
||||
// and the catalog band stays the net for every reading without one. A
|
||||
// qualitative reading has no window to record.
|
||||
sourceReferenceLow: isQualitative ? null : (sourceReferenceLow ?? null),
|
||||
sourceReferenceHigh: isQualitative ? null : (sourceReferenceHigh ?? null),
|
||||
sourceReferenceText: isQualitative ? null : (sourceReferenceText ?? null),
|
||||
takenAt,
|
||||
// v1.25 (iOS #36) — provenance: the on-device-OCR path posts
|
||||
// `source: "OCR"`; an omitted field stays "MANUAL" so the legacy
|
||||
@@ -258,6 +268,7 @@ async function postLabResult(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
invalidateUserHealthScore(user.id);
|
||||
const dto = serialiseLabResult(created, biomarker);
|
||||
|
||||
await auditLog("labResult.create", {
|
||||
userId: user.id,
|
||||
@@ -275,6 +286,9 @@ async function postLabResult(request: NextRequest) {
|
||||
structured: biomarkerId !== undefined,
|
||||
// v1.25 (iOS #36) — provenance of the reading ("MANUAL" | "OCR").
|
||||
source: source ?? "MANUAL",
|
||||
// Which window governs this reading's verdict, so a dashboard can see
|
||||
// how often a report's own range displaces the catalog band.
|
||||
referenceOrigin: dto.referenceOrigin,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -298,5 +312,5 @@ async function postLabResult(request: NextRequest) {
|
||||
source: "manual",
|
||||
}).catch(() => {});
|
||||
|
||||
return apiSuccess(serialiseLabResult(created, biomarker), 201);
|
||||
return apiSuccess(dto, 201);
|
||||
}
|
||||
@@ -620,6 +620,17 @@ async function postBatch(request: NextRequest): Promise<Response> {
|
||||
};
|
||||
failedCount++;
|
||||
continue;
|
||||
case "rejected_range":
|
||||
// The reconciler carries the same plausibility gate this route
|
||||
// already applied above, so reaching here means the two disagreed —
|
||||
// report it exactly as the earlier guard would have, and let the
|
||||
// wide-event tally record which end refused.
|
||||
results[p.index] = {
|
||||
index: p.index,
|
||||
status: "skipped",
|
||||
reason: "value_out_of_range",
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isMergeableSource(p.row.source as string)) {
|
||||
|
||||
Reference in new issue
Block a user