Tell a first-run what Apple Health is actually doing

Refs #778. Two honesty gaps from the first-run report: the web onboarding
presented Apple Health like a web connector although the connection only
completes in the iPhone app, and the initial backfill gave no sign of
progress, so a working first sync looked broken.

The onboarding source step now says plainly that Apple Health is set up in
the iPhone app after onboarding, without dead-ending the flow. The
integration card gains a server-authoritative progress summary — rows
accepted, oldest reading reached, and an honest flowing/waiting line
derived from the last accepted batch. Nothing the server does not know is
shown: no percentage, no ETA, no invented throttle state; a quiet stretch
reads as waiting for the phone, not as an error.
This commit is contained in:
Marc-André Bombeck committed 2026-08-14 09:46:24 +02:00
1 parent b1be67bfa7
commit a3561b4600
10 files changed
+486 -4

No files matched your search

+30
View File
@@ -85,6 +85,30 @@ const metricFreshnessEntry = z
"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.",
});
// Issue #778 — the two backfill-progress figures the server genuinely holds.
// The iOS app drives the backfill; its queue, throttle state, and any ETA
// live on the device and are deliberately absent here.
const appleHealthSyncProgress = z
.object({
recordsAccepted: z
.number()
.int()
.describe(
"Measurement + workout rows carrying the `APPLE_HEALTH` source (live batch ingest and the one-shot export import both write it). Soft-deleted measurement rows are excluded.",
),
oldestMeasuredAt: z.iso
.datetime({ offset: true })
.nullable()
.describe(
"Earliest instant reached across those rows (`measuredAt` for measurements, `startedAt` for workouts); null when nothing has arrived. During a first-run backfill this walks backwards as history lands.",
),
})
.meta({
id: "AppleHealthSyncProgress",
description:
"Server-side Apple Health backfill progress: how many rows have been accepted and how far back in time they reach. Only what the server actually knows — no ETA, no device-side queue state.",
});
const healthKitConfigResponse = z
.object({
entries: z.array(healthKitEntry),
@@ -116,6 +140,12 @@ const healthKitConfigResponse = z
.describe(
"Per-metric-type freshness for the `APPLE_HEALTH` source. Present on the GET read; omitted from the PATCH echo.",
),
syncProgress: appleHealthSyncProgress
.nullable()
.optional()
.describe(
"Backfill/sync progress summary. Present on the GET read (null when the progress read failed); omitted from the PATCH echo.",
),
})
.meta({
id: "HealthKitConfigResponse",