mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
feat(integrations): resolve sync health on the server
The ledger records what the last attempt did. It has no way to say what is no longer being attempted: a row frozen at `error_transient` for a month reads exactly like one retrying every hour, because nothing anywhere read the age of `lastAttemptAt`. `sync-verdict.ts` derives the missing reading as a pure read-side projection: eight verdicts down one ladder, no writer, no cron, nothing that can desynchronise from the row it reads. `stalled` is the new one, and it deliberately outranks `failing` — an integration nobody is calling is not failing, it is not running. Every entry on `/api/integrations/status` carries the verdict, computed inline from data the route already holds. That placement is the point: a card-level verdict misses a card-less provider by construction, which is how a dead sync stayed invisible. Nightscout joins the envelope so its card can stop fetching its own status. The freshness module gains the workout leg (Strava writes no measurements at all) and a source-parameterised core so the Apple Health route can reuse it. Its per-metric `stale` flag fires only while the provider itself reads healthy: that combination is the dead-pipe signature, and when the whole connection has stopped every metric ages together anyway. MCP reports the verdict beside `connected`, which only ever meant "not explicitly disconnected".
This commit is contained in:
1 parent
a1a978f5e6
commit
de47bfa5ba
6 files changed
+736
-86
No files matched your search
@@ -29,6 +29,57 @@ const healthKitEntry = z
|
||||
"One resolved HealthKit metric mapping (defaults merged with the user's stored overrides).",
|
||||
});
|
||||
|
||||
// Mirrors `src/lib/integrations/sync-verdict.ts` — the server-resolved liveness
|
||||
// verdict. Apple Health is push-based, so only the data-age arms apply.
|
||||
const syncHealth = z
|
||||
.object({
|
||||
verdict: z
|
||||
.enum([
|
||||
"fresh",
|
||||
"stale",
|
||||
"stalled",
|
||||
"failing",
|
||||
"reauth_required",
|
||||
"parked",
|
||||
"pending_first_sync",
|
||||
"disconnected",
|
||||
])
|
||||
.describe(
|
||||
"Liveness verdict. For Apple Health: `fresh` when data arrived within the last 7 days, `stale` when it did not, `pending_first_sync` when none ever arrived.",
|
||||
),
|
||||
since: z.iso
|
||||
.datetime({ offset: true })
|
||||
.nullable()
|
||||
.describe("The instant that triggered the verdict; null when none did."),
|
||||
})
|
||||
.meta({
|
||||
id: "SyncHealth",
|
||||
description:
|
||||
"Server-resolved sync-health verdict. The single source of liveness truth — `lastSyncedAt` alone cannot express 'this pipe stopped delivering'.",
|
||||
});
|
||||
|
||||
const metricFreshnessEntry = z
|
||||
.object({
|
||||
type: z
|
||||
.string()
|
||||
.describe(
|
||||
"The measurement type (e.g. `RESPIRATORY_RATE`), or `WORKOUTS` for the workout leg.",
|
||||
),
|
||||
lastSeenAt: z.iso
|
||||
.datetime({ offset: true })
|
||||
.describe("Newest recorded reading for this type from this source."),
|
||||
stale: z
|
||||
.boolean()
|
||||
.describe(
|
||||
"This type has gone quiet while the source around it reads healthy — the dead-pipe signature (e.g. a single revoked HealthKit permission). Only ever true when the verdict is `fresh`.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "MetricFreshnessEntry",
|
||||
description:
|
||||
"Per-metric-type last-seen timestamp with the server-computed staleness flag. Only types that have actually delivered appear — absence is absence, never an invented row.",
|
||||
});
|
||||
|
||||
const healthKitConfigResponse = z
|
||||
.object({
|
||||
entries: z.array(healthKitEntry),
|
||||
@@ -38,11 +89,20 @@ const healthKitConfigResponse = z
|
||||
.describe(
|
||||
"When HealthKit last synced for this user; null when never (and always null on the PATCH echo).",
|
||||
),
|
||||
syncHealth: syncHealth
|
||||
.optional()
|
||||
.describe("Present on the GET read; omitted from the PATCH echo."),
|
||||
metricFreshness: z
|
||||
.array(metricFreshnessEntry)
|
||||
.optional()
|
||||
.describe(
|
||||
"Per-metric-type freshness for the `APPLE_HEALTH` source. Present on the GET read; omitted from the PATCH echo.",
|
||||
),
|
||||
})
|
||||
.meta({
|
||||
id: "HealthKitConfigResponse",
|
||||
description:
|
||||
"The resolved HealthKit integration config: the default metric set merged with the user's stored per-metric overrides.",
|
||||
"The resolved HealthKit integration config: the default metric set merged with the user's stored per-metric overrides, plus the sync-health verdict and per-metric freshness.",
|
||||
});
|
||||
|
||||
// Mirror the route's `patchSchema` — merge-by-id; unknown ids are ignored.
|
||||
|
||||
Reference in new issue
Block a user