From 78396a279611639ec4f30270b1dc400635b83ee2 Mon Sep 17 00:00:00 2001 From: hsiegeln <37154749+hsiegeln@users.noreply.github.com> Date: Thu, 16 Apr 2026 12:42:01 +0200 Subject: [PATCH] fix: sidebar route selection and missing routes after server restart Two sidebar bugs fixed: 1. Route entries never highlighted on navigation because sidebar-utils generated /apps/ paths for route children while effectiveSelectedPath normalizes to /exchanges/. The design system does exact string matching. 2. Routes disappeared from sidebar when agents had no recent exchange data. Heartbeat carried routeStates (with route IDs as keys) but AgentRegistryService.heartbeat() never updated AgentInfo.routeIds. After server restart, auto-heal registered agents with empty routes, leaving ClickHouse (24h window) as the only discovery source. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/controller/AgentRegistrationController.java | 10 +++++++--- .../com/cameleer/server/core/agent/AgentInfo.java | 5 +++++ .../server/core/agent/AgentRegistryService.java | 12 ++++++++---- ui/src/components/sidebar-utils.ts | 2 +- 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java index 78798efd..5ab042f6 100644 --- a/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java +++ b/cameleer-server-app/src/main/java/com/cameleer/server/app/controller/AgentRegistrationController.java @@ -224,7 +224,9 @@ public class AgentRegistrationController { HttpServletRequest httpRequest) { Map capabilities = request != null ? request.getCapabilities() : null; String heartbeatEnv = request != null ? request.getEnvironmentId() : null; - boolean found = registryService.heartbeat(id, capabilities); + List routeIds = request != null && request.getRouteStates() != null + ? List.copyOf(request.getRouteStates().keySet()) : null; + boolean found = registryService.heartbeat(id, routeIds, capabilities); if (!found) { // Auto-heal: re-register agent from heartbeat body + JWT claims after server restart var jwtResult = (JwtService.JwtValidationResult) httpRequest.getAttribute( @@ -235,10 +237,12 @@ public class AgentRegistrationController { String env = heartbeatEnv != null ? heartbeatEnv : jwtResult.environment() != null ? jwtResult.environment() : "default"; Map caps = capabilities != null ? capabilities : Map.of(); + List healRouteIds = routeIds != null ? routeIds : List.of(); registryService.register(id, id, application, env, "unknown", - List.of(), caps); + healRouteIds, caps); registryService.heartbeat(id); - log.info("Auto-registered agent {} (app={}, env={}) from heartbeat after server restart", id, application, env); + log.info("Auto-registered agent {} (app={}, env={}, routes={}) from heartbeat after server restart", + id, application, env, healRouteIds.size()); } else { return ResponseEntity.notFound().build(); } diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentInfo.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentInfo.java index cb8cdaca..72533a2e 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentInfo.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentInfo.java @@ -57,6 +57,11 @@ public record AgentInfo( state, registeredAt, lastHeartbeat, newStaleTransitionTime); } + public AgentInfo withRouteIds(List newRouteIds) { + return new AgentInfo(instanceId, displayName, applicationId, environmentId, version, newRouteIds, capabilities, + state, registeredAt, lastHeartbeat, staleTransitionTime); + } + public AgentInfo withCapabilities(Map newCapabilities) { return new AgentInfo(instanceId, displayName, applicationId, environmentId, version, routeIds, newCapabilities, state, registeredAt, lastHeartbeat, staleTransitionTime); diff --git a/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentRegistryService.java b/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentRegistryService.java index 32b55245..88173dfc 100644 --- a/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentRegistryService.java +++ b/cameleer-server-core/src/main/java/com/cameleer/server/core/agent/AgentRegistryService.java @@ -71,14 +71,18 @@ public class AgentRegistryService { /** * Process a heartbeat from an agent. - * Updates lastHeartbeat, capabilities (if provided), and transitions STALE agents back to LIVE. + * Updates lastHeartbeat, routeIds (if provided), capabilities (if provided), + * and transitions STALE agents back to LIVE. * * @return true if the agent is known, false otherwise */ - public boolean heartbeat(String id, Map capabilities) { + public boolean heartbeat(String id, List routeIds, Map capabilities) { AgentInfo updated = agents.computeIfPresent(id, (key, existing) -> { Instant now = Instant.now(); AgentInfo result = existing.withLastHeartbeat(now); + if (routeIds != null && !routeIds.isEmpty()) { + result = result.withRouteIds(List.copyOf(routeIds)); + } if (capabilities != null && !capabilities.isEmpty()) { result = result.withCapabilities(Map.copyOf(capabilities)); } @@ -91,9 +95,9 @@ public class AgentRegistryService { return updated != null; } - /** Overload for callers without capabilities (backward compatibility). */ + /** Overload for callers without routeIds or capabilities (backward compatibility). */ public boolean heartbeat(String id) { - return heartbeat(id, null); + return heartbeat(id, null, null); } /** diff --git a/ui/src/components/sidebar-utils.ts b/ui/src/components/sidebar-utils.ts index 556f069e..969b45b5 100644 --- a/ui/src/components/sidebar-utils.ts +++ b/ui/src/components/sidebar-utils.ts @@ -89,7 +89,7 @@ export function buildAppTreeNodes( badge: r.routeState ? `${r.routeState.toUpperCase()} \u00b7 ${formatCount(r.exchangeCount)}` : formatCount(r.exchangeCount), - path: `/apps/${app.id}/${r.id}`, + path: `/exchanges/${app.id}/${r.id}`, starrable: true, starKey: `route:${app.id}/${r.id}`, })),