diff --git a/src/lib/__tests__/query-keys.test.ts b/src/lib/__tests__/query-keys.test.ts index b9bfbf88a..7ea7298e7 100644 --- a/src/lib/__tests__/query-keys.test.ts +++ b/src/lib/__tests__/query-keys.test.ts @@ -219,6 +219,16 @@ describe("dependent-key bundles", () => { expect(keyStrings).toContain(JSON.stringify(["chart-data"])); }); + // v1.18.9 (#38) — the dashboard snapshot must ride the measurement + // bundle too. The hero band / score ring / tile strip read ONE snapshot + // query with refetch-on-mount/focus options that otherwise leave an + // in-app-added reading invisible on the Startseite until the 120 s poll + // or a hard reload. Mirror the v1.16.11 medication-bundle fix. + it("measurementDependentKeys bundles the dashboard snapshot (v1.18.9)", () => { + const keyStrings = measurementDependentKeys.map((k) => JSON.stringify(k)); + expect(keyStrings).toContain(JSON.stringify(["dashboard", "snapshot"])); + }); + it("moodDependentKeys bundle covers mood + analytics + targets", () => { const keyStrings = moodDependentKeys.map((k) => JSON.stringify(k)); expect(keyStrings).toContain(JSON.stringify(["mood-entries"])); diff --git a/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx b/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx index 99d811821..582dcbccb 100644 --- a/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx +++ b/src/lib/queries/__tests__/use-dashboard-snapshot.test.tsx @@ -7,10 +7,12 @@ * backgrounded (`refetchIntervalInBackground: false`). The poll rides * the warm 60 s server cache and never touches the LLM surfaces. * - * The hook keeps its existing warm-cache options (`staleTime: 60_000`, - * `refetchOnMount: false`, `refetchOnWindowFocus: false`) so a - * return-to-dashboard within a minute stays a free cache hit, and the - * queryKey stays the centralised factory entry. + * The hook keeps its warm-cache options (`staleTime: 60_000`, + * `refetchOnMount: false`) so a return-to-dashboard within a minute + * stays a free cache hit, and the queryKey stays the centralised factory + * entry. v1.18.9 (#38) flips `refetchOnWindowFocus` to `true` so a + * background sync — which fires no client mutation event — surfaces on + * return-to-tab; the `staleTime` gate keeps rapid focus toggles free. */ import { describe, it, expect, vi, afterEach } from "vitest"; @@ -70,7 +72,12 @@ describe("useDashboardSnapshot — auto-refresh on an open page", () => { const opts = lastOpts(); expect(opts.staleTime).toBe(60_000); expect(opts.refetchOnMount).toBe(false); - expect(opts.refetchOnWindowFocus).toBe(false); + }); + + it("refetches on window focus so a background sync surfaces on return", () => { + useDashboardSnapshot(); + const opts = lastOpts(); + expect(opts.refetchOnWindowFocus).toBe(true); }); it("routes the queryKey through the centralised factory", () => { diff --git a/src/lib/queries/use-dashboard-snapshot.ts b/src/lib/queries/use-dashboard-snapshot.ts index eaacdbb38..77c8e093d 100644 --- a/src/lib/queries/use-dashboard-snapshot.ts +++ b/src/lib/queries/use-dashboard-snapshot.ts @@ -14,9 +14,17 @@ * already passed the auth / onboarding redirect. Firing un-gated * removes the `/api/auth/me` round-trip from the cold critical path * (R-firstpaint §1a — the single biggest first-byte win). - * - `staleTime: 60_000` + `refetchOnMount: false` + - * `refetchOnWindowFocus: false` mirror `DASHBOARD_QUERY_OPTS` so a - * return-to-dashboard within a minute is a free cache hit. + * - `staleTime: 60_000` + `refetchOnMount: false` keep a + * return-to-dashboard within a minute a free cache hit. + * - `refetchOnWindowFocus: true` (v1.18.9) closes the background-sync + * freshness gap behind #38: a Withings / Apple-Health / iOS batch + * sync writes the user's data with NO client-side mutation event, so + * the snapshot keys are never invalidated and the dashboard only + * updated on the 120 s poll or a hard reload. Refetch-on-focus means + * returning to the tab after a sync surfaces the new readings within + * a frame. The `staleTime: 60_000` gate makes rapid focus toggles + * cheap (a refetch fires only once the slot is stale), and the warm + * 60 s server SWR cache keeps the focus refetch sub-cost. * - `refetchInterval: 120_000` + `refetchIntervalInBackground: false` * keep an open dashboard live: an idle tab polls the snapshot every * two minutes so freshly-synced Withings / HealthKit readings appear @@ -162,7 +170,11 @@ export function useDashboardSnapshot(enabled = true) { enabled, staleTime: 60_000, refetchOnMount: false, - refetchOnWindowFocus: false, + // v1.18.9 (#38) — refetch on focus so a background sync (Withings / + // Apple Health / iOS batch), which produces no client mutation event, + // surfaces on return-to-tab. Gated by the 60 s `staleTime` above so a + // rapid focus toggle within the window is still a free cache hit. + refetchOnWindowFocus: true, refetchInterval: DASHBOARD_REFETCH_INTERVAL_MS, refetchIntervalInBackground: false, // v1.16.8 — one retry on network errors / 5xx (never 401/403). A diff --git a/src/lib/query-keys/index.ts b/src/lib/query-keys/index.ts index 10b64db30..d84854923 100644 --- a/src/lib/query-keys/index.ts +++ b/src/lib/query-keys/index.ts @@ -58,6 +58,16 @@ export const queryKeys = { * TanStack's hierarchical-prefix semantics — adding a measurement now * refreshes the tile strip *and* the chart row in lockstep instead of * leaving the chart row 60 s stale (audit C2). + * + * v1.18.9 — `dashboardSnapshot` joins the bundle, mirroring the + * v1.16.11 medication fix. The dashboard hero band, score ring, and + * tile strip all read ONE snapshot query configured with + * refetchOnMount/WindowFocus off and a 120 s poll; without the key here + * a blood-pressure (or any) reading added in-app stayed invisible on the + * Startseite until the poll ticked or a hard reload — the reported #38 + * stale-read. The manual create / update / delete routes already + * hard-evict the server snapshot bucket (`{ evict: true }`), so the + * refetch this invalidation triggers returns post-write data at once. */ export const measurementDependentKeys = [ queryKeys.measurements(), @@ -65,6 +75,7 @@ export const measurementDependentKeys = [ queryKeys.insightsRoot(), queryKeys.insightsTargets(), queryKeys.gamificationAchievements(), + queryKeys.dashboardSnapshot(), ["chart-data"] as const, // v1.8.5 — re-run the diversity-nudge clustering when readings change. ["measurement-diversity"] as const,