From 5b8543f47151a3aaab9263a55634958c55487c09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Thu, 12 Nov 2026 13:05:37 +0100 Subject: [PATCH] fix(settings): keep the Coach entry for a managed record The Settings Coach entry now follows whether the Coach can answer, as the sidebar does, with Settings, AI keeping the stored memory in reach. A managed record has no Settings, AI, so while its Coach could not answer yet (no operator key, no consent) the guardian lost the only way to the record's Coach data scope and its switch. For a managed record the entry follows the module switch alone again. --- .../coach-entry-follows-capability.test.tsx | 27 +++++++++++++++++-- src/components/settings/settings-shell.tsx | 16 ++++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx b/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx index c30b49e8c..79d0eef04 100644 --- a/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx +++ b/src/components/settings/__tests__/coach-entry-follows-capability.test.tsx @@ -15,7 +15,10 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; import { renderToStaticMarkup } from "react-dom/server"; -const h = vi.hoisted(() => ({ coachAvailable: true })); +const h = vi.hoisted(() => ({ + coachAvailable: true, + active: null as Record | null, +})); vi.mock("next/navigation", () => ({ usePathname: () => "/settings/ai", @@ -24,7 +27,12 @@ vi.mock("next/navigation", () => ({ vi.mock("@/hooks/use-mounted", () => ({ useMounted: () => true })); vi.mock("@/hooks/use-auth", () => ({ useAuth: () => ({ - user: { id: "u1", role: "USER", modules: { coach: true } }, + user: { + id: "u1", + role: "USER", + modules: { coach: true }, + accountAccess: { active: h.active }, + }, isAuthenticated: true, isLoading: false, refetch: vi.fn(), @@ -68,6 +76,7 @@ function render(node: React.ReactNode) { describe("the Coach entry in Settings", () => { beforeEach(() => { h.coachAvailable = true; + h.active = null; }); it("is listed while the Coach can answer, and the memory lives there", () => { @@ -86,4 +95,18 @@ describe("the Coach entry in Settings", () => { ).not.toContain('href="/settings/coach"'); expect(render()).toContain('data-slot="stored-coach-memory"'); }); + + it("stays for a managed record whose Coach cannot answer yet", () => { + // A record has no Settings, AI: the guardian reaches the record's Coach + // data scope and switch through this entry only. + h.coachAvailable = false; + h.active = { + recordKind: "managed", + level: "manage", + manageableDomains: [], + }; + expect( + render(body), + ).toContain('href="/settings/coach"'); + }); }); diff --git a/src/components/settings/settings-shell.tsx b/src/components/settings/settings-shell.tsx index a2bcb5185..8d6924f5c 100644 --- a/src/components/settings/settings-shell.tsx +++ b/src/components/settings/settings-shell.tsx @@ -356,6 +356,20 @@ function deriveActiveSlug( return isSettingsSectionSlug(candidate) ? candidate : "account"; } +/** + * The module map the section list reads, the sidebar's. A managed record has + * no Settings, AI to list its Coach settings (its data scope, its switch), so + * there the Coach entry follows the module switch alone, not whether the + * record's Coach can answer yet. + */ +function useSectionModules() { + const { user } = useAuth(); + const navModules = useNavModules(); + return user?.accountAccess?.active?.recordKind === "managed" + ? user.modules + : navModules; +} + export function SettingsShell({ active, heading, @@ -391,7 +405,7 @@ export function SettingsShell({ // effect (which the lint rule flags as a cascading render), and no risk of // the two passes disagreeing. const hydrated = useMounted(); - const modules = useNavModules(); + const modules = useSectionModules(); const activeRecord = user?.accountAccess?.active ?? null; const allVisibleSections = SETTINGS_SECTIONS.filter( (section) =>