feat: toolkit block emphasised + auto CA/EA from the CAP (v3.6.32)

The RAM install form's toolkit parameters are the make-or-break field for
toolkit applets, and CA vs EA is easy to get wrong (the earlier live 6A80s).

- "SIM/UICC Toolkit parameters" is now an accented bordered fieldset (blue
  border/legend, always visible in the cap / install / make-selectable
  modes) with the checkbox in the legend and the fields inside.
- ramCapToolkitMode(imports) maps the CAP's linked libraries to the install
  parameter family: sim.* (AID tree A0000000090003FFFFFFFF89...) -> SIM
  Toolkit (Tag CA), uicc.* / uicc.usim.* (A0000000090005FFFFFFFF89...,
  A0000000871005FFFFFFFF89...) -> UICC Toolkit (Tag EA) - family-based, so
  access-only applets get the right field too.  Applied when the block is
  enabled and when the CAP is swapped while it is on, with a "detected from
  the CAP" hint under the select; no CAP keeps the manual choice.
- Rebuilt style.css for the new accent classes; tests pin the fieldset and
  the family mapping.

663 frontend / 504 Python green; version 3.6.32; sw simple-v305.
This commit is contained in:
2026-09-28 23:49:00 +03:00
parent 30918e4f4a
commit e820a7ed41
7 changed files with 110 additions and 11 deletions
+53 -7
View File
@@ -751,13 +751,13 @@
<label class="flex items-center gap-1"><input type="checkbox" class="rc-priv-b3" value="80" onchange="updateRcPriv()"> Receipt Generation</label> <label class="flex items-center gap-1"><input type="checkbox" class="rc-priv-b3" value="80" onchange="updateRcPriv()"> Receipt Generation</label>
</div> </div>
</div> </div>
<div id="rc-toolkit-row" class="mb-3"> <fieldset id="rc-toolkit-row" class="mb-3 mt-2 rounded border border-blue-300 dark:border-blue-800 bg-blue-50/50 dark:bg-blue-900/10 p-3">
<div class="mb-2"> <legend class="px-1 text-sm font-medium text-blue-800 dark:text-blue-300">
<label class="flex items-center gap-2 text-sm font-medium text-gray-700 dark:text-slate-300"> <label class="flex items-center gap-2">
<input id="rc-toolkit-enable" type="checkbox" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="updateRcToolkit()"> <input id="rc-toolkit-enable" type="checkbox" class="rounded border-gray-300 dark:border-slate-600 dark:bg-slate-800" onchange="updateRcToolkit()">
SIM/UICC Toolkit parameters <span data-l10n="SIM/UICC Toolkit parameters">SIM/UICC Toolkit parameters</span>
</label> </label>
</div> </legend>
<div id="rc-toolkit-body" class="hidden"> <div id="rc-toolkit-body" class="hidden">
<div class="mb-3"> <div class="mb-3">
<label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Toolkit mode">Toolkit mode</label> <label class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Toolkit mode">Toolkit mode</label>
@@ -765,6 +765,7 @@
<option value="ca">SIM Toolkit (Tag CA)</option> <option value="ca">SIM Toolkit (Tag CA)</option>
<option value="ea">UICC Toolkit (Tag EA)</option> <option value="ea">UICC Toolkit (Tag EA)</option>
</select> </select>
<div id="rc-tk-mode-hint" class="hidden mt-1 text-xs text-blue-700 dark:text-blue-300"></div>
</div> </div>
<div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm"> <div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm">
<div> <div>
@@ -845,7 +846,7 @@
</div> </div>
</div> </div>
</div> </div>
</div> </fieldset>
<div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm mt-2"> <div class="grid grid-cols-2 gap-x-3 gap-y-2 text-sm mt-2">
<div> <div>
<label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Applet-specific parameters (C9, hex, optional)">Applet-specific parameters (C9, hex, optional)</label> <label class="block mb-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="Applet-specific parameters (C9, hex, optional)">Applet-specific parameters (C9, hex, optional)</label>
@@ -1713,7 +1714,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.6.31'; const SIMPLE_VERSION = '3.6.32';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -2545,9 +2546,52 @@ function updateRcPriv() {
document.getElementById('rc-priv').value = hex; document.getElementById('rc-priv').value = hex;
} }
// The install-parameter family a CAP belongs to, from its Import component:
// sim.* (sim.toolkit / sim.access, AID tree A0000000090003FFFFFFFF89...) uses
// the CA field (SIM Toolkit); uicc.* / uicc.usim.* (uicc.toolkit /
// uicc.access / ..., AID trees A0000000090005FFFFFFFF89... and
// A0000000871005FFFFFFFF89...) uses the EA field (UICC Toolkit). Returns
// 'ca', 'ea' or null when the CAP links against neither.
function ramCapToolkitMode(imports) {
const aids = (imports || []).map(im => (im.aid || '').toUpperCase());
if (aids.some(a => a.startsWith('A0000000090005FFFFFFFF89') ||
a.startsWith('A0000000871005FFFFFFFF89'))) return 'ea';
if (aids.some(a => a.startsWith('A0000000090003FFFFFFFF89'))) return 'ca';
return null;
}
// Pick the toolkit mode of the analysed RAM CAP (when its libraries decide it)
// and, when the block is in use, apply it: never make the user guess CA vs EA.
function ramApplyCapToolkitMode() {
const st = _capAnalysis.ram;
const mode = ramCapToolkitMode((st.memory && st.memory.imports) || []);
st.tk_mode = mode;
const sel = document.getElementById('rc-tk-mode');
const hint = document.getElementById('rc-tk-mode-hint');
if (hint) {
if (mode) {
hint.textContent = t('detected from the CAP') + ': ' + sel.options[sel.selectedIndex].text;
hint.classList.remove('hidden');
} else {
hint.classList.add('hidden');
}
}
const enable = document.getElementById('rc-toolkit-enable');
if (mode && enable && enable.checked) {
sel.value = mode;
if (hint) hint.textContent = t('detected from the CAP') + ': ' +
(mode === 'ea' ? 'UICC Toolkit (Tag EA)' : 'SIM Toolkit (Tag CA)');
updateRcTkMode();
updateStkParamsHex();
}
}
function updateRcToolkit() { function updateRcToolkit() {
const checked = document.getElementById('rc-toolkit-enable').checked; const checked = document.getElementById('rc-toolkit-enable').checked;
document.getElementById('rc-toolkit-body').classList.toggle('hidden', !checked); document.getElementById('rc-toolkit-body').classList.toggle('hidden', !checked);
// A CAP linked against sim.* / uicc.* decides the toolkit mode
const st = _capAnalysis.ram;
if (checked && st && st.tk_mode) document.getElementById('rc-tk-mode').value = st.tk_mode;
updateRcTkMode(); updateRcTkMode();
updateStkParamsHex(); updateStkParamsHex();
} }
@@ -8454,6 +8498,7 @@ async function capAnalyzeFile(file, kind) {
if (resp && resp.ok) { if (resp && resp.ok) {
st.status = 'ok'; st.status = 'ok';
st.memory = resp.memory || {}; st.memory = resp.memory || {};
if (kind === 'ram') ramApplyCapToolkitMode();
} else { } else {
st.status = 'error'; st.status = 'error';
st.error = (resp && resp.error) || t('Analysis failed'); st.error = (resp && resp.error) || t('Analysis failed');
@@ -16782,6 +16827,7 @@ const LANG_RU = {
'Byte 1': 'Байт 1', 'Byte 1': 'Байт 1',
'Byte 2': 'Байт 2', 'Byte 2': 'Байт 2',
'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit', 'SIM/UICC Toolkit parameters': 'Параметры SIM/UICC Toolkit',
'detected from the CAP': 'определено по CAP',
'Toolkit mode': 'Режим Toolkit', 'Toolkit mode': 'Режим Toolkit',
'Max services': 'Максимум сервисов', 'Max services': 'Максимум сервисов',
'Priority': 'Приоритет', 'Priority': 'Приоритет',
+23
View File
@@ -1296,6 +1296,11 @@ video {
border-color: rgb(251 191 36 / var(--tw-border-opacity, 1)); border-color: rgb(251 191 36 / var(--tw-border-opacity, 1));
} }
.border-blue-300 {
--tw-border-opacity: 1;
border-color: rgb(147 197 253 / var(--tw-border-opacity, 1));
}
.border-blue-400 { .border-blue-400 {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(96 165 250 / var(--tw-border-opacity, 1)); border-color: rgb(96 165 250 / var(--tw-border-opacity, 1));
@@ -1359,6 +1364,10 @@ 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\/50 {
background-color: rgb(239 246 255 / 0.5);
}
.bg-blue-600 { .bg-blue-600 {
--tw-bg-opacity: 1; --tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1)); background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
@@ -1693,6 +1702,11 @@ video {
color: rgb(29 78 216 / var(--tw-text-opacity, 1)); color: rgb(29 78 216 / var(--tw-text-opacity, 1));
} }
.text-blue-800 {
--tw-text-opacity: 1;
color: rgb(30 64 175 / var(--tw-text-opacity, 1));
}
.text-emerald-600 { .text-emerald-600 {
--tw-text-opacity: 1; --tw-text-opacity: 1;
color: rgb(5 150 105 / var(--tw-text-opacity, 1)); color: rgb(5 150 105 / var(--tw-text-opacity, 1));
@@ -1954,6 +1968,11 @@ video {
border-color: rgb(217 119 6 / var(--tw-border-opacity, 1)); border-color: rgb(217 119 6 / var(--tw-border-opacity, 1));
} }
.dark\:border-blue-800:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(30 64 175 / var(--tw-border-opacity, 1));
}
.dark\:border-red-800:is(.dark *) { .dark\:border-red-800:is(.dark *) {
--tw-border-opacity: 1; --tw-border-opacity: 1;
border-color: rgb(153 27 27 / var(--tw-border-opacity, 1)); border-color: rgb(153 27 27 / var(--tw-border-opacity, 1));
@@ -1987,6 +2006,10 @@ video {
background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1)); background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
} }
.dark\:bg-blue-900\/10:is(.dark *) {
background-color: rgb(30 58 138 / 0.1);
}
.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);
} }
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v304'; const CACHE = 'simple-v305';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+3
View File
@@ -41,6 +41,9 @@ eval(extractFunc(html, 'jcAidFamily'));
eval(extractFunc(html, 'capMemBytes')); eval(extractFunc(html, 'capMemBytes'));
eval(extractFunc(html, 'capMemHtml')); eval(extractFunc(html, 'capMemHtml'));
eval(extractFunc(html, 'capAnalyzeFile', true)); eval(extractFunc(html, 'capAnalyzeFile', true));
// The RAM CAP analysis also applies the detected toolkit mode (DOM-bound; its
// own logic is covered in ram.test.js) - stub it for these state tests.
globalThis.ramApplyCapToolkitMode = () => {};
globalThis.esc = s => s; globalThis.esc = s => s;
globalThis.t = s => s; globalThis.t = s => s;
+28 -1
View File
@@ -25,7 +25,7 @@ function extractFunc(src, name) {
const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu', const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu',
'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu',
'_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry',
'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'ramCapToolkitMode',
'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml'];
let code = ''; let code = '';
for (const f of FNS) { for (const f of FNS) {
@@ -274,6 +274,33 @@ test('ramFmtPrivileges decodes the GP privilege bytes (live ISD values)', () =>
delete global.t; delete global.t;
}); });
test('ramCapToolkitMode picks CA/EA from the CAP linked libraries', () => {
// sim.toolkit / sim.access (A0000000090003...) -> SIM Toolkit (CA field)
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710002' }]), 'ca');
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090003FFFFFFFF8910710001' }]), 'ca');
// uicc.toolkit / uicc.access (A0000000090005...) -> UICC Toolkit (EA field)
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090005FFFFFFFF8912000000' }]), 'ea');
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000090005FFFFFFFF8911000000' }]), 'ea');
// uicc.usim.* (A0000000871005...) -> UA family wins over a SIM library
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000871005FFFFFFFF8913200000' }]), 'ea');
assert.strictEqual(ramCapToolkitMode([
{ aid: 'A0000000090003FFFFFFFF8910710002' },
{ aid: 'A0000000871005FFFFFFFF8913200000' }]), 'ea');
// no toolkit/access library: leave the user's choice alone
assert.strictEqual(ramCapToolkitMode([{ aid: 'A0000000620101' }, { aid: 'A0000000620001' }]), null);
assert.strictEqual(ramCapToolkitMode([]), null);
assert.strictEqual(ramCapToolkitMode(undefined), null);
});
test('the toolkit block is an accented bordered fieldset', () => {
const m = /id="rc-toolkit-row"[^>]*class="([^"]*)"/.exec(html);
assert.ok(m, 'rc-toolkit-row not found');
assert.ok(m[1].includes('border'), m[1]);
assert.ok(m[1].includes('p-3'), m[1]);
assert.ok(/<legend[^>]*>[\s\S]*?rc-toolkit-enable[\s\S]*?<\/legend>/.test(html),
'the enable checkbox lives in the fieldset legend');
});
test('ramFmtPrivileges uses the translated (none) placeholder', () => { test('ramFmtPrivileges uses the translated (none) placeholder', () => {
global.t = s => 'XX' + s; global.t = s => 'XX' + s;
assert.strictEqual(ramFmtPrivileges(''), 'XX(none)'); assert.strictEqual(ramFmtPrivileges(''), 'XX(none)');
+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.6.31" version = "3.6.32"
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.6.31' VERSION = '3.6.32'
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