Files
cameleer-server/ui/src/api/queries/commands.ts
hsiegeln 1971c70638
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m19s
CI / docker (push) Successful in 1m4s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 40s
fix: commands respect selected environment
Backend: AgentRegistryService gains findByApplicationAndEnvironment()
and environment-aware addGroupCommandWithReplies() overload.
AgentCommandController and ApplicationConfigController accept optional
environment query parameter. When set, commands only target agents in
that environment. Backward compatible — null means all environments.

Frontend: All command mutations (config update, route control, traced
processors, tap config, route recording) now pass selectedEnv to the
backend via query parameter.

Prevents cross-environment command leakage — e.g., updating config for
prod no longer pushes to dev agents.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 16:28:09 +02:00

235 lines
8.1 KiB
TypeScript

import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { useAuthStore } from '../../auth/auth-store'
import { config } from '../../config'
// ── Application Config ────────────────────────────────────────────────────
export interface TapDefinition {
tapId: string
processorId: string
target: 'INPUT' | 'OUTPUT' | 'BOTH'
expression: string
language: string
attributeName: string
attributeType: 'BUSINESS_OBJECT' | 'CORRELATION' | 'EVENT' | 'CUSTOM'
enabled: boolean
version: number
}
export interface ApplicationConfig {
application: string
version: number
updatedAt?: string
engineLevel?: string
payloadCaptureMode?: string
applicationLogLevel?: string
agentLogLevel?: string
metricsEnabled: boolean
samplingRate: number
tracedProcessors: Record<string, string>
taps: TapDefinition[]
tapVersion: number
routeRecording: Record<string, boolean>
compressSuccess: boolean
}
/** Authenticated fetch using the JWT from auth store. Paths are relative to apiBaseUrl. */
function authFetch(path: string, init?: RequestInit): Promise<Response> {
const token = useAuthStore.getState().accessToken
const headers = new Headers(init?.headers)
if (token) headers.set('Authorization', `Bearer ${token}`)
headers.set('X-Cameleer-Protocol-Version', '1')
return fetch(`${config.apiBaseUrl}${path}`, { ...init, headers })
}
export function useAllApplicationConfigs() {
return useQuery({
queryKey: ['applicationConfig', 'all'],
queryFn: async () => {
const res = await authFetch('/config')
if (!res.ok) throw new Error('Failed to fetch configs')
return res.json() as Promise<ApplicationConfig[]>
},
})
}
export function useApplicationConfig(application: string | undefined) {
return useQuery({
queryKey: ['applicationConfig', application],
queryFn: async () => {
const res = await authFetch(`/config/${application}`)
if (!res.ok) throw new Error('Failed to fetch config')
return res.json() as Promise<ApplicationConfig>
},
enabled: !!application,
})
}
export interface ConfigUpdateResponse {
config: ApplicationConfig
pushResult: CommandGroupResponse
}
export function useUpdateApplicationConfig() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: async ({ config, environment }: { config: ApplicationConfig; environment?: string }) => {
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
const res = await authFetch(`/config/${config.application}${envParam}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config),
})
if (!res.ok) throw new Error('Failed to update config')
return res.json() as Promise<ConfigUpdateResponse>
},
onSuccess: (result) => {
queryClient.setQueryData(['applicationConfig', result.config.application], result.config)
queryClient.invalidateQueries({ queryKey: ['applicationConfig', 'all'] })
},
})
}
// ── Processor → Route Mapping ─────────────────────────────────────────────
export function useProcessorRouteMapping(application?: string) {
return useQuery({
queryKey: ['config', application, 'processor-routes'],
queryFn: async () => {
const res = await authFetch(`/config/${application}/processor-routes`)
if (!res.ok) throw new Error('Failed to fetch processor-route mapping')
return res.json() as Promise<Record<string, string>>
},
enabled: !!application,
})
}
// ── Group Command Response ───────────────────────────────────────────────
export interface CommandGroupResponse {
success: boolean
total: number
responded: number
responses: { agentId: string; status: string; message: string }[]
timedOut: string[]
}
// ── Generic Group Command (kept for non-config commands) ──────────────────
interface SendGroupCommandParams {
group: string
type: string
payload: Record<string, unknown>
environment?: string
}
export function useSendGroupCommand() {
return useMutation({
mutationFn: async ({ group, type, payload, environment }: SendGroupCommandParams) => {
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
const res = await authFetch(`/agents/groups/${encodeURIComponent(group)}/commands${envParam}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type, payload }),
})
if (!res.ok) throw new Error('Failed to send command')
return res.json() as Promise<CommandGroupResponse>
},
})
}
// ── Test Expression ───────────────────────────────────────────────────────
export function useTestExpression() {
return useMutation({
mutationFn: async ({
application,
expression,
language,
body,
target,
}: {
application: string
expression: string
language: string
body: string
target: string
}) => {
const res = await authFetch(
`/config/${encodeURIComponent(application)}/test-expression`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ expression, language, body, target }),
},
)
if (!res.ok) {
if (res.status === 404) throw new Error('No live agent available')
if (res.status === 504) throw new Error('Expression test timed out')
throw new Error('Failed to test expression')
}
return res.json() as Promise<{ result?: string; error?: string }>
},
})
}
// ── Route Control ────────────────────────────────────────────────────────
export function useSendRouteCommand() {
return useMutation({
mutationFn: async ({ application, action, routeId, environment }: {
application: string
action: 'start' | 'stop' | 'suspend' | 'resume'
routeId: string
environment?: string
}) => {
const envParam = environment ? `?environment=${encodeURIComponent(environment)}` : ''
const res = await authFetch(`/agents/groups/${encodeURIComponent(application)}/commands${envParam}`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'route-control', payload: { routeId, action, nonce: crypto.randomUUID() } }),
})
if (!res.ok) throw new Error('Failed to send route command')
return res.json() as Promise<CommandGroupResponse>
},
})
}
// ── Replay Exchange ───────────────────────────────────────────────────────
export interface ReplayResult {
status: string
message: string
data?: string
}
export function useReplayExchange() {
return useMutation({
mutationFn: async ({
agentId,
routeId,
headers,
body,
originalExchangeId,
}: {
agentId: string
routeId: string
headers?: Record<string, string>
body: string
originalExchangeId?: string
}): Promise<ReplayResult> => {
const res = await authFetch(`/agents/${encodeURIComponent(agentId)}/replay`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ routeId, body, headers: headers ?? {}, originalExchangeId }),
})
if (!res.ok) {
if (res.status === 404) throw new Error('Agent not found')
if (res.status === 504) throw new Error('Replay timed out — agent did not respond')
throw new Error('Failed to send replay command')
}
return res.json() as Promise<ReplayResult>
},
})
}