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}`, })),