ui: put the CAP report in a bordered "CAP details" container (v3.5.19)

The CAP analysis now lives in the simulator-style fieldset with the title
embedded in the border:

  <fieldset id="ram-cap-info" class="hidden mt-2 border ... p-3 text-xs ...">
    <legend ... data-l10n="CAP details">CAP details</legend>
    <div id="ram-cap-body"></div>
  </fieldset>

- Both CAP boxes (RAM installer, SCP81 Scripts) use the same bordered
  fieldset + legend pattern as the simulator/eSIM/card fieldsets.
- capRenderAnalysis toggles the fieldset and writes into the body div, so
  the border and title stay visible for analyzing / failed (Retry) / done
  states alike.
- capMemHtml is pure content now; the inner "CAP requirements (estimate)"
  heading is gone - the border legend titles the box.
- i18n: 'CAP details' -> "Детали CAP" (static legend, data-l10n).
- refreshDynamicI18n() rebuilds both CAP bodies on a language switch (the
  dynamic report previously kept the old language).

Tests: the wiring test requires both legends and body divs, a new
capRenderAnalysis test covers idle/analyzing/error/ok, and the renderer
tests no longer expect the inner heading.

593 frontend / 473 Python green; version 3.5.19; sw simple-v272.
This commit is contained in:
2026-09-27 15:02:21 +03:00
parent b43e3118cf
commit 73ef3a67ca
5 changed files with 60 additions and 13 deletions
+20 -9
View File
@@ -701,7 +701,10 @@
<div id="ram-install-params" class="hidden mb-3"> <div id="ram-install-params" class="hidden mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAP file (max 48 kB)">CAP file (max 48 kB)</label> <label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="CAP file (max 48 kB)">CAP file (max 48 kB)</label>
<input type="file" id="ram-cap-file" accept=".cap,.zip" onchange="ramCapFileChanged()" class="w-full text-sm text-gray-600 dark:text-slate-300"> <input type="file" id="ram-cap-file" accept=".cap,.zip" onchange="ramCapFileChanged()" class="w-full text-sm text-gray-600 dark:text-slate-300">
<div id="ram-cap-info" class="text-xs text-gray-500 mt-1 hidden"></div> <fieldset id="ram-cap-info" class="hidden mt-2 border border-gray-200 dark:border-slate-700 rounded p-3 text-xs text-gray-500 dark:text-slate-400">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="CAP details">CAP details</legend>
<div id="ram-cap-body"></div>
</fieldset>
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300 mt-2" data-l10n="SD AID (empty = ISD)">SD AID (empty = ISD)</label> <label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300 mt-2" data-l10n="SD AID (empty = ISD)">SD AID (empty = ISD)</label>
<input id="ram-sd-aid" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000003000000" maxlength="32"> <input id="ram-sd-aid" class="w-full font-mono border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="A000000003000000" maxlength="32">
<div id="rc-priv-row" class="mb-3 mt-2"> <div id="rc-priv-row" class="mb-3 mt-2">
@@ -1376,7 +1379,10 @@
<label class="flex items-center gap-2 text-xs text-gray-600 dark:text-slate-400"><input type="checkbox" id="scripts-cap-makesel" checked> <span data-l10n="Make selectable">Make selectable</span></label> <label class="flex items-center gap-2 text-xs text-gray-600 dark:text-slate-400"><input type="checkbox" id="scripts-cap-makesel" checked> <span data-l10n="Make selectable">Make selectable</span></label>
<button data-needs="server" data-cap-gate="scripts" onclick="scriptsGenerateInstall()" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Generate">Generate</button> <button data-needs="server" data-cap-gate="scripts" onclick="scriptsGenerateInstall()" class="px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700 disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="Generate">Generate</button>
</div> </div>
<div id="scripts-cap-info" class="text-xs text-gray-500 mt-1 hidden"></div> <fieldset id="scripts-cap-info" class="hidden mt-2 border border-gray-200 dark:border-slate-700 rounded p-3 text-xs text-gray-500 dark:text-slate-400">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="CAP details">CAP details</legend>
<div id="scripts-cap-body"></div>
</fieldset>
<div class="text-xs text-gray-500 dark:text-slate-400 mt-2" data-l10n="The .cap is only used to generate the APDUs: the script stores the APDU list, not the file.">The .cap is only used to generate the APDUs: the script stores the APDU list, not the file.</div> <div class="text-xs text-gray-500 dark:text-slate-400 mt-2" data-l10n="The .cap is only used to generate the APDUs: the script stores the APDU list, not the file.">The .cap is only used to generate the APDUs: the script stores the APDU list, not the file.</div>
</div> </div>
<div id="scripts-delete-params" class="hidden mt-3 border-t border-gray-200 dark:border-slate-700 pt-3"> <div id="scripts-delete-params" class="hidden mt-3 border-t border-gray-200 dark:border-slate-700 pt-3">
@@ -1616,7 +1622,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.5.18'; const SIMPLE_VERSION = '3.5.19';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -8047,7 +8053,7 @@ function capMemHtml(mem) {
const part = content => cell(content, 'pl-4 text-gray-400 dark:text-slate-500'); const part = content => cell(content, 'pl-4 text-gray-400 dark:text-slate-500');
const section = content => '<div class="mt-2 font-medium text-gray-500 dark:text-slate-400" style="text-transform:uppercase;letter-spacing:.06em">' + esc(content) + '</div>'; const section = content => '<div class="mt-2 font-medium text-gray-500 dark:text-slate-400" style="text-transform:uppercase;letter-spacing:.06em">' + esc(content) + '</div>';
const disclosure = content => '<details class="mt-1"><summary class="cursor-pointer mt-2 font-medium text-gray-500 dark:text-slate-400" style="text-transform:uppercase;letter-spacing:.06em">' + esc(content) + '</summary>'; const disclosure = content => '<details class="mt-1"><summary class="cursor-pointer mt-2 font-medium text-gray-500 dark:text-slate-400" style="text-transform:uppercase;letter-spacing:.06em">' + esc(content) + '</summary>';
let html = '<div class="font-medium text-gray-700 dark:text-slate-300">' + esc(t('CAP requirements (estimate)')) + '</div>'; let html = '';
// ── package identity: AIDs, header flags (Table 6-4), applets and the // ── package identity: AIDs, header flags (Table 6-4), applets and the
// compiled-against hint (javacard.framework version + CAP format) ── // compiled-against hint (javacard.framework version + CAP format) ──
@@ -8153,22 +8159,23 @@ function capMemHtml(mem) {
function capRenderAnalysis(kind) { function capRenderAnalysis(kind) {
const box = document.getElementById(kind === 'ram' ? 'ram-cap-info' : 'scripts-cap-info'); const box = document.getElementById(kind === 'ram' ? 'ram-cap-info' : 'scripts-cap-info');
if (!box) return; const body = document.getElementById(kind === 'ram' ? 'ram-cap-body' : 'scripts-cap-body');
if (!box || !body) return;
const st = _capAnalysis[kind]; const st = _capAnalysis[kind];
if (!st || st.status === 'idle') { if (!st || st.status === 'idle') {
box.classList.add('hidden'); box.classList.add('hidden');
box.innerHTML = ''; body.innerHTML = '';
return; return;
} }
box.classList.remove('hidden'); box.classList.remove('hidden');
if (st.status === 'analyzing') { if (st.status === 'analyzing') {
box.innerHTML = '<span class="text-gray-400">' + esc(t('Analyzing CAP file...')) + '</span>'; body.innerHTML = '<span class="text-gray-400">' + esc(t('Analyzing CAP file...')) + '</span>';
} else if (st.status === 'error') { } else if (st.status === 'error') {
box.innerHTML = '<span class="text-red-500">' + esc(t('Analysis failed:')) + ' ' + esc(st.error || '') + '</span>' + body.innerHTML = '<span class="text-red-500">' + esc(t('Analysis failed:')) + ' ' + esc(st.error || '') + '</span>' +
' <button class="underline text-gray-500 hover:text-gray-700 dark:text-slate-400" onclick="' + ' <button class="underline text-gray-500 hover:text-gray-700 dark:text-slate-400" onclick="' +
(kind === 'ram' ? 'ramCapFileChanged()' : 'scriptsCapFileChanged()') + '">' + esc(t('Retry')) + '</button>'; (kind === 'ram' ? 'ramCapFileChanged()' : 'scriptsCapFileChanged()') + '">' + esc(t('Retry')) + '</button>';
} else { } else {
box.innerHTML = capMemHtml(st.memory); body.innerHTML = capMemHtml(st.memory);
} }
} }
@@ -16143,6 +16150,7 @@ const LANG_RU = {
'Analysis failed:': 'Ошибка анализа:', 'Analysis failed:': 'Ошибка анализа:',
'Retry': 'Повторить', 'Retry': 'Повторить',
'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл', 'Analyze the CAP file first': 'Сначала проанализируйте CAP-файл',
'CAP details': 'Детали CAP',
'CAP requirements (estimate)': 'Требования CAP (оценка)', 'CAP requirements (estimate)': 'Требования CAP (оценка)',
'NVRAM requirement ≈': 'Требование NVRAM ≈', 'NVRAM requirement ≈': 'Требование NVRAM ≈',
'Requires:': 'Требуются:', 'Requires:': 'Требуются:',
@@ -16929,6 +16937,9 @@ function refreshDynamicI18n() {
if (isViewVisible('tab-cards')) { cardsRender(); cardsSetFormMode(); } if (isViewVisible('tab-cards')) { cardsRender(); cardsSetFormMode(); }
if (isViewVisible('scp81-sub-scripts')) { scriptsRender(); scriptsRebuildSelect(); } if (isViewVisible('scp81-sub-scripts')) { scriptsRender(); scriptsRebuildSelect(); }
if (_ramExplorerData && isViewVisible('ram-explorer')) ramRenderExplorer(); if (_ramExplorerData && isViewVisible('ram-explorer')) ramRenderExplorer();
// the CAP analysis bodies are built with t(); the legends are static
capRenderAnalysis('ram');
capRenderAnalysis('scripts');
if (isViewVisible('tab-phone')) { if (isViewVisible('tab-phone')) {
if (isViewVisible('phone-sub-phone')) { if (isViewVisible('phone-sub-phone')) {
pysimEventsRender(); pysimEventsRender();
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v271'; const CACHE = 'simple-v272';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+37 -1
View File
@@ -83,7 +83,8 @@ test('capMemHtml renders the grouped report: package, requires, memory, componen
const mem = memFixture(); const mem = memFixture();
mem.warnings = ['method class[0].token[1]: unknown opcode 0xAA, scan stopped']; mem.warnings = ['method class[0].token[1]: unknown opcode 0xAA, scan stopped'];
const out = capMemHtml(mem); const out = capMemHtml(mem);
assert.ok(out.includes('CAP requirements (estimate)'), out); // the border legend titles the box now; the body starts with the sections
assert.ok(!out.includes('CAP requirements (estimate)'), out);
// grouped, labelled sections (always visible except components/notes) // grouped, labelled sections (always visible except components/notes)
assert.ok(out.includes('>Package</div>'), out); assert.ok(out.includes('>Package</div>'), out);
assert.ok(out.includes('>Requires (1)</div>'), out); assert.ok(out.includes('>Requires (1)</div>'), out);
@@ -228,4 +229,39 @@ test('the CAP inputs and their action buttons are wired', () => {
assert.match(html, /id="ram-cap-info"/); assert.match(html, /id="ram-cap-info"/);
assert.match(html, /id="scripts-cap-info"/); assert.match(html, /id="scripts-cap-info"/);
assert.match(html, /\/api\/cap-info/); assert.match(html, /\/api\/cap-info/);
// the report lives in a bordered fieldset with the title in the border
// (the simulator/eSIM pattern); the JS fills the body div
assert.match(html, /<fieldset id="ram-cap-info"[^>]*>\s*<legend[^>]*data-l10n="CAP details">/);
assert.match(html, /<fieldset id="scripts-cap-info"[^>]*>\s*<legend[^>]*data-l10n="CAP details">/);
assert.match(html, /id="ram-cap-body"/);
assert.match(html, /id="scripts-cap-body"/);
});
test('capRenderAnalysis toggles the border box and fills its body', () => {
eval(extractFunc(html, 'capRenderAnalysis'));
const box = { hidden: true, classList: {
add() { box.hidden = true; },
remove() { box.hidden = false; },
} };
const body = { innerHTML: '' };
globalThis.document = { getElementById: id => ({
'ram-cap-info': box, 'ram-cap-body': body,
}[id] || null) };
// idle: hidden and empty
_capAnalysis.ram = { key: null, status: 'idle', memory: null, error: null, token: 0 };
capRenderAnalysis('ram');
assert.ok(box.hidden && body.innerHTML === '');
// a finished analysis: shown, body carries the report
_capAnalysis.ram = { key: null, status: 'ok', memory: memFixture(), error: null, token: 0 };
capRenderAnalysis('ram');
assert.ok(!box.hidden, 'the box is shown');
assert.ok(body.innerHTML.includes('>Requires (1)</div>'), body.innerHTML);
// a failure: shown with the error and a retry
_capAnalysis.ram = { key: null, status: 'error', memory: null, error: 'bad zip', token: 0 };
capRenderAnalysis('ram');
assert.ok(!box.hidden && body.innerHTML.includes('bad zip') && body.innerHTML.includes('Retry'), body.innerHTML);
// analyzing: shown with the progress line
_capAnalysis.ram = { key: null, status: 'analyzing', memory: null, error: null, token: 0 };
capRenderAnalysis('ram');
assert.ok(body.innerHTML.includes('Analyzing CAP file...'), body.innerHTML);
}); });
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "3.5.18" version = "3.5.19"
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.
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '3.5.18' VERSION = '3.5.19'
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