diff --git a/ui/src/pages/Exchanges/ExchangesPage.tsx b/ui/src/pages/Exchanges/ExchangesPage.tsx
index 591d7f48..e046f48f 100644
--- a/ui/src/pages/Exchanges/ExchangesPage.tsx
+++ b/ui/src/pages/Exchanges/ExchangesPage.tsx
@@ -7,7 +7,7 @@ import { useRouteCatalog } from '../../api/queries/catalog';
import { useAgents } from '../../api/queries/agents';
import { useApplicationConfig, useUpdateApplicationConfig } from '../../api/queries/commands';
import type { TapDefinition, ConfigUpdateResponse } from '../../api/queries/commands';
-import { useAuthStore } from '../../auth/auth-store';
+import { useCanControl } from '../../auth/auth-store';
import { useTracingStore } from '../../stores/tracing-store';
import type { NodeAction, NodeConfig } from '../../components/ProcessDiagram/types';
import { TapConfigModal } from '../../components/TapConfigModal';
@@ -154,8 +154,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
// Route state + capabilities for topology-only control bar
const { data: agents } = useAgents(undefined, appId);
- const roles = useAuthStore((s) => s.roles);
- const canControl = roles.some(r => r === 'OPERATOR' || r === 'ADMIN');
+ const canControl = useCanControl();
const { hasRouteControl, hasReplay } = useMemo(() => {
if (!agents) return { hasRouteControl: false, hasReplay: false };
const agentList = agents as any[];
@@ -332,7 +331,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
executionDetail={detail}
knownRouteIds={knownRouteIds}
endpointRouteMap={endpointRouteMap}
- onNodeAction={handleNodeAction}
+ onNodeAction={canControl ? handleNodeAction : undefined}
nodeConfigs={nodeConfigs}
/>
{tapModal}
@@ -359,7 +358,7 @@ function DiagramPanel({ appId, routeId, exchangeId, onCorrelatedSelect, onClearS
diagramLayout={diagramQuery.data}
knownRouteIds={knownRouteIds}
endpointRouteMap={endpointRouteMap}
- onNodeAction={handleNodeAction}
+ onNodeAction={canControl ? handleNodeAction : undefined}
nodeConfigs={nodeConfigs}
/>
{tapModal}
diff --git a/ui/src/router.tsx b/ui/src/router.tsx
index 36c4bfdf..aeb87101 100644
--- a/ui/src/router.tsx
+++ b/ui/src/router.tsx
@@ -1,5 +1,6 @@
import { createBrowserRouter, Navigate, useParams } from 'react-router';
import { ProtectedRoute } from './auth/ProtectedRoute';
+import { RequireAdmin } from './auth/RequireAdmin';
import { LoginPage } from './auth/LoginPage';
import { OidcCallback } from './auth/OidcCallback';
import { LayoutShell } from './components/LayoutShell';
@@ -76,6 +77,10 @@ export const router = createBrowserRouter([
{ path: 'logs/:appId', element:
},
{ path: 'logs/:appId/:routeId', element:
},
+ // Config tab (per-app, accessible to VIEWER+)
+ { path: 'config', element:
},
+ { path: 'config/:appId', element:
},
+
// Legacy redirects — Sidebar uses hardcoded /apps/... and /agents/... paths
{ path: 'apps', element:
},
{ path: 'apps/:appId', element:
},
@@ -84,19 +89,21 @@ export const router = createBrowserRouter([
{ path: 'agents/:appId', element:
},
{ path: 'agents/:appId/:instanceId', element:
},
- // Admin (unchanged)
+ // Admin (ADMIN role required)
{
- path: 'admin',
- element:
,
- children: [
- { index: true, element:
},
- { path: 'rbac', element:
},
- { path: 'audit', element:
},
- { path: 'oidc', element:
},
- { path: 'appconfig', element:
},
- { path: 'database', element:
},
- { path: 'clickhouse', element:
},
- ],
+ element:
,
+ children: [{
+ path: 'admin',
+ element:
,
+ children: [
+ { index: true, element:
},
+ { path: 'rbac', element:
},
+ { path: 'audit', element:
},
+ { path: 'oidc', element:
},
+ { path: 'database', element:
},
+ { path: 'clickhouse', element:
},
+ ],
+ }],
},
{ path: 'api-docs', element:
},
],