mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-08 20:42:43 +00:00
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:
1 parent
019ad52c0d
commit
0c9832eb8d
19 files changed
+639
-114
No files matched your search
+10
-5
@@ -8004,9 +8004,11 @@ paths:
|
||||
10 000 valid rows). Header (order-independent): `type,value,unit,measuredAt[,glucoseContext,notes,externalId]`.
|
||||
`measuredAt` must carry an explicit ISO-8601 offset and is bounded (no future beyond a 5-min skew, no instant
|
||||
before 1900). Glucose accepts `mmol/L` (converted to canonical `mg/dL`); weight accepts `lb` (converted to
|
||||
`kg`). An `externalId` column makes re-upload idempotent (upsert on `(userId, type, source=IMPORT,
|
||||
externalId)`); without it a re-upload duplicates. `?dryRun=1` validates + previews without writing. Shares the
|
||||
5/hour `import:` rate bucket."
|
||||
`kg`). `glucoseContext` is optional on a `BLOOD_GLUCOSE` row — a blank cell stores no context, which is the
|
||||
normal shape of a continuous-sensor export; a non-empty value is still checked against the enum, and the column
|
||||
is refused on any other type. An `externalId` column makes re-upload idempotent (upsert on `(userId, type,
|
||||
source=IMPORT, externalId)`); without it a re-upload duplicates. `?dryRun=1` validates + previews without
|
||||
writing. Shares the 5/hour `import:` rate bucket."
|
||||
parameters:
|
||||
- name: dryRun
|
||||
in: query
|
||||
@@ -8027,6 +8029,7 @@ paths:
|
||||
type,value,unit,measuredAt,glucoseContext,notes,externalId
|
||||
WEIGHT,80.5,kg,2026-05-01T08:00:00Z,,morning,
|
||||
BLOOD_GLUCOSE,5.3,mmol/L,2026-05-01T08:05:00+02:00,FASTING,,meter-001
|
||||
BLOOD_GLUCOSE,5.9,mmol/L,2026-05-01T08:20:00+02:00,,,sensor-001
|
||||
responses:
|
||||
"200":
|
||||
description: Per-row import outcome.
|
||||
@@ -17199,7 +17202,8 @@ components:
|
||||
- type: string
|
||||
- type: "null"
|
||||
glucoseContext:
|
||||
description: Set only on BLOOD_GLUCOSE rows.
|
||||
description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor
|
||||
export, a device sync). Never invent one.
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
@@ -18015,7 +18019,8 @@ components:
|
||||
- type: string
|
||||
- type: "null"
|
||||
glucoseContext:
|
||||
description: Set only on BLOOD_GLUCOSE rows.
|
||||
description: Set only on BLOOD_GLUCOSE rows, and null there too when the writer recorded no classification (a sensor
|
||||
export, a device sync). Never invent one.
|
||||
anyOf:
|
||||
- type: string
|
||||
- type: "null"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Data import
|
||||
|
||||
HealthLog has two ways to bring data in, both reachable from
|
||||
HealthLog has three ways to bring data in, all reachable from
|
||||
**Settings → Export & Import**:
|
||||
|
||||
1. An **Apple Health `export.zip`** upload — the full archive from the
|
||||
@@ -10,10 +10,18 @@ HealthLog has two ways to bring data in, both reachable from
|
||||
2. A **generic JSON import** — a small, explicit payload of measurements
|
||||
and mood entries, useful for restoring a partial export or migrating
|
||||
from another tracker. This page documents that format.
|
||||
3. A **CSV import** — one measurement per row, for a spreadsheet or a
|
||||
meter / sensor export. Documented in section 3 below.
|
||||
|
||||
Both paths skip rows that already exist, so re-running an import is
|
||||
All three paths skip rows that already exist, so re-running an import is
|
||||
safe — it merges rather than duplicating.
|
||||
|
||||
Every import reports what it actually wrote. A file where nothing was
|
||||
written is shown as a failure, a file where some rows landed and some
|
||||
were refused is shown as a partial result, and the refused rows are
|
||||
grouped by reason so one bad column reads as one line rather than a
|
||||
scroll.
|
||||
|
||||
## 1. Apple Health `export.zip`
|
||||
|
||||
On the iPhone: **Health app → profile picture → Export All Health
|
||||
@@ -56,7 +64,28 @@ skipped and counted under `skipped` in the response.
|
||||
| `measuredAt` | yes | string | ISO-8601 datetime, e.g. `2026-05-01T08:00:00.000Z`. |
|
||||
| `source` | no | string | Free-text origin label. Imported rows are tagged `IMPORT` server-side regardless. |
|
||||
| `notes` | no | string | Optional free text. |
|
||||
| `glucoseContext` | no | enum | Only for `BLOOD_GLUCOSE` (e.g. fasting / post-meal). |
|
||||
| `glucoseContext` | no | enum | Only for `BLOOD_GLUCOSE`. See [Glucose context](#glucose-context). |
|
||||
|
||||
### Glucose context
|
||||
|
||||
`glucoseContext` is one of `FASTING`, `POSTPRANDIAL`, `RANDOM`,
|
||||
`BEDTIME`, and it is genuinely optional on a `BLOOD_GLUCOSE` row. Leave
|
||||
it out (JSON) or leave the cell blank (CSV) and the reading is stored
|
||||
with no context.
|
||||
|
||||
That is the normal case for a continuous sensor or meter export, which
|
||||
records a value and a timestamp per reading and classifies nothing. A
|
||||
reading with no context still counts everywhere a reading counts: the
|
||||
chart, the all-time summary, and the trailing-30-day clinical panel
|
||||
(time in range, GMI, CV%, estimated A1c) are all context-agnostic. It is
|
||||
left out only of the per-context breakdowns — the doctor report's
|
||||
fasting / post-meal rows, the matching FHIR observations, the per-context
|
||||
targets — because no classification was recorded and none is invented.
|
||||
|
||||
Setting the column on any type other than `BLOOD_GLUCOSE` is rejected.
|
||||
Setting it to a value outside the four above is rejected. Entering a
|
||||
reading by hand in the app still asks for a context, because at that
|
||||
moment you know it.
|
||||
|
||||
### Mood entry
|
||||
|
||||
@@ -195,8 +224,48 @@ The response is a count envelope:
|
||||
}
|
||||
```
|
||||
|
||||
## 3. CSV import
|
||||
|
||||
`POST /api/import/csv` takes the raw file as `text/csv` — one measurement
|
||||
per row, no JSON wrapper. The **CSV file** control in
|
||||
**Settings → Export & Import** posts to it, and **Download example**
|
||||
mints a file with the right header.
|
||||
|
||||
A header row is required; column order does not matter and header
|
||||
matching is case-insensitive.
|
||||
|
||||
| Column | Required | Notes |
|
||||
| ---------------- | -------- | --------------------------------------------------------------------------------------------------------- |
|
||||
| `type` | yes | A `MeasurementType` from the table above. |
|
||||
| `value` | yes | A number, in the row's `unit`. |
|
||||
| `unit` | yes | The canonical unit, or a recognised alias: `mmol/L` → `mg/dL`, `lb` → `kg`, `in` → `cm`, `°F` → `°C`. |
|
||||
| `measuredAt` | yes | ISO-8601 **with an explicit offset** (`Z` or `±HH:MM`, and `±HHMM` is accepted). No offset, no import. |
|
||||
| `glucoseContext` | no | Only for `BLOOD_GLUCOSE`; blank means no context. See [Glucose context](#glucose-context). |
|
||||
| `notes` | no | Free text, up to 200 characters. |
|
||||
| `externalId` | no | A source-stable id. With it, a re-upload updates the same row; without it, a re-upload creates a new one. |
|
||||
|
||||
A sensor export therefore needs no glucose column at all:
|
||||
|
||||
```csv
|
||||
type,value,unit,measuredAt,glucoseContext,notes,externalId
|
||||
BLOOD_GLUCOSE,5.3,mmol/L,2024-04-03T13:15:00+1100,,,sensor-1
|
||||
BLOOD_GLUCOSE,5.9,mmol/L,2024-04-03T13:30:00+1100,,,sensor-2
|
||||
```
|
||||
|
||||
Limits: 16 MB per upload, 10 000 valid rows per import, 5 imports per
|
||||
hour (shared with the JSON import). `?dryRun=1` validates and previews
|
||||
without writing anything — the **Preview** button uses it.
|
||||
|
||||
The response carries per-row outcomes alongside the counts, and the card
|
||||
renders the counts, not the status code: rows written and none skipped is
|
||||
a success, a mixed result is a warning, and nothing written is a failure
|
||||
with the reasons grouped by count.
|
||||
|
||||
## Converting a CSV into the import JSON
|
||||
|
||||
The CSV import above covers measurements. Mood entries have no CSV
|
||||
route, so a mood history still goes through the JSON payload.
|
||||
|
||||
A spreadsheet export is the most common starting point. The shape is
|
||||
small enough to convert by hand for a few rows, or with a one-off script
|
||||
for many. The steps are the same either way:
|
||||
|
||||
Reference in new issue
Block a user