Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,11 @@ Control and VotingPlugin both enforce fixed quick-setup preset/option schemas; u
than becoming arbitrary YAML writes. The WebUI settings catalog is a static versioned reference over these typed paths,
not a generic setting API.

Opening Settings automatically reads current configuration; errors expose inline Retry. The Phase 2 General Settings visual
editor reads **each** selected workspace backend and previews only explicitly changed fields independently per target.
Opening Settings automatically reads current configuration; errors expose inline Retry. The General Settings visual
editor reads **each** selected workspace backend plus reporting managed proxies, using `Config.yml` and
`bungeeconfig.yml` respectively. It previews only explicitly changed allowlisted boolean, enum, and bounded-string
fields independently per target. Network-aware controls keep per-node values visible, and proxy-managed
`AllowUnjoined` repairs backend prerequisites without changing the proxy's effective preference.
The Phase 3 [Vote Sites visual editor](docs/webui-vote-sites.md) likewise reads each selected backend's main
`VoteSites.yml`, distinguishes mixed/partial/error states, and uses source-preserving per-target add/edit/remove previews.
Successful applies invalidate cached values and read the confirmed state again. Legacy guided forms and raw YAML retain
Expand Down
58 changes: 42 additions & 16 deletions docs/webui-general-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,37 @@ Full YAML, Compare, snapshots and network workflows retain their existing bounda

## Source-verified curated settings

All entries are top-level booleans in VotingPlugin `Config.yml`. Source verification used VotingPlugin `origin/master`
at `11f20c39f551c40c55923c6a686b1b6c2a260f83`, its default Config.yml, configuration reader, backend connector and
Entries are bounded top-level booleans, enums, or strings in VotingPlugin `Config.yml` and `bungeeconfig.yml`.
Source verification used VotingPlugin `origin/master` at `d22a611202aec5a7df99979390ecfbaa1d8f1e0c`, its default files,
configuration readers, backend/proxy connectors, and
`VotingPluginMain.reloadFromControl`. This is evidence, not a pinned development dependency.

| YAML path | Documented default | Effect | Runtime action |
| YAML path / control | Documented default | Effect | Runtime action |
| --- | --- | --- | --- |
| `ProcessRewards` | true | Process vote rewards (offline processing still follows VotingPlugin behavior) | Configuration reload |
| `DebugLevel` / proxy `Debug` | NONE | Backend diagnostic level; proxy offers Off/Info (Extra maps to Info) | Configuration reload |
| `OnlineMode` | true | Keep online identity handling aligned across applicable backend and proxy targets | Network configuration reload |
| `AutoCreateVoteSites` | true | Create sites for newly received services | Configuration reload |
| `ExtraAllSitesCheck` | false | Additional duplicate all-sites reward check | Configuration reload |
| `CountFakeVotes` | true | Include fake votes in points/totals | Configuration reload |
| `DisableNoServiceSiteMessage` | false | Suppress missing service-site and related site warnings | Configuration reload |
| `DisableUpdateChecking` | false | Disable update checks | Configuration reload plus backend restart to reconcile scheduler lifecycle |
| `AllowUnjoined` / proxy `AllowUnJoined` | false | Effective proxy value governs proxy-managed networks; backend requests true in that mode | Configuration reload |
| `UseVoteGUIMainCommand` | false | Open the vote GUI from the main vote command | Configuration reload |
| `GiveDefaultPermission` | true | Grant default permissions during startup | Configuration reload plus backend restart |
| `LoadCommandAliases` | true | Load command aliases during startup | Configuration reload plus backend restart |
| `CaseInsensitiveYMLFiles` | true | Use case-insensitive YAML file lookups | Configuration reload plus backend restart |
| `BedrockPlayerPrefix` | `.` | Share Bedrock player name handling across applicable backend and proxy targets | Network configuration reload |
| `PerSiteCoolDownEvents` | false | Emit cooldown events per site | Configuration reload plus backend restart |
| `CheckForUpdates` (inverted `DisableUpdateChecking`) | true | Enable update checks | Configuration reload plus backend restart to reconcile scheduler lifecycle |
| `CloseInventoryOnVote` | true | Close the vote inventory when voting | Configuration reload |
| `ExtraVoteShopCheck` | true | Additional vote shop purchase check | Configuration reload |

The visual group deliberately omits older primary controls such as `ProcessRewards`, `ExtraAllSitesCheck`,
`DisableNoServiceSiteMessage`, and `ExtraVoteShopCheck`; their existing typed or guided workflows remain available where
supported. Backend `Config.yml` and proxy `bungeeconfig.yml` are read in grouped batches. A proxy is included when it
reports one of the selected backend targets, and its effective network values are shown alongside backend values.
Missing or unsupported values remain visible and are never replaced by documented defaults. Control recommends disabling
auto-create after intended vote sites are configured, then reviewing detected service sites before creating new entries.

The update-check timer is started by `CheckUpdate.startUp` at plugin startup, not by the configuration reload path.
The editor does not restart a backend. A confirmed READ proves persisted configuration, not completion of a restart.
The editor does not restart a backend or proxy. Proxy `bungeeconfig.yml` writes always remain restart-required; a
confirmed READ proves persisted configuration, not completion of a restart.
Vote Logging and Vote Party remain accessible through their existing typed/capability-gated tools; they are deliberately
not added to this first visual group. Missing values are never synthesized from defaults or inserted automatically.

Expand All @@ -33,17 +46,17 @@ not added to this first visual group. Missing values are never synthesized from
MISSING, UNSUPPORTED and ERROR, with supported-subset values and each excluded target visible separately. Mixed controls
have a placeholder, not an implicit true/false choice. Focusing a control is not an edit. A reset removes its override.

The required connector contract is the existing `config.files.v1` on an online Bukkit node. Actual parsed file presence
and boolean type gate each field, instead of guessing support from a version string. Missing keys, wrong types and
The connector contracts remain `config.files.v1` for Bukkit and `config.proxy-files.v1` for proxies. Actual parsed file
presence and expected scalar type gate each field, instead of guessing support from a version string. Missing keys, wrong types and
anchored/aliased edited values are not writable. Malformed/ambiguous YAML fails closed with sanitized errors. Offline,
unsupported and failed targets remain visible. Applying only a supported subset requires explicit acknowledgement.

## Additive Control API (no connector/protocol change)

1. Existing `/api/v1/configuration/read` reads Config.yml from every eligible selected target in a bounded batch.
2. POST `/api/v1/configuration/general-settings/state` with `readOperationId` and `nodeId` returns nine typed fields,
that target's session and revision. It requires a retained successful READ of that node's Config.yml.
3. POST `/api/v1/configuration/general-settings/preview` adds `overrides` (one to nine allowlisted JSON booleans).
1. Existing `/api/v1/configuration/read` reads `Config.yml` and `bungeeconfig.yml` in separate bounded target batches.
2. POST `/api/v1/configuration/general-settings/state` with `readOperationId` and `nodeId` returns the profile's typed
fields, target session, revision, and backend/proxy profile. It requires a retained successful read of the matching file.
3. POST `/api/v1/configuration/general-settings/preview` adds one or more allowlisted typed overrides.
Control patches only changed scalar source spans in that target's retained redacted document and creates an ordinary
FILE PREVIEW bound to the READ revision. Comments, unknown keys, unrelated values and redacted placeholders are not
regenerated. Each target has its own proposal and approval token; no common document is distributed.
Expand All @@ -60,7 +73,9 @@ Target IDs/sessions, every relevant READ revision/state and explicit overrides f
scope changes and changed revisions invalidate approval. Before APPLY a fresh scope READ checks that signature again;
the connector revision guard closes the remaining race. Navigation only reads and never previews, applies or reloads.

At most eight changed targets can be previewed in one visual batch, within the existing bounded FILE-operation retention.
The changed-target preview limit is calculated from the actual number of bounded 100-target backend/proxy read batches.
The coordinator retains at most sixteen FILE operations, so the editor reserves one slot per required fresh read batch
before APPLY instead of allowing an approval set that cannot be revalidated.
All workspace targets are still read/displayed. Larger changed sets must be reduced explicitly, not silently truncated.
Successful revision-bound pending previews are protected from ordinary FILE-retention eviction for fifteen minutes,
or until consumed/discarded. `/api/v1/configuration/general-settings/discard-preview` releases the exact abandoned
Expand Down Expand Up @@ -92,3 +107,14 @@ single-flight and stale-result guards, mixed/partial reads, dirty fields and con
smoke evidence uses actual static assets against controlled API fixtures. A disposable Paper 26.2 server with the current
VotingPlugin connector also exercised the real HTTP connector, Config.yml READ/PREVIEW/APPLY/reload/confirmed READ,
and stale revision rejection. That local gate never points at production configuration.

### Configuration health outside the editor

Dashboard refresh (including the network workspace) and Network Doctor read the same allowlisted,
revision-bound managed configuration directly. Visiting General Settings first is not required.
Health reads use a separate read-only state so refreshing health never discards an editor draft or
approval. Reads are coalesced and cached for 30 seconds; configuration changes invalidate the cache.
Offline, unsupported, missing, or incomplete-topology observations are reported as UNKNOWN rather
than passing. Identity comparisons apply within reported proxy/backend groups, not between unrelated
standalone servers. Network Doctor exports these observations as `configurationChecks`; no settings
are written by a health check.
Original file line number Diff line number Diff line change
Expand Up @@ -149,43 +149,62 @@ public synchronized OperationView createApply(UUID previewId, String approvalTok
return view(apply);
}

/** Typed view of a retained, successful read of this particular backend's Config.yml. */
/** Typed view of a retained successful backend Config.yml or proxy bungeeconfig.yml read. */
public synchronized SettingsState generalSettingsState(UUID readId, String nodeId) {
ConfigurationTaskResult snapshot = settingsSnapshot(readId, nodeId);
SettingsSnapshot source = settingsSnapshot(readId, nodeId);
ConfigurationTaskResult snapshot = source.result();
try {
return new SettingsState(readId, nodeId, snapshot.sessionId(), snapshot.revision(),
GeneralSettingsDocument.fields(snapshot.configuration().content()));
source.profile(), GeneralSettingsDocument.fields(snapshot.configuration().content(), source.profile()));
} catch (IllegalArgumentException malformed) {
throw new ValidationException("INVALID_CONFIGURATION", "General Settings requires an unambiguous YAML mapping", List.of(nodeId));
}
}

public synchronized OperationView createGeneralSettingsPreview(UUID readId, String nodeId,
Map<String, Object> overrides) {
ConfigurationTaskResult snapshot = settingsSnapshot(readId, nodeId);
if (overrides == null || overrides.isEmpty() || overrides.size() > 9
|| overrides.values().stream().anyMatch(value -> !(value instanceof Boolean))) {
throw invalid("General Settings requires one to nine boolean edits");
SettingsSnapshot source = settingsSnapshot(readId, nodeId);
ConfigurationTaskResult snapshot = source.result();
if (overrides == null || overrides.isEmpty()
|| overrides.size() > GeneralSettingsDocument.maxOverrides(source.profile())) {
throw invalid("General Settings requires one or more bounded typed edits");
}
Map<String, Boolean> edits = new LinkedHashMap<>();
overrides.forEach((key, value) -> edits.put(key, (Boolean) value));
String proposal;
try {
proposal = GeneralSettingsDocument.patch(snapshot.configuration().content(), edits);
proposal = GeneralSettingsDocument.patch(snapshot.configuration().content(), source.profile(), overrides);
} catch (IllegalArgumentException unsupported) {
throw invalid("Only present, supported General Settings booleans can be edited");
throw invalid("Only present, supported General Settings values can be edited");
}
ManagedConfiguration configuration = ManagedConfiguration.file("Config.yml", proposal);
ValidatedTargets targets = validateTargets(List.of(nodeId), FILE_CAPABILITY);
ManagedConfiguration configuration = ManagedConfiguration.file(GeneralSettingsDocument.fileName(source.profile()), proposal);
ValidatedTargets targets = validateTargets(List.of(nodeId), configuration.capability());
validateConfigurationTargets(targets, configuration);
byte[] token = new byte[32];
random.nextBytes(token);
return create("PREVIEW", targets, configuration,
Base64.getUrlEncoder().withoutPadding().encodeToString(token), Map.of(nodeId, snapshot.revision()));
}

private ConfigurationTaskResult settingsSnapshot(UUID readId, String nodeId) {
return fileSnapshot(readId, nodeId, "Config.yml");
private SettingsSnapshot settingsSnapshot(UUID readId, String nodeId) {
prune();
StoredOperation read = operations.get(readId);
if (read == null || !"READ".equals(read.type)
|| !ManagedConfiguration.FILE.equals(read.configuration.domain())) {
throw new ValidationException("READ_REQUIRED", "A retained successful General Settings read is required",
List.of(nodeId));
}
GeneralSettingsDocument.Profile profile;
try {
profile = GeneralSettingsDocument.profileForFile(read.configuration.fileName());
} catch (IllegalArgumentException invalidFile) {
throw new ValidationException("READ_REQUIRED", "A retained successful General Settings read is required",
List.of(nodeId));
}
NodeStatus node = registry.find(nodeId);
boolean proxy = node != null && !"BUKKIT".equalsIgnoreCase(node.platform());
if (node == null || proxy != (profile == GeneralSettingsDocument.Profile.PROXY)) {
throw new ValidationException("TARGET_ROLE_CHANGED", "The target role changed; read it again", List.of(nodeId));
}
return new SettingsSnapshot(fileSnapshot(readId, nodeId, GeneralSettingsDocument.fileName(profile)), profile);
}

private ConfigurationTaskResult fileSnapshot(UUID readId, String nodeId, String fileName) {
Expand All @@ -203,14 +222,17 @@ private ConfigurationTaskResult fileSnapshot(UUID readId, String nodeId, String
List.of(nodeId));
}
if (!Objects.equals(result.sessionId(), targets.sessions().get(nodeId))) {
throw new ValidationException("TARGET_CHANGED", "The backend reconnected; read it again", List.of(nodeId));
throw new ValidationException("TARGET_CHANGED", "The node reconnected; read it again", List.of(nodeId));
}
return result;
}

public record SettingsState(UUID readOperationId, String nodeId, UUID sessionId, String revision,
GeneralSettingsDocument.Profile profile,
Map<String, GeneralSettingsDocument.Field> fields) { }

private record SettingsSnapshot(ConfigurationTaskResult result, GeneralSettingsDocument.Profile profile) { }

/** Typed, secret-bounded inventory from one retained successful VoteSites.yml read. */
public synchronized VoteSitesState voteSitesState(UUID readId, String nodeId) {
ConfigurationTaskResult snapshot = fileSnapshot(readId, nodeId, "VoteSites.yml");
Expand Down
Loading
Loading