From d48aa63b9ae731fc602e917a3b82fe1544c95891 Mon Sep 17 00:00:00 2001 From: Tobias Gesellchen Date: Sun, 10 May 2026 22:24:05 +0200 Subject: [PATCH] feat(telnet,web): relax timeouts and hint at transient probe failures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two halves of the same flakiness fix: - pkg/telnet defaults: dial 2s→4s, read 5s→7s, write 2s→3s, idleWindow 400ms→600ms. The diagnostic shell on FW 27.0.6 occasionally takes >2s to accept a fresh TCP connection (likely while servicing other work), and the previous tight budget produced flaky preflight results on healthy speakers that consistently recovered on a second attempt. - state card: when the probe error wraps an i/o timeout / "timed out" / "connection reset", the panel now appends a hint pointing the user at the ↻ refresh button next to the device dropdown — instead of leaving the user to assume telnet is permanently unreachable. looksTransient() keeps the substring match conservative so genuine "connection refused" / "host unreachable" errors keep the original framing. The 4s dial budget adds at most ~2s to summary loads on devices where telnet is genuinely down; that's an acceptable trade-off for removing the false-negative reports. Co-Authored-By: Claude Opus 4.7 (1M context) --- pkg/service/handlers/web/js/script.js | 28 ++++++++++++++++++++++++++- pkg/telnet/telnet.go | 16 +++++++++++---- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/pkg/service/handlers/web/js/script.js b/pkg/service/handlers/web/js/script.js index ba728aa..120033d 100644 --- a/pkg/service/handlers/web/js/script.js +++ b/pkg/service/handlers/web/js/script.js @@ -2670,6 +2670,16 @@ async function applySuggestedPlan() { if (status) status.innerText = ""; } +// looksTransient classifies a probe-error message as likely-flaky-but- +// retriable. Conservative substring match — only marks errors that +// pattern-match a TCP/I/O timeout, which is the exact failure shape +// observed on healthy FW 27.0.6 devices that recover on retry. +function looksTransient(msg) { + if (!msg) return false; + const m = msg.toLowerCase(); + return m.includes("timeout") || m.includes("timed out") || m.includes("connection reset"); +} + // 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, @@ -2686,7 +2696,23 @@ function renderMigrationState(summary) { 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.replaceChildren(); + const line = document.createElement("div"); + line.innerText = "Probe error: " + summary.telnet_probe_error; + errorEl.appendChild(line); + + // The diagnostic shell on FW 27.0.6 occasionally drops the + // first connection attempt under load. When the error wraps + // an i/o timeout, the next probe almost always succeeds — + // so nudge the user toward the ↻ refresh button rather than + // letting them assume telnet is permanently unreachable. + if (looksTransient(summary.telnet_probe_error)) { + const hint = document.createElement("div"); + hint.style.cssText = "margin-top: 4px; font-size: 0.85em; color: #5d4037"; + hint.innerText = "💡 Telnet probes are occasionally flaky on this firmware. Click the ↻ refresh button next to the device dropdown to retry."; + errorEl.appendChild(hint); + } + errorEl.style.display = "block"; } else { errorEl.style.display = "none"; diff --git a/pkg/telnet/telnet.go b/pkg/telnet/telnet.go index 0865442..49aaeac 100644 --- a/pkg/telnet/telnet.go +++ b/pkg/telnet/telnet.go @@ -18,14 +18,22 @@ import ( ) // Default values for a fresh Client. +// +// The dial and read budgets were originally tighter (2s / 5s); both were +// relaxed after observing transient i/o-timeout failures on healthy +// speakers that reliably resolved on a second attempt. The diagnostic +// shell on FW 27.0.6 occasionally takes >2s to accept a fresh TCP +// connection — likely while the device is servicing other work — so a +// short dial budget produces flaky preflight results without indicating +// a real reachability problem. const ( DefaultPort = 17000 - DefaultDialTimeout = 2 * time.Second - DefaultReadTimeout = 5 * time.Second - DefaultWriteTimeout = 2 * time.Second + DefaultDialTimeout = 4 * time.Second + DefaultReadTimeout = 7 * time.Second + DefaultWriteTimeout = 3 * time.Second // idleWindow is how long we wait for further bytes after the first // byte of a response before treating the response as complete. - idleWindow = 400 * time.Millisecond + idleWindow = 600 * time.Millisecond ) // Client is a connected (or about-to-be-connected) session to a SoundTouch