From 7ebbc18b31d944b1a20afaa77f9f1bf7a83e723e Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Mon, 6 Apr 2026 00:04:52 +0200 Subject: [PATCH] fix: make API calls respect BASE_PATH for subpath deployments config.apiBaseUrl now derives from tag when no explicit config is set (e.g., /server/api/v1 instead of /api/v1). commands.ts authFetch prepends apiBaseUrl and uses relative paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/src/api/queries/commands.ts | 23 ++++++++++++----------- ui/src/config.ts | 8 +++++++- 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/ui/src/api/queries/commands.ts b/ui/src/api/queries/commands.ts index 172e8ff3..f8d371df 100644 --- a/ui/src/api/queries/commands.ts +++ b/ui/src/api/queries/commands.ts @@ -1,5 +1,6 @@ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' import { useAuthStore } from '../../auth/auth-store' +import { config } from '../../config' // ── Application Config ──────────────────────────────────────────────────── @@ -32,20 +33,20 @@ export interface ApplicationConfig { compressSuccess: boolean } -/** Authenticated fetch using the JWT from auth store */ -function authFetch(url: string, init?: RequestInit): Promise { +/** Authenticated fetch using the JWT from auth store. Paths are relative to apiBaseUrl. */ +function authFetch(path: string, init?: RequestInit): Promise { 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(url, { ...init, headers }) + return fetch(`${config.apiBaseUrl}${path}`, { ...init, headers }) } export function useAllApplicationConfigs() { return useQuery({ queryKey: ['applicationConfig', 'all'], queryFn: async () => { - const res = await authFetch('/api/v1/config') + const res = await authFetch('/config') if (!res.ok) throw new Error('Failed to fetch configs') return res.json() as Promise }, @@ -56,7 +57,7 @@ export function useApplicationConfig(application: string | undefined) { return useQuery({ queryKey: ['applicationConfig', application], queryFn: async () => { - const res = await authFetch(`/api/v1/config/${application}`) + const res = await authFetch(`/config/${application}`) if (!res.ok) throw new Error('Failed to fetch config') return res.json() as Promise }, @@ -73,7 +74,7 @@ export function useUpdateApplicationConfig() { const queryClient = useQueryClient() return useMutation({ mutationFn: async (config: ApplicationConfig) => { - const res = await authFetch(`/api/v1/config/${config.application}`, { + const res = await authFetch(`/config/${config.application}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config), @@ -94,7 +95,7 @@ export function useProcessorRouteMapping(application?: string) { return useQuery({ queryKey: ['config', application, 'processor-routes'], queryFn: async () => { - const res = await authFetch(`/api/v1/config/${application}/processor-routes`) + 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> }, @@ -123,7 +124,7 @@ interface SendGroupCommandParams { export function useSendGroupCommand() { return useMutation({ mutationFn: async ({ group, type, payload }: SendGroupCommandParams) => { - const res = await authFetch(`/api/v1/agents/groups/${encodeURIComponent(group)}/commands`, { + const res = await authFetch(`/agents/groups/${encodeURIComponent(group)}/commands`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type, payload }), @@ -152,7 +153,7 @@ export function useTestExpression() { target: string }) => { const res = await authFetch( - `/api/v1/config/${encodeURIComponent(application)}/test-expression`, + `/config/${encodeURIComponent(application)}/test-expression`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -178,7 +179,7 @@ export function useSendRouteCommand() { action: 'start' | 'stop' | 'suspend' | 'resume' routeId: string }) => { - const res = await authFetch(`/api/v1/agents/groups/${encodeURIComponent(application)}/commands`, { + const res = await authFetch(`/agents/groups/${encodeURIComponent(application)}/commands`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'route-control', payload: { routeId, action, nonce: crypto.randomUUID() } }), @@ -212,7 +213,7 @@ export function useReplayExchange() { body: string originalExchangeId?: string }): Promise => { - const res = await authFetch(`/api/v1/agents/${encodeURIComponent(agentId)}/replay`, { + const res = await authFetch(`/agents/${encodeURIComponent(agentId)}/replay`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ routeId, body, headers: headers ?? {}, originalExchangeId }), diff --git a/ui/src/config.ts b/ui/src/config.ts index 41d6ee1c..f569dbab 100644 --- a/ui/src/config.ts +++ b/ui/src/config.ts @@ -6,8 +6,14 @@ declare global { } } +/** Base path from tag, or '/' if none. Always ends with '/'. */ +const basePath = document.querySelector('base')?.getAttribute('href') ?? '/'; + export const config = { get apiBaseUrl(): string { - return window.__CAMELEER_CONFIG__?.apiBaseUrl ?? '/api/v1'; + return window.__CAMELEER_CONFIG__?.apiBaseUrl ?? `${basePath}api/v1`; + }, + get basePath(): string { + return basePath; }, };