ui: header preset indicators — ADM key glyph, SCP80/SCP81 completeness (v2.6.1)

- the ADM badge gains a trailing key glyph (ADM ✓ ⚿ / ADM ✗ ⚿) when the
  preset matching the equipped card's ICCID carries an ADM key; the four
  tooltip states cover key/no-key x verified/not-verified
- new gray SCP80/SCP81 markers after ADM, shown only when that preset has
  all SCP80 fields (KIc/KID, SPI1/SPI2, counter, KIc/KID keys) or the SCP81
  PSK pair filled in; hidden without a card session, a matching preset or
  the fields, never color-coded
- cardsScp80Complete/cardsScp81Complete/cardsAdmPresent/cardsMatchedPreset
  helpers; cardsRender reuses the SCP81 predicate
- tests: ADM four-state badge, marker show/hide, predicate whitespace
  handling, header markup order; help EN/RU updated; SW cache simple-v201
This commit is contained in:
2026-09-20 01:08:44 +03:00
parent cee59bd611
commit 97bc8f929d
9 changed files with 220 additions and 24 deletions
+2 -2
View File
@@ -42,7 +42,7 @@
<h3 id="interface" class="text-lg font-medium mb-2">1.1 Интерфейс</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Шапка</strong> — версия приложения, кнопка <strong>INSTALL PWA</strong> (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка <strong>EN/RU</strong> и переключатель тёмной/светлой <strong>темы</strong>. Рядом с заголовком небольшой индикатор показывает состояние сервера/карты (серая точка = подключение, красная = нет сервера, значки карты = нет карты / инициализация / карта готова) и компактный значок <strong>ADM ✓</strong> (зелёный, администраторский PIN подтверждён) или <strong>ADM ✗</strong> (красный, не подтверждён) для текущей сессии карты.</li>
<li><strong>Шапка</strong> — версия приложения, кнопка <strong>INSTALL PWA</strong> (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка <strong>EN/RU</strong> и переключатель тёмной/светлой <strong>темы</strong>. Рядом с заголовком небольшой индикатор показывает состояние сервера/карты (серая точка = подключение, красная = нет сервера, значки карты = нет карты / инициализация / карта готова), ICCID подключённой карты, компактный значок <strong>ADM ✓</strong> (зелёный, администраторский PIN подтверждён) или <strong>ADM ✗</strong> (красный, не подтверждён) для текущей сессии карты — со значком ключа в конце (<strong>ADM ✓ ⚿</strong>), если в предустановке карты с этим ICCID есть ключ ADM — и серые маркеры <strong>SCP80</strong> / <strong>SCP81</strong>, появляющиеся, когда в этой предустановке заполнены все поля SCP80 (KIc/KID, SPI1/SPI2, счётчик и ключи) или SCP81 (PSK identity/ключ).</li>
<li>Выбор языка и темы хранится в <code class="font-mono text-sm">localStorage</code> и сохраняется между перезагрузками.</li>
<li>Вкладки верхнего уровня: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>Разбор C-APDU</strong>, <strong>&laquo;Парсер ответов&raquo;</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>RAM</strong>), <strong>SCP81</strong> (<strong>&laquo;Слушатель&raquo;</strong>, <strong>&laquo;Скрипты&raquo;</strong>), <strong>&laquo;Карты&raquo;</strong>, <strong>&laquo;Профайлер&raquo;</strong> (вкладки <strong>&laquo;Профили&raquo;</strong>, <strong>&laquo;Снимки карт&raquo;</strong>, <strong>&laquo;Пользовательские файлы&raquo;</strong>), <strong>&laquo;Картридер&raquo;</strong> (<strong>Файловый менеджер</strong>, <strong>Командная строка pySim</strong>, <strong>Отправка APDU</strong>) и <strong>&laquo;Симулятор телефона&raquo;</strong>.</li>
<li>Ссылка <strong>справка</strong> открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка &laquo;Профайлер&raquo; открывает &sect;7).</li>
@@ -299,7 +299,7 @@
<section class="mb-10">
<h2 id="cards" class="text-xl font-semibold mb-3 border-b border-gray-300 dark:border-slate-700 pb-1">4. Вкладка &laquo;Карты&raquo;</h2>
<p class="mb-2">Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. &laquo;Карты&raquo; — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях 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> находится в верхней строке и сохраняется для последующего использования файловым менеджером.</p>
<p class="mb-2">Хранит предустановки карт локально в браузере (<code class="font-mono text-sm">localStorage</code>), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. &laquo;Карты&raquo; — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях 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>
+2 -2
View File
@@ -42,7 +42,7 @@
<h3 id="interface" class="text-lg font-medium mb-2">1.1 Interface</h3>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Header</strong> — the app version, an <strong>INSTALL PWA</strong> button (shown when the browser offers installation, enabling offline use), links to the project on GitHub and to this help, an <strong>EN/RU</strong> language toggle, and a dark/light <strong>theme</strong> toggle. Next to the title a small status indicator shows the server/card state (gray dot = connecting, red dot = no server connection, card icons = no card / equipping / equipped) and a compact <strong>ADM ✓</strong> (green, administrator PIN verified) or <strong>ADM ✗</strong> (red, not verified) badge for the current card session.</li>
<li><strong>Header</strong> — the app version, an <strong>INSTALL PWA</strong> button (shown when the browser offers installation, enabling offline use), links to the project on GitHub and to this help, an <strong>EN/RU</strong> language toggle, and a dark/light <strong>theme</strong> toggle. Next to the title a small status indicator shows the server/card state (gray dot = connecting, red dot = no server connection, card icons = no card / equipping / equipped), the equipped card's <strong>ICCID</strong> digits, a compact <strong>ADM ✓</strong> (green, administrator PIN verified) or <strong>ADM ✗</strong> (red, not verified) badge for the current card session — with a trailing key glyph (<strong>ADM ✓ ⚿</strong>) when the card preset matching that ICCID carries an ADM key — and gray <strong>SCP80</strong> / <strong>SCP81</strong> markers that appear when that preset has complete SCP80 (KIc/KID, SPI1/SPI2, counter and keys) or SCP81 (PSK identity/key) settings.</li>
<li>Language and theme choices are stored in <code class="font-mono text-sm">localStorage</code> and persist across reloads.</li>
<li>Top-level tabs: <strong>Remote APDU</strong> (<strong>SIM RFM</strong>, <strong>USIM RFM</strong>, <strong>Expanded Script</strong>, <strong>RAM/GP</strong>, <strong>HTTP OTA</strong>, <strong>C-APDU Parser</strong>, <strong>Response parser</strong>), <strong>SCP80</strong> (<strong>Secured Packet</strong>, <strong>RAM</strong>), <strong>SCP81</strong> (<strong>Listener</strong>, <strong>Scripts</strong>), <strong>Cards</strong>, <strong>Profiler</strong> (list tabs <strong>Profiles</strong>, <strong>Card snapshots</strong>, <strong>Custom files</strong>), <strong>Card reader</strong> (<strong>File manager</strong>, <strong>pySim command line</strong>, <strong>Raw APDU</strong>), and <strong>Phone simulator</strong>.</li>
<li>The <strong>help</strong> link opens this documentation at the section matching the current view (e.g. the Profiler tab opens &sect;7).</li>
@@ -299,7 +299,7 @@
<section class="mb-10">
<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 locally in the browser (<code class="font-mono text-sm">localStorage</code>) so the Secured Packet view can auto-fill keys and parameters, and so the SCP81 HTTP OTA listener can look up PSK keys. 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 both SCP80 views. 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.</p>
<p class="mb-2">Stores card presets locally in the browser (<code class="font-mono text-sm">localStorage</code>) so the Secured Packet view can auto-fill keys and parameters, and so the SCP81 HTTP OTA listener can look up PSK keys. 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 both SCP80 views. 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>
+73 -8
View File
@@ -25,6 +25,8 @@
<img id="state-indicator-img" class="hidden dark:invert" style="width:30px;height:30px" alt="" src="nosim.svg">
<span id="state-indicator-iccid" class="hidden ml-0.5 text-xs font-mono text-gray-500 dark:text-slate-400 whitespace-nowrap"></span>
<span id="state-indicator-adm" class="hidden ml-2 text-xs font-mono" title=""></span>
<span id="state-indicator-scp80" class="hidden ml-2 text-xs font-mono text-gray-500 dark:text-slate-400" title=""></span>
<span id="state-indicator-scp81" class="hidden ml-1 text-xs font-mono text-gray-500 dark:text-slate-400" title=""></span>
</span>
<button id="install-btn" class="px-2 py-1 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" style="display:none">INSTALL PWA [for offline use]</button>
<a href="https://github.com/anttro/simple" target="_blank" class="text-xs text-gray-400 hover:text-gray-600 dark:text-slate-500 dark:hover:text-slate-300">github</a>
@@ -1398,7 +1400,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 = '2.6.0';
const SIMPLE_VERSION = '2.6.1';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -4841,8 +4843,11 @@ let _pysimServerAvailable = null; // null until probed
let _pysimCardEquipped = false;
let _pysimEquipping = false;
let _pysimAdmVerified = null; // null = no card session
let _pysimAdmKey = null; // ADM key present in the matching preset
let _pysimCardIccid = null; // EF.ICCID digits of the equipped card (null = unknown)
let _pysimHeaderIccid = undefined; // last value rendered in the header indicator
let _pysimHeaderScp80 = undefined; // last SCP80/SCP81 marker state
let _pysimHeaderScp81 = undefined;
let _pysimAvailabilityTimer = null;
function pysimAvailabilityState() {
@@ -4903,26 +4908,31 @@ function pysimApplyAvailability() {
}
// Compact ADM state next to the header's card indicator: "ADM ✓" when the
// administrator PIN was verified (pySim rs.adm_verified), "ADM ✗" otherwise.
// Hidden without a card session; only rewrites the DOM when the state changes
// (the 2s /api/status poll calls this on every update).
// administrator PIN was verified (pySim rs.adm_verified), "ADM ✗" otherwise;
// a trailing key glyph (⚿) marks that the matching card preset carries an ADM
// key. Hidden without a card session; only rewrites the DOM when the state
// changes (the 2s /api/status poll calls this on every update).
function pysimUpdateAdmIndicator(status) {
const el = document.getElementById('state-indicator-adm');
if (!el) return;
const verified = (status && status.connected) ? !!status.adm_verified : null;
if (verified === _pysimAdmVerified) return;
const key = verified === null ? null : cardsAdmPresent(cardsMatchedPreset());
if (verified === _pysimAdmVerified && key === _pysimAdmKey) return;
_pysimAdmVerified = verified;
_pysimAdmKey = key;
el.classList.remove('text-emerald-600', 'dark:text-emerald-400', 'text-red-500');
if (verified === null) {
el.classList.add('hidden');
el.removeAttribute('title');
return;
}
el.textContent = verified ? 'ADM ✓' : 'ADM ✗';
el.textContent = 'ADM ' + (verified ? '✓' : '✗') + (key ? ' ⚿' : '');
el.classList.remove('hidden');
el.classList.add(verified ? 'text-emerald-600' : 'text-red-500');
if (verified) el.classList.add('dark:text-emerald-400');
el.setAttribute('title', t(verified ? 'ADM verified' : 'ADM not verified'));
el.setAttribute('title', t(key
? (verified ? 'ADM key in the card preset — verified' : 'ADM key in the card preset — not verified')
: (verified ? 'Verified — no ADM key in the card preset' : 'No ADM key in the card preset — not verified')));
}
// EF.ICCID digits of the equipped card, printed next to the card image
@@ -4942,12 +4952,37 @@ function pysimUpdateIccidIndicator(iccid) {
el.classList.remove('hidden');
}
// Gray SCP80/SCP81 markers: shown only while the equipped card's ICCID matches
// a preset with complete settings for that view; never color-coded.
function pysimUpdatePresetIndicator(id, label, tip, show, prev) {
const el = document.getElementById(id);
if (!el) return prev;
if (show === prev) return prev;
if (show) {
el.textContent = label;
el.classList.remove('hidden');
el.setAttribute('title', t(tip));
} else {
el.textContent = '';
el.classList.add('hidden');
el.removeAttribute('title');
}
return show;
}
function pysimUpdatePresetIndicators(iccid) {
const c = cardsMatchedPreset(iccid);
_pysimHeaderScp80 = pysimUpdatePresetIndicator('state-indicator-scp80', 'SCP80', 'SCP80 preset complete', cardsScp80Complete(c), _pysimHeaderScp80);
_pysimHeaderScp81 = pysimUpdatePresetIndicator('state-indicator-scp81', 'SCP81', 'SCP81 preset complete', cardsScp81Complete(c), _pysimHeaderScp81);
}
function pysimSetServerAvailable(available) {
if (_pysimServerAvailable === available) return;
_pysimServerAvailable = available;
if (available !== true) {
pysimUpdateAdmIndicator(null);
pysimUpdateIccidIndicator(null);
pysimUpdatePresetIndicators(null);
}
pysimApplyAvailability();
}
@@ -6262,7 +6297,7 @@ function cardsRender() {
for (let i = 0; i < cards.length; i++) {
const c = cards[i];
let ota = '<span class="text-gray-400"></span>';
if (c.pskKey && c.pskIdentity) {
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>';
@@ -6317,6 +6352,29 @@ function cardsFindByIccid(iccid) {
return cards.findIndex(c => cardsNormIccid(c.iccid) === norm);
}
// Completeness predicates used by the header indicators: SCP80 needs all
// SPI/keys/counter fields, SCP81 the PSK pair; the ADM key is optional.
function cardsScp80Complete(c) {
return !!(c && String(c.kic || '').trim() && String(c.kid || '').trim()
&& String(c.spi1 || '').trim() && String(c.spi2 || '').trim()
&& String(c.cntr || '').trim() && String(c.kicKey || '').trim() && String(c.kidKey || '').trim());
}
function cardsScp81Complete(c) {
return !!(c && String(c.pskIdentity || '').trim() && String(c.pskKey || '').trim());
}
function cardsAdmPresent(c) {
return !!(c && String(c.adm || '').trim());
}
// The preset whose ICCID matches the equipped card (null when none/unreadable).
function cardsMatchedPreset(iccid) {
const value = iccid === undefined ? _pysimCardIccid : iccid;
const idx = value ? cardsFindByIccid(value) : -1;
return idx >= 0 ? cards[idx] : null;
}
function cardsFindDuplicateIccid(iccid, skipIdx) {
// Duplicate detection ignores the edited row and compares normalized
// values (digits, spaced digits or raw EF hex all mean the same card).
@@ -7973,6 +8031,7 @@ function pysimCardStateUpdate(status) {
_pysimCardIccid = status.connected ? (status.iccid || null) : null;
pysimUpdateAdmIndicator(status);
pysimUpdateIccidIndicator(_pysimCardIccid);
pysimUpdatePresetIndicators(_pysimCardIccid);
pysimApplyAvailability();
if (pysimProactiveSeqChanged(status.proactive_seq)
&& isViewVisible('tab-phone') && isViewVisible('phone-sub-phone')) {
@@ -13051,6 +13110,12 @@ const LANG_RU = {
'Show link events (Channel status ENVELOPEs, TS 102 223 7.5.11)': 'Показывать события канала (ENVELOPE Channel status, TS 102 223 7.5.11)',
'ADM verified': 'ADM подтверждён',
'ADM not verified': 'ADM не подтверждён',
'ADM key in the card preset — verified': 'Ключ ADM в предустановке — подтверждён',
'ADM key in the card preset — not verified': 'Ключ ADM в предустановке — не подтверждён',
'Verified — no ADM key in the card preset': 'Подтверждён — ключа ADM в предустановке нет',
'No ADM key in the card preset — not verified': 'Ключа ADM в предустановке нет — не подтверждён',
'SCP80 preset complete': 'Предустановка SCP80 заполнена',
'SCP81 preset complete': 'Предустановка SCP81 заполнена',
'TERMINAL PROFILE': 'TERMINAL PROFILE',
'Preset (device model)': 'Пресет (модель устройства)',
'Profile (hex)': 'Профиль (hex)',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v200';
const CACHE = 'simple-v201';
const URLS = [
'index.html',
'help.html',
+71 -7
View File
@@ -24,7 +24,8 @@ function extractFunc(src, name) {
let code = 'var _pysimCardStateKey = null;\nvar _pysimCardSession = null;\n'
+ 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\n'
+ 'var _pysimProactiveSeq = null;\nvar _pysimStkSig = null;\nvar _pysimAdmVerified = null;\n'
+ 'var _pysimHeaderIccid = undefined;\n'
+ 'var _pysimAdmKey = null;\n'
+ 'var _pysimHeaderIccid = undefined;\nvar _pysimHeaderScp80 = undefined;\nvar _pysimHeaderScp81 = undefined;\n'
+ 'var _cardsAutoIccid = null;\nvar _pysimCardIccid = null;\n';
code += extractFunc(html, 'pysimCardStateUpdate') + '\n';
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
@@ -33,6 +34,12 @@ code += extractFunc(html, 'pysimProactiveSeqChanged') + '\n';
code += extractFunc(html, 'pysimStkStatusChanged') + '\n';
code += extractFunc(html, 'pysimUpdateAdmIndicator') + '\n';
code += extractFunc(html, 'pysimUpdateIccidIndicator') + '\n';
code += extractFunc(html, 'pysimUpdatePresetIndicator') + '\n';
code += extractFunc(html, 'pysimUpdatePresetIndicators') + '\n';
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
code += extractFunc(html, 'cardsAdmPresent') + '\n';
code += extractFunc(html, 'cardsScp80Complete') + '\n';
code += extractFunc(html, 'cardsScp81Complete') + '\n';
code += extractFunc(html, 'pysimSetServerAvailable') + '\n';
code += '\nglobalThis.esc = s => s;\n';
code += 'globalThis.t = s => s;\n';
@@ -57,27 +64,36 @@ function setup() {
const el = { textContent: 'status line', innerHTML: '' };
const adm = fakeIndicator();
const iccidEl = fakeIndicator();
const scp80El = fakeIndicator();
const scp81El = fakeIndicator();
const calls = { connected: [], resets: [], refreshStatus: [], proactive: 0, autoIccid: [] };
_pysimCardStateKey = null;
_pysimCardSession = null;
_pysimProactiveSeq = null;
_pysimAdmVerified = null;
_pysimAdmKey = null;
_pysimHeaderIccid = undefined;
_pysimHeaderScp80 = undefined;
_pysimHeaderScp81 = undefined;
_pysimServerAvailable = null;
_cardsAutoIccid = null;
_pysimCardIccid = null;
globalThis.document = {
getElementById: id => id === 'state-indicator-adm' ? adm
: (id === 'state-indicator-iccid' ? iccidEl : el),
: (id === 'state-indicator-iccid' ? iccidEl
: (id === 'state-indicator-scp80' ? scp80El
: (id === 'state-indicator-scp81' ? scp81El : el))),
querySelectorAll: () => [],
};
globalThis.cards = [];
globalThis.cardsFindByIccid = () => -1;
globalThis.pysimSetConnected = v => calls.connected.push(v);
globalThis.pysimResetCardData = refresh => calls.resets.push(refresh);
globalThis.pysimApplyAvailability = () => {};
globalThis.isViewVisible = () => true;
globalThis.pysimProactiveLogRender = () => { calls.proactive++; };
globalThis.cardsAutoSelectByIccid = iccid => { calls.autoIccid.push(iccid); return -1; };
return { el, adm, iccidEl, calls };
return { el, adm, iccidEl, scp80El, scp81El, calls };
}
function status(extra) {
@@ -229,7 +245,7 @@ test('the header ADM badge shows verified / not verified / hidden', () => {
assert.strictEqual(adm.textContent, 'ADM ✓');
assert.ok(adm.classes.has('text-emerald-600'));
assert.ok(adm.classes.has('dark:text-emerald-400'));
assert.strictEqual(adm.title, 'ADM verified');
assert.strictEqual(adm.title, 'Verified — no ADM key in the card preset');
// an unchanged state must not rewrite the badge
adm.textContent = '';
pysimCardStateUpdate(status({ connected: true, adm_verified: true }));
@@ -239,7 +255,7 @@ test('the header ADM badge shows verified / not verified / hidden', () => {
assert.strictEqual(adm.textContent, 'ADM ✗');
assert.ok(adm.classes.has('text-red-500'));
assert.ok(!adm.classes.has('text-emerald-600'));
assert.strictEqual(adm.title, 'ADM not verified');
assert.strictEqual(adm.title, 'No ADM key in the card preset — not verified');
// no card session hides it
pysimCardStateUpdate(status({ connected: false }));
assert.ok(adm.classes.has('hidden'));
@@ -248,12 +264,60 @@ test('the header ADM badge shows verified / not verified / hidden', () => {
assert.deepStrictEqual(calls.connected, [true, false]);
});
test('losing the server hides the ADM badge', () => {
test('the ADM badge marks an ADM key in the matching preset', () => {
const { adm } = setup();
pysimCardStateUpdate(status({ connected: true, adm_verified: true }));
const st = extra => status(Object.assign({ connected: true, iccid: '89701450001700031958' }, extra));
globalThis.cards = [{ name: 'C', adm: '0011' }];
globalThis.cardsFindByIccid = () => 0;
pysimCardStateUpdate(st({ adm_verified: true }));
assert.strictEqual(adm.textContent, 'ADM ✓ ⚿');
assert.strictEqual(adm.title, 'ADM key in the card preset — verified');
// key present, verification lost (e.g. card reset)
pysimCardStateUpdate(st({ adm_verified: false }));
assert.strictEqual(adm.textContent, 'ADM ✗ ⚿');
assert.strictEqual(adm.title, 'ADM key in the card preset — not verified');
// verified manually, preset has no ADM -> no key glyph
globalThis.cards = [];
pysimCardStateUpdate(st({ adm_verified: true }));
assert.strictEqual(adm.textContent, 'ADM ✓');
assert.strictEqual(adm.title, 'Verified — no ADM key in the card preset');
// whitespace-only preset ADM counts as absent
globalThis.cards = [{ name: 'C', adm: ' ' }];
globalThis.cardsFindByIccid = () => 0;
pysimCardStateUpdate(st({ adm_verified: true }));
assert.strictEqual(adm.textContent, 'ADM ✓');
});
test('the header SCP80/SCP81 markers follow the matching preset', () => {
const { scp80El, scp81El } = setup();
globalThis.cards = [{ name: 'C', kic: '15', kid: '15', spi1: '16', spi2: '01',
cntr: '0000000001', kicKey: 'AA', kidKey: 'BB', pskIdentity: 'id', pskKey: 'KEY' }];
globalThis.cardsFindByIccid = () => 0;
pysimCardStateUpdate(status({ connected: true, card_session: 2, iccid: '89701450001700031958' }));
assert.strictEqual(scp80El.textContent, 'SCP80');
assert.strictEqual(scp81El.textContent, 'SCP81');
assert.ok(!scp80El.classes.has('hidden'));
assert.ok(!scp81El.classes.has('hidden'));
// disconnect hides them again
pysimCardStateUpdate(status({ connected: false, card_present: false, card_session: 3 }));
assert.ok(scp80El.classes.has('hidden'));
assert.ok(scp81El.classes.has('hidden'));
assert.strictEqual(scp80El.textContent, '');
});
test('losing the server hides the ADM badge and the preset markers', () => {
const { adm, scp80El, scp81El } = setup();
globalThis.cards = [{ name: 'C', adm: '0011', kic: '15', kid: '15', spi1: '16', spi2: '01',
cntr: '0000000001', kicKey: 'AA', kidKey: 'BB', pskIdentity: 'id', pskKey: 'KEY' }];
globalThis.cardsFindByIccid = () => 0;
pysimCardStateUpdate(status({ connected: true, adm_verified: true, iccid: '89701450001700031958' }));
assert.ok(!adm.classes.has('hidden'));
assert.ok(!scp80El.classes.has('hidden'));
assert.ok(!scp81El.classes.has('hidden'));
pysimSetServerAvailable(false);
assert.ok(adm.classes.has('hidden'));
assert.ok(scp80El.classes.has('hidden'));
assert.ok(scp81El.classes.has('hidden'));
});
test('the header indicator prints the equipped card ICCID', () => {
+21
View File
@@ -24,6 +24,7 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['cardsTarValue', 'cardsFormValues', 'cardsClearForm', 'cardsEdit',
'cardsAdd', 'cardsImport', 'cardsApply', 'ramApplyCard',
'cardsScp80Complete', 'cardsScp81Complete', 'cardsAdmPresent',
'spTarKeyForPack', 'spPresetTar', 'packToSp']) {
code += extractFunc(html, fn) + '\n';
}
@@ -85,6 +86,26 @@ test('cardsTarValue falls back to the per-target spec defaults', () => {
assert.strictEqual(cardsTarValue('nope', ''), '');
});
test('preset completeness predicates used by the header markers', () => {
const full = { kic: '15', kid: '15', spi1: '16', spi2: '01', cntr: '0000000001',
kicKey: 'AA', kidKey: 'BB', pskIdentity: 'id', pskKey: 'KEY', adm: '0011' };
assert.ok(cardsScp80Complete(full));
assert.ok(cardsScp81Complete(full));
assert.ok(cardsAdmPresent(full));
// every SCP80 field is required; whitespace counts as empty
for (const field of ['kic', 'kid', 'spi1', 'spi2', 'cntr', 'kicKey', 'kidKey']) {
const c = Object.assign({}, full);
c[field] = ' ';
assert.ok(!cardsScp80Complete(c), 'SCP80 marked complete with empty ' + field);
}
assert.ok(!cardsScp81Complete({ pskIdentity: 'id' }));
assert.ok(!cardsScp81Complete({ pskKey: 'KEY' }));
assert.ok(!cardsScp80Complete(null));
assert.ok(!cardsScp81Complete(null));
assert.ok(!cardsAdmPresent(null));
assert.ok(!cardsAdmPresent({ adm: ' ' }));
});
test('clearing the card form keeps the TAR spec defaults', () => {
const els = setup();
els['cards-tar'].value = 'AABBCC';
+48 -2
View File
@@ -21,13 +21,18 @@ function extractFunc(src, name) {
return src.slice(m.index, i + 1);
}
let code = 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\nvar _pysimEquipping = false;\nvar _pysimCardIccid = null;\nvar _pysimHeaderIccid = undefined;\n';
let code = 'var _pysimServerAvailable = null;\nvar _pysimCardEquipped = false;\nvar _pysimEquipping = false;\nvar _pysimCardIccid = null;\nvar _pysimHeaderIccid = undefined;\nvar _pysimHeaderScp80 = undefined;\nvar _pysimHeaderScp81 = undefined;\n';
code += extractFunc(html, 'pysimAvailabilityState') + '\n';
code += extractFunc(html, 'pysimControlDisabled') + '\n';
code += extractFunc(html, 'pysimNeedsHint') + '\n';
code += extractFunc(html, 'pysimApplyAvailability') + '\n';
code += extractFunc(html, 'pysimUpdateStateIndicator') + '\n';
code += extractFunc(html, 'pysimUpdateIccidIndicator') + '\n';
code += extractFunc(html, 'pysimUpdatePresetIndicator') + '\n';
code += extractFunc(html, 'pysimUpdatePresetIndicators') + '\n';
code += extractFunc(html, 'cardsScp80Complete') + '\n';
code += extractFunc(html, 'cardsScp81Complete') + '\n';
code += extractFunc(html, 'cardsMatchedPreset') + '\n';
code += 'globalThis.t = s => s;\n';
eval(code);
@@ -53,6 +58,8 @@ function setup() {
'state-indicator-dot': fakeEl(),
'state-indicator-img': fakeEl(),
'state-indicator-iccid': fakeEl(),
'state-indicator-scp80': fakeEl(),
'state-indicator-scp81': fakeEl(),
};
els['state-indicator-img'].src = '';
globalThis.document = { getElementById: id => els[id] || null, querySelectorAll: () => [] };
@@ -61,6 +68,10 @@ function setup() {
_pysimEquipping = false;
_pysimCardIccid = null;
_pysimHeaderIccid = undefined;
_pysimHeaderScp80 = undefined;
_pysimHeaderScp81 = undefined;
globalThis.cards = [];
globalThis.cardsFindByIccid = () => -1;
return els;
}
@@ -145,9 +156,44 @@ test('header prints the equipped card ICCID next to the card image', () => {
pysimUpdateIccidIndicator(null);
assert.strictEqual(el.textContent, '');
assert.ok(el.classes.has('hidden'));
// markup order: image, ICCID, ADM badge
// markup order: image, ICCID, ADM badge, SCP80, SCP81
assert.ok(html.indexOf('id="state-indicator-img"') < html.indexOf('id="state-indicator-iccid"'));
assert.ok(html.indexOf('id="state-indicator-iccid"') < html.indexOf('id="state-indicator-adm"'));
assert.ok(html.indexOf('id="state-indicator-adm"') < html.indexOf('id="state-indicator-scp80"'));
assert.ok(html.indexOf('id="state-indicator-scp80"') < html.indexOf('id="state-indicator-scp81"'));
});
test('SCP80/SCP81 markers follow the matching preset completeness', () => {
const els = setup();
globalThis.cards = [{ name: 'C', kic: '15', kid: '15', spi1: '16', spi2: '01',
cntr: '0000000001', kicKey: 'AA', kidKey: 'BB', pskIdentity: 'id', pskKey: 'KEY' }];
globalThis.cardsFindByIccid = () => 0;
_pysimCardIccid = '89701450001700031958';
pysimUpdatePresetIndicators();
const e80 = els['state-indicator-scp80'];
const e81 = els['state-indicator-scp81'];
assert.strictEqual(e80.textContent, 'SCP80');
assert.strictEqual(e81.textContent, 'SCP81');
assert.ok(!e80.classes.has('hidden'));
assert.ok(!e81.classes.has('hidden'));
assert.strictEqual(e80.attrs.title, 'SCP80 preset complete');
assert.strictEqual(e81.attrs.title, 'SCP81 preset complete');
// SCP80 incomplete (missing key) -> only the SCP81 marker stays
globalThis.cards[0].kicKey = '';
pysimUpdatePresetIndicators();
assert.ok(e80.classes.has('hidden'));
assert.strictEqual(e80.textContent, '');
assert.strictEqual(e80.attrs.title, undefined);
assert.ok(!e81.classes.has('hidden'));
// no matching preset -> both hidden
globalThis.cardsFindByIccid = () => -1;
pysimUpdatePresetIndicators();
assert.ok(e81.classes.has('hidden'));
// explicit null ICCID (no card / server lost) hides them
globalThis.cardsFindByIccid = () => 0;
pysimUpdatePresetIndicators(null);
assert.ok(e80.classes.has('hidden'));
assert.ok(e81.classes.has('hidden'));
});
test('card-iccid controls need an equipped card with a readable ICCID', () => {
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "2.6.0"
version = "2.6.1"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -24,7 +24,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '2.6.0'
VERSION = '2.6.1'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE