v3.28.1: the batch panel polish
- 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.
This commit is contained in:
@@ -323,7 +323,7 @@
|
||||
<tbody>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Обзор карты (все данные GP)</td><td class="py-1 px-2">Запрос GET STATUS для ISD, приложений, ELF и модулей ELF, а также GET DATA FF21 (расширенные ресурсы карты: число установленных приложений и свободная энергонезависимая/энергозависимая память). Компактный листинг содержит только AID, состояние жизненного цикла и привилегии (версии в устаревшем формате нет); если карта отвергает его (<code class="font-mono text-sm">P2.b2=0</code>, устаревший по GP), запросы повторяются в расширенном формате <code class="font-mono text-sm">E3</code>, а флажок <strong>Запрашивать расширенные данные реестра (связи load-файла и SD)</strong> дополнительно запрашивает их и объединяет расширенные поля: версию исполняемого load-файла, AID load-файла, AID Security Domain, списки модулей, пункты меню STK апплета (SCP Registry Data, TS 102 226 8.2.1.6) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) — он стоит несколько дополнительных защищённых пакетов на каждый обзор и <strong>включён по умолчанию</strong> (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах — эталонный список тегов (с <code class="font-mono text-sm">EA</code> по TS 102 226), тот же список без <code class="font-mono text-sm">EA</code> и вовсе без списка тегов (форма GlobalPlatformPro) — а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Если строки ELF всё ещё без версий, формы со списком тегов повторяются для обоих P1=10 и P1=20, пропуская уже отклонённые комбинации, и обход останавливается, как только версия найдена. Результаты отображаются в обзоре (строка успеха минимальна, счётчики — в заголовках разделов) с кнопками <strong>Удалить</strong> для каждого элемента и определённым форматом команд RAM (подсказка при наведении объясняет compact и expanded).</td></tr>
|
||||
<tr><td class="py-1 px-2">Установка пакета (.cap файл)</td><td class="py-1 px-2">Отправка <code class="font-mono text-sm">.cap</code> файла на карту через сервер: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1–240 байт полезной нагрузки, по умолчанию 240); каждый защищённый пакет доставляется одним SMS или, если он больше, конкатенированной SMS-PP загрузкой до 5 сегментов, так что большой <code>.cap</code> просто занимает несколько SMS. Сразу после выбора файл проверяется и в форме показывается <strong>оценка требований к памяти</strong>: <strong>требование NVRAM</strong> (образ кода + постоянные данные, сумма в стиле C6+C8 из GP Card Spec) и оценка RAM. Там же показаны <strong>библиотеки, с которыми слинкован CAP</strong> (его компонент Import: имя библиотеки, AID и минимальная версия <code class="font-mono text-sm">≥ x.y</code> — имена стандартных библиотек и семейства подставляются, а где известно, даётся подсказка о версии Java Card), идентификаторы пакета и апплетов и разбивка по компонентам (размеры и доли в порядке load-файла) — всё сгруппировано по разделам <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> и <strong>Components</strong>. Строка <strong>Platform</strong> показывает минимальный уровень Java Card, который должна предоставить карта (выводится из импорта <code class="font-mono text-sm">javacard.framework</code>; флаг <code class="font-mono text-sm">int</code> в заголовке дополнительно требует поддержки int — Java Card 2.2+), и <strong>релиз ETSI</strong>, из которого пришли импорты UICC API (версия <code class="font-mono text-sm">uicc.toolkit</code> → REL-N по корпусу <code class="font-mono text-sm">sim-tools</code> в рабочем каталоге; основание показано в скобках): JCRE проверяет каждый импорт по своим пакетам при верификации load-файла (JC VM spec 4.5.2), поэтому карта со старой платформой отклоняет LOAD сразу после завершения компонента <strong>Import</strong> (в строке шага виден remote SW карты и компонент, который завершил блок, например <em>завершает Import</em>): подсказка при ошибке и попап предлагают <strong>тест совместимости CAP</strong> (в один клик — тот же <code class="font-mono text-sm">.cap</code> уже выбран), чтобы найти отсутствующий пакет и версию; меньший размер блока LOAD помогает только если блок превышает буфер карты. Повреждённый файл не проходит анализ, и <strong>Execute</strong> остаётся недоступной до успешного анализа. Оценка носит справочный характер и никогда не используется при установке: NVRAM — это образ кода плюс постоянные данные (сумма в стиле C6+C8 по GP Table 11-48; управление памятью карты, округление и запись в реестре не учитываются, а образ статических полей входит в обе части); рекомендуемый <strong>C6</strong> следует за load-файлом, а рекомендуемые квоты — это десятичные значения в байтах (шестнадцатеричное значение в скобках — это кодирование параметров установки C7/C8: C6 — энергонезависимый код, C7 — энергозависимая RAM, C8 — энергонезависимые данные).</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Пакетная установка (.cap, по картам)</td><td class="py-1 px-2">Массовый сценарий: один <code class="font-mono text-sm">.cap</code> и один набор параметров установки, применяемые карта за картой. <strong>Запустить пакетную установку</strong> не требует карты — цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты <strong>пресет находится по ICCID</strong> и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт — пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается <strong>один раз за запуск</strong>: повторная установка карты даёт «уже обработана» — для повтора перезапустите пакетную установку. <strong>Остановить</strong> завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; <strong>Очистить</strong> удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Пакетная установка (.cap на множество карт)</td><td class="py-1 px-2">Массовый сценарий: один <code class="font-mono text-sm">.cap</code> и один набор параметров установки, применяемые карта за картой. <strong>Запустить пакетную установку</strong> не требует карты — цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты <strong>пресет находится по ICCID</strong> и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт — пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается <strong>один раз за запуск</strong>: повторная установка карты даёт «уже обработана» — для повтора перезапустите пакетную установку. <strong>Остановить</strong> завершает текущую карту (прерванная цепочка LOAD может оставить полузагруженный пакет), а затем завершает запуск; <strong>Очистить</strong> удаляет отчёт завершённого запуска. Цикл работает на сервере, поэтому закрытие вкладки, переключение представлений или перезагрузка страницы его не прерывают; перезапуск сервера завершает запуск. Пакетная установка и тест-скрипт исключают друг друга.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Проверка совместимости CAP</td><td class="py-1 px-2">Отвечает на один вопрос, ничего не устанавливая: <strong>может ли эта карта удовлетворить список импортов CAP?</strong> Сервер выполняет проверку формата, INSTALL[for load] и блоки LOAD только до блока, завершающего компонент <strong>Import</strong> — именно там JCRE проверяет список импортов — и останавливается: без флага последнего блока и без INSTALL[for install], поэтому ничего не фиксируется и удалять после этого ничего не нужно. Список <strong>«Версии импортов для проверки»</strong> предзаполняется из анализа CAP: <code class="font-mono text-sm">AID=version</code> переписывает запрошенную версию импорта, <code class="font-mono text-sm">+AID=version</code> добавляет синтетический импорт (запрос возможностей карты: LOAD проходит, только если карта экспортирует этот пакет версии ≥ указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» — если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID — при отказе он называет раздел CAP, который покрывает неудачный блок (диапазон байтов load-файла и компоненты, и завершает ли этот блок <strong>Import</strong>), SW команды с его значением, список импортов ровно в том виде, в каком он проверялся (версии CAP, правки пробы, добавленные синтетические записи, строки без совпадений) и что означает состояние пробы; при успехе — список импортов, который прошёл проверку.</td></tr>
|
||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (загруженный пакет)</td><td class="py-1 px-2">Один APDU INSTALL [for install] для уже загруженного на карту пакета (его Load File AID и Module AID, привилегии, параметры установки/STK и необязательное <em>make selectable</em>) — параметры установки можно перебирать, не удаляя и не загружая <code class="font-mono text-sm">.cap</code> заново.</td></tr>
|
||||
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">Один APDU INSTALL [for make selectable] (AID экземпляра) для установленного, но невыбираемого экземпляра апплета.</td></tr>
|
||||
|
||||
+9
-4
@@ -918,6 +918,7 @@
|
||||
<button type="button" id="ram-batch-start" data-needs="server" data-cap-gate="ram" onclick="ramBatchStart()" class="px-5 py-1.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Start batch">Start batch</button>
|
||||
<button type="button" id="ram-batch-stop" onclick="ramBatchStop()" class="hidden px-5 py-1.5 bg-red-600 text-white text-sm font-medium rounded hover:bg-red-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Stop">Stop</button>
|
||||
<button type="button" id="ram-batch-clear" onclick="ramBatchClear()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Clear">Clear</button>
|
||||
<span id="ram-batch-spin" class="hidden animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></span>
|
||||
<span id="ram-batch-state" class="text-sm font-medium text-gray-700 dark:text-slate-300"></span>
|
||||
</div>
|
||||
<div id="ram-batch-notice" class="hidden mb-2 px-3 py-2 rounded border border-blue-300 dark:border-blue-800 bg-blue-50 dark:bg-blue-900/20 text-sm text-blue-700 dark:text-blue-300"></div>
|
||||
@@ -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': 'попыток',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v438';
|
||||
const CACHE = 'simple-v439';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
+1
-1
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user