Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
761c4a0
Add VoteSites sync: ConfigurationOperations.java
BenCodez Aug 29, 2026
46b7052
Add VoteSites sync: InMemoryNodeRegistry.java
BenCodez Aug 29, 2026
8a5a407
Add VoteSites sync: ManagedConfiguration.java
BenCodez Aug 29, 2026
c3c749d
Add VoteSites sync: app.css
BenCodez Aug 29, 2026
9eae0dc
Add VoteSites sync: app.js
BenCodez Aug 29, 2026
a1629d5
Add VoteSites sync: index.html
BenCodez Aug 29, 2026
22603b2
Add VoteSites sync: ConfigurationOperationsTest.java
BenCodez Aug 29, 2026
909afcd
Add VoteSites sync: InMemoryNodeRegistryTest.java
BenCodez Aug 29, 2026
3724ddb
Add VoteSites sync: ControlHttpServerTest.java
BenCodez Aug 29, 2026
9142c8b
Fix configuration operation test imports
BenCodez Aug 29, 2026
9bbee68
Address Codex VoteSites sync review findings
BenCodez Aug 29, 2026
368c9c7
Align VoteSites stack with workspace fixes
BenCodez Aug 29, 2026
74d6d62
Merge updated workspace prerequisite
BenCodez Aug 29, 2026
0fbded0
Fix VoteSites sync test imports
BenCodez Aug 29, 2026
4f247b7
Merge second workspace review fixes
BenCodez Aug 29, 2026
6bf086c
Address third Codex VoteSites UI review
BenCodez Aug 29, 2026
4ff12f3
Preserve quick option character limits
BenCodez Aug 29, 2026
3fb3956
Fix quick option regression test compilation
BenCodez Aug 29, 2026
0c71946
Refresh VoteSites stack base
BenCodez Aug 29, 2026
54f7e8a
Refresh VoteSites stack base
BenCodez Aug 29, 2026
16a1f43
Align VoteSites sync content size semantics
BenCodez Aug 29, 2026
055caa9
Require complete VoteSites sync payloads
BenCodez Aug 29, 2026
78d205a
Rebuild VoteSites sync head with reviewed fixes
BenCodez Aug 29, 2026
65a14bb
Validate sync payloads at operation boundary
BenCodez Aug 29, 2026
c486ef6
Restore eligible primary targets after refresh
BenCodez Aug 29, 2026
1384f31
Bound restored targets and mark stale presence
BenCodez Aug 29, 2026
32c3c4e
Carry stale presence fix into VoteSites sync
BenCodez Aug 29, 2026
aa3785a
Count unregistered reported backends as issues
BenCodez Aug 29, 2026
7d64339
Merge WebUI registry consistency fix
BenCodez Aug 30, 2026
dba2815
Merge registry scan CI fix
BenCodez Aug 30, 2026
1cbbd96
Merge tabbed workspace fixes into VoteSites sync
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 @@ -27,6 +27,7 @@ public final class ConfigurationOperations implements AutoCloseable {
public static final String CAPABILITY = "config.proxy-routing.v1";
public static final String FILE_CAPABILITY = "config.files.v1";
public static final String QUICK_SETUP_CAPABILITY = "config.quick-setup.v1";
public static final String VOTE_SITES_SYNC_CAPABILITY = "config.vote-sites-sync.v1";
private static final int MAX_OPERATIONS = 1000;
private static final int MAX_FILE_OPERATIONS = 16;
static final int MAX_RETAINED_CHANGE_BYTES = 256 * 1024;
Expand Down Expand Up @@ -64,6 +65,7 @@ public synchronized OperationView createRead(List<String> nodeIds, ManagedConfig

public synchronized OperationView createPreview(List<String> nodeIds, ManagedConfiguration configuration) {
if (configuration == null) throw invalid("configuration is required");
configuration.validateProposal();
byte[] token = new byte[32];
random.nextBytes(token);
return create("PREVIEW", validateTargets(nodeIds, configuration.capability()), configuration,
Expand Down Expand Up @@ -207,7 +209,7 @@ private StoredOperation store(String type, List<String> targets, ManagedConfigur
}

private void ensureFileCapacity(ManagedConfiguration config, UUID protectedOperation) {
if (config == null || !ManagedConfiguration.FILE.equals(config.domain())) return;
if (config == null || !largeContentOperation(config)) return;
long retained = operations.values().stream().filter(StoredOperation::fileOperation).count();
Iterator<Map.Entry<UUID, StoredOperation>> iterator = operations.entrySet().iterator();
while (retained >= MAX_FILE_OPERATIONS && iterator.hasNext()) {
Expand All @@ -224,9 +226,16 @@ private void ensureFileCapacity(ManagedConfiguration config, UUID protectedOpera
}
}

private static boolean largeContentOperation(ManagedConfiguration config) {
return ManagedConfiguration.FILE.equals(config.domain()) ||
ManagedConfiguration.VOTE_SITES_SYNC.equals(config.preset());
}

private ConfigurationTaskResult boundedResult(StoredOperation operation, ConfigurationTaskResult result) {
ManagedConfiguration configuration = result.success() ? result.configuration() : null;
if (configuration != null && ManagedConfiguration.FILE.equals(configuration.domain())) {
if (configuration != null && ManagedConfiguration.VOTE_SITES_SYNC.equals(configuration.preset())) {
configuration = configuration.publicView();
} else if (configuration != null && ManagedConfiguration.FILE.equals(configuration.domain())) {
boolean keepContent = result.success() && "READ".equals(operation.type)
&& operation.results.values().stream().filter(ConfigurationTaskResult::success)
.map(ConfigurationTaskResult::configuration).filter(Objects::nonNull)
Expand Down Expand Up @@ -404,7 +413,7 @@ private StoredOperation(UUID id, String type, ManagedConfiguration configuration
}
private boolean complete() { return states.values().stream().allMatch("COMPLETE"::equals); }
private boolean fileOperation() {
return configuration != null && ManagedConfiguration.FILE.equals(configuration.domain());
return configuration != null && largeContentOperation(configuration);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
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, "config.file-comments.v1");
ConfigurationOperations.QUICK_SETUP_CAPABILITY, ConfigurationOperations.VOTE_SITES_SYNC_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 Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public record ManagedConfiguration(String domain, Boolean sendVotesToAllServers,
public static final String PROXY_ROUTING = "proxy-routing";
public static final String FILE = "file";
public static final String QUICK_SETUP = "quick-setup";
public static final String VOTE_SITES_SYNC = "sync-vote-sites";
public static final int MAX_CONTENT = 512 * 1024;

public ManagedConfiguration {
Expand Down Expand Up @@ -39,7 +40,7 @@ public record ManagedConfiguration(String domain, Boolean sendVotesToAllServers,
}
if (options.size() > 20 || options.entrySet().stream().anyMatch(entry -> entry.getKey() == null
|| !entry.getKey().matches("[a-z][A-Za-z0-9]{0,39}") || entry.getValue() == null
|| entry.getValue().length() > 500 || entry.getValue().indexOf('\0') >= 0)) {
|| invalidOption(entry.getKey(), entry.getValue(), VOTE_SITES_SYNC.equals(preset)))) {
Comment on lines 41 to +43

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Require the sync source document

When an API client submits the new sync-vote-sites preset directly, this validation still accepts empty options or unrelated options because it only changes the limit when an entry happens to be named sourceContent. Such a request negotiates config.vote-sites-sync.v1 and is queued to targets without any source YAML, leaving node handlers to fail or interpret an incomplete sync task; require the preset's options to contain exactly one non-null sourceContent document.

Useful? React with 👍 / 👎.

throw new IllegalArgumentException("quick setup options are invalid");
}
}
Expand All @@ -54,18 +55,39 @@ public static ManagedConfiguration file(String fileName, String content) {
return new ManagedConfiguration(FILE, null, List.of(), fileName, content, null, Map.of());
}

public void validateProposal() {
if (QUICK_SETUP.equals(domain) && VOTE_SITES_SYNC.equals(preset)
&& (options.size() != 1 || !options.containsKey("sourceContent"))) {
throw new IllegalArgumentException("VoteSites sync requires sourceContent");
}
}

public String capability() {
return switch (domain) {
case PROXY_ROUTING -> "config.proxy-routing.v1";
case FILE -> "config.files.v1";
case QUICK_SETUP -> "config.quick-setup.v1";
case QUICK_SETUP -> VOTE_SITES_SYNC.equals(preset)
? "config.vote-sites-sync.v1" : "config.quick-setup.v1";
default -> throw new IllegalStateException("unsupported configuration domain");
};
}

/** Omits file contents so proposals, including newly entered secrets, are never echoed by operation APIs. */
public ManagedConfiguration publicView() {
return FILE.equals(domain) ? file(fileName, null) : this;
if (FILE.equals(domain)) return file(fileName, null);
if (QUICK_SETUP.equals(domain) && VOTE_SITES_SYNC.equals(preset) && options.containsKey("sourceContent")) {
Map<String, String> visible = new LinkedHashMap<>(options);
visible.remove("sourceContent");
return new ManagedConfiguration(domain, sendVotesToAllServers, blockedServers, fileName, content,
preset, visible);
Comment on lines +78 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Redact source YAML from successful result configurations

When a sync target returns its successful typed configuration with the submitted sourceContent, this method redacts only the operation's top-level configuration. ConfigurationOperations.boundedResult() retains the first successful non-file result configuration unchanged, and OperationView.results then exposes the complete source YAML through the operation API. Apply publicView() to retained sync result configurations as well so VoteSites secrets are not echoed.

Useful? React with 👍 / 👎.

Comment on lines +81 to +82

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Construct redacted sync views without input validation

For every sync-vote-sites configuration, this constructs a replacement after removing sourceContent, but the canonical constructor now rejects that exact shape because the preset requires one sourceContent option. Since ConfigurationOperations.createPreview() immediately calls publicView() through view(), every sync preview throws IllegalArgumentException before returning its operation, making the new workflow unusable; represent the redacted output without passing it through the full request-payload invariant.

Useful? React with 👍 / 👎.

}
return this;
}

private static boolean invalidOption(String name, String value, boolean voteSitesSync) {
boolean sourceContent = voteSitesSync && "sourceContent".equals(name);
int maximum = sourceContent ? MAX_CONTENT : 500;
return value.indexOf('\0') >= 0 || value.length() > maximum;
}

private static void validateFileName(String value) {
Expand Down
12 changes: 10 additions & 2 deletions src/main/resources/web/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -104,12 +104,19 @@ p { line-height: 1.55; }
.backend-state { display: flex; flex-wrap: wrap; gap: 5px; color: var(--muted); font-size: .84rem; }
.backend-state span + span::before { margin-right: 5px; content: "·"; color: #53677f; }

.roadmap-card { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0; margin-bottom: 18px; padding: 0; overflow: hidden; }
.roadmap-card { display: grid; grid-template-columns: repeat(2, 1fr); gap: 0; margin-bottom: 18px; padding: 0; overflow: hidden; }
.roadmap-card > div { display: grid; grid-template-columns: auto 1fr; gap: 5px 10px; padding: 19px; border-right: 1px solid var(--border); }
.roadmap-card > div:last-child { border: 0; }
.roadmap-card small { grid-column: 1 / -1; color: var(--muted); }
.step { align-self: center; padding: 3px 6px; border: 1px solid #785927; border-radius: 5px; color: var(--amber); font-size: .62rem; font-weight: 800; letter-spacing: .08em; text-transform: uppercase; }
#configuration-card { margin-top: 0; }
.sync-card { margin-bottom: 18px; }
.sync-grid { display: grid; grid-template-columns: minmax(240px, .8fr) minmax(300px, 1.2fr); gap: 18px; margin: 20px 0; }
.sync-grid fieldset { margin: 0; }
.target-list { display: grid; max-height: 190px; gap: 7px; overflow: auto; }
.target-list.empty { color: var(--muted); }
.target-option { display: flex; align-items: center; gap: 9px; margin: 0; padding: 8px 9px; border: 1px solid var(--border); border-radius: 8px; color: #d6e5f7; font-size: .85rem; }
.target-option input { flex: none; width: 17px; height: 17px; }

.subtabs { display: flex; gap: 7px; padding: 5px; border: 1px solid var(--border); border-radius: 11px; background: #091421; }
.subtabs button { flex: 1; border-color: transparent; background: transparent; color: var(--muted); }
Expand Down Expand Up @@ -149,7 +156,7 @@ select { width: 100%; margin-bottom: 16px; }
.check-row { display: flex; align-items: center; gap: 10px; }
label span { color: var(--muted); font-weight: 400; }
.operation-actions { display: flex; flex-wrap: wrap; gap: 9px; }
#operation-status, #file-operation-status, #quick-operation-status { min-height: 84px; margin: 18px 0 0; padding: 14px; overflow: auto; border: 1px solid var(--border); border-radius: 9px; background: #050c16; color: #c6d5e8; white-space: pre-wrap; }
#operation-status, #file-operation-status, #quick-operation-status, #vote-sites-sync-status { min-height: 84px; margin: 18px 0 0; padding: 14px; overflow: auto; border: 1px solid var(--border); border-radius: 9px; background: #050c16; color: #c6d5e8; white-space: pre-wrap; }
#file-operation-status { margin: 0; border: 0; border-top: 1px solid var(--border); border-radius: 0; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin: 16px 0; }
.form-grid label { color: var(--muted); }
Expand All @@ -171,6 +178,7 @@ li span { color: var(--muted); font-size: .9rem; }
.topbar-actions { flex-wrap: wrap; }
.server-picker { order: 3; width: 100%; grid-template-columns: auto 1fr; }
.metrics, .settings-grid { grid-template-columns: repeat(2, 1fr); }
.sync-grid { grid-template-columns: 1fr; }
.roadmap-card { grid-template-columns: 1fr; }
.roadmap-card > div { border-right: 0; border-bottom: 1px solid var(--border); }
.topology-row { grid-template-columns: 1fr; }
Expand Down
Loading