fix(web): no broken Dashboard nav path for delegated-only MSP users [Phase 3b follow-up]

Addresses a Codex finding: Phase 3b made delegated-only MSP users non-regular (so
they get the group nav), which also rendered the Dashboard link for them — but
/dashboard classifies the same user as regular and bounced them to /progress. So
clicking Dashboard sent an MSP admin to the end-user progress portal.

Fixed on both ends:
- GlobalSidebar: gate the Dashboard link on isAdminOrOperator || hasGlobalScope
  (genuine own-tenant/platform users), not merely !isRegularUser. A delegated-only
  admin has no own-tenant stake, so the link is hidden; their entry point is the
  Fleet group.
- /dashboard: a delegated-only user who reaches it by deep link now redirects to
  /fleet instead of /progress, consistent with the AuthGate landing.

Web tsc clean; 476 web tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Oliver Kieselbach
2026-06-25 22:14:49 +02:00
parent 23f554f5ac
commit e2ce43a2d0
2 changed files with 10 additions and 5 deletions
@@ -144,11 +144,12 @@ function HomeContent() {
disabled: isRegularUser,
});
// Redirect regular users (non-admin, non-operator, no platform scope) to the progress portal — they
// must never see the session list. A read-only Global Reader has cross-tenant read scope → stays.
// Redirect users without own-tenant/platform scope away from the session list. A delegated ("MSP") admin
// manages OTHER tenants and belongs on the fleet overview, not the end-user progress portal; everyone else
// without scope goes to /progress. A read-only Global Reader has cross-tenant read scope → stays.
useEffect(() => {
if (user && !user.isTenantAdmin && !user.isGlobalAdmin && !user.isGlobalReader && user.role !== 'Operator') {
router.replace("/progress");
router.replace(user.isDelegated ? "/fleet" : "/progress");
}
}, [user, router]);
@@ -399,14 +399,18 @@ export function GlobalSidebar({ children }: { children: ReactNode }) {
// Regular users see minimal nav. A read-only Global Reader has platform scope, and a delegated MSP admin
// has fleet scope → both get the (group-filtered) nav rather than the minimal regular-user list.
const isRegularUser = !isAdminOrOperator && !hasFleetScope;
// The Dashboard (own-tenant session list) is only meaningful for a genuine own-tenant/platform user.
// A delegated-only MSP admin has no own-tenant stake — /dashboard would just bounce them to /fleet —
// so hide the link for them; their entry point is the Fleet group.
const showDashboard = isAdminOrOperator || hasGlobalScope;
const renderNavContent = (isMobile = false) => (
<>
{/* Global nav — only when authenticated */}
{isAuthenticated && (
<>
{/* Dashboard — hidden for regular users (no admin/operator access) */}
{!isRegularUser && (
{/* Dashboard — own-tenant session list; hidden for regular users AND delegated-only MSP admins */}
{showDashboard && (
<ul className="space-y-0.5">
{renderGlobalItem(DASHBOARD_ITEM)}
</ul>