fix(insights): offer no metric pill inside a switched-to record

Inside a managed child's record, or as a delegate with manage access,
the tab strip showed Weight, Pulse, Blood pressure and the other metric
pills, and every one of them opened an error. The strip now reads the
record's own presence answer, but the metric pages read /api/analytics,
which serves only the signed-in account's own record and refuses a
switched session. The strip again shows those pills only in the
account's own record, as before; mood, medication, workout and ECG
pills keep following the presence answer.
This commit is contained in:
Marc-André Bombeck committed 2026-11-12 15:11:40 +01:00
1 parent 6e8554fd37
commit 6f8a342edf
2 files changed
+55 -7

No files matched your search

@@ -13,12 +13,25 @@ import { renderToStaticMarkup } from "react-dom/server";
*/
const queries: Array<{ queryKey: readonly unknown[]; enabled?: boolean }> = [];
const stripProps: Array<{ onRegenerate?: unknown }> = [];
const stripProps: Array<{
onRegenerate?: unknown;
availability?: { summaries?: unknown; hasMood?: boolean };
}> = [];
/** What the presence read answers, and the record the session is inside. */
const answer: { presence: unknown; active: unknown } = {
presence: undefined,
active: null,
};
vi.mock("@tanstack/react-query", () => ({
useQuery: (options: { queryKey: readonly unknown[]; enabled?: boolean }) => {
queries.push(options);
return { data: undefined, isLoading: false, isError: false };
const presence = options.queryKey.join("/") === "insights/presence";
return {
data: presence ? answer.presence : undefined,
isLoading: false,
isError: false,
};
},
useMutation: () => ({ mutate: vi.fn(), isPending: false, data: undefined }),
useQueryClient: () => ({
@@ -26,7 +39,10 @@ vi.mock("@tanstack/react-query", () => ({
}),
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({ isAuthenticated: true, user: { modules: {} } }),
useAuth: () => ({
isAuthenticated: true,
user: { modules: {}, accountAccess: { active: answer.active } },
}),
}));
vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true }));
vi.mock("@/hooks/use-ai-capability", () => ({
@@ -41,7 +57,7 @@ vi.mock("@/hooks/use-insights-layout", () => ({
},
}));
vi.mock("@/components/insights/insights-tab-strip", () => ({
InsightsTabStrip: (props: { onRegenerate?: unknown }) => {
InsightsTabStrip: (props: (typeof stripProps)[number]) => {
stripProps.push(props);
return null;
},
@@ -50,7 +66,7 @@ vi.mock("@/components/insights/insights-tab-strip", () => ({
import { queryKeys } from "@/lib/query-keys";
import { InsightsLayoutShell } from "../insights-layout-shell";
describe("InsightsLayoutShell — what it reads on entry", () => {
describe("InsightsLayoutShell: what it reads on entry", () => {
it("keeps the regenerate button and sends no briefing read", () => {
renderToStaticMarkup(
<InsightsLayoutShell>
@@ -82,4 +98,33 @@ describe("InsightsLayoutShell — what it reads on entry", () => {
expect(sent).toEqual([JSON.stringify(queryKeys.insightsPresence())]);
expect(layoutReads).toEqual([true]);
});
it("offers no metric pill inside a switched-to record", () => {
// The metric pages read `/api/analytics`, which serves only the caller's
// own record; the mood and medication pills read their own routes.
answer.presence = {
metrics: { WEIGHT: { count: 12 } },
hasMood: true,
hasMedication: false,
hasWorkouts: false,
hasEcgRecordings: false,
};
const render = (active: unknown) => {
answer.active = active;
stripProps.length = 0;
renderToStaticMarkup(
<InsightsLayoutShell>
<div />
</InsightsLayoutShell>,
);
return stripProps[0]?.availability;
};
expect(render(null)?.summaries).toEqual({ WEIGHT: { count: 12 } });
const switched = render({ accountId: "record-2" });
expect(switched?.summaries).toBeUndefined();
expect(switched?.hasMood).toBe(true);
answer.presence = undefined;
answer.active = null;
});
});
@@ -72,19 +72,22 @@ export function InsightsLayoutShell({ children }: { children: ReactNode }) {
// on a first row as well left that page unreachable before data existed.
const hasNutrients = user?.modules?.nutrients === true;
const hasEcgRecordings = presence?.hasEcgRecordings === true;
// Inside a switched-to record the metric pages read `/api/analytics`, which
// serves only the caller's own record, so their pills would open an error.
const switched = user?.accountAccess?.active != null;
// Memoised so an unchanged answer keeps the same reference: the strip is
// wrapped in `React.memo` and reruns `buildTabs` on a new object.
const availability: InsightInputs | undefined = useMemo(() => {
if (!isAuthenticated) return undefined;
return {
summaries: presence?.metrics,
summaries: switched ? undefined : presence?.metrics,
hasMood: presence?.hasMood === true,
hasMedication: presence?.hasMedication === true,
hasWorkouts: presence?.hasWorkouts === true,
hasNutrients,
};
}, [isAuthenticated, presence, hasNutrients]);
}, [isAuthenticated, presence, hasNutrients, switched]);
// v1.15.14 — couple the tab strip to the saved insights layout.
// A sub-page pill now shows iff its metric has data AND its slug is