From 00476c974fa6f53bbc06dd6e2af0fcd93924e92e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:39:12 +0200 Subject: [PATCH] fix: vendor scoping, sidebar visibility, and landing redirect - OrgResolver merges global + org-scoped token scopes so vendor's platform:admin (from global saas-vendor role) is always visible - LandingRedirect waits for scopes to load before redirecting (prevents premature redirect to server dashboard) - Layout hides tenant portal sidebar items when vendor is on /vendor/* routes; shows them when navigating to tenant context Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/components/Layout.tsx | 7 +++++-- ui/src/router.tsx | 6 ++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 25fd0e3..c8d5de4 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -35,6 +35,9 @@ export function Layout() { const isVendor = scopes.has('platform:admin'); const isTenantAdmin = scopes.has('tenant:manage'); + const onVendorRoute = location.pathname.startsWith('/vendor'); + // Vendor on vendor routes: show only TENANTS. On tenant routes: show tenant portal too (for debugging). + const showTenantPortal = isTenantAdmin && (!isVendor || !onVendorRoute); // Determine current org slug for server dashboard link const currentOrg = organizations.find((o) => o.id === currentOrgId); @@ -68,8 +71,8 @@ export function Layout() { )} - {/* Tenant portal — only visible to tenant admins (tenant:manage scope) */} - {isTenantAdmin && ( + {/* Tenant portal — visible to tenant admins; hidden for vendor on vendor routes */} + {showTenantPortal && ( <> } diff --git a/ui/src/router.tsx b/ui/src/router.tsx index 7fd04da..da6591b 100644 --- a/ui/src/router.tsx +++ b/ui/src/router.tsx @@ -22,6 +22,12 @@ function LandingRedirect() { const { organizations, currentOrgId } = useOrgStore(); const currentOrg = organizations.find((o) => o.id === currentOrgId); + // Wait for scopes to be resolved — they're loaded async by OrgResolver. + // An empty set means "not yet loaded" (even viewer gets observe:read). + if (scopes.size === 0) { + return null; // OrgResolver is still fetching tokens + } + // Vendor → vendor console if (scopes.has('platform:admin')) { return ;