feat(integrations): let the ledger hold every failing leg at once

`failing_leg` was one slot, so two legs of the same provider failing in
overlapping windows overwrote each other. The row remembered whichever failed
last, and that leg recovering then cleared an error the other one was still
causing — the ledger forgot a live failure and painted the card green over it.

The slot becomes a set. A failure adds its leg, a success removes its own, and
the error state, the per-kind buckets and the streak anchor clear only once the
set is empty. The array is ordered oldest failure first, so the last element is
the leg that wrote the message `last_error` holds; when that leg recovers while
others are still failing, the message is cleared rather than left describing a
leg that is now healthy.

An unattributed failure adds nothing and removes nothing. A single-leg provider
therefore behaves exactly as before, and a multi-leg provider no longer loses
what its other legs already reported.

The column never reached a client — it is absent from the status serialiser and
from the OpenAPI contract — so it is replaced rather than shimmed.
This commit is contained in:
Marc-André Bombeck committed 2026-07-27 14:35:33 +02:00
1 parent 3088753a92
commit 06935256b0
18 files changed
+433 -58

No files matched your search

+26 -4
View File
@@ -12100,10 +12100,11 @@ components:
items:
$ref: "#/components/schemas/AppleHealthBatchEntry"
syncTrigger:
description: "Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake)
so an operator can see, per batch, which trigger produced it. Recorded on the ingest wide event and nowhere
else — it does not affect dedup, attribution, or how any sample is stored. Optional and backward-compatible:
pre-#66 clients omit it."
description: "Diagnostic-only. Names what triggered this sync (foreground app open, background refresh, or a push wake).
Recorded on the ingest wide event and kept on the account as `lastSyncTrigger` — plus `lastBackgroundSyncAt`
for a `background` or `push` trigger — both readable from `GET /api/integrations/healthkit`. It does not
affect dedup, attribution, or how any sample is stored. Optional and backward-compatible: pre-#66 clients
omit it."
type: string
enum:
- foreground
@@ -32631,6 +32632,27 @@ components:
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
description: When HealthKit last synced for this user; null when never (and always null on the PATCH echo).
lastSyncTrigger:
description: What the client declared triggered the most recent accepted batch. Null when the client sent no trigger —
an older build, or a batch that predates the field. Present on the GET read; omitted from the PATCH echo.
anyOf:
- type: string
enum:
- foreground
- background
- push
description: What triggered a HealthKit batch.
- type: "null"
lastBackgroundSyncAt:
description: When a batch last arrived without the app being opened (a `background` or `push` trigger). Null when that
has never happened. `lastSyncedAt` alone cannot separate a phone that delivers on its own from one that only
delivers while the app is open; this field is what does. Present on the GET read; omitted from the PATCH
echo.
anyOf:
- type: string
format: date-time
pattern: ^(?:(?:\d\d[2468][048]|\d\d[13579][26]|\d\d0[48]|[02468][048]00|[13579][26]00)-02-29|\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\d|30)|(?:02)-(?:0[1-9]|1\d|2[0-8])))T(?:(?:[01]\d|2[0-3]):[0-5]\d(?::[0-5]\d(?:\.\d+)?)?(?:Z|([+-](?:[01]\d|2[0-3]):[0-5]\d)))$
- type: "null"
syncHealth:
description: Present on the GET read; omitted from the PATCH echo.
$ref: "#/components/schemas/SyncHealth"