diff --git a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java index d567411..c25083d 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java +++ b/fleetd/src/main/java/dev/ltms/fleet/mcp/FleetMcp.java @@ -1015,6 +1015,25 @@ public final class FleetMcp { * both maps at once when it is both exhaustion-quarantined AND cooling off. */ static McpSchema.CallToolResult profiles(PeerLauncher workers, QuarantineSource quarantine, OutageSource outage) { + return text(json(profilesView(workers, quarantine, outage))); + } + + /** + * The body both front doors answer {@code profiles} with: the configured profile names, the + * default, and the two independent outage states — {@code quarantined} (the backend reported it + * out of capacity) and {@code coolingOff} (the credential threw repeated non-exhaustion backend + * errors). Each map is present only when at least one profile is in that state, and a profile + * can appear in both at once, because the two checks are separate. + * + *

fleetd #297: extracted so {@code fleet_profiles} and {@code GET /profiles} render from ONE + * body builder rather than two copies. Passing both doors the same {@link QuarantineSource} and + * {@link OutageSource} instances is necessary but not sufficient: with the loop written out + * twice, a later edit to the row shape — a renamed key, an added field — lands on one door and + * not the other, and the two then disagree about a live outage. That is exactly what fleetd + * #284 was, where one rule computed in two places was widened in only one and a single response + * contradicted itself. Shared inputs do not make duplicated computation safe. + */ + public static Map profilesView(PeerLauncher workers, QuarantineSource quarantine, OutageSource outage) { Map result = new LinkedHashMap<>(); result.put("profiles", workers.profiles()); result.put("default", workers.defaultProfile() == null ? "" : workers.defaultProfile()); @@ -1046,7 +1065,7 @@ public final class FleetMcp { if (!coolingOff.isEmpty()) { result.put("coolingOff", coolingOff); } - return text(json(result)); + return result; } /** diff --git a/fleetd/src/main/java/dev/ltms/fleet/rest/FleetApp.java b/fleetd/src/main/java/dev/ltms/fleet/rest/FleetApp.java index 94c9420..56a36fc 100644 --- a/fleetd/src/main/java/dev/ltms/fleet/rest/FleetApp.java +++ b/fleetd/src/main/java/dev/ltms/fleet/rest/FleetApp.java @@ -427,38 +427,10 @@ public final class FleetApp { if (!allow(ctx, routeAction("GET /profiles"), null)) { return; } - Map body = new LinkedHashMap<>(); - body.put("profiles", workers.profiles()); - body.put("default", workers.defaultProfile() == null ? "" : workers.defaultProfile()); - Map quarantined = new LinkedHashMap<>(); - Map coolingOff = new LinkedHashMap<>(); - for (String profile : workers.profiles()) { - String credentialId = quarantine.credentialIdFor().apply(profile); - if (credentialId != null) { - quarantine.quarantine().remainingSeconds(credentialId).ifPresent(remaining -> { - Map row = new LinkedHashMap<>(); - row.put("credentialId", credentialId); - row.put("quarantinedForSeconds", remaining); - quarantined.put(profile, row); - }); - } - String outageCredentialId = outage.credentialIdFor().apply(profile); - if (outageCredentialId != null) { - outage.outagePolicy().remainingCoolOffSeconds(outageCredentialId).ifPresent(remaining -> { - Map row = new LinkedHashMap<>(); - row.put("credentialId", outageCredentialId); - row.put("coolingOffForSeconds", remaining); - coolingOff.put(profile, row); - }); - } - } - if (!quarantined.isEmpty()) { - body.put("quarantined", quarantined); - } - if (!coolingOff.isEmpty()) { - body.put("coolingOff", coolingOff); - } - ctx.status(200).json(body); + // fleetd #297: ONE body builder, shared with fleet_profiles. Handing both doors the same + // QuarantineSource/OutageSource instances stops them reading different facts; rendering + // through the same method stops them reporting those facts differently. Both are needed. + ctx.status(200).json(FleetMcp.profilesView(workers, quarantine, outage)); } /**