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