From 91996a941ba0e85134e0e4b3c5ed7b1aef5e887a 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 18:04:22 +0300 Subject: [PATCH] v3.28.1: the batch panel polish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - A throbber (#ram-batch-spin) next to the phase line shows while a card is actually being installed (active && current - the stopping-mid- install phase included) and hides again while waiting. - The waiting prompt is explicit now: "Insert next card. Waiting for a card..." (RU "Вставьте следующую карту. Ожидание карты…"); the old "Waiting for a card..." key is gone. - The notice reports only the last *finished* card: starting the next card's install clears it (state['notice'] = '' alongside current/installing in _ram_batch_handle_card), so the previous card's verdict never lingers over the next install. It stays visible while waiting - the "report the status + ask for the next card" behavior. - The batch operation's RU label is "Пакетная установка (.cap на множество карт)"; the help row's title aligned. Tests: the render test (the new prompt, the throbber visible while installing / stopping-mid-install and hidden while waiting, an empty notice hiding the box) and the worker test (the notice is empty mid-install and set again for the new outcome). 915 frontend + 925 Python; sw cache simple-v439. --- frontend/help-ru.html | 2 +- frontend/index.html | 13 +++++++++---- frontend/sw.js | 2 +- frontend/tests/ram_batch.test.js | 24 +++++++++++++++++++----- pyproject.toml | 2 +- pysim_simple_server/server.py | 6 +++++- tests/test_ram_batch.py | 21 +++++++++++++++++++++ 7 files changed, 57 insertions(+), 13 deletions(-) diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 3f94b82..50bada3 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -323,7 +323,7 @@ Обзор карты (все данные GP)Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (P2.b2=0, устаревший по GP), запросы повторяются в расширенном формате E3, а флажок Запрашивать расширенные данные реестра (связи load-файла и SD) дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) — он стоит несколько дополнительных защищённых пакетов на каждый обзор и включён по умолчанию (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах — эталонный список тегов (с EA по TS 102 226), тот же список без EA и вовсе без списка тегов (форма GlobalPlatformPro) — а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Если строки ELF всё ещё без версий, формы со списком тегов повторяются для обоих P1=10 и P1=20, пропуская уже отклонённые комбинации, и обход останавливается, как только версия найдена. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками Удалить для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded). Установка пакета (.cap файл)Отправка .cap файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера размер блока LOAD (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой .cap просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается оценка требований к памяти: требование NVRAM (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны библиотеки, с которыми слинкован CAP (его компонент Import: имя библиотеки, AID и минимальная версия ≥ x.y — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам Package, Requires, Memory и Components. Строка Platform показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта javacard.framework; флаг int в заголовке дополнительно требует поддержки int — Java Card 2.2+), и релиз ETSI, из которого пришли импорты UICC API (версия uicc.toolkit → REL-N по корпусу sim-tools в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента Import (в строке шага виден remote SW карты и компонент, который завершил блок, например завершает Import): подсказка при ошибке и попап предлагают тест совместимости CAP (в один клик — тот же .cap уже выбран), чтобы найти отсутствующий пакет и версию; меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и Execute остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый C6 следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные). - Пакетная установка (.cap, по картам)Массовый сценарий: один .cap и один набор параметров установки, применяемые карта за картой. Запустить пакетную установку не требует карты — цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты пресет находится по ICCID и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт — пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается один раз за запуск: повторная установка карты даёт «уже обработана» — для повтора перезапустите пакетную установку. Остановить завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; Очистить удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга. + Пакетная установка (.cap на множество карт)Массовый сценарий: один .cap и один набор параметров установки, применяемые карта за картой. Запустить пакетную установку не требует карты — цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты пресет находится по ICCID и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт — пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается один раз за запуск: повторная установка карты даёт «уже обработана» — для повтора перезапустите пакетную установку. Остановить завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; Очистить удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга. Проверка совместимости CAPОтвечает на один вопрос, ничего не устанавливая: может ли эта карта удовлетворить список импортов CAP? Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент Import — именно там JCRE проверяет список импортов — и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список «Версии импортов для проверки» предзаполняется из анализа CAP: AID=version переписывает запрошенную версию импорта, +AID=version добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии ≥ указанной), а «Все 0.0» снимает все требования. Использование в два шага: нажмите «Все 0.0» — если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только этап LOAD/импортов: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID — при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок Import), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку. INSTALL [for install] (загруженный пакет)Один APDU INSTALL [for install] для уже загруженного на карту пакета (его Load File AID и Module AID, привилегии, параметры установки/STK и необязательное make selectable) — параметры установки можно перебирать, не удаляя и не загружая .cap заново. INSTALL [for make selectable]Один APDU INSTALL [for make selectable] (AID экземпляра) для установленного, но невыбираемого экземпляра апплета. diff --git a/frontend/index.html b/frontend/index.html index 73d7dd2..6e4b127 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -918,6 +918,7 @@ + @@ -1909,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.0'; +const SIMPLE_VERSION = '3.28.1'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -10216,6 +10217,10 @@ function ramBatchRender() { // the server refuses a clear while the run is active const clearBtn = document.getElementById('ram-batch-clear'); if (clearBtn) clearBtn.disabled = active; + // A throbber while a card is actually being installed (`current` is set for + // the whole install, the stopping phase included). + const spinEl = document.getElementById('ram-batch-spin'); + if (spinEl) spinEl.classList.toggle('hidden', !(active && st.current)); const stateEl = document.getElementById('ram-batch-state'); if (stateEl) { if (!active) { @@ -10228,7 +10233,7 @@ function ramBatchRender() { } else if (phase === 'stopping') { stateEl.textContent = t('Stopping after the current card...'); } else { - stateEl.textContent = t('Waiting for a card...'); + stateEl.textContent = t('Insert next card. Waiting for a card...'); } } const notice = document.getElementById('ram-batch-notice'); @@ -24405,14 +24410,14 @@ const LANG_RU = { 'Loading...': 'Загрузка...', 'Close': 'Закрыть', 'Installing package': 'Установка пакета', - 'Batch install (.cap file, per card)': 'Пакетная установка (.cap, по картам)', + 'Batch install (.cap file, per card)': 'Пакетная установка (.cap на множество карт)', 'Start batch': 'Запустить пакетную установку', 'Stopping...': 'Остановка…', 'Batch stopped': 'Пакетная установка остановлена', 'Batch finished': 'Пакетная установка завершена', 'Installing...': 'Установка…', 'Stopping after the current card...': 'Остановка после текущей карты…', - 'Waiting for a card...': 'Ожидание карты…', + 'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…', 'Installed': 'Установлено', 'Skipped': 'Пропущено', 'attempts': 'попыток', diff --git a/frontend/sw.js b/frontend/sw.js index ea18a98..b938eea 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v438'; +const CACHE = 'simple-v439'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram_batch.test.js b/frontend/tests/ram_batch.test.js index 459d449..573d9cd 100644 --- a/frontend/tests/ram_batch.test.js +++ b/frontend/tests/ram_batch.test.js @@ -165,10 +165,12 @@ test('ramBatchRender shows the phase, the counts and the stop button', () => { setEl('ram-batch-counts', ''); setEl('ram-batch-attempts', ''); setEl('ram-batch-clear', ''); - // waiting for a card + setEl('ram-batch-spin', ''); + // waiting for a card: the explicit prompt, no throbber app.setLast({ active: true, phase: 'waiting_card', attempts: [], counts: {}, attempts_total: 0, notice: '' }); app.ramBatchRender(); - assert.strictEqual(els['ram-batch-state'].textContent, 'Waiting for a card...'); + assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...'); + assert.ok(els['ram-batch-spin'].classes['hidden']); assert.ok(els['ram-batch-stop'].classes['hidden'] === undefined); assert.ok(els['ram-batch-start'].classes['hidden']); assert.ok(els['ram-batch-clear'].disabled); // the server refuses a clear while active @@ -177,12 +179,24 @@ test('ramBatchRender shows the phase, the counts and the stop button', () => { attempts_total: 3, notice: 'Card 897… installed.', current: { iccid: '8970119000004600098', preset: 'Alfa' } }); app.ramBatchRender(); assert.match(els['ram-batch-state'].textContent, /Installing\.\.\. 8970119000004600098 — Alfa/); + assert.ok(els['ram-batch-spin'].classes['hidden'] === undefined); // the install throbber assert.match(els['ram-batch-counts'].textContent, /Installed: 2 · Failed: 1 · Skipped: 0 · 3 attempts/); assert.strictEqual(els['ram-batch-notice'].textContent, 'Card 897… installed.'); - // stopping - app.setLast({ active: true, phase: 'stopping', attempts: [], counts: {}, attempts_total: 0 }); + // a new install clears the previous notice: an empty one hides the box + app.setLast({ active: true, phase: 'installing', attempts: [], counts: {}, attempts_total: 3, + notice: '', current: { iccid: '8970119000004600106' } }); app.ramBatchRender(); + assert.ok(els['ram-batch-notice'].classes['hidden']); + // stopping while a card is still being installed: the throbber stays + app.setLast({ active: true, phase: 'stopping', attempts: [], counts: {}, attempts_total: 0, + current: { iccid: '8970119000004600098' } }); + app.ramBatchRender(); + assert.ok(els['ram-batch-spin'].classes['hidden'] === undefined); assert.match(els['ram-batch-state'].textContent, /Stopping after the current card\.\.\./); + // ...but not once the install finished (stopping while waiting) + app.setLast({ active: true, phase: 'stopping', attempts: [], counts: {}, attempts_total: 0, current: null }); + app.ramBatchRender(); + assert.ok(els['ram-batch-spin'].classes['hidden']); assert.strictEqual(els['ram-batch-stop'].textContent, 'Stopping...'); assert.ok(els['ram-batch-stop'].disabled); // stopped @@ -248,7 +262,7 @@ test('the panel headline follows the live summary (phase and step)', () => { // ...and back to waiting when the card is gone app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card', attempts_total: 0, counts: { installed: 0, failed: 0, skipped: 0 } } }); - assert.strictEqual(els['ram-batch-state'].textContent, 'Waiting for a card...'); + assert.strictEqual(els['ram-batch-state'].textContent, 'Insert next card. Waiting for a card...'); }); test('the stop and clear calls hit their endpoints', async () => { diff --git a/pyproject.toml b/pyproject.toml index 301285b..584101d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.28.0" +version = "3.28.1" 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 8a755b0..87f7ee5 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.0' +VERSION = '3.28.1' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE @@ -2637,6 +2637,10 @@ def _ram_batch_handle_card(state, server): state['seen'][iccid] = 'pending' state['current'] = {'iccid': iccid, 'preset': name, 'started': time.time()} state['phase'] = 'installing' + # A new card's install supersedes the previous attempt's headline: the + # notice reports the *last finished* card, so it must not linger while + # the next card is being installed (v3.28.1). + state['notice'] = '' started = time.time() result = None error = None diff --git a/tests/test_ram_batch.py b/tests/test_ram_batch.py index 57826ab..50d3a9c 100644 --- a/tests/test_ram_batch.py +++ b/tests/test_ram_batch.py @@ -205,6 +205,27 @@ class BatchCardTests(unittest.TestCase): self.assertEqual(st['counts']['failed'], 1) self.assertEqual(st['seen']['8970119000004600098'], 'failed') + def test_a_new_install_clears_the_previous_notice(self): + """The notice reports the last *finished* card: starting the next + card's install must clear it (v3.28.1).""" + st = srv._ram_batch_new_state(_plan()) + srv._ram_batch_record(st, {'iccid': '8970119000004600000', 'result': 'installed'}) + self.assertIn('installed', st['notice']) + preset = _preset('8970119000004600098', 'Alfa') + seen = {} + + def fake_run(server, body, plan, scc): + seen['notice'] = st['notice'] # what the panel would render + seen['current'] = st['current'] + return {'success': True} + + with mock.patch.object(srv, '_ram_install_run', fake_run): + srv._ram_batch_handle_card(st, _fake_server('8970119000004600098', + _FakeStore([preset]))) + self.assertEqual(seen['notice'], '') # cleared while installing + self.assertEqual(seen['current']['iccid'], '8970119000004600098') + self.assertIn('installed', st['notice']) # ...and set for the new outcome + def test_a_card_already_processed_is_reported_not_reinstalled(self): st = srv._ram_batch_new_state(_plan()) preset = _preset('8970119000004600098', 'Alfa')