mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
fix(dashboard): unwrap shared queryFn responses in onboarding checklist
GettingStartedChecklist intentionally shares TanStack Query cache
with the dashboard via `queryKey: ["analytics"]` etc., but its
queryFns returned the raw `res.json()` (the full `{data, error}`
envelope) while the dashboard, medications page, notifications page
and withings integrations section all return `json.data` (the
unwrapped inner shape). When the checklist's queryFn populated the
cache first, the dashboard's `data?.summaries?.WEIGHT` ended up
reading a property of the envelope `{data, error}` instead of the
analytics body, came back undefined, and silently failed every
per-tile data gate (`hasWeight`, `hasBp`, `hasPulse`, `hasBodyFat`).
Marc saw only the mood tile because mood lives under a different
cache key (`["mood-analytics"]`).
Same envelope/shape mismatch existed for `["medications"]`,
`["withings", "status"]` and `["notifications", "preferences"]` —
fixed in one pass since the failure mode is identical (silent
undefined reads on shared keys, latent bug for any consumer that
mounts after the checklist).
Each queryFn now `await`s and returns `json.data`; the local
accessors collapse from `res?.data?.X` to `res?.X`. As a
side-effect this also fixes a long-standing checklist bug where
`medsRes.data.medications.length` was always 0 (the medications
API returns the array directly under `data:`, not nested under a
`medications` field — so the count silently coerced to zero and
the "log a medication" checklist item never auto-completed).
Tests + typecheck stay green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
0810a859e1
commit
58e9d1451a
1 file changed
+31
-26
@@ -59,18 +59,12 @@ const ITEM_LABEL_KEYS: Record<
|
||||
},
|
||||
};
|
||||
|
||||
interface MedicationsResponse {
|
||||
data?: { medications?: Array<{ id: string }> };
|
||||
interface AnalyticsData {
|
||||
summaries?: Record<string, { count?: number } | undefined>;
|
||||
}
|
||||
|
||||
interface AnalyticsResponse {
|
||||
data?: {
|
||||
summaries?: Record<string, { count?: number } | undefined>;
|
||||
};
|
||||
}
|
||||
|
||||
interface WithingsStatusResponse {
|
||||
data?: { connected?: boolean };
|
||||
interface WithingsStatus {
|
||||
connected?: boolean;
|
||||
}
|
||||
|
||||
interface NotificationChannel {
|
||||
@@ -78,8 +72,8 @@ interface NotificationChannel {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
interface NotificationsResponse {
|
||||
data?: { channels?: NotificationChannel[] };
|
||||
interface NotificationsPreferences {
|
||||
channels?: NotificationChannel[];
|
||||
}
|
||||
|
||||
function readDismissedSet(): Set<ChecklistItemId> {
|
||||
@@ -150,59 +144,70 @@ export function GettingStartedChecklist() {
|
||||
// Light-weight queries: each fetch is small and cached by tanstack.
|
||||
// We rely on the React Query cache the dashboard already uses for
|
||||
// analytics, so this won't fire a second request when the dashboard
|
||||
// and the checklist mount together.
|
||||
const { data: analyticsRes } = useQuery<AnalyticsResponse>({
|
||||
// and the checklist mount together. Critically, every queryFn here
|
||||
// must return `json.data` (the unwrapped shape) so it matches the
|
||||
// canonical consumer (dashboard / medications page / notifications
|
||||
// page / integrations section). Returning `res.json()` would write
|
||||
// the full `{data, error}` envelope into the shared cache, which then
|
||||
// makes the canonical consumers' `data?.summaries` / `data?.length`
|
||||
// reads silently return undefined — that's how the v1.4.2 dashboard
|
||||
// ended up rendering only the mood tile.
|
||||
const { data: analyticsData } = useQuery<AnalyticsData>({
|
||||
queryKey: ["analytics"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/analytics");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
return res.json();
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const { data: medsRes } = useQuery<MedicationsResponse>({
|
||||
const { data: medsData } = useQuery<Array<{ id: string }>>({
|
||||
queryKey: ["medications"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/medications");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
return res.json();
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const { data: withingsRes } = useQuery<WithingsStatusResponse>({
|
||||
const { data: withingsData } = useQuery<WithingsStatus>({
|
||||
queryKey: ["withings", "status"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/withings/status");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
return res.json();
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const { data: notificationsRes } = useQuery<NotificationsResponse>({
|
||||
const { data: notificationsData } = useQuery<NotificationsPreferences>({
|
||||
queryKey: ["notifications", "preferences"],
|
||||
queryFn: async () => {
|
||||
const res = await fetch("/api/notifications/preferences");
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
return res.json();
|
||||
const json = await res.json();
|
||||
return json.data;
|
||||
},
|
||||
enabled: !!user,
|
||||
});
|
||||
|
||||
const measurementCount = useMemo(() => {
|
||||
const summaries = analyticsRes?.data?.summaries ?? {};
|
||||
const summaries = analyticsData?.summaries ?? {};
|
||||
let total = 0;
|
||||
for (const key of Object.keys(summaries)) {
|
||||
total += summaries[key]?.count ?? 0;
|
||||
}
|
||||
return total;
|
||||
}, [analyticsRes]);
|
||||
}, [analyticsData]);
|
||||
|
||||
const medicationCount = medsRes?.data?.medications?.length ?? 0;
|
||||
const withingsConnected = withingsRes?.data?.connected === true;
|
||||
const notificationsConfigured = (notificationsRes?.data?.channels ?? []).some(
|
||||
const medicationCount = medsData?.length ?? 0;
|
||||
const withingsConnected = withingsData?.connected === true;
|
||||
const notificationsConfigured = (notificationsData?.channels ?? []).some(
|
||||
(channel) => channel?.enabled === true,
|
||||
);
|
||||
|
||||
|
||||
Reference in new issue
Block a user