fix(import): accept a blood-glucose reading that carries no context

A CSV import of a continuous-sensor export wrote nothing and rendered a
green success state. Three faults compounded, and the reporter took all
three apart before filing.

The parser refused every BLOOD_GLUCOSE row with a blank glucoseContext.
Behind it, the CHECK constraint from migration 0021 read both ways at
once: a glucose row MUST carry a context, every other type MUST NOT. The
second half is right and stays. The first was written when the only
glucose writer was the entry form, where a person picks fasting or
post-meal at the moment of the reading. Every machine writer that
arrived since has no such answer per sample, so the Nightscout pull, the
MCP log tool, the Telegram capture and both bulk importers were all
building rows the database then refused.

A missing context now stores as NULL, never as a fifth enum member.
Every per-context reader selects by equality against the four members,
so the reading stays out of the doctor report's fasting rows, the
matching FHIR observations and the per-context targets, while the chart,
the all-time summary and the clinical panel keep it in full. Hand entry
still asks for a context, because at that moment you know it.

The import cards now read the counts rather than the status code.
Nothing written is a failure, a mixed file is a warning, and the refused
rows are grouped by reason with a count instead of fifty identical
lines, with the per-line detail one disclosure away. Both cards render
through one outcome table, so a tick cannot reach a result that wrote
nothing.

Refs #640
This commit is contained in:
Marc-André Bombeck committed 2026-07-25 21:23:26 +02:00
1 parent 019ad52c0d
commit 0c9832eb8d
19 files changed
+639 -114

No files matched your search

+12 -2
View File
@@ -648,8 +648,10 @@ export const createMeasurementSchema = z
// `APPLE_HEALTH`; `WITHINGS` / `IMPORT` / `COMPUTED` are server-owned and
// forging them would pollute the per-source canonical picker.
source: writableMeasurementSourceEnum.optional().default("MANUAL"),
// Only applies when type === BLOOD_GLUCOSE. Mirrored by a CHECK
// constraint in Postgres (see migration 0021).
// Only applies when type === BLOOD_GLUCOSE. The "not on any other type"
// half is mirrored by a CHECK constraint in Postgres (migrations 0021 +
// 0274). The "required on a glucose row" half below is a rule of THIS
// schema only — see the refine.
glucoseContext: glucoseContextEnum.optional(),
// v1.4.25 W10 reconcile (code-review M4): the single-entry POST
// dropped `deviceType` silently because the column existed on
@@ -669,6 +671,14 @@ export const createMeasurementSchema = z
.refine((data) => validateMeasurementRange(data.type, data.value) === null, {
message: "Value out of plausible range",
})
// Deliberately asymmetric with the bulk ingest paths. This schema backs the
// hand-entry surface, where a person is present at the reading and knows
// whether it was fasting, after a meal, random or at bedtime — the form
// always offers the choice, so requiring it costs nothing and keeps the
// per-context breakdowns meaningful. A sensor or meter export has no such
// answer per sample, so the CSV import, the JSON import and the device sync
// paths accept a contextless reading and store NULL rather than guessing.
// Absence stays absence; it is never widened into a classification.
.refine(
(data) =>
(data.type === "BLOOD_GLUCOSE" && data.glucoseContext !== undefined) ||