From 16b0c349f2a1c5bfff00a1b26f69e10cd8317b87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Tue, 2 Jun 2026 13:17:06 +0200 Subject: [PATCH] feat(insights): add generic per-metric assessment loader Add a parameterised query key (insightsMetricStatus) and a useInsightMetricStatus hook that reads the generic per-metric assessment route GET /api/insights/metric-status?metric=. The seven bespoke status routes keep their own keys and hook union; this one covers the HealthKit metric sub-pages that share a single route keyed by the metric id. The hook mirrors the existing status loader: 8 s client ceiling, no inline retry, and the bounded preparing poll. Co-Authored-By: Claude Opus 4.8 (1M context) (cherry picked from commit a3407a2f6c1e9f54c92a6f182db894ae0593e69f) --- src/hooks/use-insight-status.ts | 56 +++++++++++++++++++++++++++++++++ src/lib/query-keys.ts | 10 ++++++ 2 files changed, 66 insertions(+) diff --git a/src/hooks/use-insight-status.ts b/src/hooks/use-insight-status.ts index 8b404d355..0ac0bc3ef 100644 --- a/src/hooks/use-insight-status.ts +++ b/src/hooks/use-insight-status.ts @@ -163,3 +163,59 @@ export function useInsightStatus(metric: InsightStatusMetric) { ), }); } + +/** + * v1.8.7.1 — generic per-metric assessment loader for the HealthKit + * metric sub-pages. The seven bespoke metrics above (`weight`, `pulse`, + * …) each own a hand-written `/api/insights/-status` route; the + * ~29 HealthKit pages instead share one generic route keyed by the + * metric id: + * + * GET /api/insights/metric-status?metric=&locale= + * + * where METRIC_ID is the existing HealthKit measurement identifier + * (`HEART_RATE_VARIABILITY`, `RESTING_HEART_RATE`, `SLEEP_DURATION`, …). + * The response envelope, the 8 s client ceiling, the no-retry policy and + * the bounded `preparing` poll are identical to `useInsightStatus` — the + * card consumes the same `InsightStatusData` shape for every scope. + * + * `enabled` lets the page suppress the fetch entirely when the metric has + * no data (the page renders the insufficient-data empty state instead of + * the card), so a brand-new account never fires an assessment round-trip. + */ +export function useInsightMetricStatus(metric: string, enabled = true) { + const { isAuthenticated } = useAuth(); + const { locale } = useTranslations(); + + return useQuery({ + queryKey: queryKeys.insightsMetricStatus(metric, locale), + queryFn: async (): Promise => { + const controller = new AbortController(); + const timeoutHandle = setTimeout( + () => controller.abort(), + STATUS_TIMEOUT_MS, + ); + try { + const res = await fetch( + `/api/insights/metric-status?metric=${encodeURIComponent( + metric, + )}&locale=${locale}`, + { signal: controller.signal }, + ); + if (!res.ok) throw new Error("Failed"); + const json = (await res.json()) as { data: InsightStatusData }; + return json.data; + } finally { + clearTimeout(timeoutHandle); + } + }, + enabled: isAuthenticated && enabled, + staleTime: 60 * 1000, + retry: 0, + refetchInterval: (query) => + nextStatusPollInterval( + query.state.data?.preparing, + query.state.dataUpdateCount, + ), + }); +} diff --git a/src/lib/query-keys.ts b/src/lib/query-keys.ts index dc967e3aa..0aeae6832 100644 --- a/src/lib/query-keys.ts +++ b/src/lib/query-keys.ts @@ -103,6 +103,16 @@ export const queryKeys = { ["insights", "mood-status", locale] as const, insightsMedicationComplianceStatus: (locale: string) => ["insights", "medication-compliance-status", locale] as const, + /** + * v1.8.7.1 — per-metric insight assessment for the HealthKit metric + * sub-pages. One generic route (`/api/insights/metric-status?metric=…`) + * backs every HealthKit metric, so the key is parameterised by the + * metric id alongside the locale rather than minting a bespoke factory + * entry per metric. The seven bespoke `*-status` routes above keep + * their own keys; this one covers the ~29 generic HealthKit pages. + */ + insightsMetricStatus: (metric: string, locale: string) => + ["insights", "metric-status", metric, locale] as const, medications: () => ["medications"] as const, medicationDetail: (id: string) => ["medications", id] as const,