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.
This commit is contained in:
+43
-18
@@ -684,6 +684,7 @@
|
||||
<textarea id="sp-result" rows="3" readonly class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 bg-gray-100 dark:bg-slate-800"></textarea>
|
||||
<div id="sp-size-info" class="text-xs mt-1 text-gray-500 dark:text-slate-400"></div>
|
||||
<div id="sp-send-result" class="mt-2 text-xs font-mono hidden"></div>
|
||||
<div id="sp-cntr-low" class="mt-2 hidden"></div>
|
||||
<div id="sp-verify-result" class="mt-2 text-xs font-mono hidden"></div>
|
||||
|
||||
</div>
|
||||
@@ -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 '<div class="flex items-center gap-3 text-sm text-red-600 dark:text-red-400">' +
|
||||
'<span>' + text + '</span>' +
|
||||
'<button type="button" onclick="ramSyncCounter(' + idx + ', \'' + v + '\')" class="' + btnCls + '">' +
|
||||
'<button type="button" onclick="ramSyncCounter(' + idx + ', \'' + v + '\')" class="' + syncCls + '">' +
|
||||
esc(t('Sync counter')) + '</button>' +
|
||||
'<button type="button" onclick="ramGoToPreset(' + idx + ')" class="' + btnCls + '">' +
|
||||
'<button type="button" onclick="ramGoToPreset(' + idx + ')" class="' + goCls + '">' +
|
||||
esc(t('Go to preset')) + '</button></div>';
|
||||
}
|
||||
|
||||
// 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 = '<div class="text-sm text-gray-500 dark:text-slate-400">' +
|
||||
esc(t('Syncing counter...')) + '</div>';
|
||||
if (box) box.innerHTML = '<div class="flex items-center gap-2 text-sm text-gray-500 dark:text-slate-400">' +
|
||||
'<div class="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>' +
|
||||
'<span>' + esc(t('Syncing counter...')) + '</span></div>';
|
||||
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);
|
||||
}
|
||||
|
||||
+35
-2
@@ -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);
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v415';
|
||||
const CACHE = 'simple-v416';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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(/<button[^>]*ramSyncCounter[^>]*>/)[0];
|
||||
assert.ok(syncBtn.includes('border-amber-300'), syncBtn);
|
||||
assert.ok(!syncBtn.includes('border-red-300'), syncBtn);
|
||||
const goBtn = out.match(/<button[^>]*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');
|
||||
|
||||
@@ -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',
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user