diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DiagramRenderController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DiagramRenderController.java index 0213155e..703025f1 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DiagramRenderController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/DiagramRenderController.java @@ -91,15 +91,17 @@ public class DiagramRenderController { } @GetMapping - @Operation(summary = "Find diagram by application and route ID", - description = "Resolves application to agent IDs and finds the latest diagram for the route") + @Operation(summary = "Find diagram by application, environment, and route ID", + description = "Resolves (application, environment) to agent IDs and finds the latest diagram for the route. " + + "The environment filter prevents cross-env diagram leakage — without it a dev route could return a prod diagram (or vice versa).") @ApiResponse(responseCode = "200", description = "Diagram layout returned") - @ApiResponse(responseCode = "404", description = "No diagram found for the given application and route") + @ApiResponse(responseCode = "404", description = "No diagram found for the given application, environment, and route") public ResponseEntity findByApplicationAndRoute( @RequestParam String application, + @RequestParam String environment, @RequestParam String routeId, @RequestParam(defaultValue = "LR") String direction) { - List agentIds = registryService.findByApplication(application).stream() + List agentIds = registryService.findByApplicationAndEnvironment(application, environment).stream() .map(AgentInfo::instanceId) .toList(); diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/SearchController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/SearchController.java index 68112c04..b7d5fb2d 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/SearchController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/SearchController.java @@ -165,9 +165,10 @@ public class SearchController { } @GetMapping("/attributes/keys") - @Operation(summary = "Distinct attribute key names across all executions") - public ResponseEntity> attributeKeys() { - return ResponseEntity.ok(searchService.distinctAttributeKeys()); + @Operation(summary = "Distinct attribute key names for the given environment", + description = "Scoped to an environment to prevent cross-env attribute leakage in UI completions") + public ResponseEntity> attributeKeys(@RequestParam String environment) { + return ResponseEntity.ok(searchService.distinctAttributeKeys(environment)); } @GetMapping("/errors/top") diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java index 4569af5b..d23eef3f 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/search/ClickHouseSearchIndex.java @@ -318,14 +318,14 @@ public class ClickHouseSearchIndex implements SearchIndex { } @Override - public List distinctAttributeKeys() { + public List distinctAttributeKeys(String environment) { try { return jdbc.queryForList(""" SELECT DISTINCT arrayJoin(JSONExtractKeys(attributes)) AS attr_key FROM executions FINAL - WHERE tenant_id = ? AND attributes != '' AND attributes != '{}' + WHERE tenant_id = ? AND environment = ? AND attributes != '' AND attributes != '{}' ORDER BY attr_key - """, String.class, tenantId); + """, String.class, tenantId, environment); } catch (Exception e) { log.error("Failed to query distinct attribute keys", e); return List.of(); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/search/SearchService.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/search/SearchService.java index 4f6109f7..b6e6cb3b 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/search/SearchService.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/search/SearchService.java @@ -25,8 +25,8 @@ public class SearchService { return searchIndex.count(request); } - public List distinctAttributeKeys() { - return searchIndex.distinctAttributeKeys(); + public List distinctAttributeKeys(String environment) { + return searchIndex.distinctAttributeKeys(environment); } public ExecutionStats stats(Instant from, Instant to) { diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/SearchIndex.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/SearchIndex.java index 0f02682a..1d7c9287 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/SearchIndex.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/storage/SearchIndex.java @@ -17,6 +17,6 @@ public interface SearchIndex { void delete(String executionId); - /** Returns distinct attribute key names across all executions. */ - List distinctAttributeKeys(); + /** Returns distinct attribute key names across executions in the given environment. */ + List distinctAttributeKeys(String environment); } diff --git a/ui/src/api/queries/diagrams.ts b/ui/src/api/queries/diagrams.ts index eb63d8fe..12e26cd0 100644 --- a/ui/src/api/queries/diagrams.ts +++ b/ui/src/api/queries/diagrams.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; +import { useEnvironmentStore } from '../environment-store'; export interface DiagramNode { id?: string; @@ -53,15 +54,16 @@ export function useDiagramByRoute( routeId: string | undefined, direction: 'LR' | 'TB' = 'LR', ) { + const environment = useEnvironmentStore((s) => s.environment); return useQuery({ - queryKey: ['diagrams', 'byRoute', application, routeId, direction], + queryKey: ['diagrams', 'byRoute', environment, application, routeId, direction], queryFn: async () => { const { data, error } = await api.GET('/diagrams', { - params: { query: { application: application!, routeId: routeId!, direction } }, + params: { query: { application: application!, environment: environment!, routeId: routeId!, direction } }, }); if (error) throw new Error('Failed to load diagram for route'); return data as DiagramLayout; }, - enabled: !!application && !!routeId, + enabled: !!application && !!routeId && !!environment, }); } diff --git a/ui/src/api/queries/executions.ts b/ui/src/api/queries/executions.ts index 27cdf137..d5286cfb 100644 --- a/ui/src/api/queries/executions.ts +++ b/ui/src/api/queries/executions.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query'; import { api } from '../client'; +import { useEnvironmentStore } from '../environment-store'; import type { SearchRequest } from '../types'; import { useLiveQuery } from './use-refresh-interval'; @@ -35,17 +36,20 @@ export function useExecutionStats( } export function useAttributeKeys() { + const environment = useEnvironmentStore((s) => s.environment); return useQuery({ - queryKey: ['search', 'attribute-keys'], + queryKey: ['search', 'attribute-keys', environment], queryFn: async () => { const token = (await import('../../auth/auth-store')).useAuthStore.getState().accessToken; const { config } = await import('../../config'); - const res = await fetch(`${config.apiBaseUrl}/search/attributes/keys`, { - headers: { Authorization: `Bearer ${token}` }, - }); + const res = await fetch( + `${config.apiBaseUrl}/search/attributes/keys?environment=${encodeURIComponent(environment!)}`, + { headers: { Authorization: `Bearer ${token}` } }, + ); if (!res.ok) throw new Error('Failed to load attribute keys'); return res.json() as Promise; }, + enabled: !!environment, staleTime: 60_000, }); }