From 17863001f31e5e765f4465cb5225857e097a624e 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, 7 Oct 2026 10:28:42 +0300 Subject: [PATCH] v3.23.3: the low-counter notice polish The two buttons under a cntr_low verdict shared the red warning style, and the notice only ever rendered in the RAM pill: an SCP80 send wrote it into the hidden RAM container, so the plain-send view showed the verdict text but offered no way to act on it. - The counter probe button is amber and the preset jump green (the warning text stays red). - The probe renders the inline throbber the RAM operations use (animate-spin, no modal) while it runs; its result replaces it in the same box. - Each pill carries its own notice box (#ram-cntr-low / #sp-cntr-low): ramShowCntrLow/ramSyncCounter target the originating view's box and clear the other, the SCP80 send passes 'sp' explicitly (it used to rely on the keyset-view heuristic), and a new send attempt clears the stale verdict (ramClearResults clears both boxes). - The prebuilt Tailwind output was regenerated (npm run build): border-amber-300/-700 and border-emerald-300/-700 were absent from style.css (the stale-style trap). Tests: the button classes, the spinner and the box targeting pinned in ram.test.js. 846 frontend + 830 Python; sw cache simple-v416. --- frontend/index.html | 61 +++++++++++++++++-------- frontend/style.css | 37 ++++++++++++++- frontend/sw.js | 2 +- frontend/tests/ram.test.js | 16 ++++++- frontend/tests/ram_counter_flow.test.js | 1 + pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 7 files changed, 97 insertions(+), 24 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 1e9f268..661bab9 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -684,6 +684,7 @@
+ @@ -1904,7 +1905,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.23.2'; +const SIMPLE_VERSION = '3.23.3'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -9639,6 +9640,9 @@ async function pysimSendOta() { sendResultEl.classList.add('text-gray-500'); sendResultEl.textContent = t('Sending OTA...'); porStatusEl.classList.add('hidden'); + // a new attempt invalidates the previous low-counter verdict (the notice + // stays in the pill until then; v3.23.3) + ramCntrLowClear(); try { const data = await pysimFetch('/api/send-ota', { sp, ...spParams() }); // low counter: not recoverable by retrying (v3.6.58) @@ -9646,7 +9650,7 @@ async function pysimSendOta() { sendResultEl.textContent = t('Low counter') + ' — ' + t('the card rejected the packet'); sendResultEl.classList.remove('text-gray-500', 'text-green-600'); sendResultEl.classList.add('text-red-600'); - ramShowCntrLow(); + ramShowCntrLow(undefined, 'sp'); return; } if (data.success) { @@ -9898,8 +9902,7 @@ function ramClearResults() { document.getElementById('ram-explorer').innerHTML = ''; document.getElementById('ram-steps').classList.add('hidden'); document.getElementById('ram-steps').textContent = ''; - const cntrLowEl = document.getElementById('ram-cntr-low'); - if (cntrLowEl) { cntrLowEl.innerHTML = ''; cntrLowEl.classList.add('hidden'); } + ramCntrLowClear(); } // Read a File as a hex string via FileReader (client-side, no upload) @@ -11576,7 +11579,7 @@ async function ramExplore(sp) { // the run was stopped: state the verdict, do not retry resultEl.textContent = t('Low counter') + ' — ' + t('the operation was stopped'); resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600'); - ramShowCntrLow(); + ramShowCntrLow(undefined, 'ram'); } else if (errors.length) { resultEl.textContent = t('Partial') + ' — ' + errors.join('; '); resultEl.classList.remove('hidden', 'text-green-600'); resultEl.classList.add('text-red-600'); @@ -11685,7 +11688,7 @@ async function ramDeleteFromExplorer(aid, withCascade) { const el = document.getElementById('ram-result'); el.textContent = t('Low counter') + ' — ' + t('the operation was stopped'); el.classList.remove('hidden', 'text-green-600'); el.classList.add('text-red-600'); - ramShowCntrLow(); + ramShowCntrLow(undefined, 'ram'); return; } // The card consumes the counter as soon as it accepts the packet, even @@ -12119,7 +12122,7 @@ async function ramCompatTest(sp) { if (spCntrLow(data)) { const v = ramCntrLowView(); modal += ramCntrLowHtml(ramCntrLowPresetIdx(v), v); - ramShowCntrLow(undefined, v); + ramShowCntrLow(undefined, 'ram'); } ramOpModalFinish(verdict, !!data.imports_ok, modal, true); } @@ -12510,23 +12513,44 @@ function ramCntrLowHtml(presetIdx, view) { const idx = Number.isInteger(presetIdx) && presetIdx >= 0 ? presetIdx : -1; const v = view === 'sp' ? 'sp' : 'ram'; const text = t('Low counter') + ' \u2014 ' + t('the card rejected the packet'); - const btnCls = 'px-2 py-1 text-xs rounded border border-red-300 dark:border-red-700 ' + - 'hover:bg-red-100 dark:hover:bg-red-900/40 whitespace-nowrap'; + // the probe is amber (a corrective action), the preset jump green (the + // manual fix) - v3.23.3, they used to share the red warning style + const base = 'px-2 py-1 text-xs rounded border whitespace-nowrap '; + const syncCls = base + 'border-amber-300 dark:border-amber-700 text-amber-700 ' + + 'dark:text-amber-300 hover:bg-amber-100 dark:hover:bg-amber-900/40'; + const goCls = base + 'border-emerald-300 dark:border-emerald-700 text-emerald-700 ' + + 'dark:text-emerald-300 hover:bg-emerald-100 dark:hover:bg-emerald-900/40'; return '
' + '' + text + '' + - '' + - '
'; } -// Show the low-counter notice in the RAM result area (and, when the operation -// modal is still open, in its detail block). +// The low-counter notice box of a view: the RAM and Secured packet pills +// each carry one, so the notice (and the counter probe's progress) is visible +// in the view the failed operation ran in (v3.23.3). +function ramCntrLowBox(view) { + return document.getElementById(view === 'sp' ? 'sp-cntr-low' : 'ram-cntr-low'); +} + +function ramCntrLowClear() { + ['ram-cntr-low', 'sp-cntr-low'].forEach(id => { + const el = document.getElementById(id); + if (el) { el.innerHTML = ''; el.classList.add('hidden'); } + }); +} + +// Show the low-counter notice in the originating view's box (the other one is +// cleared: only the latest verdict stays on screen) and, when the operation +// modal is still open, in its detail block. function ramShowCntrLow(presetIdx, view) { const v = view === undefined ? ramCntrLowView() : view; const idx = presetIdx === undefined ? ramCntrLowPresetIdx(v) : presetIdx; const html = ramCntrLowHtml(idx, v); - const el = document.getElementById('ram-cntr-low'); + ramCntrLowClear(); + const el = ramCntrLowBox(v); if (el && el.classList) { el.innerHTML = html; el.classList.remove('hidden'); @@ -12545,7 +12569,7 @@ function ramShowCntrLow(presetIdx, view) { async function ramSyncCounter(presetIdx, view) { const i = Number(presetIdx); const preset = (Number.isInteger(i) && i >= 0 && cards[i]) ? cards[i] : null; - const box = document.getElementById('ram-cntr-low'); + const box = ramCntrLowBox(view === 'sp' ? 'sp' : 'ram'); if (!preset) { alert(t('No card preset selected')); return; } // The keyset the failed operation used: the originating view's selector // first, the other view's as the fallback (both hold the keyset number as @@ -12572,8 +12596,9 @@ async function ramSyncCounter(presetIdx, view) { else if (val('sp-spi2-hex')) body.spi2 = val('sp-spi2-hex'); const cntr = val('sp-cntr').replace(/[^0-9a-fA-F]/g, ''); if (cntr) body.cntr = cntr; - if (box) box.innerHTML = '
' + - esc(t('Syncing counter...')) + '
'; + if (box) box.innerHTML = '
' + + '
' + + '' + esc(t('Syncing counter...')) + '
'; try { const data = await pysimFetch('/api/counter-probe', body); if (data.success || data.counter_saved) { @@ -12677,7 +12702,7 @@ function ramShowInstallResult(data, okText, capMem) { if (spCntrLow(data)) { const v = ramCntrLowView(); modal += ramCntrLowHtml(ramCntrLowPresetIdx(v), v); - ramShowCntrLow(undefined, v); + ramShowCntrLow(undefined, 'ram'); } ramOpModalFinish(resultEl.textContent, !!data.success, modal, true); } diff --git a/frontend/style.css b/frontend/style.css index c647f01..e9f5164 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -1307,6 +1307,11 @@ video { border-top-width: 1px; } +.border-amber-300 { + --tw-border-opacity: 1; + border-color: rgb(252 211 77 / var(--tw-border-opacity, 1)); +} + .border-amber-400 { --tw-border-opacity: 1; border-color: rgb(251 191 36 / var(--tw-border-opacity, 1)); @@ -1327,6 +1332,11 @@ video { border-color: rgb(59 130 246 / var(--tw-border-opacity, 1)); } +.border-emerald-300 { + --tw-border-opacity: 1; + border-color: rgb(110 231 183 / var(--tw-border-opacity, 1)); +} + .border-gray-100 { --tw-border-opacity: 1; border-color: rgb(243 244 246 / var(--tw-border-opacity, 1)); @@ -1887,6 +1897,11 @@ video { color: #f1f5f9; } +.hover\:bg-amber-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1)); +} + .hover\:bg-amber-700:hover { --tw-bg-opacity: 1; background-color: rgb(180 83 9 / var(--tw-bg-opacity, 1)); @@ -1897,6 +1912,11 @@ video { background-color: rgb(29 78 216 / var(--tw-bg-opacity, 1)); } +.hover\:bg-emerald-100:hover { + --tw-bg-opacity: 1; + background-color: rgb(209 250 229 / var(--tw-bg-opacity, 1)); +} + .hover\:bg-emerald-200:hover { --tw-bg-opacity: 1; background-color: rgb(167 243 208 / var(--tw-bg-opacity, 1)); @@ -2017,14 +2037,19 @@ video { border-color: rgb(217 119 6 / var(--tw-border-opacity, 1)); } +.dark\:border-amber-700:is(.dark *) { + --tw-border-opacity: 1; + border-color: rgb(180 83 9 / var(--tw-border-opacity, 1)); +} + .dark\:border-blue-800:is(.dark *) { --tw-border-opacity: 1; border-color: rgb(30 64 175 / var(--tw-border-opacity, 1)); } -.dark\:border-red-700:is(.dark *) { +.dark\:border-emerald-700:is(.dark *) { --tw-border-opacity: 1; - border-color: rgb(185 28 28 / var(--tw-border-opacity, 1)); + border-color: rgb(4 120 87 / var(--tw-border-opacity, 1)); } .dark\:border-red-800:is(.dark *) { @@ -2225,6 +2250,14 @@ video { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.dark\:hover\:bg-amber-900\/40:hover:is(.dark *) { + background-color: rgb(120 53 15 / 0.4); +} + +.dark\:hover\:bg-emerald-900\/40:hover:is(.dark *) { + background-color: rgb(6 78 59 / 0.4); +} + .dark\:hover\:bg-red-900\/40:hover:is(.dark *) { background-color: rgb(127 29 29 / 0.4); } diff --git a/frontend/sw.js b/frontend/sw.js index aa91ce0..5f41cd3 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v415'; +const CACHE = 'simple-v416'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 319060b..4bdb9f8 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -28,6 +28,7 @@ const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamB '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'ramParseModuleAids', 'parseTLV', '_parseE3Entry', '_parseMenuEntries', 'ramExpandedReport', 'ramExpandedMore', 'ramPageFormats', 'ramAppsMissingDetails', 'ramExpandedTags', 'ramExpandedGroups', 'ramExpandedElfForm', 'ramElfVersionHint', 'cardEtsiRelease', 'ramChainGetResponse', 'ramDeriveElfVersions', 'ramElfAppletCandidate', 'ramElfAppMatch', 'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow', + 'ramCntrLowBox', 'ramCntrLowClear', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetOptionsHtml', 'spPresetIdx', 'tarPresetIdx', 'cardsKeysetsFromForm', 'spKeysetSync', 'spKeysetApply', 'spKeysetChanged', @@ -913,6 +914,18 @@ test('ramCntrLowHtml states the facts without a fabricated counter', () => { assert.ok(noPreset.includes('onclick="ramGoToPreset(-1)"'), noPreset); assert.ok(noPreset.includes('onclick="ramSyncCounter(-1, \'ram\')"'), noPreset); assert.ok(spView.includes('onclick="ramSyncCounter(1, \'sp\')"'), spView); + // v3.23.3: the probe button is amber, the preset jump green + const syncBtn = out.match(/]*ramSyncCounter[^>]*>/)[0]; + assert.ok(syncBtn.includes('border-amber-300'), syncBtn); + assert.ok(!syncBtn.includes('border-red-300'), syncBtn); + const goBtn = out.match(/]*ramGoToPreset[^>]*>/)[0]; + assert.ok(goBtn.includes('border-emerald-300'), goBtn); + // the notice targets the originating view's box; the probe renders the + // inline throbber (the RAM inline-progress spinner, no modal) + assert.match(extractFunc(html, 'ramShowCntrLow'), /ramCntrLowBox\(v\)/); + assert.match(extractFunc(html, 'ramSyncCounter'), /animate-spin/); + assert.match(extractFunc(html, 'ramSyncCounter'), /ramCntrLowBox\(/); + assert.match(html, /id="sp-cntr-low"/); }); test('the explorer shows versions on app and ISD rows too', () => { @@ -963,7 +976,8 @@ test('every SCP80/RAM flow checks the low counter and stops', () => { const explore = extractFunc(html, 'ramExplore'); assert.ok(explore.includes('spCntrLow(res)'), 'the Explore must check every send'); assert.ok(explore.includes('if (cntrLow) break;'), 'the loops must stop'); - assert.ok(explore.includes('ramShowCntrLow()'), 'the verdict must be shown'); + assert.ok(explore.includes("ramShowCntrLow(undefined, 'ram')"), + 'the verdict must be shown in the RAM view'); // the delete flow and the plain send check it too assert.ok(extractFunc(html, 'ramDeleteFromExplorer').includes('spCntrLow(res)'), 'the delete flow must stop'); diff --git a/frontend/tests/ram_counter_flow.test.js b/frontend/tests/ram_counter_flow.test.js index 0e49c0b..f730726 100644 --- a/frontend/tests/ram_counter_flow.test.js +++ b/frontend/tests/ram_counter_flow.test.js @@ -32,6 +32,7 @@ for (const fn of ['berLenStr', 'ramDeleteApdu', 'ramIncrementCntr', 'ramSaveCntr 'spPresetIdx', 'spPresetCounterValue', 'spCntrPlaceholder', 'spCntrAhead', 'ramHideProgress', 'ramDeleteFromExplorer', 'spCntrLow', 'ramCntrLowHtml', 'ramCntrLowPresetIdx', 'ramCntrLowView', 'ramShowCntrLow', + 'ramCntrLowBox', 'ramCntrLowClear', 'escHtml', 'esc', 'spKeysetKvnOf', 'spKeysetList', 'spKeysetFor', 'spKeysetCheck', 'spKeysetOptionsHtml', 'cardsKeysetRowHtml', 'cardsKeysetRowsRender', diff --git a/pyproject.toml b/pyproject.toml index 2492189..c5644df 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.23.2" +version = "3.23.3" 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 f89d30e..e2db4fe 100644 --- a/pysim_simple_server/server.py +++ b/pysim_simple_server/server.py @@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE from osmocom.utils import rpad -VERSION = '3.23.2' +VERSION = '3.23.3' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE