mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-20 01:26:14 +00:00
feat(web): per-field telnet URL inputs, preflight status, warnings
The migration tab gains:
- Telnet (Port 17000) status line in the summary box, mirroring the
SSH connection line. Shows ✅/❌, the device's diagnostic shell
banner if any, and a probe-error block when a TCP dial succeeded
but the shell rejected getpdo.
- Cross-check warnings banner that surfaces summary.warnings (the
SSH-XML vs telnet-getpdo URL diffs from the parallel preflight) as
informational notices above the migration controls.
- URL Targets table inside the telnet method pane with four editable
inputs (Marge, Stats, Software Update, BMX Registry) pre-filled
from the canonical defaultTelnetURLs(target_url) derivation. Each
row shows the device's current value alongside, parsed from
summary.telnet_verified_config. A "Reset to defaults" button wipes
user edits in the table.
- Migrate / Reboot buttons now enable when *either* SSH or telnet is
reachable, so the SSH-less telnet path can actually be triggered
from the UI.
The four URL inputs are folded into the migrate query string as the
marge_url / stats_url / sw_update_url / bmx_url options the handler now
recognises. Empty fields are omitted so the service's
telnetURLsFromOptions canonical fallback runs.
JS helpers parseTelnetVerifiedConfig and defaultTelnetURLs mirror the
Go-side parseGetpdoConfig and defaultTelnetURLs — keep them in sync.
I cannot run a browser test from this environment, so this change is
verified only by go build, the Go test suite (setup + handlers, race),
and node --check on the modified script.js. Worth a manual smoke test
of: switching to telnet, observing the inputs pre-fill, editing one
field, kicking off a migration, and reading back the warnings banner
on a freshly-migrated speaker.
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
909a85883a
commit
27dccc779f
@@ -505,6 +505,16 @@
|
||||
<p>Migration Status: <span id="migration-status"></span></p>
|
||||
<input type="hidden" id="summary-device-id"/>
|
||||
<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>
|
||||
</p>
|
||||
<div
|
||||
id="preflight-warnings"
|
||||
style="display: none; margin: 10px 0; padding: 8px 12px; background-color: #fff8e1; border-left: 4px solid #ffb300; font-size: 0.9em"
|
||||
>
|
||||
<strong>Cross-check warnings:</strong>
|
||||
<ul id="preflight-warnings-list" style="margin: 4px 0 0 1em; padding: 0"></ul>
|
||||
</div>
|
||||
<p id="original-config-status" style="display: none">
|
||||
Backup: ✅ Found .original config at
|
||||
<code
|
||||
@@ -778,6 +788,85 @@
|
||||
the speaker with an account ID before presets and
|
||||
streaming work.
|
||||
</p>
|
||||
|
||||
<h4 style="margin: 15px 0 5px 0">URL Targets</h4>
|
||||
<p style="margin: 0 0 8px 0; font-size: 0.9em; color: #555">
|
||||
Pre-filled from the target URL above. Most users leave
|
||||
these as-is. If you redirect to a fork like
|
||||
<a href="https://github.com/deborahgu/soundcork">soundcork</a>
|
||||
that mounts marge under <code>/marge</code>, append the
|
||||
suffix here — the matching <code>envswitch boseurls set</code>
|
||||
argument is derived automatically.
|
||||
</p>
|
||||
<table style="width: 100%; border-collapse: collapse">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="text-align: left; padding: 4px 6px">Field</th>
|
||||
<th style="text-align: left; padding: 4px 6px">Current on Device</th>
|
||||
<th style="text-align: left; padding: 4px 6px">Target URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding: 4px 6px">Marge</td>
|
||||
<td id="telnet-current-marge" style="padding: 4px 6px; font-family: monospace; font-size: 0.85em; color: #555">—</td>
|
||||
<td style="padding: 4px 6px">
|
||||
<input
|
||||
type="text"
|
||||
id="telnet-marge-url"
|
||||
style="width: 100%; font-family: monospace; font-size: 0.9em; box-sizing: border-box"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 6px">Stats</td>
|
||||
<td id="telnet-current-stats" style="padding: 4px 6px; font-family: monospace; font-size: 0.85em; color: #555">—</td>
|
||||
<td style="padding: 4px 6px">
|
||||
<input
|
||||
type="text"
|
||||
id="telnet-stats-url"
|
||||
style="width: 100%; font-family: monospace; font-size: 0.9em; box-sizing: border-box"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 6px">Software Update</td>
|
||||
<td id="telnet-current-sw_update" style="padding: 4px 6px; font-family: monospace; font-size: 0.85em; color: #555">—</td>
|
||||
<td style="padding: 4px 6px">
|
||||
<input
|
||||
type="text"
|
||||
id="telnet-sw_update-url"
|
||||
style="width: 100%; font-family: monospace; font-size: 0.9em; box-sizing: border-box"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="padding: 4px 6px">BMX Registry</td>
|
||||
<td id="telnet-current-bmx" style="padding: 4px 6px; font-family: monospace; font-size: 0.85em; color: #555">—</td>
|
||||
<td style="padding: 4px 6px">
|
||||
<input
|
||||
type="text"
|
||||
id="telnet-bmx-url"
|
||||
style="width: 100%; font-family: monospace; font-size: 0.9em; box-sizing: border-box"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div style="margin-top: 8px">
|
||||
<button
|
||||
type="button"
|
||||
onclick="resetTelnetURLsToDefaults()"
|
||||
style="font-size: 0.85em"
|
||||
>
|
||||
Reset to defaults
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
id="telnet-probe-error"
|
||||
style="display: none; margin-top: 10px; padding: 6px 10px; background-color: #ffebee; border-left: 4px solid #c62828; font-size: 0.9em"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
||||
@@ -1761,6 +1761,10 @@ 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";
|
||||
|
||||
renderTelnetPreflight(summary);
|
||||
renderPreflightWarnings(summary);
|
||||
fillTelnetURLInputs(defaultTelnetURLs(targetUrl));
|
||||
|
||||
const migrationStatus = document.getElementById("migration-status");
|
||||
migrationStatus.innerText = summary.is_migrated ? "✅ Migrated to AfterTouch" : "❌ Not Migrated";
|
||||
migrationStatus.style.color = summary.is_migrated ? "green" : "red";
|
||||
@@ -1842,9 +1846,15 @@ async function showSummary(deviceId) {
|
||||
|
||||
toggleMigrationMethod();
|
||||
|
||||
// Migration and reboot are gated on having *some* transport
|
||||
// reachable. Telnet is enough for the telnet method (no SSH
|
||||
// required); the server returns a clear error if the user picks a
|
||||
// method whose transport isn't actually available.
|
||||
const anyTransport = summary.ssh_success || summary.telnet_reachable;
|
||||
|
||||
const migrateBtn = document.getElementById("confirm-migrate-btn");
|
||||
migrateBtn.onclick = () => migrate(deviceId, ip);
|
||||
migrateBtn.disabled = !summary.ssh_success;
|
||||
migrateBtn.disabled = !anyTransport;
|
||||
|
||||
const revertBtn = document.getElementById("revert-migrate-btn");
|
||||
revertBtn.onclick = () => revert(deviceId, ip);
|
||||
@@ -1853,7 +1863,7 @@ async function showSummary(deviceId) {
|
||||
|
||||
const rebootBtn = document.getElementById("reboot-speaker-btn");
|
||||
rebootBtn.onclick = () => reboot(deviceId, ip);
|
||||
rebootBtn.disabled = !summary.ssh_success;
|
||||
rebootBtn.disabled = !anyTransport;
|
||||
rebootBtn.style.border = "none"; // Reset border if it was set during migration
|
||||
|
||||
const remoteBtn = document.getElementById("ensure-remote-btn");
|
||||
@@ -2103,6 +2113,10 @@ async function migrate(deviceId, ip) {
|
||||
bmx: document.getElementById("opt-bmx").value,
|
||||
};
|
||||
|
||||
if (method === "telnet") {
|
||||
Object.assign(opts, readTelnetURLOptions());
|
||||
}
|
||||
|
||||
const summaryDiv = document.getElementById("migration-summary");
|
||||
summaryDiv.style.display = "none";
|
||||
|
||||
@@ -2369,6 +2383,151 @@ function toggleOriginalConfig() {
|
||||
pane.style.display = pane.style.display === "none" ? "block" : "none";
|
||||
}
|
||||
|
||||
// defaultTelnetURLs returns the canonical four URLs derived from a
|
||||
// service base URL. Mirrors setup.defaultTelnetURLs (Go) — keep them in
|
||||
// sync if either side changes.
|
||||
function defaultTelnetURLs(targetUrl) {
|
||||
const base = (targetUrl || "").replace(/\/+$/, "");
|
||||
return {
|
||||
marge: base,
|
||||
stats: base,
|
||||
sw_update: base + "/updates/soundtouch",
|
||||
bmx: base + "/bmx/registry/v1/services",
|
||||
};
|
||||
}
|
||||
|
||||
// fillTelnetURLInputs writes the given URL set into the four input
|
||||
// fields, but only when the field is empty (so a user's edit is never
|
||||
// clobbered by a refresh).
|
||||
function fillTelnetURLInputs(urls, {force = false} = {}) {
|
||||
const fields = [
|
||||
["telnet-marge-url", urls.marge],
|
||||
["telnet-stats-url", urls.stats],
|
||||
["telnet-sw_update-url", urls.sw_update],
|
||||
["telnet-bmx-url", urls.bmx],
|
||||
];
|
||||
for (const [id, value] of fields) {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) continue;
|
||||
if (force || !el.value) el.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
// resetTelnetURLsToDefaults wipes any user edits and reapplies the
|
||||
// canonical defaults. Wired to the "Reset to defaults" button in the
|
||||
// telnet pane.
|
||||
function resetTelnetURLsToDefaults() {
|
||||
const targetUrl = document.getElementById("target-domain").value;
|
||||
fillTelnetURLInputs(defaultTelnetURLs(targetUrl), {force: true});
|
||||
}
|
||||
|
||||
// readTelnetURLOptions returns the four per-field URL overrides as the
|
||||
// query-parameter map the handler expects (marge_url / stats_url /
|
||||
// sw_update_url / bmx_url). Empty fields are omitted so the service
|
||||
// layer's "fall back to canonical default" path is exercised.
|
||||
function readTelnetURLOptions() {
|
||||
const out = {};
|
||||
const pairs = [
|
||||
["marge_url", "telnet-marge-url"],
|
||||
["stats_url", "telnet-stats-url"],
|
||||
["sw_update_url", "telnet-sw_update-url"],
|
||||
["bmx_url", "telnet-bmx-url"],
|
||||
];
|
||||
for (const [optKey, elemId] of pairs) {
|
||||
const el = document.getElementById(elemId);
|
||||
if (el && el.value) out[optKey] = el.value;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// renderTelnetPreflight surfaces TelnetReachable / TelnetBanner /
|
||||
// TelnetProbeError on the migration summary, and populates the "Current
|
||||
// on Device" column from TelnetVerifiedConfig when the device answered.
|
||||
function renderTelnetPreflight(summary) {
|
||||
const statusEl = document.getElementById("telnet-status");
|
||||
if (statusEl) {
|
||||
if (summary.telnet_reachable) {
|
||||
statusEl.innerText = "✅ Reachable";
|
||||
statusEl.style.color = "green";
|
||||
} else if (summary.telnet_probe_error) {
|
||||
statusEl.innerText = "❌ Unreachable";
|
||||
statusEl.style.color = "red";
|
||||
} else {
|
||||
statusEl.innerText = "❓ Unknown";
|
||||
statusEl.style.color = "gray";
|
||||
}
|
||||
}
|
||||
|
||||
const bannerEl = document.getElementById("telnet-banner");
|
||||
if (bannerEl) {
|
||||
bannerEl.innerText = summary.telnet_banner ? `(${summary.telnet_banner})` : "";
|
||||
}
|
||||
|
||||
const errorEl = document.getElementById("telnet-probe-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";
|
||||
}
|
||||
}
|
||||
|
||||
const live = parseTelnetVerifiedConfig(summary.telnet_verified_config || "");
|
||||
const cells = [
|
||||
["telnet-current-marge", live.margeServerUrl],
|
||||
["telnet-current-stats", live.statsServerUrl],
|
||||
["telnet-current-sw_update", live.swUpdateUrl],
|
||||
["telnet-current-bmx", live.bmxRegistryUrl],
|
||||
];
|
||||
for (const [id, value] of cells) {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.innerText = value || "—";
|
||||
}
|
||||
}
|
||||
|
||||
// parseTelnetVerifiedConfig extracts key=value pairs from the device's
|
||||
// `getpdo CurrentSystemConfiguration` reply. Mirrors
|
||||
// setup.parseGetpdoConfig (Go) — see that function's docstring for the
|
||||
// tolerance contract.
|
||||
function parseTelnetVerifiedConfig(text) {
|
||||
const out = {};
|
||||
if (!text) return out;
|
||||
for (const raw of text.split("\n")) {
|
||||
const line = raw.trim();
|
||||
if (!line) continue;
|
||||
const i = line.indexOf("=");
|
||||
if (i <= 0) continue;
|
||||
const key = line.slice(0, i).trim();
|
||||
const val = line.slice(i + 1).trim();
|
||||
if (key) out[key] = val;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// renderPreflightWarnings shows summary.warnings as a yellow banner
|
||||
// above the migration controls. An empty/missing list hides the banner.
|
||||
function renderPreflightWarnings(summary) {
|
||||
const banner = document.getElementById("preflight-warnings");
|
||||
const list = document.getElementById("preflight-warnings-list");
|
||||
if (!banner || !list) return;
|
||||
|
||||
list.replaceChildren();
|
||||
|
||||
const warnings = summary.warnings || [];
|
||||
if (warnings.length === 0) {
|
||||
banner.style.display = "none";
|
||||
return;
|
||||
}
|
||||
|
||||
for (const w of warnings) {
|
||||
const li = document.createElement("li");
|
||||
li.innerText = w;
|
||||
list.appendChild(li);
|
||||
}
|
||||
banner.style.display = "block";
|
||||
}
|
||||
|
||||
async function toggleMigrationMethod() {
|
||||
const method = document.getElementById("migration-method").value;
|
||||
const xmlDiffPane = document.getElementById("xml-diff-pane");
|
||||
|
||||
Reference in New Issue
Block a user