diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f70d84..88b97e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,25 @@ every change carries a descriptive commit message. ## [Unreleased] +## [3.36.0] — 2026-10-23 + +### Added + +- The batch **"waiting for the next card"** state is now a deliberate cue: the status + modal shows a card-into-reader animation next to the prompt, a short chime sounds + **once per card** when the run enters the waiting phase (Web Audio — no audio asset; + edge-triggered in the 2 s poll, so it does not repeat), and while the tab is hidden the + title hints "Insert next card". The speaker button next to the status mutes the chime + and the choice is remembered (`simple_ram_batch_sound`); the chime is unlocked by the + Start click (browser autoplay policy); reduced-motion users get the static card-above- + the-slot picture. + +### Fixed + +- The prebuilt `style.css` had gone stale: a rebuild restored utility classes the UI + already used but the output lacked (e.g. `bg-blue-50` on the batch notice, `inline-flex`, + `mx-2`, `max-h-48`, `w-44`, `min-w-24`). + ## [3.35.0] — 2026-10-23 ### Added diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 8adf50d..c7e30a8 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -331,7 +331,7 @@ INSTALL [for make selectable]Один APDU INSTALL [for make selectable] (AID экземпляра) для установленного, но невыбираемого экземпляра апплета. -

Пакетная операция показывает свой статус в модальном окне: сверху — текущий прогресс (или «Вставьте следующую карту. Ожидание карты…»), ниже — обработанные карты, а внутри — Остановить и Очистить. Окно открывается при запуске и при загрузке страницы, пока запуск активен; Закрыть остаётся недоступным до окончания запуска — сам запуск выполняется на сервере и переживает перезагрузку. После завершения отчёт можно открыть заново кнопкой Показать статус в панели.

+

Пакетная операция показывает свой статус в модальном окне: сверху — текущий прогресс (или «Вставьте следующую карту. Ожидание карты…»), ниже — обработанные карты, а внутри — Остановить и Очистить. Окно открывается при запуске и при загрузке страницы, пока запуск активен; Закрыть остаётся недоступным до окончания запуска — сам запуск выполняется на сервере и переживает перезагрузку. После завершения отчёт можно открыть заново кнопкой Показать статус в панели. Пока прогон ждёт карту, в модальном окне показывается анимация вставки карты в считыватель и один раз на каждую карту звучит короткий сигнал (кнопка-динамик рядом со статусом отключает звук, выбор запоминается); если окно свёрнуто, заголовок вкладки просит вставить следующую карту.

Обзор карты (Explorer View)

После выполнения «Обзор карты» отображается:

diff --git a/frontend/help.html b/frontend/help.html index 14bb516..8fc254c 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -330,7 +330,7 @@ INSTALL [for make selectable]One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance. -

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 Stop and Clear inside. It opens on Start and on a page load while a run is active; Close stays disabled until the run ends — the run itself is server-side and survives a reload. After a finished run the panel’s Show status button reopens the report.

+

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 Stop and Clear inside. It opens on Start and on a page load while a run is active; Close stays disabled until the run ends — the run itself is server-side and survives a reload. After a finished run the panel’s Show status button reopens the report. While the run waits for a card the modal shows a card-into-reader animation and plays a short chime once per card (the speaker button next to the status mutes it and the choice is remembered); when the window is in the background the tab title asks for the next card.

Explorer View

After “Explore Card” runs, the explorer displays:

diff --git a/frontend/index.html b/frontend/index.html index aaf9adf..007a944 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1918,7 +1918,20 @@
+ Working... +
@@ -1944,7 +1957,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.35.0'; +const SIMPLE_VERSION = '3.36.0'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== view history (the browser Back/Forward buttons) ===== @@ -10273,6 +10286,8 @@ function ramBatchModalOpen() { 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'); + const sound = document.getElementById('ram-op-sound'); + if (sound) { sound.classList.remove('hidden'); ramBatchUpdateSoundButton(); } modal.classList.remove('hidden'); ramBatchRender(); // paint from the current state now } @@ -10327,6 +10342,98 @@ function ramBatchModalRender(st, okLabel, actLabel) { document.getElementById('ram-op-close').disabled = active; } +// ===== batch "insert the next card" cue (v3.36.0) ===== +// The waiting phase is the only moment the operator must act, so it gets a +// card-into-reader animation in the modal, an optional chime (edge-triggered: +// once per card, not on every 2 s poll) and a tab-title hint while the tab is +// hidden. The chime needs an AudioContext unlocked by a gesture: the Start +// click (and the sound toggle) do that; a reloaded page stays silent until the +// next click. +let _ramBatchWaiting = false; +let _ramBatchAudio = null; +let _ramBatchTitleBase = ''; + +function ramBatchSoundEnabled() { + try { return localStorage.getItem('simple_ram_batch_sound') !== '0'; } + catch (e) { return true; } +} + +function ramBatchUpdateSoundButton() { + const btn = document.getElementById('ram-op-sound'); + if (!btn) return; + const on = ramBatchSoundEnabled(); + btn.textContent = on ? '\u{1F50A}' : '\u{1F507}'; + btn.setAttribute('aria-pressed', on ? 'true' : 'false'); + btn.title = t(on ? 'Sound on' : 'Sound off'); + btn.setAttribute('aria-label', t(on ? 'Sound on' : 'Sound off')); +} + +function ramBatchToggleSound() { + try { localStorage.setItem('simple_ram_batch_sound', ramBatchSoundEnabled() ? '0' : '1'); } + catch (e) { /* private mode */ } + ramBatchUpdateSoundButton(); + if (ramBatchSoundEnabled()) ramBatchChime(); // a preview when turning it on +} + +function ramBatchAudioUnlock() { + try { + if (typeof window === 'undefined') return; + const Ctor = window.AudioContext || window.webkitAudioContext; + if (!Ctor) return; + if (!_ramBatchAudio) _ramBatchAudio = new Ctor(); + if (_ramBatchAudio.state === 'suspended') _ramBatchAudio.resume(); + } catch (e) { _ramBatchAudio = null; } +} + +function ramBatchChime() { + if (!ramBatchSoundEnabled()) return; + try { + if (typeof window === 'undefined') return; + const Ctor = window.AudioContext || window.webkitAudioContext; + if (!Ctor) return; + if (!_ramBatchAudio) _ramBatchAudio = new Ctor(); + const ctx = _ramBatchAudio; + if (ctx.state === 'suspended') { ctx.resume().catch(() => {}); } + const start = ctx.currentTime + 0.02; + [880, 660].forEach((freq, i) => { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + osc.type = 'sine'; + osc.frequency.value = freq; + const at = start + i * 0.18; + gain.gain.setValueAtTime(0.0001, at); + gain.gain.exponentialRampToValueAtTime(0.18, at + 0.02); + gain.gain.exponentialRampToValueAtTime(0.0001, at + 0.15); + osc.connect(gain); + gain.connect(ctx.destination); + osc.start(at); + osc.stop(at + 0.17); + }); + } catch (e) { /* no audio available: the visual cue remains */ } +} + +function ramBatchTitleHint(waiting) { + if (!_ramBatchTitleBase) _ramBatchTitleBase = document.title || 'SIMple'; + const hint = !!waiting && !!document.hidden; + document.title = hint ? t('Insert next card') + ' \u2014 SIMple' : _ramBatchTitleBase; +} + +// Called from ramBatchRender (the 2 s poll): returns true only on the +// transition into the waiting phase, and updates the animation/title. +function ramBatchWaitingCue(waiting) { + const entered = !!waiting && !_ramBatchWaiting; + _ramBatchWaiting = !!waiting; + const ins = document.getElementById('ram-op-insert'); + if (ins) { + ins.classList.toggle('hidden', !waiting); + ins.classList.toggle('flex', !!waiting); + } + ramBatchTitleHint(waiting); + return entered; +} + +document.addEventListener('visibilitychange', () => ramBatchTitleHint(_ramBatchWaiting)); + async function ramBatchFetch() { if (_ramBatchFetching) return; _ramBatchFetching = true; @@ -10378,6 +10485,10 @@ function ramBatchRender() { const st = ramBatchMergedState(); const active = !!st.active; const phase = st.phase || (active ? 'waiting_card' : 'idle'); + // The waiting cue: the animation in the modal, the chime once per card and + // the tab-title hint (v3.36.0). + const waiting = active && phase !== 'installing' && phase !== 'stopping'; + if (ramBatchWaitingCue(waiting)) ramBatchChime(); // The success labels follow the operation (v3.29.0): the counts bucket is // 'installed' for every kind - "the operation succeeded". The last seen // kind is remembered for the modal's title and these labels, so a cleared @@ -10526,6 +10637,8 @@ function ramInstallCapFields() { } async function ramBatchStart() { + // The click is the gesture the waiting chime needs later. + ramBatchAudioUnlock(); // The operation selects the batch kind (v3.29.0): the .cap install, the // applet reinstall (no .cap - the package is loaded) or a GP delete. The // server builds and validates the plan once at Start. @@ -10634,6 +10747,10 @@ function ramOpModalBegin(title) { '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-spin').classList.remove('hidden'); + const ins = document.getElementById('ram-op-insert'); + if (ins) { ins.classList.add('hidden'); ins.classList.remove('flex'); } + const snd = document.getElementById('ram-op-sound'); + if (snd) snd.classList.add('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'); @@ -26786,6 +26903,9 @@ const LANG_RU = { 'Reinstalling...': 'Переустановка…', 'Stopping after the current card...': 'Остановка после текущей карты…', 'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…', + 'Insert next card': 'Вставьте следующую карту', + 'Sound on': 'Звук включён', + 'Sound off': 'Звук выключен', 'Installed': 'Установлено', 'Deleted': 'Удалено', 'Reinstalled': 'Переустановлено', diff --git a/frontend/src/style.css b/frontend/src/style.css index 492f330..23ec191 100644 --- a/frontend/src/style.css +++ b/frontend/src/style.css @@ -4,4 +4,27 @@ .dark .text-heading { color: #f1f5f9; -} \ No newline at end of file +} +/* RAM batch: the "insert the next card" cue (v3.36.0). + The card slides into the reader slot and back; the slot glows while the + run waits. The static transform is the reduced-motion fallback: the card + rests above the slot. */ +.ram-insert-card { + transform: translateY(-14px); + animation: ram-insert 2.8s ease-in-out infinite; +} +@keyframes ram-insert { + 0%, 12% { transform: translateY(-14px); } + 45%, 55% { transform: translateY(10px); } + 88%, 100% { transform: translateY(-14px); } +} +.ram-insert-glow { + animation: ram-insert-glow 1.4s ease-in-out infinite; +} +@keyframes ram-insert-glow { + 0%, 100% { opacity: 0.35; } + 50% { opacity: 0.95; } +} +@media (prefers-reduced-motion: reduce) { + .ram-insert-card, .ram-insert-glow { animation: none; } +} diff --git a/frontend/style.css b/frontend/style.css index e9f5164..3d80922 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -616,6 +616,10 @@ video { pointer-events: none; } +.\!visible { + visibility: visible !important; +} + .visible { visibility: visible; } @@ -684,6 +688,11 @@ video { grid-column: span 4 / span 4; } +.mx-2 { + margin-left: 0.5rem; + margin-right: 0.5rem; +} + .mx-3 { margin-left: 0.75rem; margin-right: 0.75rem; @@ -807,6 +816,10 @@ video { display: flex; } +.inline-flex { + display: inline-flex; +} + .table { display: table; } @@ -843,6 +856,10 @@ video { max-height: 8rem; } +.max-h-48 { + max-height: 12rem; +} + .max-h-56 { max-height: 14rem; } @@ -935,6 +952,10 @@ video { width: 10rem; } +.w-44 { + width: 11rem; +} + .w-48 { width: 12rem; } @@ -980,6 +1001,10 @@ video { min-width: 0px; } +.min-w-24 { + min-width: 6rem; +} + .min-w-\[12rem\] { min-width: 12rem; } @@ -1395,6 +1420,11 @@ video { background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1)); } +.bg-blue-50 { + --tw-bg-opacity: 1; + background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1)); +} + .bg-blue-50\/50 { background-color: rgb(239 246 255 / 0.5); } @@ -1796,6 +1826,11 @@ video { color: rgb(31 41 55 / var(--tw-text-opacity, 1)); } +.text-gray-900 { + --tw-text-opacity: 1; + color: rgb(17 24 39 / var(--tw-text-opacity, 1)); +} + .text-green-600 { --tw-text-opacity: 1; color: rgb(22 163 74 / var(--tw-text-opacity, 1)); @@ -1893,10 +1928,60 @@ video { filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow); } +.transition { + transition-property: color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + .dark .text-heading { color: #f1f5f9; } +/* RAM batch: the "insert the next card" cue (v3.36.0). + The card slides into the reader slot and back; the slot glows while the + run waits. The static transform is the reduced-motion fallback: the card + rests above the slot. */ + +.ram-insert-card { + transform: translateY(-14px); + animation: ram-insert 2.8s ease-in-out infinite; +} + +@keyframes ram-insert { + 0%, 12% { + transform: translateY(-14px); + } + + 45%, 55% { + transform: translateY(10px); + } + + 88%, 100% { + transform: translateY(-14px); + } +} + +.ram-insert-glow { + animation: ram-insert-glow 1.4s ease-in-out infinite; +} + +@keyframes ram-insert-glow { + 0%, 100% { + opacity: 0.35; + } + + 50% { + opacity: 0.95; + } +} + +@media (prefers-reduced-motion: reduce) { + .ram-insert-card, .ram-insert-glow { + animation: none; + } +} + .hover\:bg-amber-100:hover { --tw-bg-opacity: 1; background-color: rgb(254 243 199 / var(--tw-bg-opacity, 1)); @@ -1956,11 +2041,6 @@ video { background-color: rgb(55 65 81 / var(--tw-bg-opacity, 1)); } -.hover\:bg-red-100:hover { - --tw-bg-opacity: 1; - background-color: rgb(254 226 226 / var(--tw-bg-opacity, 1)); -} - .hover\:bg-red-200:hover { --tw-bg-opacity: 1; background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1)); @@ -2093,6 +2173,10 @@ video { background-color: rgb(30 58 138 / 0.1); } +.dark\:bg-blue-900\/20:is(.dark *) { + background-color: rgb(30 58 138 / 0.2); +} + .dark\:bg-blue-900\/40:is(.dark *) { background-color: rgb(30 58 138 / 0.4); } @@ -2258,10 +2342,6 @@ video { background-color: rgb(6 78 59 / 0.4); } -.dark\:hover\:bg-red-900\/40:hover:is(.dark *) { - background-color: rgb(127 29 29 / 0.4); -} - .dark\:hover\:bg-red-900\/50:hover:is(.dark *) { background-color: rgb(127 29 29 / 0.5); } @@ -2289,6 +2369,11 @@ video { color: rgb(209 213 219 / var(--tw-text-opacity, 1)); } +.dark\:hover\:text-slate-200:hover:is(.dark *) { + --tw-text-opacity: 1; + color: rgb(226 232 240 / var(--tw-text-opacity, 1)); +} + .dark\:hover\:text-slate-300:hover:is(.dark *) { --tw-text-opacity: 1; color: rgb(203 213 225 / var(--tw-text-opacity, 1)); @@ -2328,7 +2413,8 @@ video { .lg\:flex-row { flex-direction: row; } -}/* ===== Theme contrast adjustments ===== */ +} +/* ===== Theme contrast adjustments ===== */ /* Light theme: darken softer (muted) text one step for higher contrast */ .text-gray-400 { diff --git a/frontend/sw.js b/frontend/sw.js index 78cfc31..b13651e 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v469'; +const CACHE = 'simple-v470'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram_batch.test.js b/frontend/tests/ram_batch.test.js index f3de529..adab833 100644 --- a/frontend/tests/ram_batch.test.js +++ b/frontend/tests/ram_batch.test.js @@ -28,7 +28,10 @@ for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchMergedState', 'ramBatchStepsText', 'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch', 'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'ramOpProgressText', - 'ramOpModalClose', 'esc']) { + 'ramOpModalClose', 'esc', + 'ramBatchSoundEnabled', 'ramBatchUpdateSoundButton', 'ramBatchToggleSound', + 'ramBatchAudioUnlock', 'ramBatchChime', 'ramBatchTitleHint', + 'ramBatchWaitingCue']) { code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n'; } @@ -39,7 +42,9 @@ const els = {}; function setEl(id, value) { els[id] = { id: id, value: value === undefined ? '' : value, textContent: '', innerHTML: '', - disabled: false, style: {}, + disabled: false, style: {}, title: '', attrs: {}, + setAttribute(k, v) { els[id].attrs[k] = String(v); }, + getAttribute(k) { return els[id].attrs[k]; }, classes: {}, classList: { add(c) { els[id].classes[c] = true; }, @@ -64,7 +69,8 @@ function clearEls() { 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, ''); + 'ram-op-batch-info', 'ram-op-batch-stop', 'ram-op-batch-clear', + 'ram-op-insert', 'ram-op-sound']) setEl(id, ''); } function setPanelEls() { for (const id of ['ram-op', 'ram-batch-panel', 'ram-batch-start', 'ram-batch-stop', @@ -75,7 +81,17 @@ function setPanelEls() { const app = new Function('els', ` var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true }; - var document = { getElementById: id => els[id] || null }; + var document = { + getElementById: id => els[id] || null, + hidden: false, title: 'SIMple', + addEventListener: () => {}, + }; + var _ls = {}; + var localStorage = { + getItem: k => (k in _ls ? _ls[k] : null), + setItem: (k, v) => { _ls[k] = String(v); }, + removeItem: k => { delete _ls[k]; }, + }; var t = k => k; var isViewVisible = () => sandbox.visibility; var alert = m => sandbox.alerts.push(String(m)); @@ -109,6 +125,9 @@ const app = new Function('els', ` var _ramBatchFetchAt = 0; var _ramBatchSummary = null; var _ramBatchProgress = null; + var _ramBatchWaiting = false; + var _ramBatchAudio = null; + var _ramBatchTitleBase = ''; var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS}; ${code} sandbox.getLast = () => _ramBatchLast; @@ -134,6 +153,16 @@ const app = new Function('els', ` sandbox.ramBatchStart = ramBatchStart; sandbox.ramBatchStop = ramBatchStop; sandbox.ramBatchClear = ramBatchClear; + sandbox.ramBatchWaitingCue = ramBatchWaitingCue; + sandbox.ramBatchTitleHint = ramBatchTitleHint; + sandbox.ramBatchSoundEnabled = ramBatchSoundEnabled; + sandbox.ramBatchUpdateSoundButton = ramBatchUpdateSoundButton; + sandbox.ramBatchToggleSound = ramBatchToggleSound; + sandbox.setHidden = v => { document.hidden = !!v; }; + sandbox.getTitle = () => document.title; + sandbox.resetWaiting = () => { _ramBatchWaiting = false; }; + sandbox.storage = _ls; + sandbox.resetStorage = () => { for (const k of Object.keys(_ls)) delete _ls[k]; }; return sandbox; `)(els); @@ -639,3 +668,70 @@ test('the stop and clear calls hit their endpoints', async () => { await new Promise(r => setImmediate(r)); assert.deepStrictEqual(app.fetched, ['/api/ram-batch/stop', '/api/ram-batch/clear', '/api/ram-batch/status']); }); + + +test('the waiting cue fires once per card and drives the insert animation (v3.36.0)', () => { + clearEls(); + setPanelEls(); + setModalEls(); + app.resetWaiting(); + assert.strictEqual(app.ramBatchWaitingCue(true), true, 'entering waiting cues'); + assert.strictEqual(app.ramBatchWaitingCue(true), false, 'still waiting: no repeat'); + assert.strictEqual(app.ramBatchWaitingCue(false), false, 'installing resets'); + assert.strictEqual(app.ramBatchWaitingCue(true), true, 'the next card cues again'); + assert.ok(els['ram-op-insert'].classes['flex'] === true); + assert.ok(!els['ram-op-insert'].classes['hidden']); + app.ramBatchWaitingCue(false); + assert.ok(els['ram-op-insert'].classes['hidden']); + // the render drives it from the phase + app.setLast({ active: true, phase: 'waiting_card', attempts: [], counts: {}, attempts_total: 0, notice: '' }); + app.ramBatchRender(); + assert.ok(!els['ram-op-insert'].classes['hidden']); + app.setLast({ active: true, phase: 'installing', current: { iccid: '8970119000004600098' }, + attempts: [], counts: {}, attempts_total: 1 }); + app.ramBatchRender(); + assert.ok(els['ram-op-insert'].classes['hidden']); +}); + +test('the sound toggle persists and updates the button (v3.36.0)', () => { + clearEls(); + setModalEls(); + app.resetStorage(); + assert.strictEqual(app.ramBatchSoundEnabled(), true, 'sound is on by default'); + app.ramBatchUpdateSoundButton(); + assert.strictEqual(els['ram-op-sound'].textContent, '\u{1F50A}'); + assert.strictEqual(els['ram-op-sound'].attrs['aria-pressed'], 'true'); + app.ramBatchToggleSound(); + assert.strictEqual(app.ramBatchSoundEnabled(), false); + assert.strictEqual(app.storage['simple_ram_batch_sound'], '0'); + assert.strictEqual(els['ram-op-sound'].textContent, '\u{1F507}'); + assert.strictEqual(els['ram-op-sound'].attrs['aria-pressed'], 'false'); + app.ramBatchToggleSound(); + assert.strictEqual(app.ramBatchSoundEnabled(), true); +}); + +test('the tab title hints while the run waits and the tab is hidden (v3.36.0)', () => { + app.setHidden(false); + app.ramBatchTitleHint(true); + assert.strictEqual(app.getTitle(), 'SIMple'); + app.setHidden(true); + app.ramBatchTitleHint(true); + assert.strictEqual(app.getTitle(), 'Insert next card \u2014 SIMple'); + app.setHidden(false); + app.ramBatchTitleHint(true); + assert.strictEqual(app.getTitle(), 'SIMple'); +}); + +test('the waiting cue is wired into the poll render, the Start gesture and the CSS (v3.36.0)', () => { + const render = extractFunc(html, 'ramBatchRender'); + assert.match(render, /const waiting = active && phase !== 'installing' && phase !== 'stopping';/); + assert.match(render, /if \(ramBatchWaitingCue\(waiting\)\) ramBatchChime\(\);/); + assert.match(extractFunc(html, 'ramBatchStart'), /ramBatchAudioUnlock\(\)/); + assert.match(html, /id="ram-op-insert"/); + assert.match(html, /id="ram-op-sound"/); + const css = fs.readFileSync(path.join(__dirname, '..', 'style.css'), 'utf8'); + assert.match(css, /\.ram-insert-card/); + assert.match(css, /@keyframes ram-insert/); + assert.match(css, /\.ram-insert-glow/); + assert.match(css, /prefers-reduced-motion: reduce/); +}); diff --git a/pyproject.toml b/pyproject.toml index bf5a4cb..73d7e9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pysim-simple-server" -version = "3.35.0" +version = "3.36.0" description = "HTTP REST server wrapping pysim for the SIMple PWA" license = { text = "GPL-2.0-or-later" } requires-python = ">=3.9" diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py index 6032835..df4c2e0 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.35.0' +VERSION = '3.36.0' MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE