mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 14:47:23 +00:00
feat(web): three-axis state card at top of migration summary
The migration summary now opens with a dedicated state panel that
surfaces, in three tight blocks:
- Transports — SSH and Telnet:17000 reachability, telnet banner if
any, and a probe-error sub-line when a TCP dial succeeded but the
shell rejected getpdo.
- Migration State — three rows for the orthogonal axes: URL
Configuration (verdict from xml_migrated/telnet_migrated, with the
four URL fields shown as on-disk vs live pairs underneath), DNS
Interception (resolv hook / hosts redirects / none), and CA / TLS
(local root CA installed yes/no).
- Preconditions — remote_services persistence, account-pairing
state (from is_paired / live margeAccountUUID), and the XML
.original backup presence.
Pure UI restructuring of data the backend already exposes. The
existing dropdown, method-specific panes, diff view, and per-field
service-options table are untouched so step 2 (the wizard refactor)
can replace them in a focused diff. The legacy SSH/Telnet status
paragraphs and the cross-check warnings banner stay below the card
during the transition; the next iteration removes them once the card
is the canonical surface.
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
bcd0970e35
commit
9160d803da
@@ -510,8 +510,71 @@
|
||||
Migration Summary for
|
||||
<span id="summary-device-display"></span>
|
||||
</h3>
|
||||
<p>Migration Status: <span id="migration-status"></span></p>
|
||||
<input type="hidden" id="summary-device-id"/>
|
||||
<p>Migration Status: <span id="migration-status"></span></p>
|
||||
|
||||
<div
|
||||
id="migration-state-card"
|
||||
style="margin: 10px 0 16px 0; padding: 12px; border: 1px solid #ddd; background: #fafafa; border-radius: 4px"
|
||||
>
|
||||
<div style="margin-bottom: 12px">
|
||||
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Transports</h4>
|
||||
<div style="display: flex; gap: 24px; flex-wrap: wrap; padding-left: 4px">
|
||||
<div>
|
||||
<strong>SSH:</strong> <span id="state-ssh"></span>
|
||||
</div>
|
||||
<div>
|
||||
<strong>Telnet (Port 17000):</strong> <span id="state-telnet"></span>
|
||||
<span id="state-telnet-banner" style="font-size: 0.85em; color: #666; margin-left: 4px"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="state-telnet-error"
|
||||
style="display: none; margin-top: 6px; padding: 4px 8px; background: #fff3e0; border-left: 3px solid #ef6c00; font-size: 0.85em; color: #5d4037"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 12px">
|
||||
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Migration State</h4>
|
||||
<table style="width: 100%; border-collapse: collapse">
|
||||
<tbody>
|
||||
<tr style="border-top: 1px solid #eee">
|
||||
<td style="padding: 6px 8px; width: 170px; vertical-align: top; color: #555">URL Configuration</td>
|
||||
<td id="state-url" style="padding: 6px 8px; vertical-align: top"></td>
|
||||
</tr>
|
||||
<tr style="border-top: 1px solid #eee">
|
||||
<td style="padding: 6px 8px; width: 170px; vertical-align: top; color: #555">DNS Interception</td>
|
||||
<td id="state-dns" style="padding: 6px 8px; vertical-align: top"></td>
|
||||
</tr>
|
||||
<tr style="border-top: 1px solid #eee">
|
||||
<td style="padding: 6px 8px; width: 170px; vertical-align: top; color: #555">CA / TLS</td>
|
||||
<td id="state-ca" style="padding: 6px 8px; vertical-align: top"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 style="margin: 0 0 6px 0; font-size: 0.95em">Preconditions</h4>
|
||||
<table style="width: 100%; border-collapse: collapse">
|
||||
<tbody>
|
||||
<tr style="border-top: 1px solid #eee">
|
||||
<td style="padding: 4px 8px; width: 170px; color: #555">remote_services</td>
|
||||
<td id="state-remote-services-cell" style="padding: 4px 8px"></td>
|
||||
</tr>
|
||||
<tr style="border-top: 1px solid #eee">
|
||||
<td style="padding: 4px 8px; color: #555">Account paired</td>
|
||||
<td id="state-paired" style="padding: 4px 8px"></td>
|
||||
</tr>
|
||||
<tr style="border-top: 1px solid #eee">
|
||||
<td style="padding: 4px 8px; color: #555">XML config backup</td>
|
||||
<td id="state-backup" style="padding: 4px 8px"></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p>SSH Connection: <span id="ssh-status"></span></p>
|
||||
<p>Telnet (Port 17000): <span id="telnet-status"></span>
|
||||
<span id="telnet-banner" style="font-size: 0.85em; color: #666; margin-left: 6px"></span>
|
||||
|
||||
@@ -1761,6 +1761,7 @@ async function showSummary(deviceId) {
|
||||
document.getElementById("ssh-status").innerText = summary.ssh_success ? "✅ Success" : "❌ Failed";
|
||||
document.getElementById("ssh-status").style.color = summary.ssh_success ? "green" : "red";
|
||||
|
||||
renderMigrationState(summary);
|
||||
renderTelnetPreflight(summary);
|
||||
renderPreflightWarnings(summary);
|
||||
fillTelnetURLInputs(defaultTelnetURLs(targetUrl));
|
||||
@@ -2445,6 +2446,175 @@ function readTelnetURLOptions() {
|
||||
return out;
|
||||
}
|
||||
|
||||
// renderMigrationState fills the three-axis state card at the top of
|
||||
// the migration summary: transports, migration-state axes (URL config,
|
||||
// DNS interception, CA/TLS), and preconditions (remote_services,
|
||||
// pairing, backup). Reads only fields the backend already exposes —
|
||||
// is_migrated remains the OR of the per-axis booleans.
|
||||
function renderMigrationState(summary) {
|
||||
// --- Transports ---
|
||||
setStateChip("state-ssh", summary.ssh_success, "Reachable", "Unreachable");
|
||||
setStateChip("state-telnet", summary.telnet_reachable, "Reachable", "Unreachable");
|
||||
|
||||
const bannerEl = document.getElementById("state-telnet-banner");
|
||||
if (bannerEl) bannerEl.innerText = summary.telnet_banner ? `(${summary.telnet_banner})` : "";
|
||||
|
||||
const errorEl = document.getElementById("state-telnet-error");
|
||||
if (errorEl) {
|
||||
if (summary.telnet_probe_error && !summary.telnet_reachable) {
|
||||
errorEl.innerText = "Probe error: " + summary.telnet_probe_error;
|
||||
errorEl.style.display = "block";
|
||||
} else {
|
||||
errorEl.style.display = "none";
|
||||
}
|
||||
}
|
||||
|
||||
// --- URL Configuration axis ---
|
||||
const urlCell = document.getElementById("state-url");
|
||||
if (urlCell) {
|
||||
urlCell.replaceChildren();
|
||||
const verdict = urlConfigVerdict(summary);
|
||||
urlCell.appendChild(stateLine(verdict.icon, verdict.text, verdict.note));
|
||||
|
||||
const live = parseTelnetVerifiedConfig(summary.telnet_verified_config || "");
|
||||
const xml = summary.parsed_current_config || {};
|
||||
const fields = [
|
||||
["margeServerUrl", "marge"],
|
||||
["statsServerUrl", "stats"],
|
||||
["swUpdateUrl", "sw_update"],
|
||||
["bmxRegistryUrl", "bmx"],
|
||||
];
|
||||
const detail = document.createElement("div");
|
||||
detail.style.cssText = "margin-top: 4px; font-family: monospace; font-size: 0.85em; color: #555";
|
||||
for (const [key] of fields) {
|
||||
const xmlVal = xml[key] || "";
|
||||
const telVal = live[key] || "";
|
||||
const row = document.createElement("div");
|
||||
row.style.cssText = "padding: 1px 0";
|
||||
const label = document.createElement("strong");
|
||||
label.style.cssText = "color: #333";
|
||||
label.textContent = key + ": ";
|
||||
row.appendChild(label);
|
||||
row.appendChild(document.createTextNode(formatURLPair(xmlVal, telVal)));
|
||||
detail.appendChild(row);
|
||||
}
|
||||
urlCell.appendChild(detail);
|
||||
}
|
||||
|
||||
// --- DNS Interception axis ---
|
||||
const dnsCell = document.getElementById("state-dns");
|
||||
if (dnsCell) {
|
||||
dnsCell.replaceChildren();
|
||||
const v = dnsInterceptionVerdict(summary);
|
||||
dnsCell.appendChild(stateLine(v.icon, v.text, v.note));
|
||||
}
|
||||
|
||||
// --- CA / TLS axis ---
|
||||
const caCell = document.getElementById("state-ca");
|
||||
if (caCell) {
|
||||
caCell.replaceChildren();
|
||||
const v = caVerdict(summary);
|
||||
caCell.appendChild(stateLine(v.icon, v.text, v.note));
|
||||
}
|
||||
|
||||
// --- Preconditions ---
|
||||
const remoteCell = document.getElementById("state-remote-services-cell");
|
||||
if (remoteCell) {
|
||||
remoteCell.replaceChildren();
|
||||
const v = remoteServicesVerdict(summary);
|
||||
remoteCell.appendChild(stateLine(v.icon, v.text, v.note));
|
||||
}
|
||||
|
||||
const pairedCell = document.getElementById("state-paired");
|
||||
if (pairedCell) {
|
||||
pairedCell.replaceChildren();
|
||||
if (summary.is_paired) {
|
||||
pairedCell.appendChild(stateLine("✅", "Paired", `(account ${summary.account_id || "?"})`));
|
||||
} else {
|
||||
pairedCell.appendChild(stateLine("❌", "Not paired", "(/setMargeAccount or telnet envswitch needed)"));
|
||||
}
|
||||
}
|
||||
|
||||
const backupCell = document.getElementById("state-backup");
|
||||
if (backupCell) {
|
||||
backupCell.replaceChildren();
|
||||
if (summary.original_config) {
|
||||
backupCell.appendChild(stateLine("✅", "Found .original", ""));
|
||||
} else {
|
||||
backupCell.appendChild(stateLine("❌", "Not found", "(only relevant for the XML migration method)"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// setStateChip writes a green ✅ / red ❌ chip into the element.
|
||||
function setStateChip(elemId, ok, okText, badText) {
|
||||
const el = document.getElementById(elemId);
|
||||
if (!el) return;
|
||||
if (ok) {
|
||||
el.innerText = "✅ " + okText;
|
||||
el.style.color = "green";
|
||||
} else {
|
||||
el.innerText = "❌ " + badText;
|
||||
el.style.color = "red";
|
||||
}
|
||||
}
|
||||
|
||||
// stateLine returns a DOM fragment "<icon> <bold text> <muted note>".
|
||||
function stateLine(icon, text, note) {
|
||||
const wrap = document.createElement("span");
|
||||
wrap.appendChild(document.createTextNode(icon + " "));
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = text;
|
||||
wrap.appendChild(strong);
|
||||
if (note) {
|
||||
const muted = document.createElement("span");
|
||||
muted.style.cssText = "color: #666; margin-left: 6px; font-size: 0.9em";
|
||||
muted.textContent = note;
|
||||
wrap.appendChild(muted);
|
||||
}
|
||||
return wrap;
|
||||
}
|
||||
|
||||
// formatURLPair renders the on-disk vs live values for one URL field
|
||||
// in a compact way: agreement → single value; disagreement → both,
|
||||
// labelled.
|
||||
function formatURLPair(xmlVal, telVal) {
|
||||
if (!xmlVal && !telVal) return "—";
|
||||
if (!xmlVal) return `live: ${telVal}`;
|
||||
if (!telVal) return `xml: ${xmlVal}`;
|
||||
if (xmlVal === telVal) return xmlVal;
|
||||
return `xml: ${xmlVal} • live: ${telVal}`;
|
||||
}
|
||||
|
||||
function urlConfigVerdict(summary) {
|
||||
const xml = !!summary.xml_migrated;
|
||||
const tel = !!summary.telnet_migrated;
|
||||
if (!xml && !tel) return {icon: "❌", text: "Original (Bose cloud)", note: ""};
|
||||
if (xml && tel) return {icon: "✅", text: "Migrated to AfterTouch", note: "(XML + telnet runtime in sync)"};
|
||||
if (xml) return {icon: "✅", text: "Migrated to AfterTouch", note: "(XML only — telnet runtime may still hold the old URLs)"};
|
||||
return {icon: "✅", text: "Migrated to AfterTouch", note: "(telnet runtime — reboot to persist into the on-disk XML)"};
|
||||
}
|
||||
|
||||
function dnsInterceptionVerdict(summary) {
|
||||
const hosts = !!summary.hosts_migrated;
|
||||
const resolv = !!summary.resolv_migrated;
|
||||
if (!hosts && !resolv) return {icon: "—", text: "None", note: ""};
|
||||
if (resolv) return {icon: "✅", text: "/etc/resolv.conf hook active", note: hosts ? "(also: /etc/hosts entries)" : ""};
|
||||
return {icon: "⚠️", text: "/etc/hosts redirects", note: "(deprecated method)"};
|
||||
}
|
||||
|
||||
function caVerdict(summary) {
|
||||
if (summary.ca_cert_trusted) return {icon: "✅", text: "Local root CA installed", note: ""};
|
||||
return {icon: "❌", text: "Not installed", note: "(HTTPS to local service will fail TLS validation until injected via SSH)"};
|
||||
}
|
||||
|
||||
function remoteServicesVerdict(summary) {
|
||||
if (!summary.ssh_success) return {icon: "❓", text: "Unknown", note: "(SSH not reachable)"};
|
||||
if (!summary.remote_services_enabled) return {icon: "❌", text: "Not enabled", note: "(SSH/telnet shells will not survive a reboot until USB-stick unlock is reapplied)"};
|
||||
if (!summary.remote_services_persistent) return {icon: "⚠️", text: "Enabled but not persistent", note: "(will be lost on reboot)"};
|
||||
return {icon: "✅", text: "Persistent", note: ""};
|
||||
}
|
||||
|
||||
// renderTelnetPreflight surfaces TelnetReachable / TelnetBanner /
|
||||
// TelnetProbeError on the migration summary, and populates the "Current
|
||||
// on Device" column from TelnetVerifiedConfig when the device answered.
|
||||
|
||||
Reference in New Issue
Block a user