From 4dddce6326eaa823b35ee436706695fc1d66445e Mon Sep 17 00:00:00 2001 From: Oliver Kieselbach Date: Mon, 29 Jun 2026 12:18:44 +0200 Subject: [PATCH] fix(global-reader): treat GlobalReader exactly like a GA in the sidebar (read-only aside) Refines a4171510. A GlobalReader is "GA minus writes" and must see the SAME sidebar as a real Global Admin: - Standard monitoring/rules/operations/configuration groups (Fleet Health, Usage Metrics, SLA, Geographic, Software, Audit, ...) were gated on `adminOrOperator`, so a pure GlobalReader (no own-tenant role) never saw them. Open them to any platform scope: `isAdminOrOperator || hasGlobalScope`. - The (purple) Global Admin section is gated back on the Global View toggle (`hasGlobalScope && globalAdminMode`) -- IDENTICAL to a GA: toggle off hides it, toggle on shows it. (a4171510 had decoupled it from the toggle; reverted so reader == GA exactly.) Read-only is enforced inside the pages (useCanMutatePlatform + backend) and by the existing item-level globalAdminOnly filter (Settings/Ops/Software drop out for a reader), not by hiding the data nav. The "fleet" MSP group stays delegated-only, so a GA/Reader gets the standard groups with no duplication. tsc clean; web 488 tests green. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/GlobalSidebar.tsx | 24 ++++++++++++------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Web/autopilot-monitor-web/components/GlobalSidebar.tsx b/src/Web/autopilot-monitor-web/components/GlobalSidebar.tsx index e1ddea7f..8fe54cd6 100644 --- a/src/Web/autopilot-monitor-web/components/GlobalSidebar.tsx +++ b/src/Web/autopilot-monitor-web/components/GlobalSidebar.tsx @@ -9,6 +9,7 @@ import { CollapseState } from "../hooks/useSidebarState"; import { DefaultSectionIcon, BookOpenIcon, RocketLaunchIcon, InformationCircleIcon, DocumentTextIcon, ShieldCheckIcon } from "../lib/sidebarIcons"; import { DASHBOARD_ITEM, NAV_GROUPS, EXPANDABLE_NAV_GROUPS, REGULAR_USER_ITEMS, NavItem, NavGroup, ExpandableNavGroup, ExpandableNavItem } from "../lib/globalNavConfig"; import { PublicSiteNavbar } from "./PublicSiteNavbar"; +import { useAdminMode } from "../hooks/useAdminMode"; // Sidebar pixel widths export const SIDEBAR_PX: Record = { @@ -45,6 +46,9 @@ export function GlobalSidebar({ children }: { children: ReactNode }) { return () => mql.removeEventListener("change", handler); }, []); + // Global admin mode toggle — gates the (purple) Global Admin section, identical to a real GA. + const { globalAdminMode } = useAdminMode(); + // --- Scroll-spy for page sections --- const [activeSectionId, setActiveSectionId] = useState(""); const observerRef = useRef(null); @@ -178,15 +182,17 @@ export function GlobalSidebar({ children }: { children: ReactNode }) { const isGroupVisible = (group: NavGroup | ExpandableNavGroup): boolean => { switch (group.visibility) { case "all": return true; - case "adminOrOperator": return isAdminOrOperator; - // Cross-tenant nav is a VISIBILITY surface → any platform scope (GA or read-only GlobalReader). - // Deliberately NOT gated on globalAdminMode: a pure GlobalReader (or a GA with no own-tenant role) - // would otherwise see only "Dashboard" until they discovered the hidden "Global View" toggle. The - // toggle now governs only the DATA scope (own-tenant vs cross-tenant) of the pages, not whether the - // platform nav is reachable. Mutating controls inside these views stay gated separately on isGlobalAdmin. - case "globalAdmin": return hasGlobalScope; + // A GlobalReader is "GA minus writes" — it must see the SAME sidebar as a real GA. The standard + // monitoring/rules/operations/configuration groups are normally tenant-admin scope; open them to any + // platform scope so a pure GlobalReader (no own-tenant role) gets Fleet Health, Usage Metrics, SLA, + // etc. Read-only is enforced inside the pages (useCanMutatePlatform + backend), not by hiding nav. + case "adminOrOperator": return isAdminOrOperator || hasGlobalScope; + // The (purple) Global Admin section is gated on the Global View toggle — IDENTICAL to a real GA: + // toggle off → hidden, toggle on → shown. Item-level globalAdminOnly entries (Settings/Ops/Software) + // still drop out for a read-only reader via the item filter below. + case "globalAdmin": return hasGlobalScope && globalAdminMode; // Fleet (MSP) nav: shown to a delegated admin who does NOT have full platform scope. A GA/Reader - // manages tenants through the richer Global Admin section instead, so it stays hidden for them. + // gets the standard monitoring groups above instead, so it stays hidden for them (no duplication). case "fleet": return isDelegated && !hasGlobalScope; default: return false; } @@ -229,7 +235,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) { return { ...group, items: filteredItems }; }); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isAdminOrOperator, isGlobalAdmin, hasMcpAccess, isAdminLike, canManageBootstrapTokens, bootstrapTokenEnabled, unrestrictedModeEnabled]); + }, [isAdminOrOperator, hasGlobalScope, isGlobalAdmin, globalAdminMode, hasMcpAccess, isAdminLike, canManageBootstrapTokens, bootstrapTokenEnabled, unrestrictedModeEnabled]); // Auto-expand the group containing the current pathname useEffect(() => {