3 Commits

Author SHA1 Message Date
hsiegeln
ebe768711b fix: Cmd-K exchange selection reads exchangeId from URL params
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 57s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 36s
ExchangesPage ignored the exchangeId URL parameter, so selecting an
exchange from the command palette navigated to the right URL but never
displayed the execution overlay. Now derives selection from URL params
as fallback, and LayoutShell passes selectedExchange in state for
exchange/attribute results.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:26:36 +02:00
hsiegeln
af45f93854 fix: add missing isReplay parameter to test constructors
All checks were successful
CI / cleanup-branch (push) Has been skipped
CI / build (push) Successful in 1m1s
CI / docker (push) Successful in 57s
CI / deploy-feature (push) Has been skipped
CI / deploy (push) Successful in 41s
The ExecutionDocument and ExecutionRecord records gained an isReplay
field but the integration tests were not updated, breaking CI.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 15:08:12 +02:00
hsiegeln
da1d74309e fix: detect replay via replayExchangeId field, not just header
Some checks failed
CI / cleanup-branch (push) Has been skipped
CI / build (push) Failing after 1m4s
CI / docker (push) Has been skipped
CI / deploy (push) Has been skipped
CI / deploy-feature (push) Has been skipped
The X-Cameleer-Replay header is only available when inputSnapshot is
captured (DETAILED/DEEP engine level). The agent always sets
replayExchangeId on RouteExecution, so check that first.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:57:59 +02:00
6 changed files with 54 additions and 17 deletions

View File

@@ -36,7 +36,7 @@ class OpenSearchIndexIT extends AbstractPostgresIT {
"OrderNotFoundException: order-12345 not found", null,
List.of(new ProcessorDoc("proc-1", "log", "COMPLETED",
null, null, "request body with customer-99", null, null, null, null)),
null, false);
null, false, false);
searchIndex.index(doc);
refreshOpenSearchIndices();
@@ -62,7 +62,7 @@ class OpenSearchIndexIT extends AbstractPostgresIT {
now, now.plusMillis(50), 50L, null, null,
List.of(new ProcessorDoc("proc-1", "bean", "COMPLETED",
null, null, "UniquePayloadIdentifier12345", null, null, null, null)),
null, false);
null, false, false);
searchIndex.index(doc);
refreshOpenSearchIndices();

View File

@@ -27,7 +27,7 @@ class PostgresExecutionStoreIT extends AbstractPostgresIT {
now, now.plusMillis(100), 100L,
null, null, null,
"REGULAR", null, null, null, null, null,
null, null, null, null, null, null, null, false);
null, null, null, null, null, null, null, false, false);
executionStore.upsert(record);
Optional<ExecutionRecord> found = executionStore.findById("exec-1");
@@ -45,12 +45,12 @@ class PostgresExecutionStoreIT extends AbstractPostgresIT {
"exec-dup", "route-a", "agent-1", "app-1",
"RUNNING", null, null, now, null, null, null, null, null,
null, null, null, null, null, null,
null, null, null, null, null, null, null, false);
null, null, null, null, null, null, null, false, false);
ExecutionRecord second = new ExecutionRecord(
"exec-dup", "route-a", "agent-1", "app-1",
"COMPLETED", null, null, now, now.plusMillis(200), 200L, null, null, null,
"COMPLETE", null, null, null, null, null,
null, null, null, null, null, null, null, false);
null, null, null, null, null, null, null, false, false);
executionStore.upsert(first);
executionStore.upsert(second);
@@ -68,7 +68,7 @@ class PostgresExecutionStoreIT extends AbstractPostgresIT {
"exec-proc", "route-a", "agent-1", "app-1",
"COMPLETED", null, null, now, now.plusMillis(50), 50L, null, null, null,
"COMPLETE", null, null, null, null, null,
null, null, null, null, null, null, null, false);
null, null, null, null, null, null, null, false, false);
executionStore.upsert(exec);
List<ProcessorRecord> processors = List.of(

View File

@@ -61,6 +61,6 @@ class PostgresStatsStoreIT extends AbstractPostgresIT {
startTime, startTime.plusMillis(durationMs), durationMs,
status.equals("FAILED") ? "error" : null, null, null,
null, null, null, null, null, null,
null, null, null, null, null, null, null, false));
null, null, null, null, null, null, null, false, false));
}
}

View File

@@ -102,8 +102,8 @@ public class IngestionService {
boolean hasTraceData = hasAnyTraceData(exec.getProcessors());
boolean isReplay = false;
if (inputSnapshot != null && inputSnapshot.getHeaders() != null) {
boolean isReplay = exec.getReplayExchangeId() != null;
if (!isReplay && inputSnapshot != null && inputSnapshot.getHeaders() != null) {
isReplay = "true".equalsIgnoreCase(
String.valueOf(inputSnapshot.getHeaders().get("X-Cameleer-Replay")));
}

View File

@@ -210,7 +210,21 @@ function LayoutContent() {
const handlePaletteSelect = useCallback((result: any) => {
if (result.path) {
navigate(result.path, { state: result.path ? { sidebarReveal: result.path } : undefined });
const state: Record<string, unknown> = { sidebarReveal: result.path };
// For exchange/attribute results, pass selectedExchange in state
if (result.category === 'exchange' || result.category === 'attribute') {
const parts = result.path.split('/').filter(Boolean);
if (parts.length === 4 && parts[0] === 'exchanges') {
state.selectedExchange = {
executionId: parts[3],
applicationName: parts[1],
routeId: parts[2],
};
}
}
navigate(result.path, { state });
}
setPaletteOpen(false);
}, [navigate, setPaletteOpen]);

View File

@@ -20,17 +20,35 @@ import type { SelectedExchange } from '../Dashboard/Dashboard';
export default function ExchangesPage() {
const navigate = useNavigate();
const location = useLocation();
const { appId: scopedAppId, routeId: scopedRouteId } = useParams<{ appId?: string; routeId?: string }>();
const { appId: scopedAppId, routeId: scopedRouteId, exchangeId: scopedExchangeId } =
useParams<{ appId?: string; routeId?: string; exchangeId?: string }>();
// Restore selection from browser history state (enables Back/Forward)
const stateSelected = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
const [selected, setSelectedInternal] = useState<SelectedExchange | null>(stateSelected ?? null);
// Sync from history state when the user navigates Back/Forward
// Derive selection from URL params when no state-based selection exists (Cmd-K, bookmarks)
const urlDerivedExchange: SelectedExchange | null =
(scopedExchangeId && scopedAppId && scopedRouteId)
? { executionId: scopedExchangeId, applicationName: scopedAppId, routeId: scopedRouteId }
: null;
const [selected, setSelectedInternal] = useState<SelectedExchange | null>(stateSelected ?? urlDerivedExchange);
// Sync selection from history state or URL params on navigation changes
useEffect(() => {
const restored = (location.state as any)?.selectedExchange as SelectedExchange | undefined;
setSelectedInternal(restored ?? null);
}, [location.state]);
if (restored) {
setSelectedInternal(restored);
} else if (scopedExchangeId && scopedAppId && scopedRouteId) {
setSelectedInternal({
executionId: scopedExchangeId,
applicationName: scopedAppId,
routeId: scopedRouteId,
});
} else {
setSelectedInternal(null);
}
}, [location.state, scopedExchangeId, scopedAppId, scopedRouteId]);
const [splitPercent, setSplitPercent] = useState(50);
const containerRef = useRef<HTMLDivElement>(null);
@@ -52,10 +70,15 @@ export default function ExchangesPage() {
});
}, [navigate, location.pathname, location.search, location.state]);
// Clear selection: push a history entry without selection (so Back returns to selected state)
// Clear selection: navigate up to route level when URL has exchangeId
const handleClearSelection = useCallback(() => {
setSelectedInternal(null);
}, []);
if (scopedExchangeId && scopedAppId && scopedRouteId) {
navigate(`/exchanges/${scopedAppId}/${scopedRouteId}`, {
state: { ...location.state, selectedExchange: undefined },
});
}
}, [scopedExchangeId, scopedAppId, scopedRouteId, navigate, location.state]);
const handleSplitterDown = useCallback((e: React.PointerEvent) => {
e.currentTarget.setPointerCapture(e.pointerId);