From 1c53c1845a56d43e6f1e3a672f420a63602a4949 Mon Sep 17 00:00:00 2001 From: BenCodez <17074231+BenCodez@users.noreply.github.com> Date: Sun, 27 Sep 2026 21:57:57 -0600 Subject: [PATCH] Present evidence-backed Setup Doctor results --- docs/control-management.md | 2 +- docs/mockup-capability-map.md | 3 +- .../control/http/ControlHttpServer.java | 1 + src/main/resources/web/app.css | 21 +++ src/main/resources/web/app.js | 56 +++--- src/main/resources/web/index.html | 11 +- src/main/resources/web/network-doctor.js | 170 ++++++++++++++++++ .../control/http/ControlHttpServerTest.java | 10 +- src/test/web/network-doctor.test.cjs | 126 +++++++++++++ 9 files changed, 369 insertions(+), 31 deletions(-) create mode 100644 src/main/resources/web/network-doctor.js create mode 100644 src/test/web/network-doctor.test.cjs diff --git a/docs/control-management.md b/docs/control-management.md index af96135..3f77d09 100644 --- a/docs/control-management.md +++ b/docs/control-management.md @@ -69,7 +69,7 @@ authenticated, CSRF-protected endpoint and node capability checks as an external | --- | --- | --- | | Navigation and context | Keeps the selected VotingPlugin node visible in the persistent header, groups existing tools in a desktop sidebar/mobile drawer, and provides bounded search shortcuts for pages, settings, and registered nodes | Search does not enumerate players; shortcut pages reuse the existing capability-gated panels; server changes keep the normal stale-result and unsaved-draft protections | | Overview dashboard | Builds six summary cards, Attention Required, quick actions, topology, logged-service activity, and recent operation activity from the node registry plus existing `overview`, `vote-site-health`, and `vote-log-summary` inspections | Read-only aggregation; failed, incomplete, or malformed sub-inspections produce a warning rather than a Healthy claim; disconnected registered nodes produce a warning; unknown Minecraft presence is not treated as offline; VoteLog counts are labeled as logged events; actions only open existing safe workflows rather than inventing auto-fixes | -| Network Doctor | Runs `diagnostics` (which includes the overview fields), combines node health with Control's current topology, and displays checks for connector, configuration, Votifier, vote sites, rewards, logging, and proxy topology | Read-only; “healthy” is bounded reported state, not a synthetic vote | +| Setup Doctor | Runs `diagnostics` (which includes the overview fields), validates that response, combines node health with Control's current topology, and displays PASS/WARNING/FAIL/UNKNOWN checks with their evidence source for connector, configuration, Votifier, vote sites, rewards, logging, and proxy topology | Read-only; “healthy” is bounded reported state, not a synthetic vote, website probe, port test, or general database-health claim | | Diagnostics download | Downloads the last Network Doctor result as local JSON | Redacted status bundle only; no raw configuration/logs/player records/infrastructure secrets | | Activity | Loads the newest 50 live/recovered configuration operation views, labels phases, lineage, reload/rollback, resumes eligible guided preview approvals, and offers retry only when `retryable` | Recovered configuration history cannot be retried; approval is single-use and apply is CSRF-protected; proxy-method apply needs a new preview | | 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 | diff --git a/docs/mockup-capability-map.md b/docs/mockup-capability-map.md index a0ec5b7..9489b2d 100644 --- a/docs/mockup-capability-map.md +++ b/docs/mockup-capability-map.md @@ -119,7 +119,8 @@ revision-aware `READ -> PREVIEW -> APPROVE -> APPLY -> CONFIRMED READ` workflow. 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 +2. **Implemented:** align Network Health and Setup Doctor navigation/layout with the approved cards and tables. The + Doctor renders PASS/WARNING/FAIL/UNKNOWN checks with evidence sources and safe navigation 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. 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 3780a3c..d0a7b75 100644 --- a/src/main/java/com/bencodez/votingplugin/control/http/ControlHttpServer.java +++ b/src/main/java/com/bencodez/votingplugin/control/http/ControlHttpServer.java @@ -92,6 +92,7 @@ public final class ControlHttpServer implements AutoCloseable { 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("/network-doctor.js", new WebResource("/web/network-doctor.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")), diff --git a/src/main/resources/web/app.css b/src/main/resources/web/app.css index 053460e..2949144 100644 --- a/src/main/resources/web/app.css +++ b/src/main/resources/web/app.css @@ -123,6 +123,23 @@ p { line-height: 1.55; } .pill.warning { border-color: #785927; background: var(--amber-soft); color: var(--amber); } .pill.neutral { border-color: var(--border-strong); background: #111e30; color: var(--muted); } +.doctor-summary { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; margin-bottom: 18px; } +.doctor-stat { display: flex; min-height: 82px; flex-direction: column; justify-content: center; padding: 14px 16px; border: 1px solid var(--border); border-radius: 11px; background: #091421; } +.doctor-stat strong { font-size: 1.5rem; } +.doctor-stat span { color: var(--muted); font-size: .72rem; font-weight: 800; letter-spacing: .08em; } +.doctor-stat.doctor-pass, .pill.doctor-pass { border-color: #2b7750; background: var(--green-soft); color: var(--green); } +.doctor-stat.doctor-warning, .pill.doctor-warning { border-color: #785927; background: var(--amber-soft); color: var(--amber); } +.doctor-stat.doctor-fail, .pill.doctor-fail { border-color: #82445a; background: #341521; color: var(--danger); } +.doctor-stat.doctor-unknown, .pill.doctor-unknown { border-color: var(--border-strong); background: #111e30; color: var(--muted); } +.doctor-groups { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; } +.doctor-group { min-width: 0; overflow: hidden; border: 1px solid var(--border); border-radius: 11px; background: #091421; } +.doctor-group h4 { padding: 13px 15px; border-bottom: 1px solid var(--border); } +.doctor-check { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; gap: 11px; align-items: start; padding: 14px 15px; border-bottom: 1px solid var(--border); } +.doctor-check:last-child { border-bottom: 0; } +.doctor-check p { margin: 4px 0; color: #d6e5f7; font-size: .84rem; line-height: 1.45; } +.doctor-check small { color: var(--muted); } +.doctor-check button { align-self: center; } + .topology { display: grid; gap: 11px; } .topology.empty, .nodes.empty { display: block; padding: 30px; border: 1px dashed var(--border-strong); border-radius: 11px; color: var(--muted); text-align: center; } .topology-row { display: grid; grid-template-columns: minmax(170px, .72fr) minmax(260px, 1.5fr); gap: 12px; align-items: stretch; } @@ -305,6 +322,9 @@ li span { color: var(--muted); font-size: .9rem; } .page-heading, .section-title, .auth-row, .editor-footer { align-items: stretch; flex-direction: column; } .page-heading > button, .auth-row button { width: 100%; } .metrics, .settings-grid, .feature-grid, .checklist { grid-template-columns: 1fr; } + .doctor-summary, .doctor-groups { grid-template-columns: 1fr 1fr; } + .doctor-check { grid-template-columns: auto minmax(0, 1fr); } + .doctor-check button { grid-column: 2; width: 100%; } .metrics article { min-height: 92px; } .subtabs button { padding-inline: 8px; } .editor-toolbar { grid-template-columns: 1fr; } @@ -324,6 +344,7 @@ li span { color: var(--muted); font-size: .9rem; } .topbar { gap: 8px; } .brand { display: none; } .server-picker select { width: min(50vw, 180px); } + .doctor-summary, .doctor-groups { grid-template-columns: 1fr; } } @media (prefers-reduced-motion: reduce) { diff --git a/src/main/resources/web/app.js b/src/main/resources/web/app.js index e805e4b..ed1d77a 100644 --- a/src/main/resources/web/app.js +++ b/src/main/resources/web/app.js @@ -1780,8 +1780,8 @@ function renderScopeOverview() { const aggregate = isWorkspaceOverview(); const source = nodeIndex.get(selectedServerId); text(document.querySelector('#overview-title'), aggregate ? workspace.managementScope === 'GLOBAL' - ? 'Network Overview' : `${workspace.selectedTargetIds.size} Servers — Workspace Overview` - : `${source?.displayName || 'Selected server'} — Server Overview`); + ? 'Network Health' : `${workspace.selectedTargetIds.size} Servers — Health Overview` + : `${source?.displayName || 'Selected server'} — Server Health`); text(document.querySelector('#overview-eyebrow'), aggregate ? 'Management workspace' : 'Individual server inspection'); document.querySelector('#individual-overview').hidden = aggregate; voteActivity.closest('section').hidden = aggregate; @@ -5307,9 +5307,9 @@ async function refreshDashboard() { refreshSetupChecklist.addEventListener('click', async () => { try { const envelope = await runInspection('diagnostics', {}, setupChecklistStatus); - lastOverview = envelope.result; + lastOverview = normalizeDashboardOverview(envelope.result).result; invalidateDashboardInspection(); - updateSetupChecklist(envelope.result); + updateSetupChecklist(lastOverview); } catch (error) { text(setupChecklistStatus, error.message); } }); refreshDataOverview.addEventListener('click', () => refreshOverview(dataOverview)); @@ -5319,34 +5319,50 @@ runNetworkDoctor.addEventListener('click', async () => { lastDiagnostics = null; try { const diagnostics = await runInspection('diagnostics', {}, networkDoctorResults); - lastOverview = diagnostics.result; + const normalizedDiagnostics = normalizeDashboardOverview(diagnostics.result); + lastOverview = normalizedDiagnostics.result; invalidateDashboardInspection(); const node = nodeIndex.get(selectedServerId); - const voteLog = diagnostics.result.voteLoggingEnabled !== true - ? {state: 'DISABLED', message: 'Vote logging is disabled; no retained logged-event history is expected.'} - : diagnostics.result.voteLogReadable === true + const report = ControlNetworkDoctor.buildReport({diagnostics: normalizedDiagnostics.result, node, + incomplete: normalizedDiagnostics.incomplete, + topologyComplete: !backendTopologyTruncated, + proxyReports: isBackend(node) ? proxyReportsFor(node.nodeId).length : 0}); + const voteLogStatus = ControlNetworkDoctor.statusFor(report, 'VoteLog readability'); + const voteLog = voteLogStatus === 'PASS' ? {state: 'READABLE', message: 'Retained logged-event history is readable. It is not a guaranteed record of every internal vote-delivery hop.'} - : {state: 'UNREADABLE', message: 'Vote logging is enabled, but retained logged-event history is not currently readable.'}; - const configuredVoteSites = finiteCount(diagnostics.result.configuredVoteSites); + : voteLogStatus === 'FAIL' + ? {state: 'UNREADABLE', message: 'Vote logging is enabled and available, but retained logged-event history is not readable.'} + : voteLogStatus === 'WARNING' + ? {state: 'UNAVAILABLE', message: 'Vote logging is enabled, but its runtime adapter is unavailable. A backend restart may be required.'} + : {state: 'UNKNOWN', message: normalizedDiagnostics.result.voteLoggingEnabled === false + ? 'Vote logging is disabled; no retained logged-event history is expected.' + : 'VoteLog state could not be classified from the available evidence.'}; const checks = { - controlConnected: Boolean(node?.online), - configurationHealthy: diagnostics.result.configurationHealthy, - votifierDetected: diagnostics.result.votifierDetected, - voteSitesConfigured: configuredVoteSites == null ? null : configuredVoteSites > 0, - voteSitesConfiguredKnown: configuredVoteSites != null, - processRewards: diagnostics.result.processRewards, + controlConnected: ControlNetworkDoctor.legacyBoolean(report, 'Control connector'), + configurationHealthy: ControlNetworkDoctor.legacyBoolean(report, 'Configuration state'), + votifierDetected: ControlNetworkDoctor.legacyBoolean(report, 'Votifier detected'), + voteSitesConfigured: ControlNetworkDoctor.statusFor(report, 'Vote Sites') === 'UNKNOWN' + ? null : normalizedDiagnostics.result.configuredVoteSites > 0, + voteSitesConfiguredKnown: ControlNetworkDoctor.statusFor(report, 'Vote Sites') !== 'UNKNOWN', + processRewards: ControlNetworkDoctor.legacyBoolean(report, 'Reward processing'), voteLogging: voteLog, - topologyReported: isBackend(node) ? proxyReportsFor(node.nodeId).length > 0 || !diagnostics.result.proxyMode : true + topologyReported: ControlNetworkDoctor.legacyBoolean(report, 'Proxy topology') }; lastDiagnostics = { schemaVersion: 1, generatedAt: new Date().toISOString(), selectedNodeId: selectedServerId, - checks, voteLog, node: diagnostics.result, + checks, voteLog, doctor: {counts: report.counts, checks: report.checks}, node: diagnostics.result, control: {application: 'VotingPlugin Control', registeredNodes: allNodeItems.length, nodes: allNodeItems.slice(0, 100).map(item => ({nodeId: item.nodeId, displayName: item.displayName, role: roleLabel(item), online: item.online, pluginVersion: item.pluginVersion}))} }; - renderJsonResult(networkDoctorResults, lastDiagnostics); - updateSetupChecklist(diagnostics.result); + ControlNetworkDoctor.render(networkDoctorResults, report, action => { + if (action.serverScoped) { + if (!node || !changeWorkspaceTargets(() => workspace.setTargets([node.nodeId]))) return; + workspace.inspect(node.nodeId); + } + openWorkspace(action.tab, action.scrollTarget || ''); + }); + updateSetupChecklist(normalizedDiagnostics.result); downloadNetworkDiagnostics.disabled = false; } catch (error) { text(networkDoctorResults, error.message); } }); diff --git a/src/main/resources/web/index.html b/src/main/resources/web/index.html index 0197976..2da257b 100644 --- a/src/main/resources/web/index.html +++ b/src/main/resources/web/index.html @@ -130,7 +130,7 @@

Sign in to your network

Workspace overview

-

Server Overview

+

Network Health

@@ -221,16 +221,16 @@

Servers