mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
feat(web): standalone Pre-flight button beside each Apply
"Test first, decide later" affordance: the same check sequence Apply
runs is now reachable without committing to the migration. Useful
for spot-checking a speaker after editing URLs, or for verifying a
fresh device is reachable before the user commits to writing
anything.
Two buttons, one per Apply path:
- #plan-preflight-btn (Suggested Plan side) — reads the chosen
method from plan-apply-btn.dataset.method, same source the
real Apply uses, so what's tested matches what would be
applied.
- #customize-preflight-btn (Custom Plan side) — walks the same
radio choices applyCustomPlan reads and builds the same
methods array, then runs the checks against it.
Both share the existing pre-flight panel and runApplyPreflight
orchestrator. New renderPreflightPreviewSummary terminates the
panel with a single Close button instead of Proceed Anyway /
Cancel — there's nothing to proceed to in preview mode.
Both Pre-flight buttons share the disabled-state gate of their
Apply counterparts (no plan / invalid URLs disables both) so users
can't accidentally pre-flight a plan that wouldn't apply.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
9ad159d41d
commit
a08c2c3072
@@ -881,6 +881,13 @@
|
||||
>
|
||||
<div id="plan-suggestion-summary" style="font-weight: bold; margin-bottom: 4px"></div>
|
||||
<ul id="plan-suggestion-steps" style="margin: 4px 0 8px 1.2em; padding: 0; font-size: 0.9em"></ul>
|
||||
<button
|
||||
type="button"
|
||||
id="plan-preflight-btn"
|
||||
onclick="preflightSuggestedPlan()"
|
||||
title="Run the same pre-flight checks Apply runs, without proceeding to migrate"
|
||||
style="font-size: 0.95em; margin-right: 6px"
|
||||
>Pre-flight</button>
|
||||
<button
|
||||
type="button"
|
||||
id="plan-apply-btn"
|
||||
@@ -957,6 +964,13 @@
|
||||
></div>
|
||||
|
||||
<div style="margin-top: 12px">
|
||||
<button
|
||||
type="button"
|
||||
id="customize-preflight-btn"
|
||||
onclick="preflightCustomPlan()"
|
||||
title="Run the same pre-flight checks Apply runs, without proceeding to migrate"
|
||||
style="padding: 8px 14px; font-size: 0.95em; margin-right: 6px"
|
||||
>Pre-flight</button>
|
||||
<button
|
||||
type="button"
|
||||
id="customize-apply-btn"
|
||||
|
||||
@@ -2616,12 +2616,14 @@ function validatePlanURLs() {
|
||||
|
||||
// Apply Suggested Plan is gated on URL validity (in addition to its
|
||||
// existing data-method check). The dataset.method field is set by
|
||||
// renderPlan based on what computeSuggestedPlan returned.
|
||||
// renderPlan based on what computeSuggestedPlan returned. The
|
||||
// Pre-flight sibling shares the same gate so the two buttons
|
||||
// enable/disable in lockstep.
|
||||
const applyBtn = document.getElementById("plan-apply-btn");
|
||||
if (applyBtn) {
|
||||
const noPlan = !applyBtn.dataset.method;
|
||||
applyBtn.disabled = noPlan || errors.length > 0;
|
||||
}
|
||||
const preflightBtn = document.getElementById("plan-preflight-btn");
|
||||
const noPlan = !applyBtn || !applyBtn.dataset.method;
|
||||
if (applyBtn) applyBtn.disabled = noPlan || errors.length > 0;
|
||||
if (preflightBtn) preflightBtn.disabled = noPlan || errors.length > 0;
|
||||
|
||||
// Refresh the client-side XML preview so the Customize panel's
|
||||
// Planned Config pane tracks every keystroke without a backend
|
||||
@@ -3232,6 +3234,100 @@ async function runPreflightCheck(deviceId, methods, opts, targetUrl) {
|
||||
return {ok: issues.length === 0, issues, summary};
|
||||
}
|
||||
|
||||
// previewPreflight runs the same check sequence as the Apply paths
|
||||
// but stops at the results — no migrate / trust-ca / pair-account
|
||||
// call happens. The panel stays open with a Close button so the
|
||||
// user can inspect the outcome ("test first, decide later").
|
||||
//
|
||||
// Called by preflightSuggestedPlan and preflightCustomPlan, which
|
||||
// share UI plumbing with their Apply counterparts.
|
||||
async function previewPreflight(deviceId, methods, opts, targetUrl) {
|
||||
const {results} = await runApplyPreflight(deviceId, methods, opts, targetUrl);
|
||||
renderPreflightPreviewSummary(results);
|
||||
}
|
||||
|
||||
// renderPreflightPreviewSummary mirrors awaitPreflightDecision's
|
||||
// rendering shape (summary line + actions row) but with a single
|
||||
// Close button instead of Proceed Anyway / Cancel — there's nothing
|
||||
// to proceed to in preview mode.
|
||||
function renderPreflightPreviewSummary(results) {
|
||||
const failed = results.filter(r => r.status === "fail").length;
|
||||
const passed = results.filter(r => r.status === "ok").length;
|
||||
const skipped = results.filter(r => r.status === "skip").length;
|
||||
|
||||
const summary = document.getElementById("apply-preflight-summary");
|
||||
const actions = document.getElementById("apply-preflight-actions");
|
||||
if (!summary || !actions) return;
|
||||
|
||||
if (failed === 0) {
|
||||
summary.innerText = `✅ Pre-flight passed — ${passed} of ${results.length} checks`
|
||||
+ (skipped > 0 ? ` (${skipped} skipped)` : "");
|
||||
summary.style.color = "green";
|
||||
} else {
|
||||
summary.innerText = `❌ Pre-flight: ${failed} of ${results.length} checks failed`;
|
||||
summary.style.color = "red";
|
||||
}
|
||||
|
||||
actions.replaceChildren();
|
||||
const close = document.createElement("button");
|
||||
close.type = "button";
|
||||
close.innerText = "Close";
|
||||
close.style.cssText = "padding: 8px 14px";
|
||||
close.onclick = () => hidePreflightPanel();
|
||||
actions.appendChild(close);
|
||||
}
|
||||
|
||||
// preflightSuggestedPlan runs the Suggested Plan's checks without
|
||||
// applying. Reads the chosen method off plan-apply-btn.dataset.method
|
||||
// (set by renderPlan from computeSuggestedPlan), same source the
|
||||
// real Apply uses, so what's tested matches what would be applied.
|
||||
async function preflightSuggestedPlan() {
|
||||
const applyBtn = document.getElementById("plan-apply-btn");
|
||||
const method = applyBtn && applyBtn.dataset.method;
|
||||
if (!method) return;
|
||||
|
||||
const deviceId = document.getElementById("summary-device-id").value;
|
||||
if (!deviceId) return;
|
||||
|
||||
if (!validatePlanURLs()) return;
|
||||
|
||||
const targetUrl = document.getElementById("plan-target-url").value;
|
||||
const opts = readPlanURLOptions();
|
||||
await previewPreflight(deviceId, [method], opts, targetUrl);
|
||||
}
|
||||
|
||||
// preflightCustomPlan walks the same radio choices applyCustomPlan
|
||||
// reads and queues the same methods array, then runs the checks
|
||||
// against it. Kept structurally close to applyCustomPlan so the two
|
||||
// stay in sync as new axes are added.
|
||||
async function preflightCustomPlan() {
|
||||
const form = document.getElementById("customize-form");
|
||||
const deviceId = form && form.dataset.deviceId;
|
||||
if (!deviceId) return;
|
||||
|
||||
if (!validatePlanURLs()) return;
|
||||
|
||||
const flip = (document.querySelector('input[name="customize-url-flip"]:checked') || {}).value || "none";
|
||||
const dns = (document.querySelector('input[name="customize-dns"]:checked') || {}).value || "none";
|
||||
const caInstall = !!(document.getElementById("customize-ca-install") || {}).checked;
|
||||
const pair = readPlanPairTarget();
|
||||
|
||||
const methods = [];
|
||||
if (flip === "xml" || flip === "telnet") methods.push(flip);
|
||||
if (dns === "resolv") methods.push("resolv");
|
||||
if (caInstall && dns !== "resolv") methods.push("trust-ca");
|
||||
if (pair && pair.valid) methods.push("pair-account");
|
||||
|
||||
if (methods.length === 0) {
|
||||
// Pre-flight with nothing queued is still useful — it shows
|
||||
// transport reachability. Run the summary check at least.
|
||||
}
|
||||
|
||||
const targetUrl = document.getElementById("plan-target-url").value;
|
||||
const opts = readPlanURLOptions();
|
||||
await previewPreflight(deviceId, methods, opts, targetUrl);
|
||||
}
|
||||
|
||||
// applySuggestedPlan triggers the recipe computeSuggestedPlan picked.
|
||||
// Passes the chosen method directly to migrate() — the legacy
|
||||
// migration-method dropdown is gone.
|
||||
@@ -3699,6 +3795,8 @@ function onCustomizeChange() {
|
||||
}
|
||||
}
|
||||
if (applyBtn) applyBtn.disabled = errors.length > 0;
|
||||
const preflightCustomBtn = document.getElementById("customize-preflight-btn");
|
||||
if (preflightCustomBtn) preflightCustomBtn.disabled = errors.length > 0;
|
||||
}
|
||||
|
||||
// applyCustomPlan runs the chosen sequence of backend operations:
|
||||
|
||||
Reference in New Issue
Block a user