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:
2026-10-14 18:04:22 +03:00
parent 262041795c
commit 91996a941b
7 changed files with 57 additions and 13 deletions
+1 -1
View File
@@ -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) и любые другие объекты реестра, которые вернула карта (показываются как есть, без потерь) &mdash; он стоит несколько дополнительных защищённых пакетов на каждый обзор и <strong>включён по умолчанию</strong> (запоминается в браузере; снимите флажок, чтобы пропустить). Карты различаются в том, что принимают, поэтому расширенный запрос пробуется в трёх формах &mdash; эталонный список тегов (с <code class="font-mono text-sm">EA</code> по TS 102 226), тот же список без <code class="font-mono text-sm">EA</code> и вовсе без списка тегов (форма GlobalPlatformPro) &mdash; а обзор сообщает, какая форма сработала и где выполнен откат к компактному списку (версии пакетов тогда недоступны). Если строки 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] &rarr; LOAD &times;N &rarr; INSTALL[for install (+make selectable)]. Load-файл делится на LOAD APDU размера <strong>размер блока LOAD</strong> (1&ndash;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> &rarr; 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> не требует карты &mdash; цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты <strong>пресет находится по ICCID</strong> и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт &mdash; пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается <strong>один раз за запуск</strong>: повторная установка карты даёт «уже обработана» &mdash; для повтора перезапустите пакетную установку. <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> не требует карты &mdash; цикл ведёт сервер: он ждёт карту, устанавливает сразу после завершения equip и затем ждёт следующую. Для каждой карты <strong>пресет находится по ICCID</strong> и задаёт keyset (по умолчанию из ролевой записи ISD TAR, v3.26.0), ключи и счётчик; карта без подходящего пресета или без пригодного keyset ISD TAR отмечается и пропускается. Каждая попытка попадает в список со временем, ICCID, пресетом, вердиктом и длительностью; при ошибке показаны шаг, на котором она произошла, и удалённый статусный байт &mdash; пакетная установка никогда не повторяет попытку и не останавливается при ошибке (ошибки остаются на ваше усмотрение). Карта обрабатывается <strong>один раз за запуск</strong>: повторная установка карты даёт «уже обработана» &mdash; для повтора перезапустите пакетную установку. <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> &mdash; именно там JCRE проверяет список импортов &mdash; и останавливается: без флага последнего блока и без 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 проходит, только если карта экспортирует этот пакет версии &ge; указанной), а <strong>«Все 0.0»</strong> снимает все требования. Использование в два шага: нажмите «Все 0.0» &mdash; если проверка проходит, причиной был список импортов; затем возвращайте по одной строке, чтобы найти пакет и версию, которых не хватает карте. Вердикт явно указывает, что проверяется только <strong>этап LOAD/импортов</strong>: этап связывания (наличие реально вызываемых API) проверяет настоящая установка на INSTALL[for install]. Запуск и результат показываются в окне (как при установке): строки шагов, вердикт и блок деталей по каждому AID &mdash; при отказе он называет раздел 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>) &mdash; параметры установки можно перебирать, не удаляя и не загружая <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
View File
@@ -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
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v438';
const CACHE = 'simple-v439';
const URLS = [
'index.html',
'help.html',
+19 -5
View File
@@ -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 () => {