v3.30.0: the batch status modal
A running batch now shows in a modal window (the same chrome as the
single-operation modal): the live status on top - the current step from
ram_progress (Step n/N - <step>, with the bar) or the waiting prompt
("Insert next card. Waiting for a card...") - plus the counts and the
last finished card's verdict, and the processed cards below (the same
attempts table with the per-step verdicts).
It opens on Start and on a page load with a run in flight (the first
poll sees ram_batch.active); Close is disabled until the run ends (the
run is server-side and survives a reload - that is what reopens the
window), and Stop/Clear live inside it (Clear only once the run ended).
The panel's new Show status button reopens a finished report. A single
operation takes the modal back; ramBatchRender feeds both the panel and
the modal (the poll's fetch decision now runs for either).
Docs: help EN/RU, the repo AGENTS. Tests: the open on Start and on the
poll, the live status/bar/counts/attempts, the close guard (active vs
ended), the reopen button. 929 frontend + 945 Python; sw cache
simple-v450.
This commit is contained in:
@@ -331,6 +331,7 @@
|
|||||||
<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>
|
<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>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p class="text-sm mb-3">Пакетная операция показывает свой статус в модальном окне: сверху — текущий прогресс (или «Вставьте следующую карту. Ожидание карты…»), ниже — обработанные карты, а внутри — <strong>Остановить</strong> и <strong>Очистить</strong>. Окно открывается при запуске и при загрузке страницы, пока запуск активен; <strong>Закрыть</strong> остаётся недоступным до окончания запуска — сам запуск выполняется на сервере и переживает перезагрузку. После завершения отчёт можно открыть заново кнопкой <strong>Показать статус</strong> в панели.</p>
|
||||||
|
|
||||||
<h4 id="ram-explorer" class="font-medium mb-1">Обзор карты (Explorer View)</h4>
|
<h4 id="ram-explorer" class="font-medium mb-1">Обзор карты (Explorer View)</h4>
|
||||||
<p class="text-sm mb-2">После выполнения «Обзор карты» отображается:</p>
|
<p class="text-sm mb-2">После выполнения «Обзор карты» отображается:</p>
|
||||||
|
|||||||
@@ -330,6 +330,7 @@
|
|||||||
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
<p class="text-sm mb-3">A batch operation shows its status in a modal window: the live progress (or “Insert next card. Waiting for a card...”) on top and the processed cards below, with <strong>Stop</strong> and <strong>Clear</strong> inside. It opens on Start and on a page load while a run is active; <strong>Close</strong> stays disabled until the run ends — the run itself is server-side and survives a reload. After a finished run the panel’s <strong>Show status</strong> button reopens the report.</p>
|
||||||
|
|
||||||
<h4 id="ram-explorer" class="font-medium mb-1">Explorer View</h4>
|
<h4 id="ram-explorer" class="font-medium mb-1">Explorer View</h4>
|
||||||
<p class="text-sm mb-2">After “Explore Card” runs, the explorer displays:</p>
|
<p class="text-sm mb-2">After “Explore Card” runs, the explorer displays:</p>
|
||||||
|
|||||||
+126
-10
@@ -936,6 +936,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-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-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>
|
<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>
|
||||||
|
<button type="button" id="ram-batch-show" onclick="ramBatchModalOpen()" class="hidden 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="Show status">Show status</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-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>
|
<span id="ram-batch-state" class="text-sm font-medium text-gray-700 dark:text-slate-300"></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -1910,6 +1911,8 @@
|
|||||||
<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="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">
|
<div class="flex justify-between items-center mb-2 gap-3">
|
||||||
<span id="ram-op-title" class="font-bold text-sm flex-1"></span>
|
<span id="ram-op-title" class="font-bold text-sm flex-1"></span>
|
||||||
|
<button id="ram-op-batch-stop" onclick="ramBatchStop()" class="hidden px-3 py-1 text-xs rounded-full bg-red-600 text-white hover:bg-red-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Stop">Stop</button>
|
||||||
|
<button id="ram-op-batch-clear" onclick="ramBatchClear()" class="hidden 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="Clear">Clear</button>
|
||||||
<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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-slate-400">
|
<div class="flex items-center gap-2 text-sm text-gray-600 dark:text-slate-400">
|
||||||
@@ -1919,6 +1922,7 @@
|
|||||||
<div class="mt-2 w-full bg-gray-200 dark:bg-slate-700 rounded h-1.5">
|
<div class="mt-2 w-full bg-gray-200 dark:bg-slate-700 rounded h-1.5">
|
||||||
<div id="ram-op-bar" class="bg-blue-600 dark:bg-blue-500 h-1.5 rounded" style="width:0%"></div>
|
<div id="ram-op-bar" class="bg-blue-600 dark:bg-blue-500 h-1.5 rounded" style="width:0%"></div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="ram-op-batch-info" class="hidden mt-1 text-xs whitespace-pre-line text-gray-500 dark:text-slate-400"></div>
|
||||||
<div id="ram-op-steps" class="mt-2 text-xs font-mono whitespace-pre-wrap overflow-auto text-gray-700 dark:text-slate-300"></div>
|
<div id="ram-op-steps" class="mt-2 text-xs font-mono whitespace-pre-wrap overflow-auto text-gray-700 dark:text-slate-300"></div>
|
||||||
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400" id="ram-op-warn" data-l10n="Do not close or reload this window while the operation is running.">Do not close or reload this window while the operation is running.</div>
|
<div class="mt-2 text-xs text-amber-600 dark:text-amber-400" id="ram-op-warn" data-l10n="Do not close or reload this window while the operation is running.">Do not close or reload this window while the operation is running.</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1928,7 +1932,7 @@
|
|||||||
// ===== Version =====
|
// ===== Version =====
|
||||||
// Single source of truth for the PWA version: shown in the header and used
|
// Single source of truth for the PWA version: shown in the header and used
|
||||||
// by the server version check in pysimConnect().
|
// by the server version check in pysimConnect().
|
||||||
const SIMPLE_VERSION = '3.29.2';
|
const SIMPLE_VERSION = '3.30.0';
|
||||||
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
|
||||||
|
|
||||||
// ===== view history (the browser Back/Forward buttons) =====
|
// ===== view history (the browser Back/Forward buttons) =====
|
||||||
@@ -10186,6 +10190,7 @@ let _ramBatchFetching = false;
|
|||||||
let _ramBatchFetchAt = 0; // when the in-flight fetch started (the watchdog)
|
let _ramBatchFetchAt = 0; // when the in-flight fetch started (the watchdog)
|
||||||
let _ramBatchSummary = null; // /api/status.ram_batch
|
let _ramBatchSummary = null; // /api/status.ram_batch
|
||||||
let _ramBatchProgress = null; // /api/status.ram_progress
|
let _ramBatchProgress = null; // /api/status.ram_progress
|
||||||
|
let _ramBatchModalShown = false; // the batch status modal is open (v3.30.0)
|
||||||
// A fetch that never settled must not block the report forever (v3.28.7).
|
// A fetch that never settled must not block the report forever (v3.28.7).
|
||||||
const RAM_BATCH_FETCH_STALE_MS = 10000;
|
const RAM_BATCH_FETCH_STALE_MS = 10000;
|
||||||
|
|
||||||
@@ -10195,6 +10200,94 @@ function ramBatchPanelVisible() {
|
|||||||
&& !!op && ramIsBatchOp(op.value);
|
&& !!op && ramIsBatchOp(op.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The merged batch state: the summary (the 2 s poll) wins over the last
|
||||||
|
// detailed fetch.
|
||||||
|
function ramBatchMergedState() {
|
||||||
|
return Object.assign({ active: false, phase: 'idle', attempts: [], counts: {} },
|
||||||
|
_ramBatchLast || {}, _ramBatchSummary || {});
|
||||||
|
}
|
||||||
|
|
||||||
|
function ramBatchTitle(kind) {
|
||||||
|
return kind === 'delete' ? t('Batch delete (per card)')
|
||||||
|
: (kind === 'reinstall-app' ? t('Batch reinstall app (per card)')
|
||||||
|
: t('Batch install (.cap file, per card)'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== the batch status modal (v3.30.0) =====
|
||||||
|
// The same chrome as the single-operation modal, owned by the batch: the live
|
||||||
|
// status on top (the current step or the waiting prompt) and the processed
|
||||||
|
// cards below. It opens on Start and on a page load with a run in flight;
|
||||||
|
// Close stays disabled while the run is active (the run itself survives a
|
||||||
|
// reload - that is what reopens this window) and Stop/Clear live inside it.
|
||||||
|
function ramBatchModalOpen() {
|
||||||
|
const modal = document.getElementById('ram-op-modal');
|
||||||
|
if (!modal) return; // no modal in the document (tests)
|
||||||
|
_ramBatchModalShown = true;
|
||||||
|
document.getElementById('ram-op-title').textContent =
|
||||||
|
ramBatchTitle(ramBatchMergedState().kind || 'install-cap');
|
||||||
|
document.getElementById('ram-op-status').textContent = t('Working...');
|
||||||
|
document.getElementById('ram-op-status').className = 'text-sm text-gray-600 dark:text-slate-400';
|
||||||
|
document.getElementById('ram-op-bar').style.width = '0%';
|
||||||
|
const stepsEl = document.getElementById('ram-op-steps');
|
||||||
|
stepsEl.className = 'mt-2 text-xs overflow-auto text-gray-700 dark:text-slate-300';
|
||||||
|
stepsEl.innerHTML = '';
|
||||||
|
document.getElementById('ram-op-spin').classList.add('hidden');
|
||||||
|
document.getElementById('ram-op-warn').classList.add('hidden');
|
||||||
|
document.getElementById('ram-op-batch-info').classList.remove('hidden');
|
||||||
|
modal.classList.remove('hidden');
|
||||||
|
ramBatchRender(); // paint from the current state now
|
||||||
|
}
|
||||||
|
|
||||||
|
// The modal's live render: the status (the current step or the waiting
|
||||||
|
// prompt), the counts and the last verdict, the processed cards, the
|
||||||
|
// Stop/Clear/Close states.
|
||||||
|
function ramBatchModalRender(st, okLabel, actLabel) {
|
||||||
|
const statusEl = document.getElementById('ram-op-status');
|
||||||
|
if (!statusEl) return;
|
||||||
|
document.getElementById('ram-op-title').textContent =
|
||||||
|
ramBatchTitle(st.kind || 'install-cap');
|
||||||
|
const active = !!st.active;
|
||||||
|
const phase = st.phase || (active ? 'waiting_card' : 'idle');
|
||||||
|
let status;
|
||||||
|
if (phase === 'installing' || phase === 'stopping') {
|
||||||
|
status = phase === 'stopping' ? t('Stopping after the current card...')
|
||||||
|
: (ramOpProgressText(_ramBatchProgress)
|
||||||
|
|| (actLabel + ((st.current && st.current.iccid) ? ' ' + st.current.iccid : '')));
|
||||||
|
} else if (active) {
|
||||||
|
status = t('Insert next card. Waiting for a card...');
|
||||||
|
} else {
|
||||||
|
status = phase === 'stopped' ? t('Batch stopped')
|
||||||
|
: (phase === 'finished' ? t('Batch finished') : '');
|
||||||
|
}
|
||||||
|
statusEl.textContent = status;
|
||||||
|
// the bar follows the current operation's step; a finished report fills it
|
||||||
|
const p = _ramBatchProgress;
|
||||||
|
const pct = !active ? ((st.attempts_total || 0) > 0 ? 100 : 0)
|
||||||
|
: ((p && p.active && p.total) ? Math.min(100, Math.round(p.step * 100 / p.total)) : 0);
|
||||||
|
document.getElementById('ram-op-bar').style.width = pct + '%';
|
||||||
|
// the counts and the last finished card's verdict
|
||||||
|
const c = st.counts || {};
|
||||||
|
let info = okLabel + ': ' + (c.installed || 0) + ' \u00b7 ' + t('Failed') + ': ' + (c.failed || 0)
|
||||||
|
+ ' \u00b7 ' + t('Skipped') + ': ' + (c.skipped || 0);
|
||||||
|
if (st.attempts_total) info += ' \u00b7 ' + st.attempts_total + ' ' + t('attempts');
|
||||||
|
if (st.notice) info += '\n' + st.notice;
|
||||||
|
document.getElementById('ram-op-batch-info').textContent = info;
|
||||||
|
// the processed cards (the reader's scroll position is kept across renders)
|
||||||
|
const box = document.getElementById('ram-op-steps');
|
||||||
|
const prev = box.scrollTop;
|
||||||
|
box.innerHTML = ramBatchAttemptsHtml(st.attempts || []);
|
||||||
|
if (prev) box.scrollTop = prev;
|
||||||
|
// Stop/Clear live here; Close only once the run ended
|
||||||
|
const stopBtn = document.getElementById('ram-op-batch-stop');
|
||||||
|
stopBtn.classList.toggle('hidden', !active);
|
||||||
|
stopBtn.textContent = phase === 'stopping' ? t('Stopping...') : t('Stop');
|
||||||
|
stopBtn.disabled = phase === 'stopping';
|
||||||
|
const clearBtn = document.getElementById('ram-op-batch-clear');
|
||||||
|
clearBtn.classList.remove('hidden');
|
||||||
|
clearBtn.disabled = active;
|
||||||
|
document.getElementById('ram-op-close').disabled = active;
|
||||||
|
}
|
||||||
|
|
||||||
async function ramBatchFetch() {
|
async function ramBatchFetch() {
|
||||||
if (_ramBatchFetching) return;
|
if (_ramBatchFetching) return;
|
||||||
_ramBatchFetching = true;
|
_ramBatchFetching = true;
|
||||||
@@ -10212,7 +10305,12 @@ function ramBatchApplyStatus(status) {
|
|||||||
if (!status) return;
|
if (!status) return;
|
||||||
_ramBatchSummary = status.ram_batch || null;
|
_ramBatchSummary = status.ram_batch || null;
|
||||||
_ramBatchProgress = status.ram_progress || null;
|
_ramBatchProgress = status.ram_progress || null;
|
||||||
if (!ramBatchPanelVisible()) return;
|
// The status modal opens itself for a run in flight: Start opens it, and a
|
||||||
|
// page load with an active batch reopens it on the first poll (v3.30.0).
|
||||||
|
if (!_ramBatchModalShown && status.ram_batch && status.ram_batch.active) {
|
||||||
|
ramBatchModalOpen();
|
||||||
|
}
|
||||||
|
if (!ramBatchPanelVisible() && !_ramBatchModalShown) return;
|
||||||
// The summary carries the headline (the phase, the current card, the
|
// The summary carries the headline (the phase, the current card, the
|
||||||
// counts, the notice): render it on every poll, even when a detailed fetch
|
// counts, the notice): render it on every poll, even when a detailed fetch
|
||||||
// is due or stuck - a hung fetch used to freeze the whole panel at the
|
// is due or stuck - a hung fetch used to freeze the whole panel at the
|
||||||
@@ -10233,17 +10331,12 @@ function ramBatchApplyStatus(status) {
|
|||||||
|
|
||||||
function ramBatchRender() {
|
function ramBatchRender() {
|
||||||
const panel = document.getElementById('ram-batch-panel');
|
const panel = document.getElementById('ram-batch-panel');
|
||||||
if (!panel) return;
|
const visible = ramBatchPanelVisible();
|
||||||
if (!ramBatchPanelVisible()) {
|
if (panel) panel.classList.toggle('hidden', !visible);
|
||||||
panel.classList.add('hidden');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
panel.classList.remove('hidden');
|
|
||||||
// The summary (the 2 s status poll) is fresher than the last detailed
|
// The summary (the 2 s status poll) is fresher than the last detailed
|
||||||
// fetch: it carries the phase/current/notice/counts, the attempts list
|
// fetch: it carries the phase/current/notice/counts, the attempts list
|
||||||
// comes from the detailed status.
|
// comes from the detailed status.
|
||||||
const st = Object.assign({ active: false, phase: 'idle', attempts: [], counts: {} },
|
const st = ramBatchMergedState();
|
||||||
_ramBatchLast || {}, _ramBatchSummary || {});
|
|
||||||
const active = !!st.active;
|
const active = !!st.active;
|
||||||
const phase = st.phase || (active ? 'waiting_card' : 'idle');
|
const phase = st.phase || (active ? 'waiting_card' : 'idle');
|
||||||
// The success labels follow the operation (v3.29.0): the counts bucket is
|
// The success labels follow the operation (v3.29.0): the counts bucket is
|
||||||
@@ -10253,6 +10346,10 @@ function ramBatchRender() {
|
|||||||
: (kind === 'reinstall-app' ? t('Reinstalled') : t('Installed'));
|
: (kind === 'reinstall-app' ? t('Reinstalled') : t('Installed'));
|
||||||
const actLabel = kind === 'delete' ? t('Deleting...')
|
const actLabel = kind === 'delete' ? t('Deleting...')
|
||||||
: (kind === 'reinstall-app' ? t('Reinstalling...') : t('Installing...'));
|
: (kind === 'reinstall-app' ? t('Reinstalling...') : t('Installing...'));
|
||||||
|
// The status modal (when open) follows the same state - it must update
|
||||||
|
// even when the panel is not on screen (v3.30.0).
|
||||||
|
if (_ramBatchModalShown) ramBatchModalRender(st, okLabel, actLabel);
|
||||||
|
if (!visible) return;
|
||||||
const startBtn = document.getElementById('ram-batch-start');
|
const startBtn = document.getElementById('ram-batch-start');
|
||||||
if (startBtn) startBtn.classList.toggle('hidden', active);
|
if (startBtn) startBtn.classList.toggle('hidden', active);
|
||||||
const stopBtn = document.getElementById('ram-batch-stop');
|
const stopBtn = document.getElementById('ram-batch-stop');
|
||||||
@@ -10264,6 +10361,9 @@ function ramBatchRender() {
|
|||||||
// the server refuses a clear while the run is active
|
// the server refuses a clear while the run is active
|
||||||
const clearBtn = document.getElementById('ram-batch-clear');
|
const clearBtn = document.getElementById('ram-batch-clear');
|
||||||
if (clearBtn) clearBtn.disabled = active;
|
if (clearBtn) clearBtn.disabled = active;
|
||||||
|
// the status modal can be reopened for a running or finished report
|
||||||
|
const showBtn = document.getElementById('ram-batch-show');
|
||||||
|
if (showBtn) showBtn.classList.toggle('hidden', !(active || (st.attempts_total || 0) > 0));
|
||||||
// A throbber while a card is actually being installed (`current` is set for
|
// A throbber while a card is actually being installed (`current` is set for
|
||||||
// the whole install, the stopping phase included).
|
// the whole install, the stopping phase included).
|
||||||
const spinEl = document.getElementById('ram-batch-spin');
|
const spinEl = document.getElementById('ram-batch-spin');
|
||||||
@@ -10430,6 +10530,8 @@ async function ramBatchStart() {
|
|||||||
try {
|
try {
|
||||||
_ramBatchLast = await pysimFetchOk('/api/ram-batch/start', fields);
|
_ramBatchLast = await pysimFetchOk('/api/ram-batch/start', fields);
|
||||||
_ramBatchSummary = null;
|
_ramBatchSummary = null;
|
||||||
|
// the run's status window: the progress on top, the cards below
|
||||||
|
ramBatchModalOpen();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
alert(t('Error') + ': ' + e.message);
|
alert(t('Error') + ': ' + e.message);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -10442,6 +10544,9 @@ async function ramBatchStart() {
|
|||||||
async function ramBatchStop() {
|
async function ramBatchStop() {
|
||||||
const stopBtn = document.getElementById('ram-batch-stop');
|
const stopBtn = document.getElementById('ram-batch-stop');
|
||||||
if (stopBtn) stopBtn.disabled = true;
|
if (stopBtn) stopBtn.disabled = true;
|
||||||
|
// the modal's Stop too (the render restores the states after the request)
|
||||||
|
const modalStop = document.getElementById('ram-op-batch-stop');
|
||||||
|
if (modalStop) modalStop.disabled = true;
|
||||||
try {
|
try {
|
||||||
_ramBatchLast = await pysimFetchOk('/api/ram-batch/stop', {});
|
_ramBatchLast = await pysimFetchOk('/api/ram-batch/stop', {});
|
||||||
_ramBatchSummary = null; // the poll refreshes it; the fresh detail wins
|
_ramBatchSummary = null; // the poll refreshes it; the fresh detail wins
|
||||||
@@ -10477,14 +10582,20 @@ function ramOpBeforeUnload(e) {
|
|||||||
|
|
||||||
function ramOpModalBegin(title) {
|
function ramOpModalBegin(title) {
|
||||||
_ramOpActive = true;
|
_ramOpActive = true;
|
||||||
|
_ramBatchModalShown = false; // a single operation owns the modal
|
||||||
document.getElementById('ram-op-title').textContent = title || '';
|
document.getElementById('ram-op-title').textContent = title || '';
|
||||||
document.getElementById('ram-op-status').textContent = t('Working...');
|
document.getElementById('ram-op-status').textContent = t('Working...');
|
||||||
document.getElementById('ram-op-status').className = 'text-sm text-gray-600 dark:text-slate-400';
|
document.getElementById('ram-op-status').className = 'text-sm text-gray-600 dark:text-slate-400';
|
||||||
document.getElementById('ram-op-bar').style.width = '0%';
|
document.getElementById('ram-op-bar').style.width = '0%';
|
||||||
document.getElementById('ram-op-steps').textContent = '';
|
document.getElementById('ram-op-steps').textContent = '';
|
||||||
|
document.getElementById('ram-op-steps').className =
|
||||||
|
'mt-2 text-xs font-mono whitespace-pre-wrap overflow-auto text-gray-700 dark:text-slate-300';
|
||||||
document.getElementById('ram-op-close').disabled = true;
|
document.getElementById('ram-op-close').disabled = true;
|
||||||
document.getElementById('ram-op-spin').classList.remove('hidden');
|
document.getElementById('ram-op-spin').classList.remove('hidden');
|
||||||
document.getElementById('ram-op-warn').classList.remove('hidden');
|
document.getElementById('ram-op-warn').classList.remove('hidden');
|
||||||
|
document.getElementById('ram-op-batch-info').classList.add('hidden');
|
||||||
|
document.getElementById('ram-op-batch-stop').classList.add('hidden');
|
||||||
|
document.getElementById('ram-op-batch-clear').classList.add('hidden');
|
||||||
document.getElementById('ram-op-modal').classList.remove('hidden');
|
document.getElementById('ram-op-modal').classList.remove('hidden');
|
||||||
window.addEventListener('beforeunload', ramOpBeforeUnload);
|
window.addEventListener('beforeunload', ramOpBeforeUnload);
|
||||||
}
|
}
|
||||||
@@ -10532,6 +10643,10 @@ function ramOpModalFinish(summary, ok, content, asHtml) {
|
|||||||
|
|
||||||
function ramOpModalClose() {
|
function ramOpModalClose() {
|
||||||
if (_ramOpActive) return;
|
if (_ramOpActive) return;
|
||||||
|
// the batch window stays while its run is active: the button is disabled
|
||||||
|
// by the render and this guard covers the window before the next render
|
||||||
|
if (_ramBatchModalShown && ramBatchMergedState().active) return;
|
||||||
|
_ramBatchModalShown = false;
|
||||||
document.getElementById('ram-op-modal').classList.add('hidden');
|
document.getElementById('ram-op-modal').classList.add('hidden');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24545,6 +24660,7 @@ const LANG_RU = {
|
|||||||
'Batch (per card)': 'Пакетные операции (по картам)',
|
'Batch (per card)': 'Пакетные операции (по картам)',
|
||||||
'Batch reinstall app (per card)': 'Пакетная переустановка приложения (по картам)',
|
'Batch reinstall app (per card)': 'Пакетная переустановка приложения (по картам)',
|
||||||
'Batch delete (per card)': 'Пакетное удаление (по картам)',
|
'Batch delete (per card)': 'Пакетное удаление (по картам)',
|
||||||
|
'Show status': 'Показать статус',
|
||||||
'AID (hex)': 'AID (hex)',
|
'AID (hex)': 'AID (hex)',
|
||||||
'Target': 'Цель',
|
'Target': 'Цель',
|
||||||
'Application (instance only)': 'Приложение (только экземпляр)',
|
'Application (instance only)': 'Приложение (только экземпляр)',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v449';
|
const CACHE = 'simple-v450';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -23,9 +23,11 @@ function extractFunc(src, name, asyncFn) {
|
|||||||
|
|
||||||
const ASYNC_FNS = ['ramBatchFetch', 'ramBatchStart', 'ramBatchStop', 'ramBatchClear'];
|
const ASYNC_FNS = ['ramBatchFetch', 'ramBatchStart', 'ramBatchStop', 'ramBatchClear'];
|
||||||
let code = '';
|
let code = '';
|
||||||
for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchStepsText',
|
for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchMergedState',
|
||||||
|
'ramBatchTitle', 'ramBatchModalOpen', 'ramBatchModalRender', 'ramBatchStepsText',
|
||||||
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
||||||
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'esc']) {
|
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'ramOpProgressText',
|
||||||
|
'ramOpModalClose', 'esc']) {
|
||||||
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +38,7 @@ const els = {};
|
|||||||
function setEl(id, value) {
|
function setEl(id, value) {
|
||||||
els[id] = {
|
els[id] = {
|
||||||
id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '',
|
id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '',
|
||||||
disabled: false,
|
disabled: false, style: {},
|
||||||
classes: {},
|
classes: {},
|
||||||
classList: {
|
classList: {
|
||||||
add(c) { els[id].classes[c] = true; },
|
add(c) { els[id].classes[c] = true; },
|
||||||
@@ -57,6 +59,18 @@ function setEl(id, value) {
|
|||||||
function clearEls() {
|
function clearEls() {
|
||||||
for (const k of Object.keys(els)) delete els[k];
|
for (const k of Object.keys(els)) delete els[k];
|
||||||
}
|
}
|
||||||
|
// The modal chrome and the batch panel: the element sets the modal tests need.
|
||||||
|
function setModalEls() {
|
||||||
|
for (const id of ['ram-op-modal', 'ram-op-title', 'ram-op-status', 'ram-op-bar',
|
||||||
|
'ram-op-steps', 'ram-op-spin', 'ram-op-warn', 'ram-op-close',
|
||||||
|
'ram-op-batch-info', 'ram-op-batch-stop', 'ram-op-batch-clear']) setEl(id, '');
|
||||||
|
}
|
||||||
|
function setPanelEls() {
|
||||||
|
for (const id of ['ram-op', 'ram-batch-panel', 'ram-batch-start', 'ram-batch-stop',
|
||||||
|
'ram-batch-clear', 'ram-batch-show', 'ram-batch-spin', 'ram-batch-state',
|
||||||
|
'ram-batch-notice', 'ram-batch-counts', 'ram-batch-attempts']) setEl(id, '');
|
||||||
|
els['ram-op'].value = 'batch';
|
||||||
|
}
|
||||||
|
|
||||||
const app = new Function('els', `
|
const app = new Function('els', `
|
||||||
var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true };
|
var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true };
|
||||||
@@ -83,6 +97,8 @@ const app = new Function('els', `
|
|||||||
var updateRcPriv = () => {};
|
var updateRcPriv = () => {};
|
||||||
var updateStkParamsHex = () => {};
|
var updateStkParamsHex = () => {};
|
||||||
var pysimApplyAvailability = () => {};
|
var pysimApplyAvailability = () => {};
|
||||||
|
var _ramOpActive = false;
|
||||||
|
var _ramBatchModalShown = false;
|
||||||
var _ramBatchLast = null;
|
var _ramBatchLast = null;
|
||||||
var _ramBatchFetching = false;
|
var _ramBatchFetching = false;
|
||||||
var _ramBatchFetchAt = 0;
|
var _ramBatchFetchAt = 0;
|
||||||
@@ -93,6 +109,7 @@ const app = new Function('els', `
|
|||||||
sandbox.getLast = () => _ramBatchLast;
|
sandbox.getLast = () => _ramBatchLast;
|
||||||
sandbox.setLast = v => { _ramBatchLast = v; };
|
sandbox.setLast = v => { _ramBatchLast = v; };
|
||||||
sandbox.getSummary = () => _ramBatchSummary;
|
sandbox.getSummary = () => _ramBatchSummary;
|
||||||
|
sandbox.setSummary = v => { _ramBatchSummary = v; };
|
||||||
sandbox.isFetching = () => _ramBatchFetching;
|
sandbox.isFetching = () => _ramBatchFetching;
|
||||||
sandbox.setFetchingStale = ms => { _ramBatchFetching = true; _ramBatchFetchAt = Date.now() - ms; };
|
sandbox.setFetchingStale = ms => { _ramBatchFetching = true; _ramBatchFetchAt = Date.now() - ms; };
|
||||||
sandbox.resetFetching = () => { _ramBatchFetching = false; };
|
sandbox.resetFetching = () => { _ramBatchFetching = false; };
|
||||||
@@ -100,6 +117,11 @@ const app = new Function('els', `
|
|||||||
sandbox.ramBatchPanelVisible = ramBatchPanelVisible;
|
sandbox.ramBatchPanelVisible = ramBatchPanelVisible;
|
||||||
sandbox.ramBatchAttemptsHtml = ramBatchAttemptsHtml;
|
sandbox.ramBatchAttemptsHtml = ramBatchAttemptsHtml;
|
||||||
sandbox.ramBatchStepsText = ramBatchStepsText;
|
sandbox.ramBatchStepsText = ramBatchStepsText;
|
||||||
|
sandbox.ramBatchModalOpen = ramBatchModalOpen;
|
||||||
|
sandbox.ramOpModalClose = ramOpModalClose;
|
||||||
|
sandbox.isBatchModalShown = () => _ramBatchModalShown;
|
||||||
|
sandbox.resetBatchModal = () => { _ramBatchModalShown = false; };
|
||||||
|
sandbox.setProgress = v => { _ramBatchProgress = v; };
|
||||||
sandbox.ramBatchRender = ramBatchRender;
|
sandbox.ramBatchRender = ramBatchRender;
|
||||||
sandbox.ramBatchApplyStatus = ramBatchApplyStatus;
|
sandbox.ramBatchApplyStatus = ramBatchApplyStatus;
|
||||||
sandbox.ramBatchFetch = ramBatchFetch;
|
sandbox.ramBatchFetch = ramBatchFetch;
|
||||||
@@ -475,6 +497,105 @@ test('the attempts render the per-step tail for short chains (v3.29.0)', () => {
|
|||||||
assert.strictEqual(app.ramBatchStepsText({ steps: [{}, {}, {}, {}], reason: '' }), '');
|
assert.strictEqual(app.ramBatchStepsText({ steps: [{}, {}, {}, {}], reason: '' }), '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the batch status modal opens on Start and on a poll with a run in flight (v3.30.0)', async () => {
|
||||||
|
// the markup: the window's Stop/Clear/info and the panel's Show status
|
||||||
|
assert.match(html, /id="ram-op-batch-stop"/);
|
||||||
|
assert.match(html, /id="ram-op-batch-clear"/);
|
||||||
|
assert.match(html, /id="ram-op-batch-info"/);
|
||||||
|
assert.match(html, /id="ram-batch-show" onclick="ramBatchModalOpen\(\)"/);
|
||||||
|
assert.match(html, /'Show status': /);
|
||||||
|
// Start opens it
|
||||||
|
clearEls();
|
||||||
|
setModalEls();
|
||||||
|
setPanelEls();
|
||||||
|
app.setSummary(null);
|
||||||
|
els['ram-op'].value = 'batch-delete';
|
||||||
|
setEl('ram-del-aid', 'F0414C4641610101');
|
||||||
|
setEl('ram-del-target', 'app');
|
||||||
|
app.fetched = [];
|
||||||
|
await app.ramBatchStart();
|
||||||
|
assert.ok(app.isBatchModalShown());
|
||||||
|
assert.ok(els['ram-op-modal'].classes['hidden'] === undefined);
|
||||||
|
// a page load with an active run reopens it on the first poll
|
||||||
|
clearEls();
|
||||||
|
setModalEls();
|
||||||
|
setPanelEls();
|
||||||
|
app.resetBatchModal();
|
||||||
|
app.ramBatchApplyStatus({ ram_batch: { active: true, phase: 'waiting_card',
|
||||||
|
attempts_total: 0, counts: { installed: 0, failed: 0, skipped: 0 } },
|
||||||
|
ram_progress: null });
|
||||||
|
assert.ok(app.isBatchModalShown());
|
||||||
|
assert.ok(els['ram-op-modal'].classes['hidden'] === undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the batch modal follows the run: the step, the bar, the cards (v3.30.0)', () => {
|
||||||
|
clearEls();
|
||||||
|
setModalEls();
|
||||||
|
setPanelEls();
|
||||||
|
app.setSummary(null);
|
||||||
|
app.ramBatchModalOpen();
|
||||||
|
// waiting for a card
|
||||||
|
app.setLast({ kind: 'reinstall-app', active: true, phase: 'waiting_card',
|
||||||
|
attempts: [], counts: {}, attempts_total: 0 });
|
||||||
|
app.ramBatchRender();
|
||||||
|
assert.strictEqual(els['ram-op-title'].textContent, 'Batch reinstall app (per card)');
|
||||||
|
assert.strictEqual(els['ram-op-status'].textContent, 'Insert next card. Waiting for a card...');
|
||||||
|
assert.strictEqual(els['ram-op-bar'].style.width, '0%');
|
||||||
|
assert.ok(els['ram-op-close'].disabled);
|
||||||
|
assert.ok(els['ram-op-batch-stop'].classes['hidden'] === undefined);
|
||||||
|
assert.ok(els['ram-op-batch-clear'].disabled);
|
||||||
|
// installing: the live step and the bar
|
||||||
|
app.setLast({ kind: 'reinstall-app', active: true, phase: 'installing',
|
||||||
|
attempts: [], counts: { installed: 1, failed: 0, skipped: 0 },
|
||||||
|
attempts_total: 1, current: { iccid: '8970119000004600098' } });
|
||||||
|
app.setProgress({ active: true, step: 2, total: 4, name: 'INSTALL [for install]' });
|
||||||
|
app.ramBatchRender();
|
||||||
|
assert.match(els['ram-op-status'].textContent, /Step 2\/4 — INSTALL \[for install\]/);
|
||||||
|
assert.strictEqual(els['ram-op-bar'].style.width, '50%');
|
||||||
|
assert.match(els['ram-op-batch-info'].textContent, /Reinstalled: 1/);
|
||||||
|
// the finished report: the cards list, the notice, Close enabled
|
||||||
|
app.setLast({ kind: 'reinstall-app', active: false, phase: 'finished', attempts: [
|
||||||
|
{ iccid: '8970119000004600098', result: 'installed', steps: [
|
||||||
|
{ name: 'DELETE (application)', sw: '6101', ok: true },
|
||||||
|
{ name: 'INSTALL [for install]', sw: '6101', ok: true }] }],
|
||||||
|
counts: { installed: 1, failed: 0, skipped: 0 }, attempts_total: 1,
|
||||||
|
notice: 'Card 8970119000004600098 reinstalled.' });
|
||||||
|
app.setProgress(null);
|
||||||
|
app.ramBatchRender();
|
||||||
|
assert.match(els['ram-op-steps'].innerHTML, /DELETE \(application\) 6101 · INSTALL \[for install\] 6101/);
|
||||||
|
assert.match(els['ram-op-batch-info'].textContent, /Card 8970119000004600098 reinstalled\./);
|
||||||
|
assert.strictEqual(els['ram-op-bar'].style.width, '100%');
|
||||||
|
assert.ok(!els['ram-op-close'].disabled);
|
||||||
|
assert.ok(els['ram-op-batch-stop'].classes['hidden']); // the run ended
|
||||||
|
assert.ok(!els['ram-op-batch-clear'].disabled);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the batch modal stays open while the run is active (v3.30.0)', () => {
|
||||||
|
clearEls();
|
||||||
|
setModalEls();
|
||||||
|
setPanelEls();
|
||||||
|
app.setSummary(null);
|
||||||
|
app.ramBatchModalOpen();
|
||||||
|
app.setLast({ active: true, phase: 'waiting_card', attempts: [], counts: {}, attempts_total: 0 });
|
||||||
|
app.ramBatchRender();
|
||||||
|
app.ramOpModalClose();
|
||||||
|
assert.ok(app.isBatchModalShown(), 'the run keeps the window open');
|
||||||
|
assert.ok(els['ram-op-modal'].classes['hidden'] === undefined);
|
||||||
|
// the run ended: Close works, and the panel offers the reopen button
|
||||||
|
app.setLast({ active: false, phase: 'stopped', attempts: [], counts: { installed: 1 },
|
||||||
|
attempts_total: 1 });
|
||||||
|
app.ramBatchRender();
|
||||||
|
app.ramOpModalClose();
|
||||||
|
assert.ok(!app.isBatchModalShown());
|
||||||
|
assert.ok(els['ram-op-modal'].classes['hidden']);
|
||||||
|
app.ramBatchRender();
|
||||||
|
assert.ok(els['ram-batch-show'].classes['hidden'] === undefined);
|
||||||
|
// and it reopens for the finished report
|
||||||
|
app.ramBatchModalOpen();
|
||||||
|
assert.ok(app.isBatchModalShown());
|
||||||
|
assert.ok(els['ram-op-modal'].classes['hidden'] === undefined);
|
||||||
|
});
|
||||||
|
|
||||||
test('the stop and clear calls hit their endpoints', async () => {
|
test('the stop and clear calls hit their endpoints', async () => {
|
||||||
clearEls();
|
clearEls();
|
||||||
app.fetched = [];
|
app.fetched = [];
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.29.2"
|
version = "3.30.0"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
requires-python = ">=3.8"
|
requires-python = ">=3.8"
|
||||||
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
|
# 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
|
from osmocom.utils import rpad
|
||||||
|
|
||||||
|
|
||||||
VERSION = '3.29.2'
|
VERSION = '3.30.0'
|
||||||
|
|
||||||
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user