mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
feat(web): live planned-XML preview + reset stale form state on device switch
Two related fixes for the Plan-card → Customize-pane preview flow: 1. Live planned-XML preview. The Customize panel's "Planned Config (AfterTouch)" pane previously showed summary.planned_config — server-rendered, only updated on the next showSummary fetch. So editing a URL field in the Plan card had no visible effect on the preview until the user manually refreshed. The new renderPlannedXMLPreview composes the same XML client-side from plan-target-url + the four override inputs, mirroring exactly what migrateViaXML writes (target-derived defaults + applyURLOverrides), and is called from validatePlanURLs which already runs on every keystroke. 2. Per-device form-state isolation on speaker switch. The Plan card inputs preserve manual edits across summary refreshes (force=false) so a user's typed URL doesn't get clobbered by a re-fetch. That semantic is right within one device but wrong across devices: if the user edited a URL on speaker A and then picked speaker B in the dropdown, A's value silently appeared in B's preview. showSummary now compares the previous summary-device-id to the new one and, on change, calls resetPlanCardForDeviceSwitch to clear the four URL inputs, the Soundcork checkbox, the "saved" hint dataset, the URL-validation banner, and both apply-status lines. The downstream fillPlanURLInputs(defaults, force=false) then fills the now-empty inputs with the new device's canonical defaults. 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
967d516d4d
commit
af6fe78f3f
@@ -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 <pre> 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 = [
|
||||
'<?xml version="1.0" encoding="utf-8"?>',
|
||||
'<SoundTouchSdkPrivateCfg>',
|
||||
` <margeServerUrl>${escapeForXMLText(marge)}</margeServerUrl>`,
|
||||
` <statsServerUrl>${escapeForXMLText(stats)}</statsServerUrl>`,
|
||||
` <swUpdateUrl>${escapeForXMLText(swUpdate)}</swUpdateUrl>`,
|
||||
' <usePandoraProductionServer>true</usePandoraProductionServer>',
|
||||
' <isZeroconfEnabled>true</isZeroconfEnabled>',
|
||||
' <saveMargeCustomerReport>false</saveMargeCustomerReport>',
|
||||
` <bmxRegistryUrl>${escapeForXMLText(bmx)}</bmxRegistryUrl>`,
|
||||
'</SoundTouchSdkPrivateCfg>',
|
||||
].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, "<")
|
||||
.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).
|
||||
|
||||
Reference in New Issue
Block a user