From 84590408564ccf6a0417385a8e4d05ca1c67b95f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Sat, 12 Sep 2026 22:22:55 +0300 Subject: [PATCH] ui: auto-refresh the proactive log and STK menu state The proactive command list only re-rendered on tab/pill switches, so new fetched commands (background STATUS polling, menu traffic) stayed invisible while the Phone view was open. - /api/status now exposes proactive_seq (_PROACTIVE_ENTRY_ID, monotonic and not reset with the log), so the existing 2s status poll detects changes with no extra request; pysimCardStateUpdate() re-renders the log when the sequence changed and the Phone/Phone view is visible - the 5s backend timer no longer fetches the log (it was discarded) and now uses the already-fetched stk-status: when active/pending changes while the Phone tab is visible, stkCheckMenu() refreshes the menu button without a tab switch - pysimProactiveLogRender() keeps its scroll position and only shows Loading... on a first/empty paint - tests: proactive seq + STK signature helpers and poll-driven render behaviour in card_state.test.js; SW cache v123 -> v124. --- frontend/index.html | 30 ++++++++++++++++++--- frontend/sw.js | 2 +- frontend/tests/card_state.test.js | 45 +++++++++++++++++++++++++++++-- pysim_otaman_server/server.py | 1 + 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 3d346ac..aaf925d 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -6713,6 +6713,10 @@ function pysimCardStateUpdate(status) { _pysimCardEquipped = !!status.connected; _pysimEquipping = !!status.equipping; pysimApplyAvailability(); + if (pysimProactiveSeqChanged(status.proactive_seq) + && isViewVisible('tab-phone') && isViewVisible('phone-sub-phone')) { + pysimProactiveLogRender(); + } const key = [status.connected, !!status.card_present, !!status.equipping, !!status.auto_equip, status.card_session].join('|'); if (key === _pysimCardStateKey) return; _pysimCardStateKey = key; @@ -6752,17 +6756,35 @@ function pysimStartBackendPoll() { }, 2000); _pysimPollTimer = setInterval(async () => { try { - const [log, stk, ps] = await Promise.all([ - pysimFetch('/api/proactive-log'), + const [stk, ps] = await Promise.all([ pysimFetch('/api/stk-status'), pysimFetch('/api/poll-status'), ]); pysimUpdatePollUI(ps.enabled, ps.interval); + if (pysimStkStatusChanged(stk) && isViewVisible('tab-phone')) stkCheckMenu(); } catch (e) { /* ignore */ } }, 5000); } +let _pysimStkSig = null; + +function pysimStkStatusChanged(stk) { + if (!stk) return false; + const sig = [!!stk.active, !!stk.pending, stk.pending_type || ''].join('|'); + if (sig === _pysimStkSig) return false; + _pysimStkSig = sig; + return true; +} + // ===== proactive command log ===== +let _pysimProactiveSeq = null; + +function pysimProactiveSeqChanged(seq) { + if (seq === undefined || seq === null) return false; + if (_pysimProactiveSeq === seq) return false; + _pysimProactiveSeq = seq; + return true; +} const CMD_NAMES = { '03': 'POLL INTERVAL', '05': 'SET UP EVENT LIST', '13': 'SEND SHORT MESSAGE', '20': 'PLAY TONE', @@ -6818,7 +6840,8 @@ function pysimLogFields(decoded) { async function pysimProactiveLogRender() { const el = document.getElementById('pysim-proactive-log'); - el.innerHTML = '' + t('Loading...') + ''; + const scrollTop = el.scrollTop; + if (!el.firstChild) el.innerHTML = '' + t('Loading...') + ''; try { const log = await pysimFetch('/api/proactive-log'); if (!log || !log.length) { @@ -6865,6 +6888,7 @@ async function pysimProactiveLogRender() { html += ''; }); el.innerHTML = html; + el.scrollTop = scrollTop; } catch (err) { el.innerHTML = 'Error: ' + esc(err.message) + ''; } diff --git a/frontend/sw.js b/frontend/sw.js index 7e1c982..be4cafd 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'otaman-v123'; +const CACHE = 'otaman-v124'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/card_state.test.js b/frontend/tests/card_state.test.js index 335a338..db6bda8 100644 --- a/frontend/tests/card_state.test.js +++ b/frontend/tests/card_state.test.js @@ -22,23 +22,29 @@ function extractFunc(src, name) { } let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n' - + 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n'; + + 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n' + + 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\n'; code += extractFunc(html, 'pysimCardStateUpdate') + '\n'; code += extractFunc(html, 'pysimAvailabilityState') + '\n'; code += extractFunc(html, 'pysimControlDisabled') + '\n'; +code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n'; +code += extractFunc(html, 'pysimStkStatusChanged') + '\n'; code += '\nglobalThis.esc = s => s;\n'; code += 'globalThis.t = s => s;\n'; eval(code); function setup() { const el = { textContent: 'status line', innerHTML: '' }; - const calls = { connected: [], resets: [], refreshStatus: [] }; + const calls = { connected: [], resets: [], refreshStatus: [], proactive: 0 }; _pysimCardStateKey = null; _pysimCardSession = null; + _pysimProactiveSeq = null; globalThis.document = { getElementById: () => el, querySelectorAll: () => [] }; globalThis.pysimSetConnected = v => calls.connected.push(v); globalThis.pysimResetCardData = refresh => calls.resets.push(refresh); globalThis.pysimApplyAvailability = () => {}; + globalThis.isViewVisible = () => true; + globalThis.pysimProactiveLogRender = () => { calls.proactive++; }; return { el, calls }; } @@ -127,3 +133,38 @@ test('no card with auto-equip enabled still shows the no-card message', () => { assert.ok(el.innerHTML.includes('No card detected'), el.innerHTML); assert.ok(!el.innerHTML.includes('initializing'), el.innerHTML); }); + +test('proactive log refreshes when the status sequence changes', () => { + const { calls } = setup(); + pysimCardStateUpdate(status({ proactive_seq: 7 })); + pysimCardStateUpdate(status({ proactive_seq: 7 })); + assert.strictEqual(calls.proactive, 1); + pysimCardStateUpdate(status({ proactive_seq: 8 })); + assert.strictEqual(calls.proactive, 2); +}); + +test('proactive log is not refreshed while the phone view is hidden', () => { + const { calls } = setup(); + globalThis.isViewVisible = () => false; + pysimCardStateUpdate(status({ proactive_seq: 3 })); + assert.strictEqual(calls.proactive, 0); +}); + +test('pysimProactiveSeqChanged tracks the last sequence', () => { + _pysimProactiveSeq = null; + assert.ok(pysimProactiveSeqChanged(4)); + assert.ok(!pysimProactiveSeqChanged(4)); + assert.ok(pysimProactiveSeqChanged(5)); + assert.ok(!pysimProactiveSeqChanged(undefined)); + assert.ok(!pysimProactiveSeqChanged(null)); +}); + +test('pysimStkStatusChanged detects menu state transitions', () => { + _pysimStkSig = null; + assert.ok(pysimStkStatusChanged({ active: false, pending: false })); + assert.ok(!pysimStkStatusChanged({ active: false, pending: false })); + assert.ok(pysimStkStatusChanged({ active: true, pending: true, pending_type: 'select_item' })); + assert.ok(!pysimStkStatusChanged({ active: true, pending: true, pending_type: 'select_item' })); + assert.ok(pysimStkStatusChanged({ active: true, pending: false })); + assert.ok(!pysimStkStatusChanged(null)); +}); diff --git a/pysim_otaman_server/server.py b/pysim_otaman_server/server.py index 76539fc..768c878 100644 --- a/pysim_otaman_server/server.py +++ b/pysim_otaman_server/server.py @@ -1821,6 +1821,7 @@ class PysimHandler(BaseHTTPRequestHandler): 'connected': connected, 'card_present': bool(getattr(self.server, 'card_present', False)), 'card_session': int(getattr(self.server, 'card_session', 0)), + 'proactive_seq': _PROACTIVE_ENTRY_ID, 'equipping': bool(getattr(self.server, 'equipping', False)), 'auto_equip': bool(_AUTO_EQUIP), 'card': card.name if card else None,