mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 06:32:03 +00:00
Merge remote-tracking branch 'origin/main' into release/v1.40.0
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
ad0f35eadd
26 files changed
+1439
-61
No files matched your search
@@ -19,7 +19,7 @@ Every JSON route answers the same shape: `{ data, error }`, with `meta` on the e
|
||||
- Refresh tokens rotate per device and are one-time-use. Reusing an old one revokes that device's whole token family, on purpose. Persist the newest pair atomically and never retry a refresh with a token that already succeeded once.
|
||||
- Bearer token scopes are fail-closed. A token minted for one purpose does not open unrelated routes, and a route that does not declare a scope refuses narrow tokens outright. Expect `403` to be a normal, recoverable answer, not an anomaly.
|
||||
- Admin surfaces are cookie-session only, by construction. A native client cannot be an admin client, whatever scopes its token carries. Do not build admin features.
|
||||
- Mutations accept an `Idempotency-Key` header. Replays answer with the original result plus `X-Idempotent-Replay`, so retrying on a dropped connection is safe. Use it for anything a user would notice twice.
|
||||
- Mutations accept an `Idempotency-Key` header. A repeat of the same key with the same body answers with the original result plus `X-Idempotent-Replay`, so retrying on a dropped connection is safe. A different body under the same key is treated as a new request, not a replay; still, give each distinct write its own key. Use it for anything a user would notice twice.
|
||||
|
||||
## Data semantics worth knowing before you sync
|
||||
|
||||
|
||||
@@ -162,12 +162,14 @@ paths:
|
||||
pattern: ^[A-Za-z0-9_\-:.]{8,128}$
|
||||
minLength: 8
|
||||
maxLength: 128
|
||||
description: "Makes this write safe to retry. Send the same key again and the first response is replayed rather than the
|
||||
work repeated, marked with `X-Idempotent-Replay: true`. A key that does not match the pattern is IGNORED
|
||||
rather than refused — the write then proceeds unprotected and looks exactly like a request that carried no
|
||||
key, so validate the key on your side. Scoped per user and per (method, path): the same key on a different
|
||||
route is a different key. While a first request under this key is still in flight, a second is refused with
|
||||
409 rather than queued."
|
||||
description: "Makes this write safe to retry. Send the same key with the same body again and the first response is
|
||||
replayed rather than the work repeated, marked with `X-Idempotent-Replay: true`. The body is compared as
|
||||
JSON with key order ignored; a different body under the same key is not a retry and runs as a new write,
|
||||
while the first response stays stored for retries of the first request. A key that does not match the
|
||||
pattern is IGNORED rather than refused — the write then proceeds unprotected and looks exactly like a
|
||||
request that carried no key, so validate the key on your side. Scoped per user and per (method, path): the
|
||||
same key on a different route is a different key. While a first request under this key is still in flight, a
|
||||
second with the same body is refused with 409 rather than queued."
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
|
||||
@@ -171,8 +171,9 @@ tells you which rows to retry.
|
||||
## Retries and duplicates
|
||||
|
||||
Send an `Idempotency-Key` header with a value your automation can reproduce for
|
||||
the same reading. A retry with the same key returns the original response
|
||||
instead of writing a second row:
|
||||
the same reading. A retry with the same key and the same body returns the
|
||||
original response instead of writing a second row. A different body under the
|
||||
same key is a different reading and is written:
|
||||
|
||||
```yaml
|
||||
headers:
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Blood pressure readings missing their diastolic half
|
||||
|
||||
## What happened
|
||||
|
||||
The iPhone app (every App Store build up to 1.0.3 and TestFlight 1.1.0)
|
||||
saves a manually entered blood pressure as two requests to
|
||||
`POST /api/measurements`: one `BLOOD_PRESSURE_SYS`, one
|
||||
`BLOOD_PRESSURE_DIA`, both carrying the same `Idempotency-Key`. Until the
|
||||
replay cache learned to compare request bodies, the server treated the second
|
||||
request as a retry of the first. It answered with the first request's cached
|
||||
`201` (header `X-Idempotent-Replay: true`) and never wrote the diastolic
|
||||
value. The app showed success, so nobody noticed.
|
||||
|
||||
The fix makes the cache replay only a request with the same body. A different
|
||||
body under the same key now runs normally. Nothing recovers the values that
|
||||
were never sent to the database: they exist only on the person's phone, if at
|
||||
all.
|
||||
|
||||
## Finding the affected readings
|
||||
|
||||
Read-only. Run it against the HealthLog database (with the bundled compose
|
||||
file: `docker compose exec db psql -U healthlog healthlog`). It lists every live systolic reading that has no
|
||||
live diastolic reading for the same account at the same instant.
|
||||
|
||||
<!-- bp-missing-diastolic:start -->
|
||||
|
||||
```sql
|
||||
SELECT u.username,
|
||||
m.id,
|
||||
m.measured_at,
|
||||
m.value AS systolic,
|
||||
m.source
|
||||
FROM measurements m
|
||||
JOIN users u ON u.id = m.user_id
|
||||
WHERE m.type = 'BLOOD_PRESSURE_SYS'
|
||||
AND m.deleted_at IS NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM measurements d
|
||||
WHERE d.user_id = m.user_id
|
||||
AND d.measured_at = m.measured_at
|
||||
AND d.type = 'BLOOD_PRESSURE_DIA'
|
||||
AND d.deleted_at IS NULL
|
||||
)
|
||||
ORDER BY u.username, m.measured_at;
|
||||
```
|
||||
|
||||
<!-- bp-missing-diastolic:end -->
|
||||
|
||||
Readings from the phone app carry `source = 'MANUAL'`. Rows from a synced
|
||||
source (Withings, Apple Health) that show up here are a different gap, not
|
||||
this one: those sources write both halves themselves.
|
||||
|
||||
## What to tell users
|
||||
|
||||
There is nothing the server can repair: the diastolic value never reached
|
||||
it. A person whose reading appears in the list can re-enter it: delete the
|
||||
incomplete reading, then add the blood pressure again with both values and
|
||||
the original date and time. From this release on, both halves are saved on
|
||||
the first try, from any app version.
|
||||
Reference in new issue
Block a user