From 5ae8b1fdd1cf84a564b023314ac7e5b339f5b3b7 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: Wed, 14 Oct 2026 23:32:04 +0300 Subject: [PATCH] v3.28.8: the batch panel's scroll preservation The attempts box's content is replaced on every render (the batch panel is static markup; the render fills the dynamic bits) and replacing a scrollable element's content resets its scroll. That was harmless while the render ran once per attempt, but v3.28.7 made it run on every 2 s poll, so scrolling back through the attempts during a run snapped to the top each poll. The render now captures the box's scrollTop before refilling it and restores it afterwards. Test: the stub box resets its scroll on an innerHTML write (like a browser) and the render must keep the reader's position - verified to fail without the restore. 920 frontend + 928 Python; sw cache simple-v446. --- frontend/index.html | 10 ++++++++-- frontend/sw.js | 2 +- frontend/tests/ram_batch.test.js | 28 ++++++++++++++++++++++++++++ pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 5 files changed, 39 insertions(+), 5 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index aadaadc..c7d0055 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1910,7 +1910,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.28.7'; +const SIMPLE_VERSION = '3.28.8'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -10267,7 +10267,13 @@ function ramBatchRender() { counts.textContent = text; } const box = document.getElementById('ram-batch-attempts'); - if (box) box.innerHTML = ramBatchAttemptsHtml(st.attempts || []); + if (box) { + // Replacing the box's content resets its scroll: the render now runs on + // every 2 s poll, so keep the reader's position (v3.28.8). + const prevScroll = box.scrollTop; + box.innerHTML = ramBatchAttemptsHtml(st.attempts || []); + if (prevScroll) box.scrollTop = prevScroll; + } } function ramBatchAttemptsHtml(attempts) { diff --git a/frontend/sw.js b/frontend/sw.js index 7d2b5b6..2c5d0d5 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v445'; +const CACHE = 'simple-v446'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram_batch.test.js b/frontend/tests/ram_batch.test.js index 8384de8..9532a3d 100644 --- a/frontend/tests/ram_batch.test.js +++ b/frontend/tests/ram_batch.test.js @@ -45,6 +45,13 @@ function setEl(id, value) { contains(c) { return !!els[id].classes[c]; }, }, }; + if (id === 'ram-batch-attempts') { + // like a browser: replacing the content resets the scroll position + Object.defineProperty(els[id], 'innerHTML', { + get() { return this._html || ''; }, + set(v) { this._html = v; this.scrollTop = 0; }, + }); + } return els[id]; } function clearEls() { @@ -302,6 +309,27 @@ test('a hung detailed fetch does not freeze the counts (v3.28.7)', async () => { app.hangs = false; }); +test('the attempts table keeps its scroll position across a poll render (v3.28.8)', () => { + clearEls(); + setEl('ram-op', 'batch'); + setEl('ram-batch-panel', ''); + setEl('ram-batch-start', ''); + setEl('ram-batch-stop', ''); + setEl('ram-batch-clear', ''); + setEl('ram-batch-spin', ''); + setEl('ram-batch-state', ''); + setEl('ram-batch-notice', ''); + setEl('ram-batch-counts', ''); + setEl('ram-batch-attempts', ''); + app.setLast({ active: true, phase: 'waiting_card', attempts: [ + { ts: 1, iccid: '8970119000004600098', preset: 'p', result: 'installed', duration_ms: 1 }], + counts: { installed: 1, failed: 0, skipped: 0 }, attempts_total: 1 }); + app.ramBatchRender(); + els['ram-batch-attempts'].scrollTop = 120; // the reader scrolled back + app.ramBatchRender(); // the next 2 s poll render + assert.strictEqual(els['ram-batch-attempts'].scrollTop, 120); +}); + test('a stale fetch guard is released so the report recovers (v3.28.7)', async () => { clearEls(); setEl('ram-op', 'batch'); diff --git a/pyproject.toml b/pyproject.toml index a8caa52..4565e59 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.28.7" +version = "3.28.8" 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 ee63dd9..12a9dec 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.28.7' +VERSION = '3.28.8' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE