From 4dea5263eb4343660d33b27b83520a5c0160dab7 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Mon, 28 Sep 2026 20:01:20 -0600 Subject: [PATCH 1/6] Expose proxy and reward YAML configuration --- README.md | 5 + docs/control-management.md | 11 ++ src/main/resources/web/app.css | 2 +- src/main/resources/web/app.js | 54 +++++++--- src/main/resources/web/index.html | 11 +- .../control/http/ControlHttpServerTest.java | 4 +- src/test/web/app-workspace.test.cjs | 14 +++ src/test/web/configuration-target.test.cjs | 100 ++++++++++++++++++ 8 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 src/test/web/configuration-target.test.cjs diff --git a/README.md b/README.md index badb32a..5666adf 100644 --- a/README.md +++ b/README.md @@ -311,6 +311,11 @@ The disposable running-connector and packaged-browser validation boundaries are at most 64 MiB, Control validates the ZIP structure and root `plugin.yml`, and every node re-verifies the SHA-256 before staging. Staging never reloads or restarts a server; success is reported as `RESTART_REQUIRED`. Interrupted Control attempts become failed durable history and require an explicit retry, preventing a pre-restart lease from authorizing a download. +Capability absence is not proof that the connector is old: deployment is also withheld unless the endpoint is HTTPS or +VotingPlugin directly hosts Control at a loopback HTTP endpoint, and proxy staging is unavailable on Windows. Bootstrap by +installing a current VotingPlugin JAR manually once, then use HTTPS for a separate Control service. The browser cannot +securely add missing staging code to an old node by itself. If a current node still omits the capability, check its log for +the connector's staging-unavailable warning; a node-side staging path or loaded-JAR discovery failure may need correction. `data.inspect.v1` is a separate read-only lane for overview, vote-site health (including persisted unconfigured service observations), exact-player data, bounded VoteLog summary/search/correlation trace, non-creating service-site resolution, diff --git a/docs/control-management.md b/docs/control-management.md index af96135..3ff59e5 100644 --- a/docs/control-management.md +++ b/docs/control-management.md @@ -431,6 +431,17 @@ configured site's `ServiceSite`; its separate truncation flag reports overflow. VoteLogging is disabled and when automatic site creation is turned off. It is an inbox for review, not an automatic create or approval action. +VotingPlugin stores that observation list at `VotingPlugin.GottenServiceSites` in `ServerData.yml`. Control reads it only +through the bounded inspection capability. The Vote Sites page and health results can copy one observed service into the +existing `vote-site` preset; the administrator must finish its fields and complete READ/PREVIEW/APPROVE/APPLY before the +site is created. + +The Full YAML workbench has its own explicit single connected-node selector, independent of the multi-backend workspace +target set. Bukkit targets expose the fixed managed files and +existing named `Rewards/*.yml` files through their respective capabilities; proxy targets expose only the complete +`bungeeconfig.yml`. Named reward files are discovered by the Rewards inventory and opened in the same source-preserving, +redacted editor. The selector never grants arbitrary path access. + The dedicated `vote-logging` quick setup owns only `VoteLogging.Enabled`, `VoteLogging.PurgeDays` (`-1` disables automatic purging, otherwise `1`–`3650`; `0` and other negatives are invalid), and `VoteLogging.UseMainMySQL`. It rejects unknown options and never accepts or returns database connection fields or credentials. Its READ form takes no options and diff --git a/src/main/resources/web/app.css b/src/main/resources/web/app.css index 053460e..b83999b 100644 --- a/src/main/resources/web/app.css +++ b/src/main/resources/web/app.css @@ -179,7 +179,7 @@ p { line-height: 1.55; } .setting-icon { display: grid; width: 39px; height: 39px; margin-bottom: 16px; place-items: center; border: 1px solid #37628d; border-radius: 10px; background: var(--blue-soft); color: #a9d4ff; font-size: 1.1rem; } .editor-card { padding: 0; overflow: hidden; } .editor-card > .section-title { padding: 21px 22px 17px; border-bottom: 1px solid var(--border); } -.editor-toolbar { display: grid; grid-template-columns: auto minmax(180px, 280px) 1fr; align-items: center; gap: 12px; padding: 12px 18px; border-bottom: 1px solid var(--border); background: #091421; } +.editor-toolbar { display: grid; grid-template-columns: auto minmax(180px, 280px) auto minmax(180px, 280px) 1fr; align-items: center; gap: 12px; padding: 12px 18px; border-bottom: 1px solid var(--border); background: #091421; } .editor-toolbar label { margin: 0; } .editor-toolbar select { margin: 0; } .editor-toolbar > span { justify-self: end; color: var(--muted); font-size: .78rem; } diff --git a/src/main/resources/web/app.js b/src/main/resources/web/app.js index 7ba0d13..79b5851 100644 --- a/src/main/resources/web/app.js +++ b/src/main/resources/web/app.js @@ -76,6 +76,7 @@ const previewConfiguration = document.querySelector('#preview-configuration'); const applyConfiguration = document.querySelector('#apply-configuration'); const operationStatus = document.querySelector('#operation-status'); const configurationForm = document.querySelector('#configuration-form'); +const configurationTarget = document.querySelector('#configuration-target'); const configurationFile = document.querySelector('#configuration-file'); const configurationContent = document.querySelector('#configuration-content'); const editorPosition = document.querySelector('#editor-position'); @@ -444,18 +445,16 @@ function selectedFileCapability(fileName = configurationFile.value) { function fileTargetsForSelection(fileName = configurationFile.value) { const capability = selectedFileCapability(fileName); const selected = nodeIndex.get(selectedServerId); - if (capability === 'config.proxy-files.v1') { - return workspace.managementScope === 'GLOBAL' && selected?.online && isProxy(selected) && selected.acceptedCapabilities.includes(capability) - ? [selected.nodeId] : []; - } - return targets(capability).filter(nodeId => isBackend(nodeIndex.get(nodeId))); + if (!selected?.online || !selected.acceptedCapabilities.includes(capability)) return []; + if (capability === 'config.proxy-files.v1') return isProxy(selected) ? [selected.nodeId] : []; + return isBackend(selected) ? [selected.nodeId] : []; } function fileTargetDescription(fileName = configurationFile.value) { const targetsForFile = fileTargetsForSelection(fileName); if (fileName === 'bungeeconfig.yml') return targetsForFile.length ? `the selected proxy (${targetsForFile[0]})` : 'the selected proxy'; - return `every selected Bukkit node`; + return targetsForFile.length ? `the selected Bukkit node (${targetsForFile[0]})` : 'the selected Bukkit node'; } function boundedLines(value, maximum = 20) { @@ -1671,7 +1670,7 @@ function renderWorkspaceChrome(tab) { : tab === 'general-settings' ? 'General Settings reads each workspace target independently. Only explicit edits are proposed, with per-target revision protection.' : tab === 'vote-sites' ? 'Vote Sites reads each workspace target independently. Site keys are identity; property editing never invokes synchronization or rewrites rewards.' : tab === 'rewards' ? 'Rewards reads each workspace target independently. Only explicit bounded operations can be previewed and applied.' - : `Legacy tools: rewards, guided presets and YAML use only ${source?.displayName || 'a selected source server'} (${selectedServerId || 'not selected'}). They do not copy its configuration to the workspace.${source && !workspace.selectedTargetIds.has(source.nodeId) ? ' This inspected server is outside the workspace; its configuration tools are disabled.' : ''}`); + : `Legacy tools use only ${source?.displayName || 'a selected source server'} (${selectedServerId || 'not selected'}). They do not copy its configuration to the workspace. Full YAML has an explicit single-node target; other legacy tools remain disabled when that source is outside the workspace.${source && !workspace.selectedTargetIds.has(source.nodeId) ? ' The current source is outside the workspace.' : ''}`); document.querySelector('#scope-overview').hidden = global; } @@ -2096,7 +2095,7 @@ function chooseDefaultServer(items) { function renderServerPicker() { const previousValue = selectedServerId; - const ordered = allNodeItems.filter(node => workspace.managementScope === 'GLOBAL' + const ordered = allNodeItems.filter(node => workspace.managementScope === 'GLOBAL' || node.nodeId === selectedServerId || isBackend(node) && (workspace.selectedTargetIds.has(node.nodeId) || node.nodeId === workspace.inspectedServerId)).sort((left, right) => { const roleOrder = Number(isProxy(left)) - Number(isProxy(right)); return roleOrder || left.displayName.localeCompare(right.displayName); @@ -2119,6 +2118,28 @@ function renderServerPicker() { populateGlobalSearch(); } +function configurationTargetNodes() { + return allNodeItems.filter(node => node.online && (isProxy(node) + ? node.acceptedCapabilities.includes('config.proxy-files.v1') + : isBackend(node) && node.acceptedCapabilities.includes('config.files.v1'))) + .sort((left, right) => Number(isProxy(left)) - Number(isProxy(right)) + || left.displayName.localeCompare(right.displayName)); +} + +function renderConfigurationTargetPicker() { + const eligible = configurationTargetNodes(); + const placeholder = text(document.createElement('option'), eligible.length + ? 'Choose a connected backend or proxy' : 'No connected YAML-capable nodes'); + placeholder.value = ''; + configurationTarget.replaceChildren(placeholder, ...eligible.map(node => { + const option = text(document.createElement('option'), `${node.displayName} · ${roleLabel(node)}`); + option.value = node.nodeId; + return option; + })); + configurationTarget.value = eligible.some(node => node.nodeId === selectedServerId) ? selectedServerId : ''; + configurationTarget.disabled = !authenticated || eligible.length === 0; +} + function renderSelectedServer() { const selected = nodeIndex.get(selectedServerId); selectedServerCapabilities.replaceChildren(); @@ -2127,7 +2148,7 @@ function renderSelectedServer() { text(selectedServerState, 'No selection'); selectedServerState.className = 'pill neutral'; text(selectedServerSummary, 'Use the server picker above to keep configuration and setup actions focused on one node.'); - text(configurationContext, 'Choose a backend from the server picker to work with its VotingPlugin configuration.'); + text(configurationContext, 'Choose a backend or proxy to work with its VotingPlugin configuration.'); text(commentPreservationState, 'Comment support unknown'); commentPreservationState.className = 'pill warning'; syncFileSelection(); @@ -3180,6 +3201,7 @@ function updateExtendedButtons() { function renderNodeViews() { renderServerPicker(); + renderConfigurationTargetPicker(); renderHomeChooser(); nodes.replaceChildren(); nodes.classList.toggle('empty', visibleNodeItems.length === 0); @@ -3209,7 +3231,7 @@ function renderDeploymentEligibility() { const incompatible = connected.filter(node => !node.acceptedCapabilities.includes('plugin.deploy.v1')); const batches = Math.ceil(eligible.length / MAX_OPERATION_TARGETS); const bootstrap = incompatible.length - ? ` · ${incompatible.length} connected ${incompatible.length === 1 ? 'node needs' : 'nodes need'} a one-time VotingPlugin update with verified staging support` + ? ` · ${incompatible.length} connected ${incompatible.length === 1 ? 'node is' : 'nodes are'} missing verified staging prerequisites` : ''; text(deploymentEligibility, `${eligible.length}/${connected.length} connected nodes eligible` + bootstrap + (batches > 1 ? ` · ${batches} bounded deployment batches` : '')); @@ -3951,9 +3973,12 @@ async function startConfigurationOperation(path, body, statusElement = operation && path !== '/api/v1/configuration/vote-sites/preview' && path !== '/api/v1/configuration/rewards/preview' && !['proxy-method', 'sync-vote-sites', 'communication-test'].includes(body.configuration?.preset)) { - const allowed = ordinaryTargetIds(); + const allowed = body.configuration?.domain === 'file' + ? fileTargetsForSelection(body.configuration.fileName) : ordinaryTargetIds(); if (!Array.isArray(body.nodeIds) || body.nodeIds.length !== 1 || !allowed.includes(body.nodeIds[0])) { - throw new Error('Choose a workspace source server. This form previews only that server, not the whole workspace.'); + throw new Error(body.configuration?.domain === 'file' + ? 'Choose the connected Full YAML target again. This editor previews exactly one capability-gated node.' + : 'Choose a workspace source server. This form previews only that server, not the whole workspace.'); } } const applyOperation = path.endsWith('/apply'); @@ -5865,6 +5890,9 @@ quickPreset.addEventListener('input', () => { void autoLoadTab('quick-setup'); }); serverPicker.addEventListener('change', () => selectPrimaryServer(serverPicker.value)); +configurationTarget.addEventListener('change', () => { + if (!selectPrimaryServer(configurationTarget.value)) configurationTarget.value = selectedServerId; +}); homeSearch.addEventListener('input', renderHomeChooser); document.querySelector('#home-refresh').addEventListener('click', () => loadNodes()); document.querySelector('#home-select-all').addEventListener('click', () => changeWorkspaceTargets(() => workspace.selectEligible(allNodeItems, MAX_CONFIGURATION_TARGETS))); @@ -5897,7 +5925,7 @@ deployPlugin.addEventListener('click', async () => { const confirmation = `Upload ${file.name} (${file.size.toLocaleString()} bytes) and stage it on ` + `${eligible.length} deployment-capable node(s)? Servers will require a restart. Automatic restart is disabled.` + (batches.length > 1 ? ` Control will use ${batches.length} bounded operations.` : '') - + (ineligible.length ? ` Older/incompatible nodes excluded: ${ineligible.join(', ')}.` : ''); + + (ineligible.length ? ` Nodes without verified staging capability excluded: ${ineligible.join(', ')}. Install a current VotingPlugin JAR once and use HTTPS unless VotingPlugin directly hosts Control on loopback; Windows proxy staging is unavailable. Check node logs for the staging-unavailable warning.` : ''); if (!window.confirm(confirmation)) return; const deploymentRun = ++deploymentRunGeneration; deploymentInFlight = true; diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index 0197976..4baf30f 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -214,7 +214,7 @@
Nodes running an older connector remain connected but require one manual update before web deployment is available.+
If staging is unavailable, install a current VotingPlugin JAR once. Use HTTPS unless VotingPlugin directly hosts Control on loopback; Windows proxy staging is unavailable. Check the node log for the staging-unavailable warning.@@ -316,7 +316,7 @@
Selected VotingPlugin server
Choose a backend from the server picker to work with its VotingPlugin configuration.
+Choose a backend or proxy to work with its VotingPlugin configuration.