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) <noreply@anthropic.com>
This commit is contained in:
@@ -224,7 +224,9 @@ public class AgentRegistrationController {
|
|||||||
HttpServletRequest httpRequest) {
|
HttpServletRequest httpRequest) {
|
||||||
Map<String, Object> capabilities = request != null ? request.getCapabilities() : null;
|
Map<String, Object> capabilities = request != null ? request.getCapabilities() : null;
|
||||||
String heartbeatEnv = request != null ? request.getEnvironmentId() : null;
|
String heartbeatEnv = request != null ? request.getEnvironmentId() : null;
|
||||||
boolean found = registryService.heartbeat(id, capabilities);
|
List<String> routeIds = request != null && request.getRouteStates() != null
|
||||||
|
? List.copyOf(request.getRouteStates().keySet()) : null;
|
||||||
|
boolean found = registryService.heartbeat(id, routeIds, capabilities);
|
||||||
if (!found) {
|
if (!found) {
|
||||||
// Auto-heal: re-register agent from heartbeat body + JWT claims after server restart
|
// Auto-heal: re-register agent from heartbeat body + JWT claims after server restart
|
||||||
var jwtResult = (JwtService.JwtValidationResult) httpRequest.getAttribute(
|
var jwtResult = (JwtService.JwtValidationResult) httpRequest.getAttribute(
|
||||||
@@ -235,10 +237,12 @@ public class AgentRegistrationController {
|
|||||||
String env = heartbeatEnv != null ? heartbeatEnv
|
String env = heartbeatEnv != null ? heartbeatEnv
|
||||||
: jwtResult.environment() != null ? jwtResult.environment() : "default";
|
: jwtResult.environment() != null ? jwtResult.environment() : "default";
|
||||||
Map<String, Object> caps = capabilities != null ? capabilities : Map.of();
|
Map<String, Object> caps = capabilities != null ? capabilities : Map.of();
|
||||||
|
List<String> healRouteIds = routeIds != null ? routeIds : List.of();
|
||||||
registryService.register(id, id, application, env, "unknown",
|
registryService.register(id, id, application, env, "unknown",
|
||||||
List.of(), caps);
|
healRouteIds, caps);
|
||||||
registryService.heartbeat(id);
|
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 {
|
} else {
|
||||||
return ResponseEntity.notFound().build();
|
return ResponseEntity.notFound().build();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ public record AgentInfo(
|
|||||||
state, registeredAt, lastHeartbeat, newStaleTransitionTime);
|
state, registeredAt, lastHeartbeat, newStaleTransitionTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AgentInfo withRouteIds(List<String> newRouteIds) {
|
||||||
|
return new AgentInfo(instanceId, displayName, applicationId, environmentId, version, newRouteIds, capabilities,
|
||||||
|
state, registeredAt, lastHeartbeat, staleTransitionTime);
|
||||||
|
}
|
||||||
|
|
||||||
public AgentInfo withCapabilities(Map<String, Object> newCapabilities) {
|
public AgentInfo withCapabilities(Map<String, Object> newCapabilities) {
|
||||||
return new AgentInfo(instanceId, displayName, applicationId, environmentId, version, routeIds, newCapabilities,
|
return new AgentInfo(instanceId, displayName, applicationId, environmentId, version, routeIds, newCapabilities,
|
||||||
state, registeredAt, lastHeartbeat, staleTransitionTime);
|
state, registeredAt, lastHeartbeat, staleTransitionTime);
|
||||||
|
|||||||
@@ -71,14 +71,18 @@ public class AgentRegistryService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Process a heartbeat from an agent.
|
* 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
|
* @return true if the agent is known, false otherwise
|
||||||
*/
|
*/
|
||||||
public boolean heartbeat(String id, Map<String, Object> capabilities) {
|
public boolean heartbeat(String id, List<String> routeIds, Map<String, Object> capabilities) {
|
||||||
AgentInfo updated = agents.computeIfPresent(id, (key, existing) -> {
|
AgentInfo updated = agents.computeIfPresent(id, (key, existing) -> {
|
||||||
Instant now = Instant.now();
|
Instant now = Instant.now();
|
||||||
AgentInfo result = existing.withLastHeartbeat(now);
|
AgentInfo result = existing.withLastHeartbeat(now);
|
||||||
|
if (routeIds != null && !routeIds.isEmpty()) {
|
||||||
|
result = result.withRouteIds(List.copyOf(routeIds));
|
||||||
|
}
|
||||||
if (capabilities != null && !capabilities.isEmpty()) {
|
if (capabilities != null && !capabilities.isEmpty()) {
|
||||||
result = result.withCapabilities(Map.copyOf(capabilities));
|
result = result.withCapabilities(Map.copyOf(capabilities));
|
||||||
}
|
}
|
||||||
@@ -91,9 +95,9 @@ public class AgentRegistryService {
|
|||||||
return updated != null;
|
return updated != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Overload for callers without capabilities (backward compatibility). */
|
/** Overload for callers without routeIds or capabilities (backward compatibility). */
|
||||||
public boolean heartbeat(String id) {
|
public boolean heartbeat(String id) {
|
||||||
return heartbeat(id, null);
|
return heartbeat(id, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -89,7 +89,7 @@ export function buildAppTreeNodes(
|
|||||||
badge: r.routeState
|
badge: r.routeState
|
||||||
? `${r.routeState.toUpperCase()} \u00b7 ${formatCount(r.exchangeCount)}`
|
? `${r.routeState.toUpperCase()} \u00b7 ${formatCount(r.exchangeCount)}`
|
||||||
: formatCount(r.exchangeCount),
|
: formatCount(r.exchangeCount),
|
||||||
path: `/apps/${app.id}/${r.id}`,
|
path: `/exchanges/${app.id}/${r.id}`,
|
||||||
starrable: true,
|
starrable: true,
|
||||||
starKey: `route:${app.id}/${r.id}`,
|
starKey: `route:${app.id}/${r.id}`,
|
||||||
})),
|
})),
|
||||||
|
|||||||
Reference in New Issue
Block a user