-
Notifications
You must be signed in to change notification settings - Fork 73k
Description
Summary
Nightscout v1 accepts client-supplied string _id values when creating treatments, but later treats those same _id values as if they must be Mongo ObjectIds.
This is a problem for Loop Temporary Override treatments, because Loop uploads them with UUID-style _id values, for example:
{
"_id": "69F15FD2-8075-4DEB-AEA3-4352F455840D",
"enteredBy": "Loop",
"eventType": "Temporary Override",
"created_at": "2026-02-17T02:00:16.000Z",
"timestamp": "2026-02-17T02:00:16Z",
"durationType": "indefinite",
"correctionRange": [90, 110],
"insulinNeedsScaleFactor": 1.2,
"reason": "Override Name",
"utcOffset": 0
}
The initial POST succeeds, and the treatment is stored.
Later update/delete operations against that same treatment can fail because Nightscout v1 tries to coerce _id into ObjectId, or because it upserts by created_at + eventType instead of _id.
This can cause Loop override treatment sync to get stuck after an indefinite override, and subsequent override banners on the graph may stop appearing.
User-visible impact
Symptoms reported from real deployments:
A Temporary Override banner appears on the graph and persists.
Later devicestatus.override.active=false clears the live override pill, but the graph overlay remains because the overlay is driven by treatments, not devicestatus.
After the original indefinite override ages out of the graph window, the old banner disappears.
From that point onward, future Loop override banners may stop appearing entirely because override treatment sync is stalled by a failed update/delete on the earlier UUID-backed treatment.
Why this shows up specifically with Loop
Loop/NightscoutService uploads override treatments with:
eventType: "Temporary Override"
durationType: "indefinite" for infinite overrides
_id = override.syncIdentifier.uuidString
That means the _id is a UUID string, not a Mongo ObjectId.
Nightscout accepts that on create, but v1 treatment update/delete paths are not consistent with that acceptance.
Reproduction
POST a treatment with a UUID _id:
{
"_id": "69F15FD2-8075-4DEB-AEA3-4352F455840D",
"eventType": "Temporary Override",
"created_at": "2026-02-17T02:00:16.000Z",
"durationType": "indefinite",
"correctionRange": [90, 110],
"insulinNeedsScaleFactor": 1.2,
"reason": "test override"
}
Nightscout stores it successfully.
Later attempt one of the following:
DELETE /api/v1/treatments/69F15FD2-8075-4DEB-AEA3-4352F455840D
PUT /api/v1/treatments/ with the same _id
POST /api/v1/treatments/ again with the same _id but a changed created_at
Actual behavior
Initial create succeeds.
Delete can fail because _id is treated as ObjectId.
Update can fail for the same reason, or miss the existing treatment because matching is based on created_at + eventType.
Reposting with the same _id but a different created_at can fail or behave inconsistently because the dedupe/upsert key is not _id.
Loop can keep retrying the same failed override sync batch, preventing later override treatments from syncing.
Expected behavior
Nightscout v1 should handle client-supplied treatment _id values consistently across create, update, and delete:
If _id is a UUID/string, it should remain a string.
Only 24-hex _id values should be converted to Mongo ObjectId.
If a client supplies _id, v1 treatment upserts should match by _id.
DELETE and PUT should work for both Mongo ObjectId ids and string UUID ids.
Root cause
There are two related problems in the v1 treatment path:
_id coercion is too aggressive.
Query/delete/update paths assume _id must be ObjectId.
UUID strings like 69F15FD2-8075-4DEB-AEA3-4352F455840D are not valid ObjectIds.
Treatment upsert matching ignores client _id.
v1 POST/replace logic matches treatments by created_at + eventType.
If the client reuses the same _id but changes created_at, Nightscout does not reliably target the existing treatment.
Why this matters beyond one bad treatment
Loop advances its override sync anchor only after the override upload operation succeeds. If one UUID-backed override treatment cannot be updated or deleted, the same override batch can be retried repeatedly, and newer override treatments never get through. That makes the graph overlay behavior look like “one indefinite override happened, then future override banners stopped working.”
Proposed fix
In the v1 treatment API/storage layer:
Only convert _id to ObjectId when it is a 24-character hex string.
Leave UUID/string _id values as strings.
Prefer _id as the upsert key when the client supplied one.
Fall back to created_at + eventType only for legacy callers that did not supply _id.
Ensure PUT /api/v1/treatments/ and DELETE /api/v1/treatments/:id work for both string and ObjectId ids.
Additional note
This issue affects the graph override banner path because that banner is driven by treatments, not devicestatus. So even when live override state in devicestatus is correct, graph overlays can diverge if treatment sync breaks.
If useful, I can also provide a minimal regression test matrix for:
UUID _id POST
UUID _id repost with changed created_at
UUID _id PUT
UUID _id DELETE