fix(global-reader): show platform nav on global scope regardless of the Global View toggle

The cross-tenant nav group (Managed Tenants, Fleet Health, Software,
Geographic, SLA, Usage, Audit...) was gated on `hasGlobalScope &&
globalAdminMode`. globalAdminMode defaults to false (localStorage) and there
is only auto-DISABLE logic, no auto-enable, so a fresh GlobalReader (or a GA
with no own-tenant role) saw only "Dashboard" until they discovered the hidden
"Global View" toggle in the avatar menu -- a dead end. A delegated MSP admin,
by contrast, gets the full fleet nav with no toggle.

Decouple nav VISIBILITY from the toggle: the platform nav is reachable for any
caller with global scope (GA or read-only GlobalReader). The toggle now governs
only the DATA scope (own-tenant vs cross-tenant) of the pages, not whether the
nav is reachable. Mutating controls inside these views stay gated on
isGlobalAdmin. Removed the now-dead globalAdminMode dependency from the sidebar.

tsc clean; web 485 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-29 11:47:19 +02:00
parent 7742113b29
commit a41715105d
@@ -9,7 +9,6 @@ import { CollapseState } from "../hooks/useSidebarState";
import { DefaultSectionIcon, BookOpenIcon, RocketLaunchIcon, InformationCircleIcon, DocumentTextIcon, ShieldCheckIcon } from "../lib/sidebarIcons"; 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 { DASHBOARD_ITEM, NAV_GROUPS, EXPANDABLE_NAV_GROUPS, REGULAR_USER_ITEMS, NavItem, NavGroup, ExpandableNavGroup, ExpandableNavItem } from "../lib/globalNavConfig";
import { PublicSiteNavbar } from "./PublicSiteNavbar"; import { PublicSiteNavbar } from "./PublicSiteNavbar";
import { useAdminMode } from "../hooks/useAdminMode";
// Sidebar pixel widths // Sidebar pixel widths
export const SIDEBAR_PX: Record<CollapseState, number> = { export const SIDEBAR_PX: Record<CollapseState, number> = {
@@ -46,9 +45,6 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
return () => mql.removeEventListener("change", handler); return () => mql.removeEventListener("change", handler);
}, []); }, []);
// Global admin mode from hook
const { globalAdminMode } = useAdminMode();
// --- Scroll-spy for page sections --- // --- Scroll-spy for page sections ---
const [activeSectionId, setActiveSectionId] = useState(""); const [activeSectionId, setActiveSectionId] = useState("");
const observerRef = useRef<IntersectionObserver | null>(null); const observerRef = useRef<IntersectionObserver | null>(null);
@@ -184,8 +180,11 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
case "all": return true; case "all": return true;
case "adminOrOperator": return isAdminOrOperator; case "adminOrOperator": return isAdminOrOperator;
// Cross-tenant nav is a VISIBILITY surface → any platform scope (GA or read-only GlobalReader). // Cross-tenant nav is a VISIBILITY surface → any platform scope (GA or read-only GlobalReader).
// Mutating controls inside these views are gated separately on isGlobalAdmin. // Deliberately NOT gated on globalAdminMode: a pure GlobalReader (or a GA with no own-tenant role)
case "globalAdmin": return hasGlobalScope && globalAdminMode; // 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;
// Fleet (MSP) nav: shown to a delegated admin who does NOT have full platform scope. A GA/Reader // 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. // manages tenants through the richer Global Admin section instead, so it stays hidden for them.
case "fleet": return isDelegated && !hasGlobalScope; case "fleet": return isDelegated && !hasGlobalScope;
@@ -230,7 +229,7 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
return { ...group, items: filteredItems }; return { ...group, items: filteredItems };
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [isAdminOrOperator, isGlobalAdmin, globalAdminMode, hasMcpAccess, isAdminLike, canManageBootstrapTokens, bootstrapTokenEnabled, unrestrictedModeEnabled]); }, [isAdminOrOperator, isGlobalAdmin, hasMcpAccess, isAdminLike, canManageBootstrapTokens, bootstrapTokenEnabled, unrestrictedModeEnabled]);
// Auto-expand the group containing the current pathname // Auto-expand the group containing the current pathname
useEffect(() => { useEffect(() => {