diff --git a/src/components/insights/insights-layout-shell.tsx b/src/components/insights/insights-layout-shell.tsx index c4e34fa26..c5ad3679d 100644 --- a/src/components/insights/insights-layout-shell.tsx +++ b/src/components/insights/insights-layout-shell.tsx @@ -106,20 +106,15 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) { // dashboard tile so navigation between surfaces is a free cache hit. const workoutsProbe = useWorkouts({ limit: 1 }); - // v1.29 — nutrients-presence gate. The nutrients pill gates on at - // least one `NutrientIntakeDay` row in a 1-day probe window (any - // nutrient, any source). Enabled only when the opt-in module is on — - // the route 403s "module.disabled" otherwise, so probing while off - // would just burn a request every mount. + // v1.29 — nutrients gate. `nutrients` is opt-in (default off), unlike + // the always-on metric clusters, so the pill's floor is the module + // toggle alone — not a data-presence probe. Gating on BOTH the toggle + // AND a first row was a catch-22: the in-context enable CTA lives on + // `/insights/nutrients` itself, and that page was only reachable once + // data already existed. A user who just flipped the module on (or + // wants to) can now always reach the page; the page itself still + // degrades from "enable" → "no rows yet" → the three-card spine. const nutrientsEnabled = user?.modules?.nutrients === true; - const nutrientsProbe = useQuery({ - queryKey: queryKeys.nutrientIntake(1), - queryFn: () => - apiGet<{ nutrients: Array<{ nutrient: string }> }>( - "/api/nutrients?days=1", - ), - enabled: isAuthenticated && nutrientsEnabled, - }); // v1.4.31 — memoise the `availability` prop so an unchanged // payload doesn't recreate the object on every cache-write of @@ -132,7 +127,7 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) { const hasMood = (comprehensiveQuery.data?.moodSummary?.count ?? 0) > 0; const hasMedication = (comprehensiveQuery.data?.medications?.length ?? 0) > 0; const hasWorkouts = (workoutsProbe.data?.workouts.length ?? 0) > 0; - const hasNutrients = (nutrientsProbe.data?.nutrients.length ?? 0) > 0; + const hasNutrients = nutrientsEnabled; const availability: InsightInputs | undefined = useMemo(() => { if (!isAuthenticated) return undefined; return { summaries, hasMood, hasMedication, hasWorkouts, hasNutrients }; diff --git a/src/components/insights/insights-tab-strip.tsx b/src/components/insights/insights-tab-strip.tsx index 34339ce0b..d2fd0060b 100644 --- a/src/components/insights/insights-tab-strip.tsx +++ b/src/components/insights/insights-tab-strip.tsx @@ -366,8 +366,10 @@ const SUB_PAGE_MODULE: Partial> = { "breathing-disturbances": "sleep", "blood-glucose": "glucose", workouts: "workouts", - // v1.29 — the nutrients pill gates on the opt-in `nutrients` module on - // top of the `hasNutrients` data floor. + // v1.29 — the nutrients pill gates on the opt-in `nutrients` module + // alone; there is no data floor on top (see `hasNutrients` in + // `insights-layout-shell.tsx` — reachability, not row count, is the + // gate once the user has opted in). nutrients: "nutrients", }; diff --git a/src/lib/insights/metric-availability.ts b/src/lib/insights/metric-availability.ts index 434655e91..74e790e44 100644 --- a/src/lib/insights/metric-availability.ts +++ b/src/lib/insights/metric-availability.ts @@ -123,7 +123,8 @@ export type InsightMetric = // v1.29 — nutrient intake (hydration + micronutrients). Event/store // driven (`NutrientIntakeDay`, not a `Measurement` series) like // `MOOD` / `MEDICATION` / `WORKOUTS`, so it reads the boolean - // `hasNutrients` flag rather than a `summaries[…].count`. + // `hasNutrients` flag rather than a `summaries[…].count` — and unlike + // those, the flag tracks the opt-in module toggle, not row presence. | "NUTRIENTS"; /** @@ -147,10 +148,13 @@ export interface InsightInputs { */ hasWorkouts?: boolean; /** - * v1.29 — whether the user has at least one `NutrientIntakeDay` row in - * the probe window (any nutrient, any source). Drives the nutrients - * pill. Optional so legacy mounts keep type-checking; the helper - * treats `undefined` as "not available". + * v1.29 — whether the opt-in `nutrients` module is on. Drives the + * nutrients pill directly (no row-count floor on top): the module is + * a deliberate user choice, and the in-context enable CTA lives on + * `/insights/nutrients` itself, so the pill must stay reachable the + * moment the module is on even with zero rows recorded yet. Optional + * so legacy mounts keep type-checking; the helper treats `undefined` + * as "not available". */ hasNutrients?: boolean; } diff --git a/src/lib/insights/sub-page-metric.ts b/src/lib/insights/sub-page-metric.ts index 16180fd6a..bbe2bc391 100644 --- a/src/lib/insights/sub-page-metric.ts +++ b/src/lib/insights/sub-page-metric.ts @@ -138,8 +138,8 @@ const SUB_PAGE_METRIC = { // v1.29 — nutrient intake (hydration + micronutrients). Backed by // `NutrientIntakeDay`, not a `Measurement` series, so the metric list // stays empty like `medications` / `workouts`; the tab-strip pill gates - // on `hasNutrients` (threaded from a lightweight probe read) instead of - // a `summaries[…].count`. + // on `hasNutrients`, which tracks the opt-in module toggle rather than + // a `summaries[…].count` or a row-presence probe. nutrients: [], } as const satisfies Record;