From a85f196c9753642d7c4138c7011e0445b1f2a758 Mon Sep 17 00:00:00 2001 From: Uladzimir Karpenka Date: Thu, 4 Jun 2026 11:16:26 +0300 Subject: [PATCH] Phase 3 (slice): make Mute sync UI->web in the web client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The outbound channel already existed: PushLoop broadcasts the full BuildStateJson (including "mute") to web clients every 200ms. The gap was purely client-side — applyState never rendered the mute icon, so a UI-initiated mute change was not reflected in the browser (the icon was only updated on the client's own click). WebPageHtml.pas: - Add updMuteIc() helper; render it from applyState (guarded by isPending("mute")), so periodic state updates the icon. - Click handler calls updMuteIc + pend("mute"); add "mute" to PEND_MAP so the optimistic local state isn't clobbered by an in-flight stale push. Mute is now bidirectional. Other fields already render in applyState, so UI->web works for them; this confirms the web outbound path is the periodic full-state broadcast, not a per-field event push (the latter will matter for the future serial/Arduino frontend). Co-Authored-By: Claude Opus 4.8 --- WebPageHtml.pas | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/WebPageHtml.pas b/WebPageHtml.pas index 033c23d..182dc90 100644 --- a/WebPageHtml.pas +++ b/WebPageHtml.pas @@ -377,7 +377,8 @@ begin 'function updRunBtn(r){var b=q("startStopBtn");if(!b)return;b.textContent=r?"STOP":"START";b.classList.toggle("rx-tx",!!r);b.classList.toggle("on",!r);}' + 'q("startStopBtn").onclick=function(){audioKick();cur.running=!cur.running;pend("run");updRunBtn(cur.running);cmd("set_run",cur.running);};' + - 'q("muteIc").onclick=()=>{cur.mute=!cur.mute;q("muteIc").textContent=cur.mute?"\uD83D\uDD07":"\uD83D\uDD0A";cmd("set_mute",cur.mute);};' + + 'function updMuteIc(){var mi=q("muteIc");if(mi)mi.textContent=cur.mute?"\uD83D\uDD07":"\uD83D\uDD0A";}' + + 'q("muteIc").onclick=()=>{cur.mute=!cur.mute;updMuteIc();pend("mute");cmd("set_mute",cur.mute);};' + 'let stA=0,stAmem=100,stB=0,stBmem=100;' + 'let freqMhzDigits=3,freqMaxHz=999999999;' + @@ -596,7 +597,7 @@ begin 'function isPending(field){return !!(pendingCmds[field]&&pendingCmds[field]>Date.now());}' + // Маппинг: server JSON field → pend key - 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter"};' + + 'var PEND_MAP={vfo_a_hz:"freq",vfo_b_hz:"freq",nr:"nr",nr_mode:"nr",nb:"nb",nb_mode:"nb",snb:"snb",anf:"anf",running:"run",mode:"mode",agc_mode:"agc",transmitting:"mox",drive:"drive",attn_idx:"attn",tuning:"tun",filter_bw:"filter",mute:"mute"};' + 'function applyState(msg){' + 'Object.keys(msg).forEach(function(k){var pk=PEND_MAP[k];if(!pk||!isPending(pk))cur[k]=msg[k];});' + @@ -637,6 +638,7 @@ begin 'if(!isPending("run"))updRunBtn(!!cur.running);' + 'if(!isPending("mox"))updMoxBtn(!!cur.transmitting);' + 'if(!isPending("tun"))updToneBtn(!!cur.tuning);' + + 'if(!isPending("mute"))updMuteIc();' + 'if(!isPending("drive")){const ts=q("txSl");if(ts&&!ts.matches(":active")){ts.value=cur.drive||0;var tv=q("txVal");if(tv)tv.textContent=cur.drive||0;}}' + 'if(!isPending("mode")){syncModGrp(cur.mode||0);syncStepSel();}' + 'if(!isPending("attn")){const at=q("attSel");if(at)at.value=cur.attn_idx||0;}' +