From 15d8c085a764dbfe26c92262946e72c8f784ce5d Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:27:28 -0600 Subject: [PATCH 1/2] Improve the Full YAML configuration workbench --- docs/control-management.md | 5 + docs/mockup-capability-map.md | 132 ++++++++++++++ .../control/http/ControlHttpServer.java | 23 +-- src/main/resources/web/app.css | 14 +- src/main/resources/web/app.js | 86 ++++++++- src/main/resources/web/index.html | 18 +- src/main/resources/web/yaml-editor.js | 170 ++++++++++++++++++ .../control/http/ControlHttpServerTest.java | 4 + src/test/web/app-file-navigation.test.cjs | 31 +++- src/test/web/app-workspace.test.cjs | 1 + src/test/web/authentication-state.test.cjs | 4 +- src/test/web/yaml-editor.test.cjs | 40 +++++ 12 files changed, 505 insertions(+), 23 deletions(-) create mode 100644 docs/mockup-capability-map.md create mode 100644 src/main/resources/web/yaml-editor.js create mode 100644 src/test/web/yaml-editor.test.cjs diff --git a/docs/control-management.md b/docs/control-management.md index c4c592e..af96135 100644 --- a/docs/control-management.md +++ b/docs/control-management.md @@ -5,6 +5,10 @@ is an optional local-first management plane. It discovers enrolled nodes, coordi requests typed read-only inspections. It never receives or processes votes, and VotingPlugin remains fully operational when Control is stopped. +The approved administration mockups are reconciled with current capabilities in +[Approved mockups: capability map](mockup-capability-map.md). That map guides truthful labels and phased work; mockup-only +telemetry is not treated as observed health. + ## Mental model There are four independent lanes: @@ -71,6 +75,7 @@ authenticated, CSRF-protected endpoint and node capability checks as an external | Plugin update | Uploads one bounded JAR, shows the deployment-capable subset, and stages it on those nodes | SHA-256 and JAR identity are verified; session/attempt leases authorize downloads; Control never automatically reloads or restarts nodes; private storage is capped at 32 artifacts / 512 MiB and evicts only artifacts not referenced by retained deployment history | | Fast file reads | Caches a successful file read for 30 seconds by node ID, node session, and file | Browser memory only; cleared on logout and successful relevant writes; session binding prevents reuse after reconnect | | Full-YAML drafts | Keeps unsaved editor contents during a registry refresh | A dirty draft is bound to its source node, session, and file; it cannot preview or apply after that session changes. The operator must explicitly confirm a current-file read/reload, which discards the retained draft and rebinds the editor. | +| Full-YAML workbench | Shows the exact source node, file and retained revision, synchronized line numbers, document size, bounded in-document search, cursor position, and confirmed/unsaved state | Search and editor state remain browser-memory only; the workbench does not parse, normalize, save, or bypass the connector's redaction and preview validation | | Proxy configuration | Opens `bungeeconfig.yml` only for the selected online proxy that negotiated `config.proxy-files.v1` | Fixed one-file capability, never proxy file browsing; redacted READ, PREVIEW, and one-time approved APPLY still apply | | Configuration drift | Reads the same redacted managed file from two or more selected capable nodes, groups exact revisions, and compares each target with the first successful baseline | Read-only; renders at most 50 differing line pairs per target and truncates each redacted line to 200 characters | | Snapshots | Creates a named durable snapshot from the last completed file read and loads one document into the editor | Stores the full redacted read result; restore is proposed content and must be freshly previewed/approved | diff --git a/docs/mockup-capability-map.md b/docs/mockup-capability-map.md new file mode 100644 index 0000000..a0ec5b7 --- /dev/null +++ b/docs/mockup-capability-map.md @@ -0,0 +1,132 @@ +# Approved mockups: capability map + +This document compares the nine approved Control mockups with the current management protocol and WebUI. The mockups +define the visual direction. Current Control and VotingPlugin contracts define which labels and actions are truthful. + +The classifications are: + +- **ALREADY REAL**: current Control and connector capabilities provide the data or operation. +- **IMPLEMENTABLE**: current bounded data and APIs are sufficient, but the WebUI does not yet present the complete flow. +- **FUTURE / UNSUPPORTED**: a new additive, negotiated connector capability or a new bounded data model is required. +- **MOCKUP-ONLY / INVALID**: the mockup implies evidence or a VotingPlugin setting that does not exist. It must be + removed or replaced with a truthful observation. + +Unknown or unavailable evidence must never be rendered as healthy. Every mutation continues to use the authenticated, +revision-aware `READ -> PREVIEW -> APPROVE -> APPLY -> CONFIRMED READ` workflow. + +## Network Health + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Connected nodes, roles, versions, capabilities | Registration and heartbeat expose these values | ALREADY REAL | Keep the node table and summary cards | +| Online/offline state | Control observes authenticated heartbeat liveness | ALREADY REAL | Label as Control connectivity, separate from Minecraft presence | +| Proxy/backend topology and presence | Proxy presence snapshots expose bounded backend state | ALREADY REAL | Show the reporting proxy and label stale/unknown evidence | +| Configuration and operation alerts | Redacted durable operation history and overview inspection exist | ALREADY REAL | Link alerts to the relevant safe workflow | +| Vote Site state and unmatched services | `vote-site-health` inspection reports configuration and retained observations | ALREADY REAL | Describe this as configuration/log evidence, never website uptime | +| Vote activity | Bounded VoteLog summaries exist when VoteLog is enabled and readable | ALREADY REAL | Label values as retained logged events and show unavailable explicitly | +| CPU/RAM, internet, vote-site website uptime | No current measurement exists | MOCKUP-ONLY / INVALID | Omit | +| Reward execution health or end-to-end vote health | No complete delivery trace exists | MOCKUP-ONLY / INVALID | Replace with configuration state and bounded logged-event evidence | + +## Setup Doctor + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Enrollment, connector, version, capability and topology checks | Node registry and diagnostics inspection expose these values | ALREADY REAL | Keep evidence and source with PASS/WARNING/FAIL/UNKNOWN | +| Vote Site, logging, storage mode and proxy configuration checks | Bounded diagnostics/overview fields exist | ALREADY REAL | Navigate to configuration; never mutate silently | +| Proxy communication test | `config.transport-test.v1` performs a correlated status request without a vote | ALREADY REAL | Keep as an explicit diagnostic action | +| Safe recommended actions | Existing pages can be opened with the affected target selected | IMPLEMENTABLE | Expand navigation links as typed editors grow | +| Vote-site website responding | No reliable remote probe exists | MOCKUP-ONLY / INVALID | Replace with configured/enabled and observed ServiceSite evidence | +| Votifier port reachability | Votifier detection does not prove external reachability | MOCKUP-ONLY / INVALID | Show detection only; do not claim reachability | +| Database health | Current overview can report configured storage/readability, not general database health | MOCKUP-ONLY / INVALID | Use narrow storage/VoteLog evidence and UNKNOWN otherwise | + +## Configuration Workspace + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Target chips and SAME/MIXED/MISSING/UNSUPPORTED/ERROR | Workspace state already reads each selected target independently | ALREADY REAL | Preserve per-target source and dirty-only edits | +| General settings | Typed `Config.yml` paths are source-preserving | ALREADY REAL | Expand only from verified current configuration paths | +| Vote Sites | Typed VoteSite property editing is source-preserving | ALREADY REAL | Keep rewards and unknown fields untouched | +| Rewards | Bounded reward inventory and simple edits exist | ALREADY REAL | Keep Full YAML available for advanced structures | +| Vote Party, vote logging, proxy setup | Guided capability-gated presets exist | ALREADY REAL | Move into concept navigation without changing their contracts | +| GUI, Shop, reminders, streaks, milestones, storage | Full YAML supports them, but typed coverage is incomplete | IMPLEMENTABLE | Add typed sections incrementally from current defaults/loaders | +| Full YAML for managed files and existing named reward files | Revisioned redacted file workflow exists | ALREADY REAL | Improve editor ergonomics; retain single-source identity | +| Comment/format fidelity | Connector support is negotiated and full-file apply may normalize YAML | FUTURE / UNSUPPORTED | Report capability accurately and fail closed where fidelity is unavailable | +| Mockup labels that resemble settings | Some do not map to real paths | MOCKUP-ONLY / INVALID | Derive every field from current defaults and loaders | + +## Rewards Designer + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Reward scopes and existing named reward files | Current connector can inventory supported scopes and files | ALREADY REAL | Keep per-target presence and advanced fallback | +| Commands, messages, broadcast, money and basic item leaves | Current bounded reward proposal and parser understand a safe subset | ALREADY REAL | Present only supported operations | +| No-side-effect validation/simulation | `reward-simulation` inspection exists | ALREADY REAL | Call it validation/simulation and never claim execution | +| Nested rewards, chance, choices, priority and conditions | Real reward structures exist, but complete visual editing is not implemented | IMPLEMENTABLE | Add one verified structure at a time without flattening YAML | +| Universal execution preview or guaranteed result | Simulation cannot promise external command/economy outcomes | MOCKUP-ONLY / INVALID | Show parsed actions and warnings only | + +## Updates & Deployments + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Uploaded artifact validation and per-node staging | `plugin.deploy.v1` validates JAR identity/SHA-256 and stages without restart | ALREADY REAL | Keep restart-required results explicit | +| Current installed versions | Node registration provides the running plugin version | ALREADY REAL | Display per target | +| Re-stage a retained verified artifact | Artifacts and deployment history are retained with bounds | IMPLEMENTABLE | Add selection only when retention still contains the artifact | +| Stable/development build discovery from bencodez.com | No verified server-defined source is implemented | FUTURE / UNSUPPORTED | First discover and document the real allowlisted source and metadata | +| Automatic restart/reload or arbitrary URL download | No safe capability exists | MOCKUP-ONLY / INVALID | Omit; never accept a browser-supplied download URL | + +## Configuration Drift & Sync + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Exact redacted file comparison and revisions | Current compare view reads selected capable nodes | ALREADY REAL | Keep bounded line comparison | +| VoteSites reward-safe source synchronization | `config.vote-sites-sync.v1` exists | ALREADY REAL | Keep explicit source and destination selection | +| Semantic property differences for typed fields | Typed General/Vote Sites/Rewards models already retain per-target values | IMPLEMENTABLE | Build property-level views from those models | +| Selective source-to-target property copy | Current typed preview APIs can support explicit edits | IMPLEMENTABLE | Generate ordinary per-target previews, never hidden writes | +| Copying secrets between nodes | Redacted values cannot safely be reconstructed | MOCKUP-ONLY / INVALID | Preserve each destination secret; require a separate future migration design | + +## Backups & Restore + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Named redacted configuration snapshots | Durable bounded snapshots exist | ALREADY REAL | Keep restore as proposed content requiring a fresh preview | +| Node-local rollback during apply | Connectors already create a local backup and roll back failed reloads | ALREADY REAL | Surface the result; do not present it as a downloadable backup | +| Multi-file configuration backup metadata/download | Current snapshots are operation-derived documents, not a full backup set | IMPLEMENTABLE | Extend with a typed bounded configuration-backup capability | +| SQLite/local user-data backup | No connector capability exists | FUTURE / UNSUPPORTED | Add a narrowly scoped application-owned export capability first | +| MySQL user-data backup/restore | No application-aware export/restore capability exists | FUTURE / UNSUPPORTED | Keep database credentials on the node; never add arbitrary SQL | +| Automated schedules and retention policies | No scheduler contract exists | FUTURE / UNSUPPORTED | Design after manual backups are safe | + +## Vote Analytics + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Bounded vote totals and top services/servers over a date range | VoteLog summaries expose retained logged events | ALREADY REAL | State the date range and logging/readability boundary | +| Exact player lookup and top voters | Exact player lookup exists; arbitrary player enumeration does not | FUTURE / UNSUPPORTED | Add only bounded indexed aggregate queries through a new inspection schema | +| Unique voters and trends | Derivable only when retained VoteLog data is complete enough | IMPLEMENTABLE | Add bounded server-side aggregates with explicit coverage labels | +| Backend attribution | Available only when the retained event contains a trustworthy server field | IMPLEMENTABLE | Display UNKNOWN when attribution is absent | +| Vote/reward failure rate and reward execution statistics | No authoritative persisted evidence exists | MOCKUP-ONLY / INVALID | Omit rather than infer from missing log rows | + +## Network Report + +| Mockup element | Reality | Classification | Action | +| --- | --- | --- | --- | +| Version, platform, capabilities and topology | Current registry exposes them | ALREADY REAL | Include selected nodes and evidence timestamps | +| Diagnostics, Doctor and operation summaries | Redacted bounded results exist | ALREADY REAL | Include only safe retained summaries | +| Configuration and Vote Site excerpts | Redacted reads exist for a clearly identified source | IMPLEMENTABLE | Add category selection and preserve per-node identity | +| Player privacy controls | Exact player inspection is separate and need not be included | IMPLEMENTABLE | Default to exclusion; redact name/UUID when explicitly included later | +| Human summary, JSON and ZIP bundle | Only the current diagnostics JSON download exists | FUTURE / UNSUPPORTED | Add a bounded report capability/store before advertising a support bundle | +| Raw logs, credentials, private keys or automatic upload | Prohibited by the Control trust boundary | MOCKUP-ONLY / INVALID | Never include or upload them | + +## Phased PR plan + +1. Improve the Full YAML workbench and continue splitting focused frontend modules. Add visible source/revision context, + line navigation/search, line numbers, and a reliable unsaved state without changing connector semantics. +2. Align Network Health and Setup Doctor navigation/layout with the approved cards and tables while retaining the current + observed/unknown evidence model. +3. Expand typed configuration in small groups sourced from current defaults and loaders. Prioritize reminders, Vote Party, + vote logging, proxy/storage, GUI, and Shop. Full YAML remains available for every managed file. +4. Expand the Rewards Designer one understood reward structure at a time and retain the no-side-effect simulator boundary. +5. Investigate the real bencodez.com source, then add allowlisted server-side build discovery and verified staging. +6. Add semantic Drift & Sync over typed models. +7. Add manual typed configuration/user-data backup capabilities before scheduling or database restore. +8. Add bounded analytics aggregates and a redacted network-report bundle. + +Native vote tracing/debugger work remains deferred. diff --git a/src/main/java/com/bencodez/votingplugin/control/http/ControlHttpServer.java b/src/main/java/com/bencodez/votingplugin/control/http/ControlHttpServer.java index a32050a..3780a3c 100644 --- a/src/main/java/com/bencodez/votingplugin/control/http/ControlHttpServer.java +++ b/src/main/java/com/bencodez/votingplugin/control/http/ControlHttpServer.java @@ -87,17 +87,18 @@ public final class ControlHttpServer implements AutoCloseable { private static final String AUTH_SETUP = "/api/v1/auth/setup"; private static final String ENROLLMENTS = "/api/v1/enrollments"; private static final String SESSION_COOKIE = "vpctl_session"; - private static final Map WEB_RESOURCES = Map.of( - "/", new WebResource("/web/index.html", "text/html; charset=utf-8"), - "/index.html", new WebResource("/web/index.html", "text/html; charset=utf-8"), - "/app.js", new WebResource("/web/app.js", "text/javascript; charset=utf-8"), - "/workspace.js", new WebResource("/web/workspace.js", "text/javascript; charset=utf-8"), - "/configuration-state.js", new WebResource("/web/configuration-state.js", "text/javascript; charset=utf-8"), - "/general-settings.js", new WebResource("/web/general-settings.js", "text/javascript; charset=utf-8"), - "/vote-sites-state.js", new WebResource("/web/vote-sites-state.js", "text/javascript; charset=utf-8"), - "/vote-sites.js", new WebResource("/web/vote-sites.js", "text/javascript; charset=utf-8"), - "/rewards.js", new WebResource("/web/rewards.js", "text/javascript; charset=utf-8"), - "/app.css", new WebResource("/web/app.css", "text/css; charset=utf-8")); + private static final Map WEB_RESOURCES = Map.ofEntries( + Map.entry("/", new WebResource("/web/index.html", "text/html; charset=utf-8")), + Map.entry("/index.html", new WebResource("/web/index.html", "text/html; charset=utf-8")), + Map.entry("/app.js", new WebResource("/web/app.js", "text/javascript; charset=utf-8")), + Map.entry("/workspace.js", new WebResource("/web/workspace.js", "text/javascript; charset=utf-8")), + Map.entry("/configuration-state.js", new WebResource("/web/configuration-state.js", "text/javascript; charset=utf-8")), + Map.entry("/yaml-editor.js", new WebResource("/web/yaml-editor.js", "text/javascript; charset=utf-8")), + Map.entry("/general-settings.js", new WebResource("/web/general-settings.js", "text/javascript; charset=utf-8")), + Map.entry("/vote-sites-state.js", new WebResource("/web/vote-sites-state.js", "text/javascript; charset=utf-8")), + Map.entry("/vote-sites.js", new WebResource("/web/vote-sites.js", "text/javascript; charset=utf-8")), + Map.entry("/rewards.js", new WebResource("/web/rewards.js", "text/javascript; charset=utf-8")), + Map.entry("/app.css", new WebResource("/web/app.css", "text/css; charset=utf-8"))); private static final int MAX_AUTH_FAILURES_PER_MINUTE = 100; private static final int MAX_PASSWORD_ATTEMPTS_PER_CLIENT = 8; private static final int MAX_PASSWORD_FAILURES_PER_CLIENT = 5; diff --git a/src/main/resources/web/app.css b/src/main/resources/web/app.css index 051f790..053460e 100644 --- a/src/main/resources/web/app.css +++ b/src/main/resources/web/app.css @@ -183,7 +183,13 @@ p { line-height: 1.55; } .editor-toolbar label { margin: 0; } .editor-toolbar select { margin: 0; } .editor-toolbar > span { justify-self: end; color: var(--muted); font-size: .78rem; } -.yaml-editor { display: block; width: 100%; min-height: 520px; margin: 0; padding: 20px 22px; border: 0; border-radius: 0; background: #050c16; color: #dbeaff; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: .88rem; line-height: 1.55; resize: vertical; tab-size: 2; } +.yaml-search-toolbar { display: grid; grid-template-columns: auto minmax(180px, 340px) auto auto 1fr auto; align-items: center; gap: 9px; padding: 10px 18px; border-bottom: 1px solid var(--border); background: #0b1726; } +.yaml-search-toolbar label { margin: 0; } +.yaml-search-toolbar input { margin: 0; } +.yaml-search-toolbar output { color: var(--muted); font-size: .78rem; } +.yaml-workbench { display: grid; grid-template-columns: auto minmax(0, 1fr); min-height: 520px; overflow: hidden; background: #050c16; } +.yaml-line-numbers { min-width: 54px; height: 520px; margin: 0; padding: 20px 12px; overflow: hidden; border-right: 1px solid var(--border); background: #08111d; color: #62748a; text-align: right; user-select: none; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: .88rem; line-height: 1.55; } +.yaml-editor { display: block; width: 100%; height: 520px; min-height: 520px; margin: 0; padding: 20px 22px; border: 0; border-radius: 0; background: #050c16; color: #dbeaff; font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace; font-size: .88rem; line-height: 1.55; resize: none; tab-size: 2; white-space: pre; overflow: auto; } .yaml-editor:focus { outline: 2px solid var(--blue); outline-offset: -2px; } .editor-footer { padding: 13px 18px; border-top: 1px solid var(--border); background: #091421; } .editor-footer > span { color: var(--muted); font-size: .8rem; } @@ -303,7 +309,11 @@ li span { color: var(--muted); font-size: .9rem; } .subtabs button { padding-inline: 8px; } .editor-toolbar { grid-template-columns: 1fr; } .editor-toolbar > span { justify-self: start; } - .yaml-editor { min-height: 420px; padding: 16px; } + .yaml-search-toolbar { grid-template-columns: 1fr 1fr; } + .yaml-search-toolbar label, .yaml-search-toolbar input, .yaml-search-toolbar output { grid-column: 1 / -1; } + .yaml-workbench { min-height: 420px; } + .yaml-line-numbers { height: 420px; min-width: 46px; padding: 16px 9px; } + .yaml-editor { height: 420px; min-height: 420px; padding: 16px; } .operation-actions button { width: 100%; } .result-item { align-items: stretch; flex-direction: column; } .attention-item { grid-template-columns: auto minmax(0, 1fr); } diff --git a/src/main/resources/web/app.js b/src/main/resources/web/app.js index a9518c1..e805e4b 100644 --- a/src/main/resources/web/app.js +++ b/src/main/resources/web/app.js @@ -79,6 +79,14 @@ const configurationForm = document.querySelector('#configuration-form'); const configurationFile = document.querySelector('#configuration-file'); const configurationContent = document.querySelector('#configuration-content'); const editorPosition = document.querySelector('#editor-position'); +const yamlEditorStatistics = document.querySelector('#yaml-editor-statistics'); +const yamlEditorSource = document.querySelector('#yaml-editor-source'); +const yamlEditorState = document.querySelector('#yaml-editor-state'); +const yamlEditorSearch = document.querySelector('#yaml-editor-search'); +const yamlSearchPrevious = document.querySelector('#yaml-search-previous'); +const yamlSearchNext = document.querySelector('#yaml-search-next'); +const yamlSearchCount = document.querySelector('#yaml-search-count'); +const yamlLineNumbers = document.querySelector('#yaml-line-numbers'); const readFileConfiguration = document.querySelector('#read-file-configuration'); const previewFileConfiguration = document.querySelector('#preview-file-configuration'); const applyFileConfiguration = document.querySelector('#apply-file-configuration'); @@ -308,6 +316,9 @@ let configurationDirty = false; let configurationDraftNodeId = ''; let configurationDraftSessionId = ''; let configurationDraftFileName = ''; +let configurationSourceRevision = ''; +let configurationSourceNodeId = ''; +let configurationSourceKind = ''; let routingDirty = false; let routingDraftNodeId = ''; let configurationFileSelection = configurationFile.value; @@ -373,6 +384,31 @@ function text(element, value) { return element; } +const yamlEditorView = window.ControlYamlEditor.create({ + textarea: configurationContent, + gutter: yamlLineNumbers, + position: editorPosition, + statistics: yamlEditorStatistics, + source: yamlEditorSource, + state: yamlEditorState, + search: yamlEditorSearch, + previous: yamlSearchPrevious, + next: yamlSearchNext, + matchCount: yamlSearchCount +}); + +function syncYamlEditorView() { + yamlEditorView.sync({ + loaded: configurationContentPresent, + dirty: configurationDirty, + draft: configurationSourceKind === 'SNAPSHOT', + busy: configurationContent.disabled, + nodeId: configurationContentPresent ? configurationSourceNodeId || selectedServerId : '', + fileName: configurationContentPresent ? configurationFile.value : '', + revision: configurationContentPresent ? configurationSourceRevision : '' + }); +} + const SETTINGS_SCHEMA = Object.freeze([ {key: 'AutoCreateVoteSites', file: 'Config.yml', type: 'boolean', defaultValue: 'true', effect: 'Create a VoteSites.yml entry when an unknown service votes.'}, {key: 'ProcessRewards', file: 'Config.yml', type: 'boolean', defaultValue: 'true', effect: 'Run configured vote rewards on this backend.'}, @@ -443,11 +479,11 @@ function cachedFile(key) { return value; } -function cacheFile(key, content, operationId) { +function cacheFile(key, content, operationId, revision = '') { if (typeof content !== 'string') return; pruneFileReadCache(); fileReadCache.delete(key); - fileReadCache.set(key, {content, operationId, loadedAt: Date.now()}); + fileReadCache.set(key, {content, operationId, revision, loadedAt: Date.now()}); pruneFileReadCache(); } @@ -1361,9 +1397,13 @@ function applyAuthenticatedSession(body) { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; routingDirty = false; routingDraftNodeId = ''; configurationFileSelection = configurationFile.value; + syncYamlEditorView(); autoLoadInFlight.clear(); autoLoadPending.clear(); inputGeneration++; @@ -1884,11 +1924,15 @@ function resetFileEditorForSelection(message) { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; lastFileReadOperation = null; approvedFilePreview = null; updateEditorPosition(); configurationContent.disabled = false; configurationContent.removeAttribute('aria-busy'); + syncYamlEditorView(); readFileConfiguration.hidden = true; text(fileOperationStatus, message); } @@ -3679,6 +3723,9 @@ function discardAuthenticationState(reason) { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; routingDirty = false; routingDraftNodeId = ''; autoLoadInFlight.clear(); @@ -3716,6 +3763,7 @@ function discardAuthenticationState(reason) { text(driftResults, 'Authenticate and choose two or more readable nodes.'); text(snapshotList, 'Authenticate to view manual snapshots.'); text(snapshotStatus, ''); + syncYamlEditorView(); renderOperationHistory(); nodes.replaceChildren(); nodes.classList.add('empty'); @@ -3854,6 +3902,10 @@ function invalidateConfigurationReads() { if (!configurationDirty) { configurationContent.value = ''; configurationContentPresent = false; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; + syncYamlEditorView(); text(fileOperationStatus, 'Configuration changed; read the current file before previewing changes.'); if (tabFromHash() === 'configurations') window.setTimeout(() => void autoLoadTab('configurations'), 0); } @@ -4433,8 +4485,12 @@ async function loadFileConfiguration(automatic = false) { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = cached.revision || ''; + configurationSourceNodeId = selectedServerId; + configurationSourceKind = 'READ'; lastFileReadOperation = {operationId: cached.operationId}; updateEditorPosition(); + syncYamlEditorView(); text(fileOperationStatus, `Cached read · ${selectedServerId} · ${selectedFile}\nLoaded instantly; cache expires after 30 seconds. Preview still checks the live revision.`); inputGeneration++; updateConfigurationButtons(); @@ -4444,8 +4500,12 @@ async function loadFileConfiguration(automatic = false) { } configurationContent.value = ''; configurationContentPresent = false; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; configurationContent.disabled = true; configurationContent.setAttribute('aria-busy', 'true'); + syncYamlEditorView(); readFileConfiguration.hidden = true; text(fileOperationStatus, `Loading ${selectedFile} from ${selectedReadNodeId}…`); try { @@ -4464,9 +4524,13 @@ async function loadFileConfiguration(automatic = false) { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = contentResult.revision || ''; + configurationSourceNodeId = selectedReadNodeId; + configurationSourceKind = 'READ'; lastFileReadOperation = {operationId: operation.operationId}; - cacheFile(cacheKey, contentResult.configuration.content, operation.operationId); + cacheFile(cacheKey, contentResult.configuration.content, operation.operationId, contentResult.revision || ''); updateEditorPosition(); + syncYamlEditorView(); text(fileOperationStatus, operationSummary(operation)); inputGeneration++; updateConfigurationButtons(); @@ -4483,6 +4547,10 @@ async function loadFileConfiguration(automatic = false) { && selectedNode.sessionId === nodeIndex.get(selectedReadNodeId)?.sessionId) { configurationContent.value = ''; configurationContentPresent = false; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; + syncYamlEditorView(); text(fileOperationStatus, `Could not load ${selectedFile}: ${error.message}`); readFileConfiguration.hidden = false; } @@ -4492,6 +4560,7 @@ async function loadFileConfiguration(automatic = false) { && selectedNode.sessionId === nodeIndex.get(selectedReadNodeId)?.sessionId) { configurationContent.disabled = false; configurationContent.removeAttribute('aria-busy'); + syncYamlEditorView(); updateConfigurationButtons(); } } @@ -4559,8 +4628,12 @@ applyFileConfiguration.addEventListener('click', async () => { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = ''; + configurationSourceNodeId = ''; + configurationSourceKind = ''; configurationContent.value = ''; configurationContentPresent = false; + syncYamlEditorView(); updateExtendedButtons(); await loadFileConfiguration(true); } else if (operation.state === 'SUCCEEDED') { @@ -5323,7 +5396,7 @@ runDriftCheck.addEventListener('click', async () => { lastFileReadOperation = comparable.length > 0 ? {operationId: operation.operationId} : null; comparable.forEach(row => { const session = nodeIndex.get(row.nodeId)?.sessionId || ''; - cacheFile(`${row.nodeId}|${session}|${selectedFile}`, row.content, operation.operationId); + cacheFile(`${row.nodeId}|${session}|${selectedFile}`, row.content, operation.operationId, row.revision || ''); }); const baseline = comparable[0]; const differences = comparable.filter(row => row !== baseline).map(row => { @@ -5391,8 +5464,12 @@ async function loadSnapshots() { configurationDraftNodeId = ''; configurationDraftSessionId = ''; configurationDraftFileName = ''; + configurationSourceRevision = document.revision || ''; + configurationSourceNodeId = document.nodeId; + configurationSourceKind = 'SNAPSHOT'; lastFileReadOperation = null; updateEditorPosition(); + syncYamlEditorView(); approvedFilePreview = null; inputGeneration++; setActiveTab('configurations', true); @@ -5755,6 +5832,7 @@ configurationContent.addEventListener('input', () => { configurationDirty = true; clearApprovals(); updateEditorPosition(); + syncYamlEditorView(); }); configurationContent.addEventListener('click', updateEditorPosition); configurationContent.addEventListener('keyup', updateEditorPosition); diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index a732ce6..0197976 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -352,12 +352,23 @@

Full VotingPlugin configuration

- YAML · 512 KiB maximum + No document loaded + + - +
+ + +