fix(insights): hide the Medications and recovery metric pills with their modules off

The tab strip's slug map is gone; every pill asks the surface map by its
slug, the same entry the page notice reads. That adds the Medications
pill and the cardio recovery, six-minute walk and stair speed pills,
which stayed visible with their modules off.
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 09:48:48 +02:00
1 parent 39ae034b8c
commit 74154460fd
2 files changed
+32 -32

No files matched your search

@@ -546,6 +546,28 @@ describe("<InsightsTabStrip> — module enable/disable gate (v1.18.0)", () => {
expect(enabled).toContain('href="/insights/recovery"');
});
it("hides the Medications pill with the medications module off", () => {
const withMeds: InsightInputs = {
...moduleAvailability,
hasMedication: true,
};
const on = render(
<InsightsTabStrip
availability={withMeds}
modules={{ medications: true }}
/>,
);
expect(on).toContain('href="/insights/medications"');
const off = render(
<InsightsTabStrip
availability={withMeds}
modules={{ medications: false }}
/>,
);
expect(off).not.toContain('href="/insights/medications"');
expect(off).toContain(">Overview<");
});
it("fails open: an empty / omitted module map keeps every module pill", () => {
// Default-on contract — a stale /me payload (no module map) must not
// blank the strip. An empty map and an omitted prop both keep pills.
+10 -32
View File
@@ -37,6 +37,7 @@ import {
type InsightMetric,
} from "@/lib/insights/metric-availability";
import type { ModuleKey } from "@/lib/modules/registry";
import { isSurfaceVisible } from "@/lib/modules/surface";
/**
* v1.4.25 W4 — routed tab strip for `/insights`.
@@ -392,32 +393,6 @@ export const SUB_PAGE_TABS: Record<
nutrients: { labelKey: "insights.navNutrients", metric: "NUTRIENTS" },
};
/**
* v1.18.0 — slug → module gate. A sub-page pill belonging to a toggleable
* module is hidden when that module is disabled in the account's resolved
* module map (from `GET /api/auth/me`'s `modules`), on TOP of the data +
* layout gates. Slugs absent from this map are core metrics (BP, pulse,
* weight, body composition, activity, cardio, hearing, environment,
* medications …) and never gate on a module — only on data availability.
*
* `labs` has no metric pill; it surfaces via the left nav only. The
* Recovery pill is gated separately below (it has no slug — it is a
* composite always-present link, so it reads the `recovery` module key
* directly in `buildTabs`).
*/
const SUB_PAGE_MODULE: Partial<Record<SubPageSlug, ModuleKey>> = {
mood: "mood",
sleep: "sleep",
"breathing-disturbances": "sleep",
"blood-glucose": "glucose",
workouts: "workouts",
// 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",
};
/**
* v1.18.0 — a partial `ModuleKey → enabled` map (the `modules` field
* `GET /api/auth/me` returns; cycle + coach already delegated). A pill
@@ -427,14 +402,17 @@ const SUB_PAGE_MODULE: Partial<Record<SubPageSlug, ModuleKey>> = {
*/
export type InsightsModuleMap = Partial<Record<ModuleKey, boolean>>;
/** True unless the slug's module is explicitly disabled in the map. */
/**
* True unless the slug's module is explicitly off. A sub-page's owner comes
* from the one surface map (`insights-page:<slug>`), the same entry the
* direct-URL notice reads, so a pill and its page cannot disagree. Slugs with
* no entry are core metrics and gate on data only.
*/
function isSlugModuleEnabled(
slug: SubPageSlug,
modules: InsightsModuleMap | undefined,
): boolean {
const key = SUB_PAGE_MODULE[slug];
if (!key) return true;
return modules?.[key] !== false;
return isSurfaceVisible(`insights-page:${slug}`, modules);
}
/**
@@ -510,7 +488,7 @@ function buildTabs(
// group child `order` follows the layout (via `orderedSlugs` below),
// falling back to `SUB_PAGE_GROUP_ORDER` when the layout is absent.
// v1.18.0 — a THIRD gate on top of data + layout: a pill belonging to a
// toggleable module (mood / sleep / glucose / workouts) is hidden when
// toggleable module (the `insights-page:*` surfaces) is hidden when
// that module is disabled in the account's resolved module map. Core
// metric pills carry no module key and pass this gate unconditionally.
const visibleSlugs = availability
@@ -551,7 +529,7 @@ function buildTabs(
// `summaries[METRIC].count` to gate on — the page data-gates every block
// and falls back to a calm empty note. The pill is otherwise present like
// Overview, but it is hidden when the `recovery` module is disabled.
if (modules?.recovery !== false) {
if (isSurfaceVisible("insights-page:recovery", modules)) {
entries.push({
kind: "link",
href: `${INSIGHTS_OVERVIEW_PATH}/recovery`,