From 31ece16ada39016323a903ed694b430f1a331ab4 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: Thu, 24 Sep 2026 21:41:40 +0300 Subject: [PATCH] fix: animated card icon while the card is being initialized (v3.5.5) The Card reader view showed the static nosim.svg while the status line said "Card inserted - initializing..." (the header indicator was correct): the 2 s /api/status poll called pysimSetConnected(false) unconditionally in its not-connected branch, ignoring the equipping / auto-equip-pending state. - pysimCardStateUpdate(): one `initializing` flag (equipping || card_present && auto_equip) now drives both the status text and the icon (pysimSetConnected('spin') -> sim_anim.svg), matching the header indicator. - pysimRefresh() ("Check status"): a reachable server is not an equipped card; the icon now follows connected / initializing / none instead of always showing the equipped icon. - card_state.test.js: initializing states -> 'spin', cardless -> false, and the four Check-status combinations. 552 frontend / 421 Python green; version 3.5.5; sw cache simple-v258. --- frontend/index.html | 14 +++++++++---- frontend/sw.js | 2 +- frontend/tests/card_state.test.js | 34 +++++++++++++++++++++++++++++-- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 5 files changed, 45 insertions(+), 9 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 81b2a26..b5c2c1f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1559,7 +1559,7 @@ // ===== Version ===== // Single source of truth for the PWA version: shown in the header and used // by the server version check in pysimConnect(). -const SIMPLE_VERSION = '3.5.4'; +const SIMPLE_VERSION = '3.5.5'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -6707,7 +6707,10 @@ async function pysimRefresh() { const statusEl = document.getElementById('pysim-status'); try { const data = await pysimFetch('/api/status'); - pysimSetConnected(true); + // Server reachable is not the same as card equipped: keep the icon in + // step with the card state (animated while it is being initialized). + const refreshingInit = !!data.equipping || (!!data.card_present && !!data.auto_equip); + pysimSetConnected(data.connected ? true : (refreshingInit ? 'spin' : false)); let html = 'Reader: ' + esc(data.reader || 'none') + '' + ' | Card: ' + esc(data.card || 'none') + '' + ' | Profile: ' + esc(data.profile || 'none') + '
' + @@ -9782,11 +9785,14 @@ function pysimCardStateUpdate(status) { cardsAutoSelectByIccid(status.iccid); return; } - pysimSetConnected(false); + // A card being initialized (or waiting for auto-equip) shows the animated + // icon, matching the header indicator - not the static no-card one. + const initializing = status.equipping || (status.card_present && status.auto_equip); + pysimSetConnected(initializing ? 'spin' : false); _cardsAutoIccid = null; const statusEl = document.getElementById('pysim-status'); if (statusEl && statusEl.textContent.trim() !== '') { - if (status.equipping || (status.card_present && status.auto_equip)) { + if (initializing) { statusEl.innerHTML = '' + esc(t('Card inserted — initializing...')) + ''; } else if (status.card_present) { statusEl.innerHTML = '' + esc(t('Card inserted — press Equip')) + ''; diff --git a/frontend/sw.js b/frontend/sw.js index cfac815..21b8e92 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v257'; +const CACHE = 'simple-v258'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/card_state.test.js b/frontend/tests/card_state.test.js index 47bdaf7..2789f26 100644 --- a/frontend/tests/card_state.test.js +++ b/frontend/tests/card_state.test.js @@ -5,7 +5,7 @@ const path = require('node:path'); const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8'); -function extractFunc(src, name) { +function extractFunc(src, name, asyncFn) { const re = new RegExp('function\\s+' + name + '\\s*\\([^)]*\\)\\s*\\{'); const m = re.exec(src); if (!m) throw new Error('function ' + name + ' not found'); @@ -18,7 +18,7 @@ function extractFunc(src, name) { if (depth === 0) break; } } - return src.slice(m.index, i + 1); + return (asyncFn ? 'async ' : '') + src.slice(m.index, i + 1); } let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n' @@ -31,6 +31,7 @@ let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n' + 'var _pysimHeaderIccid = undefined;\nvar _pysimHeaderScp80 = undefined;\nvar _pysimHeaderScp81 = undefined;\n' + 'var _cardsAutoIccid = null;\nvar _pysimCardIccid = null;\n'; code += extractFunc(html, 'pysimCardStateUpdate') + '\n'; +code += extractFunc(html, 'pysimRefresh', true) + '\n'; code += extractFunc(html, 'pysimAvailabilityState') + '\n'; code += extractFunc(html, 'pysimControlDisabled') + '\n'; code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n'; @@ -130,6 +131,35 @@ test('disconnect with auto-equip shows the initializing message', () => { assert.ok(el.innerHTML.includes('initializing'), el.innerHTML); }); +test('initializing states show the animated indicator', () => { + for (const extra of [{ card_present: true, equipping: true }, + { card_present: true, auto_equip: true }]) { + const { calls } = setup(); + pysimCardStateUpdate(status(extra)); + assert.deepStrictEqual(calls.connected, ['spin'], JSON.stringify(extra)); + } +}); + +test('a cardless session without auto-equip shows the static no-card icon', () => { + const { calls } = setup(); + pysimCardStateUpdate(status({})); + assert.deepStrictEqual(calls.connected, [false]); +}); + +test('Check status keeps the icon in step with the card state', async () => { + for (const entry of [ + [{ connected: true }, true], + [{ connected: false, card_present: true, equipping: true }, 'spin'], + [{ connected: false, card_present: true, auto_equip: true }, 'spin'], + [{ connected: false }, false], + ]) { + const { calls } = setup(); + globalThis.pysimFetch = async () => entry[0]; + await pysimRefresh(); + assert.deepStrictEqual(calls.connected, [entry[1]], JSON.stringify(entry[0])); + } +}); + test('unchanged state key does not touch the UI again', () => { const { el, calls } = setup(); pysimCardStateUpdate(status({ card_session: 7 })); diff --git a/pyproject.toml b/pyproject.toml index 1b455f8..231e854 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.5.4" +version = "3.5.5" description = "HTTP REST server wrapping pysim for the SIMple PWA" requires-python = ">=3.8" # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 678bfdb..a2eb4d3 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.5.4' +VERSION = '3.5.5' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE