diff --git a/frontend/index.html b/frontend/index.html index a21908b..d043dd5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -23,6 +23,7 @@ + @@ -1383,7 +1384,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 = '2.5.1'; +const SIMPLE_VERSION = '2.5.2'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -4803,6 +4804,7 @@ let _pysimCardEquipped = false; let _pysimEquipping = false; let _pysimAdmVerified = null; // null = no card session let _pysimCardIccid = null; // EF.ICCID digits of the equipped card (null = unknown) +let _pysimHeaderIccid = undefined; // last value rendered in the header indicator let _pysimAvailabilityTimer = null; function pysimAvailabilityState() { @@ -4885,10 +4887,30 @@ function pysimUpdateAdmIndicator(status) { el.setAttribute('title', t(verified ? 'ADM verified' : 'ADM not verified')); } +// EF.ICCID digits of the equipped card, printed next to the card image +// (hidden while there is no card session or the ICCID is unreadable). +function pysimUpdateIccidIndicator(iccid) { + const el = document.getElementById('state-indicator-iccid'); + if (!el) return; + const value = iccid || null; + if (value === _pysimHeaderIccid) return; + _pysimHeaderIccid = value; + if (!value) { + el.classList.add('hidden'); + el.textContent = ''; + return; + } + el.textContent = value; + el.classList.remove('hidden'); +} + function pysimSetServerAvailable(available) { if (_pysimServerAvailable === available) return; _pysimServerAvailable = available; - if (available !== true) pysimUpdateAdmIndicator(null); + if (available !== true) { + pysimUpdateAdmIndicator(null); + pysimUpdateIccidIndicator(null); + } pysimApplyAvailability(); } @@ -7886,6 +7908,7 @@ function pysimCardStateUpdate(status) { _pysimEquipping = !!status.equipping; _pysimCardIccid = status.connected ? (status.iccid || null) : null; pysimUpdateAdmIndicator(status); + pysimUpdateIccidIndicator(_pysimCardIccid); pysimApplyAvailability(); if (pysimProactiveSeqChanged(status.proactive_seq) && isViewVisible('tab-phone') && isViewVisible('phone-sub-phone')) { diff --git a/frontend/sw.js b/frontend/sw.js index 9c59592..2a256eb 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v196'; +const CACHE = 'simple-v197'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/card_state.test.js b/frontend/tests/card_state.test.js index 85c0b58..3c63b5b 100644 --- a/frontend/tests/card_state.test.js +++ b/frontend/tests/card_state.test.js @@ -24,6 +24,7 @@ function extractFunc(src, name) { let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n' + 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n' + 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\nvar _pysimAdmVerified = null;\n' + + 'var _pysimHeaderIccid = undefined;\n' + 'var _cardsAutoIccid = null;\nvar _pysimCardIccid = null;\n'; code += extractFunc(html, 'pysimCardStateUpdate') + '\n'; code += extractFunc(html, 'pysimAvailabilityState') + '\n'; @@ -31,6 +32,7 @@ code += extractFunc(html, 'pysimControlDisabled') + '\n'; code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n'; code += extractFunc(html, 'pysimStkStatusChanged') + '\n'; code += extractFunc(html, 'pysimUpdateAdmIndicator') + '\n'; +code += extractFunc(html, 'pysimUpdateIccidIndicator') + '\n'; code += extractFunc(html, 'pysimSetServerAvailable') + '\n'; code += '\nglobalThis.esc = s => s;\n'; code += 'globalThis.t = s => s;\n'; @@ -54,16 +56,19 @@ function fakeIndicator() { function setup() { const el = { textContent: 'status line', innerHTML: '' }; const adm = fakeIndicator(); + const iccidEl = fakeIndicator(); const calls = { connected: [], resets: [], refreshStatus: [], proactive: 0, autoIccid: [] }; _pysimCardStateKey = null; _pysimCardSession = null; _pysimProactiveSeq = null; _pysimAdmVerified = null; + _pysimHeaderIccid = undefined; _pysimServerAvailable = null; _cardsAutoIccid = null; _pysimCardIccid = null; globalThis.document = { - getElementById: id => id === 'state-indicator-adm' ? adm : el, + getElementById: id => id === 'state-indicator-adm' ? adm + : (id === 'state-indicator-iccid' ? iccidEl : el), querySelectorAll: () => [], }; globalThis.pysimSetConnected = v => calls.connected.push(v); @@ -72,7 +77,7 @@ function setup() { globalThis.isViewVisible = () => true; globalThis.pysimProactiveLogRender = () => { calls.proactive++; }; globalThis.cardsAutoSelectByIccid = iccid => { calls.autoIccid.push(iccid); return -1; }; - return { el, adm, calls }; + return { el, adm, iccidEl, calls }; } function status(extra) { @@ -250,3 +255,18 @@ test('losing the server hides the ADM badge', () => { pysimSetServerAvailable(false); assert.ok(adm.classes.has('hidden')); }); + +test('the header indicator prints the equipped card ICCID', () => { + const { iccidEl } = setup(); + pysimCardStateUpdate(status({ connected: true, card_present: true, card_session: 2, iccid: '89701450001700031958' })); + assert.strictEqual(iccidEl.textContent, '89701450001700031958'); + assert.ok(!iccidEl.classes.has('hidden')); + // equipped but unreadable -> hidden again + pysimCardStateUpdate(status({ connected: true, card_present: true, card_session: 2, iccid: null })); + assert.strictEqual(iccidEl.textContent, ''); + assert.ok(iccidEl.classes.has('hidden')); + // losing the server hides it too + pysimCardStateUpdate(status({ connected: true, card_present: true, card_session: 2, iccid: '89701450001700031958' })); + pysimSetServerAvailable(false); + assert.ok(iccidEl.classes.has('hidden')); +}); diff --git a/frontend/tests/indicator.test.js b/frontend/tests/indicator.test.js index 2803cf3..ae8f332 100644 --- a/frontend/tests/indicator.test.js +++ b/frontend/tests/indicator.test.js @@ -21,12 +21,13 @@ function extractFunc(src, name) { return src.slice(m.index, i + 1); } -let code = 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\nvar _pysimEquipping = false;\nvar _pysimCardIccid = null;\n'; +let code = 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\nvar _pysimEquipping = false;\nvar _pysimCardIccid = null;\nvar _pysimHeaderIccid = undefined;\n'; code += extractFunc(html, 'pysimAvailabilityState') + '\n'; code += extractFunc(html, 'pysimControlDisabled') + '\n'; code += extractFunc(html, 'pysimNeedsHint') + '\n'; code += extractFunc(html, 'pysimApplyAvailability') + '\n'; code += extractFunc(html, 'pysimUpdateStateIndicator') + '\n'; +code += extractFunc(html, 'pysimUpdateIccidIndicator') + '\n'; code += 'globalThis.t = s => s;\n'; eval(code); @@ -51,6 +52,7 @@ function setup() { 'state-indicator': fakeEl(), 'state-indicator-dot': fakeEl(), 'state-indicator-img': fakeEl(), + 'state-indicator-iccid': fakeEl(), }; els['state-indicator-img'].src = ''; globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] }; @@ -58,6 +60,7 @@ function setup() { _pysimCardEquipped = false; _pysimEquipping = false; _pysimCardIccid = null; + _pysimHeaderIccid = undefined; return els; } @@ -132,6 +135,21 @@ test('indicator markup carries the dot and image elements', () => { assert.match(html, /id="state-indicator-img"[^>]*src="nosim\.svg"/); }); +test('header prints the equipped card ICCID next to the card image', () => { + const els = setup(); + pysimUpdateIccidIndicator('89701450001700031958'); + const el = els['state-indicator-iccid']; + assert.strictEqual(el.textContent, '89701450001700031958'); + assert.ok(!el.classes.has('hidden')); + // no card session -> hidden again + pysimUpdateIccidIndicator(null); + assert.strictEqual(el.textContent, ''); + assert.ok(el.classes.has('hidden')); + // markup order: image, ICCID, ADM badge + assert.ok(html.indexOf('id="state-indicator-img"') < html.indexOf('id="state-indicator-iccid"')); + assert.ok(html.indexOf('id="state-indicator-iccid"') < html.indexOf('id="state-indicator-adm"')); +}); + test('card-iccid controls need an equipped card with a readable ICCID', () => { const check = (state, iccid) => { const el = fakeEl(); diff --git a/pyproject.toml b/pyproject.toml index 610d066..5352811 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "2.5.1" +version = "2.5.2" 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 457a57b..ade76dc 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -24,7 +24,7 @@ from osmocom.construct import GsmOrUcs2Adapter from osmocom.tlv import BER_TLV_IE -VERSION = '2.5.1' +VERSION = '2.5.2' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE