v3.36.0: a deliberate "insert the next card" cue for batch runs
The waiting phase is the only moment the operator must act, so it now: - shows a card-into-reader SVG animation in the status modal (inline SVG, CSS keyframes in src/style.css; reduced-motion users get the static card-above-the-slot picture); - plays a short Web Audio chime once per card, edge-triggered in the 2 s poll so it never repeats (the AudioContext is unlocked by the Start click); - sets the tab title to "Insert next card" while the window is hidden; - offers a speaker toggle next to the status (persisted as simple_ram_batch_sound, on by default). The prebuilt style.css was rebuilt: it had gone stale and lacked utilities the UI already used (bg-blue-50 on the batch notice, inline-flex, mx-2, max-h-48, w-44, min-w-24). 1022 Python + 1062 frontend green; simple-v470.
This commit is contained in:
@@ -9,6 +9,25 @@ every change carries a descriptive commit message.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [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
|
## [3.35.0] — 2026-10-23
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -331,7 +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>
|
<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>
|
||||||
|
|||||||
+1
-1
@@ -330,7 +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>
|
<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. 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.</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>
|
||||||
|
|||||||
+121
-1
@@ -1918,7 +1918,20 @@
|
|||||||
</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">
|
||||||
<div id="ram-op-spin" class="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
|
<div id="ram-op-spin" class="animate-spin w-4 h-4 border-2 border-blue-500 border-t-transparent rounded-full"></div>
|
||||||
|
<span id="ram-op-insert" class="hidden items-center" aria-hidden="true">
|
||||||
|
<svg viewBox="0 0 72 44" width="64" height="39" class="shrink-0">
|
||||||
|
<g class="ram-insert-card text-blue-600 dark:text-blue-400">
|
||||||
|
<rect x="27" y="1" width="18" height="26" rx="2.5" fill="currentColor"></rect>
|
||||||
|
<rect x="31" y="6" width="10" height="7" rx="1" fill="#fff" opacity="0.9"></rect>
|
||||||
|
<path d="M31 8.5h10M31 10.5h10M34.5 6v7M38.5 6v7" stroke="currentColor" stroke-width="0.6" opacity="0.55"></path>
|
||||||
|
</g>
|
||||||
|
<rect x="3" y="18" width="66" height="22" rx="4" fill="currentColor" class="text-gray-500 dark:text-slate-400"></rect>
|
||||||
|
<rect x="8" y="26" width="56" height="5" rx="2.5" fill="currentColor" class="text-gray-900" opacity="0.55"></rect>
|
||||||
|
<rect class="ram-insert-glow text-amber-500 dark:text-amber-400" x="8" y="26" width="56" height="5" rx="2.5" fill="none" stroke="currentColor" stroke-width="1.2"></rect>
|
||||||
|
</svg>
|
||||||
|
</span>
|
||||||
<span id="ram-op-status" data-l10n="Working...">Working...</span>
|
<span id="ram-op-status" data-l10n="Working...">Working...</span>
|
||||||
|
<button type="button" id="ram-op-sound" onclick="ramBatchToggleSound()" class="ml-auto hidden px-2 py-0.5 text-xs rounded-full border border-gray-300 dark:border-slate-600 text-gray-600 dark:text-slate-300 hover:bg-gray-200 dark:hover:bg-slate-700" aria-pressed="true" title="Sound on" aria-label="Sound on">🔊</button>
|
||||||
</div>
|
</div>
|
||||||
<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>
|
||||||
@@ -1944,7 +1957,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.35.0';
|
const SIMPLE_VERSION = '3.36.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) =====
|
||||||
@@ -10273,6 +10286,8 @@ function ramBatchModalOpen() {
|
|||||||
document.getElementById('ram-op-spin').classList.add('hidden');
|
document.getElementById('ram-op-spin').classList.add('hidden');
|
||||||
document.getElementById('ram-op-warn').classList.add('hidden');
|
document.getElementById('ram-op-warn').classList.add('hidden');
|
||||||
document.getElementById('ram-op-batch-info').classList.remove('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');
|
modal.classList.remove('hidden');
|
||||||
ramBatchRender(); // paint from the current state now
|
ramBatchRender(); // paint from the current state now
|
||||||
}
|
}
|
||||||
@@ -10327,6 +10342,98 @@ function ramBatchModalRender(st, okLabel, actLabel) {
|
|||||||
document.getElementById('ram-op-close').disabled = active;
|
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() {
|
async function ramBatchFetch() {
|
||||||
if (_ramBatchFetching) return;
|
if (_ramBatchFetching) return;
|
||||||
_ramBatchFetching = true;
|
_ramBatchFetching = true;
|
||||||
@@ -10378,6 +10485,10 @@ function ramBatchRender() {
|
|||||||
const st = ramBatchMergedState();
|
const st = ramBatchMergedState();
|
||||||
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 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
|
// The success labels follow the operation (v3.29.0): the counts bucket is
|
||||||
// 'installed' for every kind - "the operation succeeded". The last seen
|
// 'installed' for every kind - "the operation succeeded". The last seen
|
||||||
// kind is remembered for the modal's title and these labels, so a cleared
|
// kind is remembered for the modal's title and these labels, so a cleared
|
||||||
@@ -10526,6 +10637,8 @@ function ramInstallCapFields() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function ramBatchStart() {
|
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
|
// 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
|
// applet reinstall (no .cap - the package is loaded) or a GP delete. The
|
||||||
// server builds and validates the plan once at Start.
|
// 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';
|
'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');
|
||||||
|
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-warn').classList.remove('hidden');
|
||||||
document.getElementById('ram-op-batch-info').classList.add('hidden');
|
document.getElementById('ram-op-batch-info').classList.add('hidden');
|
||||||
document.getElementById('ram-op-batch-stop').classList.add('hidden');
|
document.getElementById('ram-op-batch-stop').classList.add('hidden');
|
||||||
@@ -26786,6 +26903,9 @@ const LANG_RU = {
|
|||||||
'Reinstalling...': 'Переустановка…',
|
'Reinstalling...': 'Переустановка…',
|
||||||
'Stopping after the current card...': 'Остановка после текущей карты…',
|
'Stopping after the current card...': 'Остановка после текущей карты…',
|
||||||
'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…',
|
'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…',
|
||||||
|
'Insert next card': 'Вставьте следующую карту',
|
||||||
|
'Sound on': 'Звук включён',
|
||||||
|
'Sound off': 'Звук выключен',
|
||||||
'Installed': 'Установлено',
|
'Installed': 'Установлено',
|
||||||
'Deleted': 'Удалено',
|
'Deleted': 'Удалено',
|
||||||
'Reinstalled': 'Переустановлено',
|
'Reinstalled': 'Переустановлено',
|
||||||
|
|||||||
@@ -5,3 +5,26 @@
|
|||||||
.dark .text-heading {
|
.dark .text-heading {
|
||||||
color: #f1f5f9;
|
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; }
|
||||||
|
}
|
||||||
|
|||||||
+96
-10
@@ -616,6 +616,10 @@ video {
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.\!visible {
|
||||||
|
visibility: visible !important;
|
||||||
|
}
|
||||||
|
|
||||||
.visible {
|
.visible {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
}
|
}
|
||||||
@@ -684,6 +688,11 @@ video {
|
|||||||
grid-column: span 4 / span 4;
|
grid-column: span 4 / span 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mx-2 {
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mx-3 {
|
.mx-3 {
|
||||||
margin-left: 0.75rem;
|
margin-left: 0.75rem;
|
||||||
margin-right: 0.75rem;
|
margin-right: 0.75rem;
|
||||||
@@ -807,6 +816,10 @@ video {
|
|||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-flex {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.table {
|
.table {
|
||||||
display: table;
|
display: table;
|
||||||
}
|
}
|
||||||
@@ -843,6 +856,10 @@ video {
|
|||||||
max-height: 8rem;
|
max-height: 8rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-h-48 {
|
||||||
|
max-height: 12rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-h-56 {
|
.max-h-56 {
|
||||||
max-height: 14rem;
|
max-height: 14rem;
|
||||||
}
|
}
|
||||||
@@ -935,6 +952,10 @@ video {
|
|||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-44 {
|
||||||
|
width: 11rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-48 {
|
.w-48 {
|
||||||
width: 12rem;
|
width: 12rem;
|
||||||
}
|
}
|
||||||
@@ -980,6 +1001,10 @@ video {
|
|||||||
min-width: 0px;
|
min-width: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.min-w-24 {
|
||||||
|
min-width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.min-w-\[12rem\] {
|
.min-w-\[12rem\] {
|
||||||
min-width: 12rem;
|
min-width: 12rem;
|
||||||
}
|
}
|
||||||
@@ -1395,6 +1420,11 @@ video {
|
|||||||
background-color: rgb(219 234 254 / var(--tw-bg-opacity, 1));
|
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 {
|
.bg-blue-50\/50 {
|
||||||
background-color: rgb(239 246 255 / 0.5);
|
background-color: rgb(239 246 255 / 0.5);
|
||||||
}
|
}
|
||||||
@@ -1796,6 +1826,11 @@ video {
|
|||||||
color: rgb(31 41 55 / var(--tw-text-opacity, 1));
|
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 {
|
.text-green-600 {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(22 163 74 / var(--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);
|
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 {
|
.dark .text-heading {
|
||||||
color: #f1f5f9;
|
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 {
|
.hover\:bg-amber-100:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(254 243 199 / var(--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));
|
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 {
|
.hover\:bg-red-200:hover {
|
||||||
--tw-bg-opacity: 1;
|
--tw-bg-opacity: 1;
|
||||||
background-color: rgb(254 202 202 / var(--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);
|
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 *) {
|
.dark\:bg-blue-900\/40:is(.dark *) {
|
||||||
background-color: rgb(30 58 138 / 0.4);
|
background-color: rgb(30 58 138 / 0.4);
|
||||||
}
|
}
|
||||||
@@ -2258,10 +2342,6 @@ video {
|
|||||||
background-color: rgb(6 78 59 / 0.4);
|
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 *) {
|
.dark\:hover\:bg-red-900\/50:hover:is(.dark *) {
|
||||||
background-color: rgb(127 29 29 / 0.5);
|
background-color: rgb(127 29 29 / 0.5);
|
||||||
}
|
}
|
||||||
@@ -2289,6 +2369,11 @@ video {
|
|||||||
color: rgb(209 213 219 / var(--tw-text-opacity, 1));
|
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 *) {
|
.dark\:hover\:text-slate-300:hover:is(.dark *) {
|
||||||
--tw-text-opacity: 1;
|
--tw-text-opacity: 1;
|
||||||
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
|
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
|
||||||
@@ -2328,7 +2413,8 @@ video {
|
|||||||
.lg\:flex-row {
|
.lg\:flex-row {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
}
|
}
|
||||||
}/* ===== Theme contrast adjustments ===== */
|
}
|
||||||
|
/* ===== Theme contrast adjustments ===== */
|
||||||
|
|
||||||
/* Light theme: darken softer (muted) text one step for higher contrast */
|
/* Light theme: darken softer (muted) text one step for higher contrast */
|
||||||
.text-gray-400 {
|
.text-gray-400 {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v469';
|
const CACHE = 'simple-v470';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -28,7 +28,10 @@ for (const fn of ['ramIsBatchOp', 'ramBatchPanelVisible', 'ramBatchMergedState',
|
|||||||
'ramBatchStepsText',
|
'ramBatchStepsText',
|
||||||
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
'ramBatchAttemptsHtml', 'ramBatchRender', 'ramBatchApplyStatus', 'ramBatchFetch',
|
||||||
'ramBatchStart', 'ramBatchStop', 'ramBatchClear', 'ramOpProgressText',
|
'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';
|
code += extractFunc(html, fn, ASYNC_FNS.indexOf(fn) >= 0) + '\n';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +42,9 @@ 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, style: {},
|
disabled: false, style: {}, title: '', attrs: {},
|
||||||
|
setAttribute(k, v) { els[id].attrs[k] = String(v); },
|
||||||
|
getAttribute(k) { return els[id].attrs[k]; },
|
||||||
classes: {},
|
classes: {},
|
||||||
classList: {
|
classList: {
|
||||||
add(c) { els[id].classes[c] = true; },
|
add(c) { els[id].classes[c] = true; },
|
||||||
@@ -64,7 +69,8 @@ function clearEls() {
|
|||||||
function setModalEls() {
|
function setModalEls() {
|
||||||
for (const id of ['ram-op-modal', 'ram-op-title', 'ram-op-status', 'ram-op-bar',
|
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-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() {
|
function setPanelEls() {
|
||||||
for (const id of ['ram-op', 'ram-batch-panel', 'ram-batch-start', 'ram-batch-stop',
|
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', `
|
const app = new Function('els', `
|
||||||
var sandbox = { fetched: [], replies: {}, fetchError: null, alerts: [], visibility: true };
|
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 t = k => k;
|
||||||
var isViewVisible = () => sandbox.visibility;
|
var isViewVisible = () => sandbox.visibility;
|
||||||
var alert = m => sandbox.alerts.push(String(m));
|
var alert = m => sandbox.alerts.push(String(m));
|
||||||
@@ -109,6 +125,9 @@ const app = new Function('els', `
|
|||||||
var _ramBatchFetchAt = 0;
|
var _ramBatchFetchAt = 0;
|
||||||
var _ramBatchSummary = null;
|
var _ramBatchSummary = null;
|
||||||
var _ramBatchProgress = null;
|
var _ramBatchProgress = null;
|
||||||
|
var _ramBatchWaiting = false;
|
||||||
|
var _ramBatchAudio = null;
|
||||||
|
var _ramBatchTitleBase = '';
|
||||||
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
|
var RAM_BATCH_FETCH_STALE_MS = ${RAM_BATCH_FETCH_STALE_MS};
|
||||||
${code}
|
${code}
|
||||||
sandbox.getLast = () => _ramBatchLast;
|
sandbox.getLast = () => _ramBatchLast;
|
||||||
@@ -134,6 +153,16 @@ const app = new Function('els', `
|
|||||||
sandbox.ramBatchStart = ramBatchStart;
|
sandbox.ramBatchStart = ramBatchStart;
|
||||||
sandbox.ramBatchStop = ramBatchStop;
|
sandbox.ramBatchStop = ramBatchStop;
|
||||||
sandbox.ramBatchClear = ramBatchClear;
|
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;
|
return sandbox;
|
||||||
`)(els);
|
`)(els);
|
||||||
|
|
||||||
@@ -639,3 +668,70 @@ test('the stop and clear calls hit their endpoints', async () => {
|
|||||||
await new Promise(r => setImmediate(r));
|
await new Promise(r => setImmediate(r));
|
||||||
assert.deepStrictEqual(app.fetched, ['/api/ram-batch/stop', '/api/ram-batch/clear', '/api/ram-batch/status']);
|
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/);
|
||||||
|
});
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "pysim-simple-server"
|
name = "pysim-simple-server"
|
||||||
version = "3.35.0"
|
version = "3.36.0"
|
||||||
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
description = "HTTP REST server wrapping pysim for the SIMple PWA"
|
||||||
license = { text = "GPL-2.0-or-later" }
|
license = { text = "GPL-2.0-or-later" }
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9"
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ from osmocom.tlv import BER_TLV_IE
|
|||||||
from osmocom.utils import rpad
|
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
|
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user