[fix](trx-frontend-http): guard CW auto UI against concurrent SSE updates

Add cwAutoLocalOverride flag in cw.js to block server-state snapshots
from overriding the checkbox while a user-initiated POST is in-flight.
Expose applyCwAutoUiFromServer for app.js render() to call instead of
applyCwAutoUi, preventing a racing SSE event carrying the old cw_auto
value from immediately undoing the user's toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Signed-off-by: Stan Grams <sjg@haxx.space>
This commit is contained in:
2026-03-06 22:07:05 +01:00
parent 91562b2a4b
commit 175abe35f9
2 changed files with 26 additions and 15 deletions
@@ -2479,28 +2479,24 @@ function render(update) {
const cwAutoEl = document.getElementById("cw-auto"); const cwAutoEl = document.getElementById("cw-auto");
const cwWpmEl = document.getElementById("cw-wpm"); const cwWpmEl = document.getElementById("cw-wpm");
const cwToneEl = document.getElementById("cw-tone"); const cwToneEl = document.getElementById("cw-tone");
if (cwAutoEl && typeof update.cw_auto === "boolean") {
cwAutoEl.checked = update.cw_auto;
}
if (cwWpmEl && typeof update.cw_wpm === "number") { if (cwWpmEl && typeof update.cw_wpm === "number") {
cwWpmEl.value = update.cw_wpm; cwWpmEl.value = update.cw_wpm;
} }
if (cwToneEl && typeof update.cw_tone_hz === "number") { if (cwToneEl && typeof update.cw_tone_hz === "number") {
cwToneEl.value = update.cw_tone_hz; cwToneEl.value = update.cw_tone_hz;
} }
if ((cwWpmEl || cwToneEl) && typeof update.cw_auto === "boolean") { if (typeof update.cw_auto === "boolean") {
const disabled = update.cw_auto; if (typeof window.applyCwAutoUiFromServer === "function") {
if (typeof window.applyCwAutoUi === "function") { // cw.js is loaded: use the guarded path that respects in-flight user
window.applyCwAutoUi(disabled); // changes, preventing a concurrent SSE poll from re-enabling auto just
// after the user disabled it.
window.applyCwAutoUiFromServer(update.cw_auto);
} else if (typeof window.applyCwAutoUi === "function") {
window.applyCwAutoUi(update.cw_auto);
} else { } else {
if (cwWpmEl) { if (cwAutoEl) cwAutoEl.checked = update.cw_auto;
cwWpmEl.disabled = disabled; if (cwWpmEl) { cwWpmEl.disabled = update.cw_auto; cwWpmEl.readOnly = update.cw_auto; }
cwWpmEl.readOnly = disabled; if (cwToneEl) { cwToneEl.disabled = update.cw_auto; cwToneEl.readOnly = update.cw_auto; }
}
if (cwToneEl) {
cwToneEl.disabled = disabled;
cwToneEl.readOnly = disabled;
}
} }
} }
let activeFreqColor = "var(--accent-green)"; let activeFreqColor = "var(--accent-green)";
@@ -21,6 +21,11 @@ let cwLastAppendTime = 0;
let cwTonePickerRaf = null; let cwTonePickerRaf = null;
let cwPaused = false; let cwPaused = false;
let cwBufferedWhilePaused = 0; let cwBufferedWhilePaused = 0;
// Tracks a user-initiated auto toggle that is in-flight (POST not yet
// acknowledged). While set, server-state updates must not override the
// checkbox so that a concurrent SSE event carrying the *old* cw_auto value
// does not immediately undo the user's choice.
let cwAutoLocalOverride = null;
function applyCwAutoUi(enabled) { function applyCwAutoUi(enabled) {
if (cwAutoInput) cwAutoInput.checked = enabled; if (cwAutoInput) cwAutoInput.checked = enabled;
@@ -38,6 +43,13 @@ function applyCwAutoUi(enabled) {
} }
window.applyCwAutoUi = applyCwAutoUi; window.applyCwAutoUi = applyCwAutoUi;
// Called by app.js render() when a server-state snapshot arrives. Ignores
// the update while cwAutoLocalOverride is set (user change still in-flight).
window.applyCwAutoUiFromServer = function(enabled) {
if (cwAutoLocalOverride !== null) return;
applyCwAutoUi(enabled);
};
function clampCwWpm(wpm) { function clampCwWpm(wpm) {
const numeric = Number(wpm); const numeric = Number(wpm);
if (!Number.isFinite(numeric)) return 15; if (!Number.isFinite(numeric)) return 15;
@@ -232,12 +244,15 @@ async function setCwTone(tone, { syncInput = true } = {}) {
if (cwAutoInput) { if (cwAutoInput) {
cwAutoInput.addEventListener("change", async () => { cwAutoInput.addEventListener("change", async () => {
const enabled = cwAutoInput.checked; const enabled = cwAutoInput.checked;
cwAutoLocalOverride = enabled;
applyCwAutoUi(enabled); applyCwAutoUi(enabled);
try { try {
await postPath(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`); await postPath(`/set_cw_auto?enabled=${enabled ? "true" : "false"}`);
drawCwTonePicker(); drawCwTonePicker();
} catch (e) { } catch (e) {
console.error("CW auto toggle failed", e); console.error("CW auto toggle failed", e);
} finally {
cwAutoLocalOverride = null;
} }
}); });
} }