ui: the preset list highlights the equipped card's preset
The Cards list now marks the row whose ICCID matches the equipped card (cardsFindByIccid handles digits / spaced / raw EF hex alike): an emerald row tint plus a small dot with a "The equipped card's preset" tooltip in the Name cell. There is no highlight while the ICCID is unknown (no card, unreadable EF.ICCID) or when no preset matches - the amber banner already covers that. pysimCardStateUpdate() re-renders the list when the ICCID actually changes, so a visible Cards tab follows equip/removal live (opening the tab re-renders anyway through switchTab). Tests: cards_iccid.test.js drives cardsRender with a fake tbody - the matching preset is highlighted (both ICCID forms), the others stay plain and no ICCID means no highlight; card_state.test.js stubs cardsRender. Help EN/RU and the repo AGENTS updated; sw cache simple-v344.
This commit is contained in:
@@ -346,7 +346,7 @@
|
||||
<p class="mb-2">Каждый принятый картой пакет <strong>расходует счётчик</strong>, который сохраняется в keyset пресета — проба отклоняется, если в SPI1 нет проверки счётчика или в пресете нет такого номера keyset. На живой карте: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> и <code class="font-mono text-sm">B00001</code> зарегистрированы (приложения компактного формата), расширенный формат, USAT, мультиплексирование и CASD отклонены с <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка «Карты»</h2>
|
||||
<p class="mb-2">Хранит пресеты карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит <strong>несколько keysets</strong> — номер keyset задаётся нибблом <code class="font-mono text-sm">b8..b5</code> его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками <strong>Добавить keyset</strong> / <strong>Удалить keyset</strong>; номер в строке вычисляется из байта KIc/KID, а <code class="font-mono text-sm">00</code> («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает <strong>ни с одним</strong> пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<p class="mb-2">Хранит пресеты карт на сервере (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; переопределяется <code class="font-mono text-sm">--card-presets</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, слушатель SCP81 HTTP OTA — находить PSK-ключи, а счётчики SCP80, израсходованные операцией, сохранялись самой операцией — закрытая вкладка или второе окно браузера больше не могут потерять инкремент. Пресет содержит <strong>несколько keysets</strong> — номер keyset задаётся нибблом <code class="font-mono text-sm">b8..b5</code> его KIc/KID (TS 102 225 §5.1.2/A.2): KIc и KID одного keyset должны нести один и тот же номер (01–0F; A.2 отвергает несовпадение), и у каждого keyset на карте свой счётчик (Annex A.1), поэтому счётчики ведутся по keysets. В форме keysets редактируются строками — по одной на номер, с кнопками <strong>Добавить keyset</strong> / <strong>Удалить keyset</strong>; номер в строке вычисляется из байта KIc/KID, а <code class="font-mono text-sm">00</code> («без защиты») не принимается как keyset. Список загружается при подключении PWA и экспортируется/импортируется как JSON (импорт принимает и пресеты старых сборок — плоские и с keysets). «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и пресет с тем же ICCID автоматически выбирается во всех трёх представлениях SCP80; если подключённая карта не совпадает <strong>ни с одним</strong> пресетом — или выбранный пресет принадлежит другой карте — все три представления SCP80 показывают жёлтое предупреждение над выбором пресета. В списке подсвечивается пресет подключённой карты. Поля формы сгруппированы в два блока с рамкой: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле <strong>ADM</strong> находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры <strong>SCP80</strong>/<strong>SCP81</strong> и значок ключа на бейдже ADM, когда в пресете с ICCID подключённой карты эти поля заполнены.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Поле</th><th class="text-left py-1 px-2">Описание</th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
+1
-1
@@ -345,7 +345,7 @@
|
||||
<p class="mb-2">Each packet the card accepts <strong>consumes a counter</strong>, which is saved into the preset's keyset — the probe is refused when SPI1 has no counter check or the named preset does not define the keyset number. Observed on a live card: <code class="font-mono text-sm">000000</code>, <code class="font-mono text-sm">B00000</code> and <code class="font-mono text-sm">B00001</code> registered (compact-format applications), the expanded-format TARs, USAT, multiplexing and CASD refused with <code class="font-mono text-sm">6200</code>.</p>
|
||||
|
||||
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Cards tab</h2>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The optional <strong>ADM</strong> field sits in the top row and is stored for later use by the file manager. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<p class="mb-2">Stores card presets on the server (<code class="font-mono text-sm">~/.pysim-simple-server/card_presets.json</code>; <code class="font-mono text-sm">--card-presets</code> overrides it) so the Secured Packet view can auto-fill keys and parameters, the SCP81 HTTP OTA listener can look up PSK keys, and the SCP80 counters an operation consumed are persisted with the operation itself — a closed tab or a second browser window can no longer lose an increment. A preset carries <strong>several keysets</strong> — a keyset's number is the <code class="font-mono text-sm">b8..b5</code> nibble of its KIc/KID (TS 102 225 §5.1.2/A.2): KIc and KID of a keyset must carry the same number (01–0F; A.2 rejects a mismatch), and each keyset has its own counter on the card (Annex A.1), so counters are tracked per keyset. In the form the keysets are edited as rows — one per number, with <strong>Add keyset</strong> / <strong>Remove keyset</strong>; each row shows the number derived from its KIc/KID byte, and <code class="font-mono text-sm">00</code> (no security) is not accepted as a keyset. The list is loaded when the PWA connects and can be exported/imported as JSON (the import also takes presets exported by older builds, flat or keyset-shaped). Cards is a top-level tab. When the card is equipped its EF.ICCID is read and the preset with the same ICCID is selected automatically in all three SCP80 views; when the equipped card matches <strong>no</strong> preset — or the selected preset belongs to another card — all three SCP80 views show an amber warning above the preset selector. The list highlights the equipped card's preset. The form groups the fields into two bordered blocks: <strong>SCP80 (GSM 03.48, ETSI TS 102 225)</strong> (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note). The optional <strong>ADM</strong> field sits in the top row and is stored for later use by the file manager. The header shows gray <strong>SCP80</strong>/<strong>SCP81</strong> markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.</p>
|
||||
<table class="w-full text-sm mb-3 border-collapse">
|
||||
<thead><tr class="border-b border-gray-300 dark:border-slate-700"><th class="text-left py-1 px-2">Field</th><th class="text-left py-1 px-2">Description</th></tr></thead>
|
||||
<tbody>
|
||||
|
||||
+14
-2
@@ -11741,17 +11741,26 @@ function cardsRender() {
|
||||
tbody.innerHTML = '<tr><td colspan="10" class="py-2 text-gray-400">' + t('No cards defined.') + '</td></tr>';
|
||||
return;
|
||||
}
|
||||
// The preset of the equipped card (normalised ICCID match): its row is
|
||||
// highlighted so the list shows which preset belongs to the card in the
|
||||
// reader (no highlight while the ICCID is unknown).
|
||||
const equippedIdx = cardsFindByIccid(_pysimCardIccid);
|
||||
let html = '';
|
||||
for (let i = 0; i < cards.length; i++) {
|
||||
const c = cards[i];
|
||||
const equipped = i === equippedIdx;
|
||||
let ota = '<span class="text-gray-400">—</span>';
|
||||
if (cardsScp81Complete(c)) {
|
||||
ota = '<span class="text-emerald-600 dark:text-emerald-400" title="' + esc(c.pskIdentity) + '">✓</span>';
|
||||
} else if (c.pskKey || c.pskIdentity) {
|
||||
ota = '<span class="text-amber-500" title="' + esc(t('PSK identity and PSK key must be set together')) + '">⚠</span>';
|
||||
}
|
||||
html += '<tr class="border-b border-gray-100 dark:border-slate-700">';
|
||||
html += '<td class="py-1 px-2 font-medium">' + esc(c.name) + '</td>';
|
||||
html += '<tr class="border-b border-gray-100 dark:border-slate-700' +
|
||||
(equipped ? ' bg-emerald-50 dark:bg-emerald-900/30' : '') + '">';
|
||||
html += '<td class="py-1 px-2 font-medium">' +
|
||||
(equipped ? '<span class="mr-1 text-emerald-600 dark:text-emerald-400" title="' +
|
||||
esc(t("The equipped card's preset")) + '">●</span>' : '') +
|
||||
esc(c.name) + '</td>';
|
||||
html += '<td class="py-1 px-2 font-mono text-xs">' + esc(c.iccid) + '</td>';
|
||||
const ksText = spKeysetList(c).map(ks => {
|
||||
const kvn = spKeysetKvnOf(ks.kic);
|
||||
@@ -13794,7 +13803,9 @@ function pysimCardStateUpdate(status) {
|
||||
_pysimLastStatus = status;
|
||||
_pysimCardEquipped = !!status.connected;
|
||||
_pysimEquipping = !!status.equipping;
|
||||
const prevIccid = _pysimCardIccid;
|
||||
_pysimCardIccid = status.connected ? (status.iccid || null) : null;
|
||||
if (prevIccid !== _pysimCardIccid) cardsRender(); // equipped-preset highlight
|
||||
pysimUpdateAdmIndicator(status);
|
||||
pysimUpdateIccidIndicator(_pysimCardIccid);
|
||||
pysimUpdatePresetIndicators(_pysimCardIccid);
|
||||
@@ -19197,6 +19208,7 @@ const LANG_RU = {
|
||||
'Snapshot comparison results:': 'Результаты сравнения снимков:',
|
||||
'RAM operations perform atomic GlobalPlatform commands over SCP80. Card keys are taken from the saved preset.': 'RAM-операции выполняют атомарные команды GlobalPlatform через SCP80. Ключи карты берутся из сохранённого пресета.',
|
||||
'No cards defined.': 'Карты не заданы.',
|
||||
"The equipped card's preset": 'Пресет подключённой карты',
|
||||
'Card presets': 'Выбор карты',
|
||||
'Card preset': 'Пресет карты',
|
||||
'— Select card —': '— Выберите карту —',
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v343';
|
||||
const CACHE = 'simple-v344';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -51,6 +51,7 @@ code += extractFunc(html, 'cardsScp81Complete') + '\n';
|
||||
code += extractFunc(html, 'pysimSetServerAvailable') + '\n';
|
||||
code += '\nglobalThis.esc = s => s;\n';
|
||||
code += 'globalThis.t = s => s;\n';
|
||||
code += 'globalThis.cardsRender = () => {};\n'; // equipped-preset highlight
|
||||
eval(code);
|
||||
|
||||
function fakeIndicator() {
|
||||
|
||||
@@ -24,7 +24,8 @@ function extractFunc(src, name) {
|
||||
let code = '';
|
||||
for (const fn of ['swapNibbles', 'encIccid', 'decIccid',
|
||||
'cardsNormIccid', 'cardsFindByIccid', 'cardsFindDuplicateIccid',
|
||||
'cardsIccidFromCard', 'cardsAutoSelectByIccid']) {
|
||||
'cardsIccidFromCard', 'cardsAutoSelectByIccid', 'cardsRender',
|
||||
'esc', 'spKeysetList', 'spKeysetKvnOf', 'cardsTarValue', 'cardsScp81Complete']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
eval(code);
|
||||
@@ -169,3 +170,28 @@ test('the card-state update wires the ICCID into the preset selection', () => {
|
||||
'the auto-selection must cover the TAR probe view');
|
||||
assert.ok(html.includes("(data.iccid ? ' | ICCID: <b>' + esc(data.iccid) + '</b>' : '')"));
|
||||
});
|
||||
|
||||
test('cardsRender highlights the equipped card preset', () => {
|
||||
globalThis.t = s => s;
|
||||
const els = { 'cards-tbody': { innerHTML: '' } };
|
||||
globalThis.document = { getElementById: id => els[id] || null };
|
||||
globalThis.cards = [
|
||||
{ name: 'other', iccid: '1111111111111111111', spi1: '16', spi2: '01', keysets: [] },
|
||||
{ name: 'live', iccid: DIGITS, spi1: '16', spi2: '01', keysets: [] },
|
||||
];
|
||||
globalThis._pysimCardIccid = DIGITS;
|
||||
cardsRender();
|
||||
let rows = els['cards-tbody'].innerHTML.split('</tr>');
|
||||
assert.ok(!/bg-emerald-50/.test(rows[0]), 'the other preset must stay plain');
|
||||
assert.match(rows[1], /bg-emerald-50/);
|
||||
assert.match(rows[1], /The equipped card's preset/);
|
||||
// the same card stored as raw EF hex matches too
|
||||
globalThis.cards[1].iccid = RAW_HEX;
|
||||
cardsRender();
|
||||
rows = els['cards-tbody'].innerHTML.split('</tr>');
|
||||
assert.match(rows[1], /bg-emerald-50/);
|
||||
// no readable ICCID -> no highlight
|
||||
globalThis._pysimCardIccid = null;
|
||||
cardsRender();
|
||||
assert.ok(!/bg-emerald-50/.test(els['cards-tbody'].innerHTML));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user