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 ;