Шапка — версия приложения, кнопка INSTALL PWA (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка EN/RU и переключатель тёмной/светлой темы. Рядом с заголовком небольшой индикатор показывает состояние сервера/карты (серая точка = подключение, красная = нет сервера, значки карты = нет карты / инициализация / карта готова) и компактный значок ADM ✓ (зелёный, администраторский PIN подтверждён) или ADM ✗ (красный, не подтверждён) для текущей сессии карты.
+
Шапка — версия приложения, кнопка INSTALL PWA (появляется, когда браузер предлагает установку, для офлайн-работы), ссылки на проект на GitHub и на эту справку, переключатель языка EN/RU и переключатель тёмной/светлой темы. Рядом с заголовком небольшой индикатор показывает состояние сервера/карты (серая точка = подключение, красная = нет сервера, значки карты = нет карты / инициализация / карта готова), ICCID подключённой карты, компактный значок ADM ✓ (зелёный, администраторский PIN подтверждён) или ADM ✗ (красный, не подтверждён) для текущей сессии карты — со значком ключа в конце (ADM ✓ ⚿), если в предустановке карты с этим ICCID есть ключ ADM — и серые маркеры SCP80 / SCP81, появляющиеся, когда в этой предустановке заполнены все поля SCP80 (KIc/KID, SPI1/SPI2, счётчик и ключи) или SCP81 (PSK identity/ключ).
Выбор языка и темы хранится в localStorage и сохраняется между перезагрузками.
Ссылка справка открывает эту документацию на разделе, соответствующем текущему представлению (например, вкладка «Профайлер» открывает §7).
@@ -299,7 +299,7 @@
4. Вкладка «Карты»
-
Хранит предустановки карт локально в браузере (localStorage), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле ADM находится в верхней строке и сохраняется для последующего использования файловым менеджером.
+
Хранит предустановки карт локально в браузере (localStorage), чтобы представление Secured Packet могло автоматически подставлять ключи и параметры, а слушатель SCP81 HTTP OTA — находить PSK-ключи. «Карты» — верхнеуровневая вкладка. При подключении карты читается её EF.ICCID, и предустановка с тем же ICCID автоматически выбирается в обоих представлениях SCP80. Поля формы сгруппированы в два блока с рамкой: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, TAR-ы ISD/UICC RFM/ADF RFM, счётчик и ключи) и SCP81 (HTTP OTA) (пара PSK identity/ключ с пояснением о выборе ключа). Необязательное поле ADM находится в верхней строке и сохраняется для последующего использования файловым менеджером. В шапке отображаются серые маркеры SCP80/SCP81 и значок ключа на бейдже ADM, когда в предустановке с ICCID подключённой карты эти поля заполнены.
Header — the app version, an INSTALL PWA button (shown when the browser offers installation, enabling offline use), links to the project on GitHub and to this help, an EN/RU language toggle, and a dark/light theme 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 ADM ✓ (green, administrator PIN verified) or ADM ✗ (red, not verified) badge for the current card session.
+
Header — the app version, an INSTALL PWA button (shown when the browser offers installation, enabling offline use), links to the project on GitHub and to this help, an EN/RU language toggle, and a dark/light theme 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 ICCID digits, a compact ADM ✓ (green, administrator PIN verified) or ADM ✗ (red, not verified) badge for the current card session — with a trailing key glyph (ADM ✓ ⚿) when the card preset matching that ICCID carries an ADM key — and gray SCP80 / SCP81 markers that appear when that preset has complete SCP80 (KIc/KID, SPI1/SPI2, counter and keys) or SCP81 (PSK identity/key) settings.
Language and theme choices are stored in localStorage and persist across reloads.
The help link opens this documentation at the section matching the current view (e.g. the Profiler tab opens §7).
@@ -299,7 +299,7 @@
4. Cards tab
-
Stores card presets locally in the browser (localStorage) 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: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The optional ADM field sits in the top row and is stored for later use by the file manager.
+
Stores card presets locally in the browser (localStorage) 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: SCP80 (GSM 03.48, ETSI TS 102 225) (KIc/KID, SPI1/SPI2, the ISD/UICC RFM/ADF RFM TARs, counter and keys) and SCP81 (HTTP OTA) (the PSK identity/key pair with the key-selection note). The optional ADM field sits in the top row and is stored for later use by the file manager. The header shows gray SCP80/SCP81 markers and a key glyph on the ADM badge when the preset matching the equipped card's ICCID has those settings filled in.
Field
Description
diff --git a/frontend/index.html b/frontend/index.html
index 3eb1ea4..5c58eee 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -25,6 +25,8 @@
+
+
github
@@ -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 = '—';
- if (c.pskKey && c.pskIdentity) {
+ if (cardsScp81Complete(c)) {
ota = '✓';
} else if (c.pskKey || c.pskIdentity) {
ota = '⚠';
@@ -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)',
diff --git a/frontend/sw.js b/frontend/sw.js
index 6942546..6461300 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v200';
+const CACHE = 'simple-v201';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/card_state.test.js b/frontend/tests/card_state.test.js
index 3c63b5b..1c9ff69 100644
--- a/frontend/tests/card_state.test.js
+++ b/frontend/tests/card_state.test.js
@@ -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', () => {
diff --git a/frontend/tests/cards_form.test.js b/frontend/tests/cards_form.test.js
index f484200..ffaf7d0 100644
--- a/frontend/tests/cards_form.test.js
+++ b/frontend/tests/cards_form.test.js
@@ -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';
diff --git a/frontend/tests/indicator.test.js b/frontend/tests/indicator.test.js
index ae8f332..d27667b 100644
--- a/frontend/tests/indicator.test.js
+++ b/frontend/tests/indicator.test.js
@@ -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', () => {
diff --git a/pyproject.toml b/pyproject.toml
index 8636b5a..d2fa680 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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.
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index f4450aa..6aa18de 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -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