diff --git a/docs/api/openapi.yaml b/docs/api/openapi.yaml index ae4fc16ec..c17830d99 100644 --- a/docs/api/openapi.yaml +++ b/docs/api/openapi.yaml @@ -77130,7 +77130,9 @@ components: glucose while their entry is `bidirectional` or `writeOnly` and enabled): when the newest reading of the type was entered here rather than arriving from Apple Health, or null when none was. Such readings go to the phone and never come back as an arrival, so `stale` already counts them: the type is quiet only when both - this and `lastSeenAt` are. Show it as the server sends it; do not recompute `stale`." + are. Since v1.43.9 `lastSeenAt` on such a type is the newer of this and the newest reading from Apple + Health, because a reading entered here is in Apple Health too; show both as the server sends them and do not + recompute `stale`." anyOf: - type: string format: date-time diff --git a/src/app/api/integrations/healthkit/__tests__/route.test.ts b/src/app/api/integrations/healthkit/__tests__/route.test.ts index d8f44da69..52b77eb46 100644 --- a/src/app/api/integrations/healthkit/__tests__/route.test.ts +++ b/src/app/api/integrations/healthkit/__tests__/route.test.ts @@ -406,6 +406,9 @@ describe("GET /api/integrations/healthkit — sync health", () => { expect(entries.WEIGHT.lastEnteredHereAt).toBe( enteredHereAt.toISOString(), ); + // The reading entered here is in Apple Health too, so it is the type's + // newest reading: the row and the verdict read the same instant. + expect(entries.WEIGHT.lastSeenAt).toBe(enteredHereAt.toISOString()); // A read-only type keeps its own verdict and carries no such field. expect(entries.PULSE.stale).toBe(true); expect(entries.PULSE).not.toHaveProperty("lastEnteredHereAt"); diff --git a/src/lib/integrations/sync-verdict.ts b/src/lib/integrations/sync-verdict.ts index 5b911fc40..8a0863c19 100644 --- a/src/lib/integrations/sync-verdict.ts +++ b/src/lib/integrations/sync-verdict.ts @@ -342,21 +342,27 @@ export function classifyMetricFreshness( return samples.map((sample) => { // A type the app writes back to the phone (#1247): what is entered here // goes to Apple Health and never arrives back, so a person who keeps - // their readings here would otherwise watch the type go quiet. The newer - // of the two is what says whether the type is still in use. + // their readings here would otherwise watch the type go quiet. That + // reading is in Apple Health too, so the newer of the two is the type's + // newest reading: it is what the row shows and what the verdict reads. const written = enteredHere?.has(sample.type) ? { lastEnteredHereAt: enteredHere.get(sample.type) ?? null } : {}; - if (verdict !== "fresh" || SPARSE_METRIC_TYPES.has(sample.type)) { - return { ...sample, ...written, stale: false }; - } const seen = newest( toMillis(sample.lastSeenAt), toMillis(written.lastEnteredHereAt), ); - return { + const resolved = { ...sample, ...written, + lastSeenAt: + seen != null ? new Date(seen).toISOString() : sample.lastSeenAt, + }; + if (verdict !== "fresh" || SPARSE_METRIC_TYPES.has(sample.type)) { + return { ...resolved, stale: false }; + } + return { + ...resolved, stale: seen != null && nowMs - seen > METRIC_QUIET_AFTER_MS, }; }); diff --git a/src/lib/openapi/routes/integrations.ts b/src/lib/openapi/routes/integrations.ts index 9fc6ec356..9e36dfcb6 100644 --- a/src/lib/openapi/routes/integrations.ts +++ b/src/lib/openapi/routes/integrations.ts @@ -293,7 +293,7 @@ const appleHealthMetricFreshnessEntry = metricFreshnessEntry .nullable() .optional() .describe( - "v1.43.7 — present only on a type the app writes back to Apple Health (weight, blood pressure, blood glucose while their entry is `bidirectional` or `writeOnly` and enabled): when the newest reading of the type was entered here rather than arriving from Apple Health, or null when none was. Such readings go to the phone and never come back as an arrival, so `stale` already counts them: the type is quiet only when both this and `lastSeenAt` are. Show it as the server sends it; do not recompute `stale`.", + "v1.43.7 — present only on a type the app writes back to Apple Health (weight, blood pressure, blood glucose while their entry is `bidirectional` or `writeOnly` and enabled): when the newest reading of the type was entered here rather than arriving from Apple Health, or null when none was. Such readings go to the phone and never come back as an arrival, so `stale` already counts them: the type is quiet only when both are. Since v1.43.9 `lastSeenAt` on such a type is the newer of this and the newest reading from Apple Health, because a reading entered here is in Apple Health too; show both as the server sends them and do not recompute `stale`.", ), }) .meta({