Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
db294c8
Redesign hosted WebUI: index.html
BenCodez Aug 29, 2026
0eb42e0
Redesign hosted WebUI: app.css
BenCodez Aug 29, 2026
e6dc81d
Redesign hosted WebUI: app.js
BenCodez Aug 29, 2026
f05e739
Redesign hosted WebUI: ControlHttpServerTest.java
BenCodez Aug 29, 2026
a860312
Negotiate comment-preserving edits: InMemoryNodeRegistry.java
BenCodez Aug 29, 2026
3415501
Negotiate comment-preserving edits: index.html
BenCodez Aug 29, 2026
d04a4c5
Negotiate comment-preserving edits: app.js
BenCodez Aug 29, 2026
4931392
Negotiate comment-preserving edits: InMemoryNodeRegistryTest.java
BenCodez Aug 29, 2026
6f1639d
Negotiate comment-preserving edits: ControlHttpServerTest.java
BenCodez Aug 29, 2026
5473027
Address Codex workspace review findings
BenCodez Aug 29, 2026
0a72641
Restore stack boundary after workspace fixes
BenCodez Aug 29, 2026
3a7a12a
Address second Codex workspace review
BenCodez Aug 29, 2026
26373bb
Refresh comment guarantee for target changes
BenCodez Aug 29, 2026
97873af
Fix refreshed server editor state
BenCodez Aug 29, 2026
0e240be
Update editor reset regression assertion
BenCodez Aug 29, 2026
7ec18a1
Exclude offline proxy topology reports
BenCodez Aug 29, 2026
6ed82c3
Keep primary server in configuration targets
BenCodez Aug 29, 2026
0d711c3
Align primary workflow and issue metrics
BenCodez Aug 29, 2026
e93462d
Preserve unknown enrollment state
BenCodez Aug 29, 2026
477f54f
Restore eligible primary targets after refresh
BenCodez Aug 29, 2026
73bdcbb
Bound restored targets and mark stale presence
BenCodez Aug 29, 2026
a5b3a63
Mark proxy card presence stale when disconnected
BenCodez Aug 29, 2026
dd6afd4
Count unregistered reported backends as issues
BenCodez Aug 29, 2026
33882b8
Keep WebUI registry scans consistent
BenCodez Aug 30, 2026
30dd016
Update registry scan WebUI assertion
BenCodez Aug 30, 2026
cd3e981
Clear stale WebUI state after registry load failures
BenCodez Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
public final class InMemoryNodeRegistry implements NodeRegistry {
public static final Set<String> SUPPORTED_CAPABILITIES = Set.of("discovery.read", "presence.snapshot",
ConfigurationOperations.CAPABILITY, ConfigurationOperations.FILE_CAPABILITY,
ConfigurationOperations.QUICK_SETUP_CAPABILITY);
ConfigurationOperations.QUICK_SETUP_CAPABILITY, "config.file-comments.v1");
private static final Pattern ID = Pattern.compile("[A-Za-z0-9][A-Za-z0-9._-]{0,63}");
private static final Pattern CAPABILITY = Pattern.compile("[a-z][a-z0-9.-]{0,63}");
private static final Set<String> PLATFORMS = Set.of("BUNGEECORD", "VELOCITY", "BUKKIT");
Expand All @@ -43,6 +43,7 @@ public final class InMemoryNodeRegistry implements NodeRegistry {
private final int maxTotalDetectedPlugins;
private int retainedBackends;
private int retainedDetectedPlugins;
private long membershipRevision;

public InMemoryNodeRegistry(Clock clock, Duration offlineTimeout) {
this(clock, offlineTimeout, MAX_TOTAL_BACKENDS, MAX_TOTAL_DETECTED_PLUGINS);
Expand All @@ -69,7 +70,7 @@ public InMemoryNodeRegistry(Clock clock, Duration offlineTimeout) {
public synchronized RegistrationResult register(NodeRegistration registration) {
validate(registration);
Instant now = clock.instant();
reclaimOfflineTopology(now, registration.nodeId());
if (reclaimOfflineTopology(now, registration.nodeId())) membershipRevision++;
StoredNode current = nodes.get(registration.nodeId());
int replacementPluginTotal = retainedDetectedPlugins
- (current == null ? 0 : current.detectedPlugins.size()) + registration.detectedPlugins().size();
Expand All @@ -94,6 +95,7 @@ public synchronized RegistrationResult register(NodeRegistration registration) {
return replacement;
});
retainedDetectedPlugins = replacementPluginTotal;
if (created.get()) membershipRevision++;
return new RegistrationResult(view(result.get()), created.get());
}

Expand Down Expand Up @@ -128,7 +130,7 @@ public synchronized SnapshotResult replacePresence(String nodeId, PresenceSnapsh
validateId(nodeId, "nodeId");
validateSnapshot(snapshot);
Instant now = clock.instant();
reclaimOfflineTopology(now, nodeId);
if (reclaimOfflineTopology(now, nodeId)) membershipRevision++;
AtomicBoolean applied = new AtomicBoolean();
AtomicReference<StoredNode> result = new AtomicReference<>();
nodes.computeIfPresent(nodeId, (ignored, existing) -> {
Expand Down Expand Up @@ -161,11 +163,21 @@ public synchronized SnapshotResult replacePresence(String nodeId, PresenceSnapsh

@Override
public List<NodeStatus> list(int offset, int limit) {
return page(offset, limit, null).items();
}

@Override
public synchronized RegistryPage page(int offset, int limit, Long expectedRevision) {
if (offset < 0 || limit < 1 || limit > 100) {
throw invalid("offset must be >= 0 and limit must be between 1 and 100");
}
return nodes.values().stream().sorted(Comparator.comparing(node -> node.nodeId))
if (expectedRevision != null && expectedRevision.longValue() != membershipRevision) {
throw new ValidationException("REGISTRY_CHANGED", "Node registry changed during pagination",
List.of("expectedRevision=" + expectedRevision, "actualRevision=" + membershipRevision));
}
List<NodeStatus> items = nodes.values().stream().sorted(Comparator.comparing(node -> node.nodeId))
.skip(offset).limit(limit).map(this::view).toList();
return new RegistryPage(items, membershipRevision, nodes.size());
}

@Override
Expand Down Expand Up @@ -210,16 +222,19 @@ private NodeStatus view(StoredNode node) {
node.backends, node.snapshotSequence, node.lastSeen, node.lastAuthenticatedUpdate, online);
}

private void reclaimOfflineTopology(Instant now, String excludedNodeId) {
private boolean reclaimOfflineTopology(Instant now, String excludedNodeId) {
AtomicBoolean reclaimed = new AtomicBoolean();
for (String candidate : List.copyOf(nodes.keySet())) {
if (candidate.equals(excludedNodeId)) continue;
nodes.computeIfPresent(candidate, (ignored, existing) -> {
if (now.isBefore(existing.lastSeen.plus(offlineTimeout))) return existing;
retainedDetectedPlugins -= existing.detectedPlugins.size();
retainedBackends -= existing.backends.size();
reclaimed.set(true);
return null;
});
}
return reclaimed.get();
}

private static void validate(NodeRegistration registration) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ public interface NodeRegistry {
NodeStatus heartbeat(String nodeId, Heartbeat heartbeat);
SnapshotResult replacePresence(String nodeId, PresenceSnapshot snapshot);
List<NodeStatus> list(int offset, int limit);
RegistryPage page(int offset, int limit, Long expectedRevision);
NodeStatus find(String nodeId);
void requireSession(String nodeId, java.util.UUID sessionId);
<T> T withSession(String nodeId, java.util.UUID sessionId,
java.util.function.Function<NodeStatus, T> action);

record RegistrationResult(NodeStatus node, boolean created) { }
record SnapshotResult(NodeStatus node, boolean applied) { }
record RegistryPage(List<NodeStatus> items, long revision, int total) { }
}
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,8 @@ private void handle(HttpExchange exchange) throws IOException {
case "NODE_NOT_FOUND", "OPERATION_NOT_FOUND" -> 404;
case "UNSUPPORTED_PROTOCOL", "INCOMPATIBLE_CAPABILITIES", "SESSION_MISMATCH",
"PREVIEW_INCOMPLETE", "APPROVAL_REQUIRED", "NODE_UNAVAILABLE", "OPERATION_LIMIT",
"REGISTRY_LIMIT", "TASK_NOT_CLAIMED", "TASK_LEASE_EXPIRED", "SETUP_COMPLETE" -> 409;
"REGISTRY_LIMIT", "REGISTRY_CHANGED", "TASK_NOT_CLAIMED", "TASK_LEASE_EXPIRED",
"SETUP_COMPLETE" -> 409;
case "UNSUPPORTED_MEDIA_TYPE" -> 415;
default -> 400;
};
Expand Down Expand Up @@ -380,8 +381,12 @@ private void route(HttpExchange exchange) throws IOException {
Map<String, String> query = query(uri.getRawQuery());
int offset = integer(query.getOrDefault("offset", "0"), "offset");
int limit = integer(query.getOrDefault("limit", "50"), "limit");
BackendPage page = boundedNodePage(registry.list(offset, limit));
Long expectedRevision = query.containsKey("revision")
? longInteger(query.get("revision"), "revision") : null;
NodeRegistry.RegistryPage registryPage = registry.page(offset, limit, expectedRevision);
BackendPage page = boundedNodePage(registryPage.items());
send(exchange, 200, Map.of("items", page.items(), "offset", offset, "limit", limit,
"registryRevision", registryPage.revision(), "total", registryPage.total(),
"backendItemsReturned", page.backendItemsReturned(),
"backendItemsTruncated", page.backendItemsTruncated()));
return;
Expand Down Expand Up @@ -788,6 +793,17 @@ private static int integer(String value, String name) {
}
}

private static long longInteger(String value, String name) {
try {
long parsed = Long.parseLong(value);
if (parsed < 0) throw new NumberFormatException();
return parsed;
} catch (NumberFormatException e) {
throw new ValidationException("VALIDATION_ERROR", "Request validation failed",
List.of(name + " must be a non-negative integer"));
}
}

private static Map<String, String> query(String raw) {
Map<String, String> result = new HashMap<>();
if (raw == null || raw.isBlank()) {
Expand Down
Loading
Loading