feat(telnet,web): relax timeouts and hint at transient probe failures

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) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-05-11 00:37:11 +02:00
co-authored by Claude Opus 4.7
parent c5362be11b
commit d48aa63b9a
2 changed files with 39 additions and 5 deletions
+27 -1
View File
@@ -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";
+12 -4
View File
@@ -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