fix(insights): make the nutrients pill reachable once the module is on

The nutrients pill required BOTH the opt-in module on AND at least one
NutrientIntakeDay row — but the in-context enable CTA lives on
/insights/nutrients itself, so a user who had just turned the module on
(or wanted to) had no way to reach the page that would let them. Drop
the row-count floor for this one metric: the pill now gates on the
module toggle alone, matching the deliberate opt-in the user already
made. The page itself is unchanged and still degrades from "enable" to
"no rows yet" to the three-card spine.
This commit is contained in:
Marc-André Bombeck committed 2026-07-17 23:50:06 +02:00
1 parent d124a3239d
commit 5f253f7d09
4 files changed
+24 -23

No files matched your search

@@ -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 };
@@ -366,8 +366,10 @@ const SUB_PAGE_MODULE: Partial<Record<SubPageSlug, ModuleKey>> = {
"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",
};
+9 -5
View File
@@ -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;
}
+2 -2
View File
@@ -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<string, readonly string[]>;