From 215d0a0ba050c10abc126fb6bdba57729d4af588 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: Tue, 27 Oct 2026 18:58:18 +0300 Subject: [PATCH] frontend: fix equipped-card preset auto-selection race cardsAutoSelectByIccid latched its 'already handled' guard even when no matching preset was found, so a card equipped before /api/presets had loaded (the connect-time race with the status/availability polls) never got auto-selected: navigating to SCP80/RAM showed no preset until the card was re-inserted. The guard is now latched only after a successful selection, and cardsFetch re-runs the auto-selection once the store arrives, so the card's preset is picked as soon as both resources are available. Bumps VERSION to 3.40.3 and the SW cache to simple-v488. --- CHANGELOG.md | 12 ++++++++++++ frontend/index.html | 13 +++++++++++-- frontend/sw.js | 2 +- frontend/tests/cards_iccid.test.js | 31 ++++++++++++++++++++++++++++++ pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 6 files changed, 57 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e7588eb..5ac130a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,18 @@ every change carries a descriptive commit message. ## [Unreleased] +## [3.40.3] — 2026-10-27 + +### Fixed + +- **Equipped card not always auto-selected in SCP80/RAM.** The ICCID-based + preset auto-selection latched its "already handled" guard even when no + matching preset was found, so a card equipped before `/api/presets` had + loaded (the connect-time race) never got auto-selected — navigating to + SCP80/RAM showed no preset until the card was re-inserted. The guard is + now latched only after a successful selection, and `cardsFetch` re-runs + the auto-selection once the store arrives. + ## [3.40.2] — 2026-10-26 ### Added diff --git a/frontend/index.html b/frontend/index.html index 76c0762..4615d18 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1990,7 +1990,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.40.2'; +const SIMPLE_VERSION = '3.40.3'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -14315,6 +14315,11 @@ async function cardsFetch() { cardsRebuildSelect(); cardsRender(); tarRender(); + // A card may already be equipped when the presets arrive (the server + // starts with a card, or the fetch simply raced the first status poll): + // run the auto-selection now that the store is loaded. It is a no-op + // once the card's preset is already selected (the latched ICCID guard). + cardsAutoSelectByIccid(_pysimCardIccid); scp81PushPskMap(); spPresetWarningRender(); return true; @@ -15209,12 +15214,16 @@ let _cardsAutoIccid = null; function cardsAutoSelectByIccid(iccid) { // Act only when the equip reports a *new* ICCID: a manual preset choice // for the same card must not be overridden by later status updates. + // The guard is latched only once the card's preset is actually found and + // selected - a miss (the preset store not loaded yet, or no preset for + // this card) is retried when the store arrives (cardsFetch) so a card + // equipped before the presets loaded still gets auto-selected. const norm = cardsNormIccid(iccid); if (!norm) { _cardsAutoIccid = null; return -1; } if (norm === _cardsAutoIccid) return -1; - _cardsAutoIccid = norm; const idx = cardsFindByIccid(norm); if (idx < 0) return -1; + _cardsAutoIccid = norm; const sp = document.getElementById('sp-card-sel'); if (sp) sp.value = String(idx); const ram = document.getElementById('ram-card-sel'); diff --git a/frontend/sw.js b/frontend/sw.js index ec983b0..7a3f747 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v487'; +const CACHE = 'simple-v488'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/cards_iccid.test.js b/frontend/tests/cards_iccid.test.js index 7d04d43..02f5ce0 100644 --- a/frontend/tests/cards_iccid.test.js +++ b/frontend/tests/cards_iccid.test.js @@ -146,6 +146,30 @@ test('cardsAutoSelectByIccid reacts to a card swap and to unreadable ICCIDs', () assert.strictEqual(cardsAutoSelectByIccid('1234567890123456789'), -1); }); +test('a miss is not latched, so the store arriving later still auto-selects', () => { + // The card equips before /api/presets fills cards[] (the connect-time + // race): the auto-selection must not latch the ICCID on a miss, or the + // later cardsFetch would be a no-op and the preset would stay unselected + // until the card is re-inserted. + const els = fakeDoc(); + const applied = []; + globalThis.cardsApply = idx => applied.push(idx); + globalThis.cards = []; // presets not loaded yet + globalThis._ramCardIdx = null; + globalThis._cardsAutoIccid = null; + assert.strictEqual(cardsAutoSelectByIccid(DIGITS), -1); + assert.strictEqual(globalThis._cardsAutoIccid, null, + 'a missed auto-selection must not latch the ICCID'); + // the presets arrive: the retry (from cardsFetch) finds and selects the card + globalThis.cards = [{ name: 'A', iccid: RAW_HEX }]; + assert.strictEqual(cardsAutoSelectByIccid(DIGITS), 0); + assert.strictEqual(els['sp-card-sel'].value, '0'); + assert.strictEqual(els['ram-card-sel'].value, '0'); + assert.strictEqual(els['tar-card-sel'].value, '0'); + assert.deepStrictEqual(applied, ['0']); + assert.strictEqual(globalThis._ramCardIdx, 0); +}); + test('cardsFindDuplicateIccid catches duplicates across stored formats', () => { globalThis.cards = [ { name: 'A', iccid: '1111111111111111111' }, @@ -201,6 +225,13 @@ test('the card-state update wires the ICCID into the preset selection', () => { assert.ok(html.includes("(data.iccid ? ' | ICCID: ' + esc(data.iccid) + '' : '')")); }); +test('cardsFetch retries the auto-selection once the presets load', () => { + // The equip may be reported before /api/presets resolves, so the fetch + // must re-run the auto-selection (a no-op once the card is selected). + assert.ok(html.includes('cardsAutoSelectByIccid(_pysimCardIccid);'), + 'cardsFetch must re-run the auto-selection after loading the store'); +}); + test('cardsRender highlights the equipped card preset', () => { globalThis.t = s => s; const els = { 'cards-tbody': { innerHTML: '' } }; diff --git a/pyproject.toml b/pyproject.toml index 7429f40..114232e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.40.2" +version = "3.40.3" description = "HTTP REST server wrapping pysim for the SIMple PWA" license = { text = "GPL-2.0-or-later" } requires-python = ">=3.9" diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index e7edd71..db1a422 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.40.2' +VERSION = '3.40.3' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE