mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-13 22:32:04 +00:00
fix(integrations): escalate a long transient failure streak, and scope a success to its own leg
A sync failure the classifier bucketed as `transient` had no exit. The park test read `kind === "persistent"` and the persistent streak anchor was only ever stamped by a persistent failure, so a streak could run for a thousand attempts with the persistent counter at zero, the state frozen at `error_transient`, and no ladder it could climb. `transient` is a hypothesis about retryability, and the classifier defaults every timeout, 429, 5xx and unrecognised response to it precisely because the wire cannot tell a reboot from a permanent break. That default is right for one attempt and wrong for a thousand: an unbroken streak retried hourly for a full day has refuted it by experiment. Record it as `persistent` from there, which feeds the park ladder that already exists — 48 h of unbroken failure end to end before an integration stops attempting. The counters could not accumulate anyway. `IntegrationStatus` is keyed (user, integration) while Withings runs four legs on four crons and WHOOP four resources on four more, and a success cleared the error, every bucket, the streak anchor and the state whatever leg had recorded them. The ECG tick at :41 wiped what the sleep tick recorded at :15 — including a reauth park — so the strike ladder was reset several times an hour and the card painted a green "connected · 12 minutes ago" over a data class that had stopped arriving. A success now clears only what it is entitled to clear. Two columns carry it: `failing_since_at` dates the streak, `failing_leg` names its owner. The migration backfills the anchor for rows already in an error state so a connection broken for weeks escalates on its next attempt rather than restarting its clock.
This commit is contained in:
1 parent
84e38ab110
commit
527dd6c1fa
32 files changed
+823
-65
No files matched your search
@@ -168,6 +168,7 @@ export const GET = apiHandler(async () => {
|
||||
state: string;
|
||||
lastSuccessAt: string | null;
|
||||
lastAttemptAt: string | null;
|
||||
failingSince: string | null;
|
||||
},
|
||||
opts: {
|
||||
connected?: boolean;
|
||||
@@ -181,6 +182,7 @@ export const GET = apiHandler(async () => {
|
||||
state: snapshot.state,
|
||||
lastSuccessAt: snapshot.lastSuccessAt,
|
||||
lastAttemptAt: snapshot.lastAttemptAt,
|
||||
failingSinceAt: snapshot.failingSince,
|
||||
legacyLastSyncedAt: opts.legacyLastSyncedAt ?? null,
|
||||
cadence: INTEGRATION_CADENCE[integration],
|
||||
now,
|
||||
@@ -381,6 +383,12 @@ interface IntegrationViewModel {
|
||||
lastSuccessAt: string | null;
|
||||
lastAttemptAt: string | null;
|
||||
lastError: string | null;
|
||||
/**
|
||||
* Start of the current unbroken failure streak, or null when the provider is
|
||||
* not failing. `syncHealth.since` is derived from it; the raw instant rides
|
||||
* along so a client can render the age without re-deriving the verdict.
|
||||
*/
|
||||
failingSince: string | null;
|
||||
/**
|
||||
* The server-resolved liveness verdict. Present on EVERY entry, whether or
|
||||
* not a card renders it — the unit of coverage is the envelope entry, not
|
||||
|
||||
Reference in new issue
Block a user