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,