diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index 1262705..9d72fa3 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -1730,9 +1730,22 @@ async function showSummary(deviceId) { const ip = summary.ip_address || deviceId; const finalDisplay = summary.device_name ? `${summary.device_name} (${ip})` : ip; document.getElementById("summary-device-display").innerText = finalDisplay; + + // Detect a device switch BEFORE we clobber the hidden id input. + // When the user moves between speakers in the dropdown, any + // per-device form state (plan-card URL edits, the soundcork + // checkbox, the "saved" hint) belongs to the previous device + // and would otherwise leak into the new device's preview. + const prevDeviceId = document.getElementById("summary-device-id").value; + const deviceChanged = prevDeviceId && prevDeviceId !== deviceId; + // Keep deviceId hidden for subsequent calls document.getElementById("summary-device-id").value = deviceId; + if (deviceChanged) { + resetPlanCardForDeviceSwitch(); + } + // Update table row if it exists const rowId = "device-row-" + deviceId; const row = document.getElementById(rowId); @@ -2590,6 +2603,11 @@ function validatePlanURLs() { applyBtn.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 + // round-trip. + renderPlannedXMLPreview(); + return errors.length === 0; } @@ -2603,6 +2621,46 @@ function resetPlanURLsToDefaults() { fillPlanURLInputs(defaultServiceURLs(targetUrl, {soundcorkMode: soundcork}), {force: true}); } +// resetPlanCardForDeviceSwitch clears every per-device form state on +// the Plan card so the previously-edited values don't leak into the +// new device's preview. Called from showSummary the moment the user +// picks a different speaker in the dropdown. +// +// Doesn't refill defaults itself — that happens further down in +// showSummary via fillPlanURLInputs(defaults), which writes only into +// empty inputs. +function resetPlanCardForDeviceSwitch() { + const inputs = ["plan-marge-url", "plan-stats-url", "plan-sw_update-url", "plan-bmx-url"]; + for (const id of inputs) { + const el = document.getElementById(id); + if (el) { + el.value = ""; + el.style.borderColor = ""; + } + } + + const soundcork = document.getElementById("plan-soundcork-mode"); + if (soundcork) soundcork.checked = false; + + const saved = document.getElementById("plan-target-saved"); + if (saved) { + saved.innerText = ""; + delete saved.dataset.savedValue; + } + + const validationBox = document.getElementById("plan-url-validation"); + if (validationBox) { + validationBox.style.display = "none"; + validationBox.replaceChildren(); + } + + const applyStatus = document.getElementById("plan-apply-status"); + if (applyStatus) applyStatus.innerText = ""; + + const customizeStatus = document.getElementById("customize-apply-status"); + if (customizeStatus) customizeStatus.innerText = ""; +} + // toggleSoundcorkMode reapplies defaults so the /marge suffix appears // or disappears on margeServerUrl. Manual edits are intentionally // reset — the checkbox is a deliberate "give me the canonical @@ -2657,6 +2715,55 @@ function computeSuggestedPlan(summary) { }; } +// renderPlannedXMLPreview rewrites the #planned-config
 from the
+// current Plan card inputs so the preview tracks the user's edits live
+// — without a backend round-trip. The output mirrors what
+// migrateViaXML actually writes: target-derived defaults, with each
+// per-field override (marge_url / stats_url / sw_update_url /
+// bmx_url) substituted in. The three boolean fields are constants
+// matching the Go-side PrivateCfg{} the migration constructs.
+//
+// Pure client-side once GetMigrationSummary has populated everything,
+// so the preview updates on every keystroke and stays in sync with
+// validatePlanURLs's input border / Apply-button state.
+function renderPlannedXMLPreview() {
+    const targetUrl = (document.getElementById("plan-target-url") || {}).value || "";
+    const overrides = readPlanURLOptions();
+    const defaults = defaultServiceURLs(targetUrl);
+
+    const marge    = overrides.marge_url     || defaults.marge;
+    const stats    = overrides.stats_url     || defaults.stats;
+    const swUpdate = overrides.sw_update_url || defaults.sw_update;
+    const bmx      = overrides.bmx_url       || defaults.bmx;
+
+    const xml = [
+        '',
+        '',
+        `  ${escapeForXMLText(marge)}`,
+        `  ${escapeForXMLText(stats)}`,
+        `  ${escapeForXMLText(swUpdate)}`,
+        '  true',
+        '  true',
+        '  false',
+        `  ${escapeForXMLText(bmx)}`,
+        '',
+    ].join("\n");
+
+    const el = document.getElementById("planned-config");
+    if (el) el.innerText = xml;
+}
+
+// escapeForXMLText escapes the characters that have special meaning
+// inside XML text content. URLs typically only need & escaping, but
+// covering < and > too keeps the preview honest if the user pastes
+// something exotic.
+function escapeForXMLText(s) {
+    return String(s)
+        .replace(/&/g, "&")
+        .replace(//g, ">");
+}
+
 // renderPlanCurrentURLs populates the "Current on Device" cells in the
 // Service URLs table from whichever transport answered (telnet
 // getpdo, falling back to the SSH-read XML config).