fix: cached GETs bypass the card lock; compact install step lines (v3.6.47)

The install modal's progress bar froze around 70% and jumped to 100% at
the end: not a math bug - the UI's other polls (/api/stk-status,
/api/poll-status, /api/proactive-log, /api/menu, ...) were card-locked in
do_GET even though they only read cached state, queued behind the install
and exhausted the browser's 6-connections-per-host budget, starving
/api/status.  _CARD_FREE_GET lists every card-free GET (status, version,
stk-status, poll-status, proactive-log, menu, events, terminal-profile,
pli-*, commands, net-state, mcc-mnc, scp81/bip status|script|log);
/api/cardinfo and /api/esim/* stay locked (they touch the card).

tests/test_fetch_routes.py holds _CARD_LOCK and asserts the cached
endpoints still answer - verified to time out on the pre-fix list.

The install modal is wider (max-w-3xl) and successful step lines are
compact: '✅ Step 3: LOAD (1/240) - 6101 · 274 B / 3 SMS' (no verbose SW
name, B instead of bytes, the SMS count only when >1); failed steps keep
the full detail and the hint.

sw cache simple-v320; tests: 533 Python / 684 Node.
This commit is contained in:
2026-09-30 00:02:52 +03:00
parent 55adeb0fbd
commit ef949e6a17
7 changed files with 118 additions and 33 deletions
+27 -16
View File
@@ -1725,7 +1725,7 @@
</div>
<div id="ram-op-modal" class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center hidden">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 max-w-lg w-full max-h-[85vh] flex flex-col">
<div class="bg-white dark:bg-slate-800 rounded-lg p-4 max-w-3xl w-full max-h-[85vh] flex flex-col">
<div class="flex justify-between items-center mb-2 gap-3">
<span id="ram-op-title" class="font-bold text-sm flex-1"></span>
<button id="ram-op-close" onclick="ramOpModalClose()" disabled class="px-3 py-1 text-xs rounded-full bg-gray-200 dark:bg-slate-700 text-gray-700 dark:text-slate-300 hover:bg-gray-300 dark:hover:bg-slate-600 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Close">Close</button>
@@ -1746,7 +1746,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.6.46';
const SIMPLE_VERSION = '3.6.47';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -9432,20 +9432,31 @@ function ramStepLine(s, idx, comps, blockSize) {
const ok = !s.por_error && (s.por_status === 'por_ok' || s.por_status === 'no_por');
let line = (ok ? '\u2705' : '\u274c') + ' ' + t('Step') + ' ' + (idx + 1) + ': ' +
s.name + ' \u2014 ';
if (s.por_status === 'por_ok') line += t('PoR ok');
else if (s.por_status === 'no_por') line += t('no PoR');
else if (s.por_status && s.por_status.indexOf('por_error') === 0) {
const legacy = s.por_status.replace(/^por_error_?/, '');
line += t('PoR error') + (legacy ? ' ' + legacy : '');
} else if (s.por_status) line += t('PoR error') + ' ' + s.por_status;
else line += t('PoR unknown');
if (s.por_sw) {
const name = lookupSw(s.por_sw.substr(0, 2), s.por_sw.substr(2, 2), 'gp');
line += ' \u00b7 ' + t('remote SW') + ' ' + s.por_sw + (name ? ' (' + name + ')' : '');
}
if (s.sw && s.sw !== '9000') line += ' \u00b7 ' + t('transport SW') + ' ' + s.sw;
if (s.bytes && s.segments) {
line += ' \u00b7 ' + s.bytes + ' ' + t('bytes') + ' / ' + s.segments + ' SMS';
if (ok) {
// successful step: the verdict symbol carries the meaning - keep the
// remote status word (without its verbose name), the size and the SMS
// count only when the packet was concatenated
if (s.por_sw) line += s.por_sw;
else if (s.por_status === 'no_por') line += t('no PoR');
else line += t('OK');
if (s.bytes) line += ' \u00b7 ' + s.bytes + ' B';
if (s.segments > 1) line += ' / ' + s.segments + ' SMS';
} else {
if (s.por_status === 'por_ok') line += t('PoR ok');
else if (s.por_status === 'no_por') line += t('no PoR');
else if (s.por_status && s.por_status.indexOf('por_error') === 0) {
const legacy = s.por_status.replace(/^por_error_?/, '');
line += t('PoR error') + (legacy ? ' ' + legacy : '');
} else if (s.por_status) line += t('PoR error') + ' ' + s.por_status;
else line += t('PoR unknown');
if (s.por_sw) {
const name = lookupSw(s.por_sw.substr(0, 2), s.por_sw.substr(2, 2), 'gp');
line += ' \u00b7 ' + t('remote SW') + ' ' + s.por_sw + (name ? ' (' + name + ')' : '');
}
if (s.sw && s.sw !== '9000') line += ' \u00b7 ' + t('transport SW') + ' ' + s.sw;
if (s.bytes && s.segments) {
line += ' \u00b7 ' + s.bytes + ' ' + t('bytes') + ' / ' + s.segments + ' SMS';
}
}
const sc = ramStepComponents(s.name, comps, blockSize);
if (sc) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v319';
const CACHE = 'simple-v320';
const URLS = [
'index.html',
'help.html',
+6
View File
@@ -486,3 +486,9 @@ test('the RAM form shows Card preset and Operation in one row', () => {
const row = /<div class="mb-3 flex gap-2 items-end">\s*<div class="flex-1">\s*<label[^>]*data-l10n="Card preset"[\s\S]*?id="ram-card-sel"[\s\S]*?data-l10n="Operation"[\s\S]*?id="ram-op"[\s\S]*?data-l10n="Execute"/.exec(html);
assert.ok(row, 'Card preset + Operation + Execute must share one row');
});
test('the RAM operation modal is wide enough for the step lines', () => {
const m = /<div id="ram-op-modal"[\s\S]*?<div class="([^"]*max-w-[^"]*)"/.exec(html);
assert.ok(m, 'RAM op modal not found');
assert.ok(m[1].includes('max-w-3xl'), 'the modal was widened to fit the step lines: ' + m[1]);
});
+6 -4
View File
@@ -374,12 +374,14 @@ test('ramGetStatusApdu builds the compact chain, never the expanded form', () =>
test('ramStepLine shows the PoR verdict and the remote status word', () => {
globalThis.t = s => s;
globalThis.lookupSw = (a, b) => (a + b === '6700' ? 'Wrong length in Lc' : '');
// successful steps are compact: verdict symbol, remote SW (no name),
// size; the SMS count only for concatenated packets
const okLine = ramStepLine({ name: 'INSTALL [for load]', por_status: 'por_ok',
por_sw: '9000', sw: '9000', bytes: 50, segments: 1 }, 0);
assert.ok(okLine.startsWith('\u2705'), okLine);
assert.ok(okLine.includes('PoR ok'), okLine);
assert.ok(okLine.includes('remote SW 9000'), okLine);
assert.ok(okLine.includes('50 bytes / 1 SMS'), okLine);
assert.strictEqual(okLine, '\u2705 Step 1: INSTALL [for load] \u2014 9000 \u00b7 50 B');
const multi = ramStepLine({ name: 'LOAD (1/64)', por_status: 'por_ok',
por_sw: '6101', bytes: 274, segments: 3 }, 1);
assert.strictEqual(multi, '\u2705 Step 2: LOAD (1/64) \u2014 6101 \u00b7 274 B / 3 SMS');
const badLine = ramStepLine({ name: 'LOAD (1/9)', por_status: 'por_ok', por_sw: '6700',
por_error: 'remote SW 6700', sw: '9000', bytes: 274, segments: 3 }, 1);
assert.ok(badLine.startsWith('\u274c'), badLine);