cards: group the form into SCP80/SCP81 fieldsets, close the RU i18n gaps (v2.2.19)

- The card create/edit form now surrounds the SCP80 parameters (KIc/KID,
  SPI1/SPI2, TAR, counter, keys) with a bordered <fieldset> labelled
  'SCP80 (GSM 03.48, ETSI TS 102 225)' and the PSK pair (with the listener
  explanation) with one labelled 'SCP81 (HTTP OTA)'; the name/ICCID row and
  the list stay outside the groups.
- i18n: the SCP81 PSK note is translated now, plus the keys an audit found
  missing from the RU dictionary (Start, Clear, Loading..., Working...,
  APDU hex, CAP file (max 48 kB), Install from .cap, Card reader server is
  not connected, KIc/KID required messages, Memory (GET DATA FF21), the two
  no-secured-packet alerts, Select, event notification to the card?).
- Tests: html.test.js guards both fieldsets, their legends and that the PSK
  note and fields live inside the SCP81 group (401 frontend tests).
- Docs: help EN+RU Cards intro. SW cache otaman-v186.
This commit is contained in:
2026-09-19 00:15:18 +03:00
parent ce7d91cfe0
commit 2a03c74941
7 changed files with 65 additions and 23 deletions
+1 -1
View File
@@ -298,7 +298,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.</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, счётчик и ключи) и <strong>SCP81 (HTTP OTA)</strong> (пара PSK identity/ключ с пояснением о выборе ключа).</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
View File
@@ -298,7 +298,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.</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, TAR, counter and keys) and <strong>SCP81 (HTTP OTA)</strong> (the PSK identity/key pair with the key-selection note).</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>
+32 -5
View File
@@ -18,7 +18,7 @@
<div class="max-w-7xl mx-auto px-6 py-2">
<div class="flex items-center justify-between mb-3">
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.18</span></h1>
<h1 class="text-2xl font-bold text-heading">OTAMan <span id="slogan" class="text-sm font-normal text-gray-500 dark:text-slate-400 ml-2" data-l10n="SIM OTA with a Human Face">SIM OTA with a Human Face</span> <span class="text-xs text-gray-400 dark:text-slate-500 ml-1">v2.2.19</span></h1>
<div class="flex items-center gap-4">
<span id="state-indicator" class="flex items-center select-none" style="cursor:default" title="Connecting...">
<span id="state-indicator-dot" class="text-xs text-gray-400" title="Connecting..."></span>
@@ -1141,7 +1141,9 @@
<button id="cards-add-btn" onclick="cardsAdd()" class="px-4 py-2.5 bg-blue-600 text-white text-sm font-medium rounded hover:bg-blue-700 whitespace-nowrap" data-l10n="Add">Add</button>
<button id="cards-cancel-btn" onclick="cardsCancelEdit()" class="hidden px-4 py-2.5 bg-gray-500 text-white text-sm font-medium rounded hover:bg-gray-600 whitespace-nowrap" data-l10n="Cancel">Cancel</button>
</div>
<div class="flex gap-2 mb-3 flex-wrap">
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="SCP80 (GSM 03.48, ETSI TS 102 225)">SCP80 (GSM 03.48, ETSI TS 102 225)</legend>
<div class="flex gap-2 mb-2 flex-wrap">
<input id="cards-kic" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="KIc" maxlength="2">
<input id="cards-kid" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="KID" maxlength="2">
<input id="cards-spi1" class="w-12 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" placeholder="SPI1" maxlength="2">
@@ -1149,15 +1151,19 @@
<input id="cards-tar" class="w-20 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="ISD TAR" maxlength="6">
<input id="cards-cntr" class="w-24 border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono" placeholder="Counter" maxlength="10">
</div>
<div class="flex gap-2 mb-3">
<div class="flex gap-2">
<input id="cards-kic-key" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="KIc key (32 hex chars)">
<input id="cards-kid-key" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="KID key (32 hex chars)">
</div>
<div class="flex gap-2 mb-3">
</fieldset>
<fieldset class="border border-gray-200 dark:border-slate-700 rounded p-3 mb-3">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="SCP81 (HTTP OTA)">SCP81 (HTTP OTA)</legend>
<div class="flex gap-2 mb-2">
<input id="cards-psk-id" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="PSK identity, SCP81 HTTP OTA (e.g. 89012345678901234567)">
<input id="cards-psk-key" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="PSK key (hex, 32 chars), SCP81 HTTP OTA">
</div>
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.">SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.</p>
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.">SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.</p>
</fieldset>
<div id="cards-list" class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr>
@@ -11666,6 +11672,27 @@ const LANG_RU = {
'pending': 'ожидает',
'Name is required': 'Укажите имя',
'From card': 'С карты',
'SCP80 (GSM 03.48, ETSI TS 102 225)': 'SCP80 (GSM 03.48, ETSI TS 102 225)',
'SCP81 (HTTP OTA)': 'SCP81 (HTTP OTA)',
'Start': 'Запустить',
'Clear': 'Очистить',
'Loading...': 'Загрузка...',
'Working...': 'Выполняется...',
'APDU hex': 'APDU (hex)',
'CAP file (max 48 kB)': 'CAP-файл (не более 48 кБ)',
'Install from .cap': 'Установить из .cap',
'RAM': 'RAM',
'SCP81': 'SCP81',
'Secured packet': 'Secured packet',
'Card reader server is not connected': 'Сервер картридера не подключён',
'KIc and KID are required': 'Укажите KIc и KID',
'KIc key and KID key are required': 'Укажите ключ KIc и ключ KID',
'Memory (GET DATA FF21)': 'Память (GET DATA FF21)',
'No secured packet to send. Generate a secure packet first.': 'Нет защищённого пакета для отправки. Сначала соберите пакет.',
'No secured packet to verify. Generate a secure packet first.': 'Нет защищённого пакета для проверки. Сначала соберите пакет.',
'Select': 'Выборка',
'event notification to the card?': 'уведомление о событии на карту?',
'SCP81 HTTP OTA: the listener picks the key by the PSK identity the card sends in the TLS handshake. Cards without a PSK are ignored by the listener.': 'SCP81 HTTP OTA: слушатель выбирает ключ по PSK identity, которую карта присылает в TLS-рукопожатии. Карты без PSK слушателем игнорируются.',
'Card with this ICCID already exists': 'Карта с таким ICCID уже есть в списке',
'Card equipped but its ICCID is not readable': 'Карта подключена, но её ICCID не читается',
'Saved.': 'Сохранено.',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'otaman-v185';
const CACHE = 'otaman-v186';
const URLS = [
'index.html',
'help.html',
+15
View File
@@ -25,6 +25,21 @@ test('cards list shows the SCP81 PSK column with blue/red row buttons', () => {
assert.match(fn[0], /cardsRemove\(' \+ i \+ '\)" class="[^"]*bg-red-600 text-white/);
});
test('the card form groups the SCP80 and SCP81 fields into labelled fieldsets', () => {
const fieldsets = [...html.matchAll(/<fieldset[\s\S]*?<\/fieldset>/g)].map(m => m[0]);
assert.strictEqual(fieldsets.length, 2);
assert.match(fieldsets[0], /<legend[^>]*data-l10n="SCP80 \(GSM 03\.48, ETSI TS 102 225\)"/);
assert.match(fieldsets[0], /id="cards-kic"/);
assert.match(fieldsets[0], /id="cards-kid-key"/);
assert.match(fieldsets[1], /<legend[^>]*data-l10n="SCP81 \(HTTP OTA\)"/);
assert.match(fieldsets[1], /id="cards-psk-id"/);
assert.match(fieldsets[1], /id="cards-psk-key"/);
// the PSK explanation lives inside the SCP81 group, not outside it
assert.match(fieldsets[1], /data-l10n="SCP81 HTTP OTA: the listener picks the key/);
// the ICCID field is not in either group
assert.ok(!fieldsets.some(f => f.includes('id="cards-iccid"')));
});
test('PLI qualifier tables cover all standard qualifiers', () => {
// ESN (07), MEID (0B) and Supported RATs (1A) must at least be named, in
// both the TR Config dictionary and the proactive-log short labels.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-otaman-server"
version = "2.2.18"
version = "2.2.19"
description = "HTTP REST server wrapping pysim for the OTAMan PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -21,7 +21,7 @@ from osmocom.construct import GsmOrUcs2Adapter
from osmocom.tlv import BER_TLV_IE
VERSION = '2.2.18'
VERSION = '2.2.19'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE