v3.11.0: Cards tab redesign (list + editor), preset PIN/PUK codes

The Cards tab now shows the preset list by default; "Add preset" / "Edit"
open a separate editor view (Save stores and returns to the list, Cancel
discards; ramGoToPreset opens it too).  The editor keeps the name/ICCID
header (with From card) and groups its fields into bordered blocks:
- PIN/ADM: the new optional card codes PIN1/PUK1/PIN2/PUK2 (plain text,
  4-8 decimal digits) plus ADM, moved out of the top row; nothing consumes
  the PIN/PUK codes yet.
- TAR: one uniform table (TAR, MSL, description); the first three rows are
  the fixed role entries with the immutable English descriptions "ISD
  compact format" / "UICC shared FS RFM compact format" / "ADF FS RFM
  compact format"; further rows are deletable with an optional description.
- SCP80 (keyset editor) and SCP81 (PSK pair) as before.

Store: PRESET_FIELDS gains pin1/puk1/pin2/puk2 (optional; whitespace
stripped; 4-8 decimal digits validated on save and import), SCHEMA_VERSION
4, docs/help/AGENTS updated.  TAR rows use unified ids (cards-tr-N-*), the
editor switches via cardsShowList/cardsShowEditor.

Tests: 665 Python + 783 frontend green; the whole inline PWA script
syntax-checked.  sw cache simple-v361.
This commit is contained in:
2026-10-02 09:32:45 +03:00
parent 5dd056beb8
commit 65c565e058
12 changed files with 451 additions and 188 deletions
+194 -92
View File
@@ -1593,49 +1593,95 @@
<div id="tab-cards" class="tab-content hidden">
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).">Card presets are stored on the server. JSON export/import buttons are below the card list; a preset carries several keysets, each with its own number and counter, and a TAR table — every TAR with its MSL (Minimum SPI1).</p>
<!-- ===== preset list (default view) ===== -->
<div id="cards-list-view">
<p class="text-xs text-gray-500 dark:text-slate-400 mb-3" data-l10n="Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.">Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.</p>
<div class="flex flex-wrap gap-2 mb-3">
<button id="cards-new-btn" onclick="cardsNew()" 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 preset">Add preset</button>
<button onclick="cardsExportFile()" class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Export to file">Export to file</button>
<button onclick="document.getElementById('cards-file').click()" class="px-3 py-2 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Import from file">Import from file</button>
</div>
<div id="cards-list" class="overflow-x-auto">
<table class="w-full text-sm">
<thead><tr>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Name">Name</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="ICCID">ICCID</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keysets (number: KIc/KID — counter)">Keysets (number: KIc/KID \u2014 counter)</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TARs (MSL)">TARs (MSL)</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="SCP81">SCP81</th>
<th class="border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
<tbody id="cards-tbody"></tbody>
</table>
</div>
<input type="file" id="cards-file" accept=".json,application/json" class="hidden" onchange="cardsImportFile(this)">
<div id="cards-io" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400"></div>
</div>
<!-- ===== preset editor ===== -->
<div id="cards-editor-view" class="hidden">
<div class="flex items-center justify-between mb-3">
<h3 id="cards-editor-title" class="text-lg font-semibold text-gray-800 dark:text-slate-200"></h3>
<button onclick="cardsCancelEdit()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300" data-l10n="Cancel">Cancel</button>
</div>
<div class="flex gap-2 mb-3">
<input id="cards-name" class="flex-1 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
<input id="cards-iccid" 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="ICCID (optional)">
<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
<input id="cards-adm" class="w-40 border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800 font-mono" placeholder="ADM (optional)">
<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 class="flex-1">
<label for="cards-name" class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="Name">Name</label>
<input id="cards-name" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-3 py-1.5 dark:bg-slate-800" placeholder="Name (e.g. Foobar SIM)">
</div>
<div class="flex-1">
<label for="cards-iccid" class="block mb-1 text-sm font-medium text-gray-700 dark:text-slate-300" data-l10n="ICCID">ICCID</label>
<div class="flex gap-1">
<input id="cards-iccid" 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="ICCID (optional)">
<button id="cards-iccid-from-card" data-needs="card-iccid" onclick="cardsIccidFromCard()" class="px-3 py-1.5 text-sm rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700 text-gray-700 dark:text-slate-300 whitespace-nowrap disabled:opacity-40 disabled:cursor-not-allowed" data-l10n="From card">From card</button>
</div>
</div>
</div>
<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-3 mb-2 flex-wrap items-end">
<legend class="px-1 text-xs font-medium text-gray-500 dark:text-slate-400" data-l10n="PIN/ADM">PIN/ADM</legend>
<div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-5 gap-2 mb-2">
<div>
<label for="cards-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="ISD TAR">ISD TAR</label>
<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" maxlength="6" value="000000">
<label for="cards-pin1" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PIN1</label>
<input id="cards-pin1" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" inputmode="numeric" placeholder="4-8 digits">
</div>
<div>
<label for="cards-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
<input id="cards-tar-msl" 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" maxlength="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
<label for="cards-puk1" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PUK1</label>
<input id="cards-puk1" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" inputmode="numeric" placeholder="4-8 digits">
</div>
<div>
<label for="cards-uicc-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="UICC RFM TAR">UICC RFM TAR</label>
<input id="cards-uicc-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" maxlength="6" value="B00000">
<label for="cards-pin2" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PIN2</label>
<input id="cards-pin2" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" inputmode="numeric" placeholder="4-8 digits">
</div>
<div>
<label for="cards-uicc-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
<input id="cards-uicc-tar-msl" 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" maxlength="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
<label for="cards-puk2" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">PUK2</label>
<input id="cards-puk2" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" inputmode="numeric" placeholder="4-8 digits">
</div>
<div>
<label for="cards-usim-tar" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="ADF RFM TAR">ADF RFM TAR</label>
<input id="cards-usim-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" maxlength="6" value="B00001">
</div>
<div>
<label for="cards-usim-tar-msl" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400" data-l10n="MSL">MSL</label>
<input id="cards-usim-tar-msl" 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" maxlength="2" value="16" data-l10n-title="MSL (Minimum SPI1, one hex byte)">
<label for="cards-adm" class="block mb-0.5 text-xs text-gray-600 dark:text-slate-400">ADM</label>
<input id="cards-adm" class="w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono text-center" maxlength="8" placeholder="ADM (optional)">
</div>
</div>
<p class="text-xs text-gray-500 dark:text-slate-400 mb-2" data-l10n="Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.">Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.</p>
<div class="mb-1 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="More TARs — each with its MSL and an optional description">More TARs — each with its MSL and an optional description</span>
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.">All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.</p>
</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="TAR">TAR</legend>
<table class="w-full text-sm mb-2">
<thead><tr>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TAR">TAR</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="MSL">MSL</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Description">Description</th>
<th class="w-8 border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
<tbody id="cards-tars"></tbody>
</table>
<div class="flex items-center justify-between mb-2">
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="The first three rows are the fixed role TARs; add further TARs as needed.">The first three rows are the fixed role TARs; add further TARs as needed.</span>
<button type="button" onclick="cardsTarAdd()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Add TAR">Add TAR</button>
</div>
<div id="cards-tars"></div>
<p class="text-xs text-gray-500 dark:text-slate-400" data-l10n="Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.">Each TAR carries its MSL (Minimum SPI1) — the card checks it before the security processing (TS 102 226 8.2.1.3.2.4); the packet SPI1 is derived from it.</p>
</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="SCP80 (GSM 03.48, ETSI TS 102 225)">SCP80 (GSM 03.48, ETSI TS 102 225)</legend>
<div class="mb-1 flex items-center justify-between">
<span class="text-xs text-gray-600 dark:text-slate-400" data-l10n="OTA keysets — each with its own number and counter">OTA keysets — each with its own number and counter</span>
<button type="button" onclick="cardsKeysetAdd()" class="px-2 py-0.5 text-xs rounded border border-gray-300 dark:border-slate-600 hover:bg-gray-200 dark:hover:bg-slate-700" data-l10n="Add keyset">Add keyset</button>
@@ -1651,25 +1697,11 @@
</div>
<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>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Name">Name</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="ICCID">ICCID</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Keysets (number: KIc/KID — counter)">Keysets (number: KIc/KID \u2014 counter)</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="TARs (MSL)">TARs (MSL)</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="SCP81">SCP81</th>
<th class="border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
<tbody id="cards-tbody"></tbody>
</table>
<div class="flex gap-2">
<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" data-l10n="Save">Save</button>
<button onclick="cardsCancelEdit()" class="px-4 py-2.5 bg-gray-500 text-white text-sm font-medium rounded hover:bg-gray-600" data-l10n="Cancel">Cancel</button>
</div>
<input type="file" id="cards-file" accept=".json,application/json" class="hidden" onchange="cardsImportFile(this)">
<div class="flex flex-wrap gap-2 mt-2">
<button onclick="cardsExportFile()" class="px-2 py-1 text-xs rounded bg-blue-600 text-white hover:bg-blue-700" data-l10n="Export to file">Export to file</button>
<button onclick="document.getElementById('cards-file').click()" class="px-2 py-1 text-xs rounded bg-gray-600 text-white hover:bg-gray-700" data-l10n="Import from file">Import from file</button>
</div>
<div id="cards-io" class="hidden mt-2 text-xs text-gray-500 dark:text-slate-400"></div>
</div>
</div>
@@ -1816,7 +1848,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 = '3.10.0';
const SIMPLE_VERSION = '3.11.0';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -11855,46 +11887,72 @@ function cardsKeysetsFromForm() {
return out;
}
// ===== TAR table editor (Cards form) =====
// The three role entries have fixed inputs (their labels are the immutable
// descriptions); further TARs are free rows with an optional description.
// Every entry carries the TAR and its MSL (Minimum SPI1, one hex byte).
// ===== TAR table editor (Cards editor) =====
// One uniform table: the first three rows are the fixed role entries - their
// descriptions are the immutable role labels (English fixed text) and they
// carry no remove button; further rows are free entries with an optional
// description. Every row carries the TAR and its MSL (Minimum SPI1, one hex
// byte).
let _cardsTarCount = 0;
// The fixed role descriptions (spec role names; not translated).
const CARDS_TAR_ROLE_DESC = {
isd: 'ISD compact format',
uiccRfm: 'UICC shared FS RFM compact format',
usimRfm: 'ADF FS RFM compact format',
};
function cardsTarRowHtml(e, i) {
const cls = 'border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
return '<div class="flex gap-2 items-center mb-1">' +
'<input id="cards-ft-' + i + '-tar" class="w-20 ' + cls + ' text-center" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '">' +
'<input id="cards-ft-' + i + '-msl" class="w-12 ' + cls + ' text-center" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '">' +
'<input id="cards-ft-' + i + '-desc" class="flex-1 ' + cls + '" placeholder="' + esc(t('Description (optional)')) + '" value="' + esc(e.desc || '') + '">' +
'<button type="button" onclick="cardsTarRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove TAR')) + '">\u2715</button>' +
'</div>';
const role = CARDS_TAR_ROLES[i] || '';
let cells = '<td class="py-1 px-1"><input id="cards-tr-' + i + '-tar" class="w-24 ' + cls + ' text-center" maxlength="6" placeholder="TAR" value="' + esc(e.tar || '') + '"></td>' +
'<td class="py-1 px-1"><input id="cards-tr-' + i + '-msl" class="w-14 ' + cls + ' text-center" maxlength="2" placeholder="MSL" value="' + esc(e.msl || '') + '" title="' + esc(t('MSL (Minimum SPI1, one hex byte)')) + '"></td>';
if (role) {
cells += '<td class="py-1 px-2 text-xs text-gray-600 dark:text-slate-400">' + esc(CARDS_TAR_ROLE_DESC[role] || '') + '</td>' +
'<td class="py-1 px-1"></td>';
} else {
cells += '<td class="py-1 px-1"><input id="cards-tr-' + i + '-desc" class="w-full ' + cls + '" placeholder="' + esc(t('Description (optional)')) + '" value="' + esc(e.desc || '') + '"></td>' +
'<td class="py-1 px-1"><button type="button" onclick="cardsTarRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove TAR')) + '">\u2715</button></td>';
}
return '<tr>' + cells + '</tr>';
}
// Render the whole table from a preset's TAR list (roles first, then free
// rows); the role rows are always shown, with the pre-settled defaults when
// the list carries none.
function cardsTarRowsRender(entries) {
const box = document.getElementById('cards-tars');
if (!box) return;
const list = entries || [];
_cardsTarCount = list.length;
box.innerHTML = list.map((e, i) => cardsTarRowHtml(e, i)).join('');
const roles = CARDS_TAR_ROLES.map(role => {
const e = list.find(x => x.role === role) || {};
return { tar: e.tar || cardsRoleDefault(role), msl: e.msl || CARDS_MSL_DEFAULT };
});
const free = list.filter(e => !e.role);
_cardsTarCount = free.length;
box.innerHTML = roles.map((e, i) => cardsTarRowHtml(e, i)).join('') +
free.map((e, i) => cardsTarRowHtml(e, CARDS_TAR_ROLES.length + i)).join('');
}
function cardsTarAdd() {
cardsTarRowsRender(cardsFreeTarsFromForm().concat([{ tar: '', msl: '', desc: '' }]));
cardsTarRowsRender(cardsTarsFromForm().concat([{ tar: '', msl: '', desc: '' }]));
}
// `i` is the table row index; the role rows are fixed and never removed.
function cardsTarRemove(i) {
const rows = cardsFreeTarsFromForm();
if (i < CARDS_TAR_ROLES.length) return;
const rows = cardsTarsFromForm();
rows.splice(i, 1);
cardsTarRowsRender(rows);
}
// The free editor rows (fully empty rows are dropped).
// The free table rows (fully empty rows are dropped).
function cardsFreeTarsFromForm() {
const out = [];
for (let i = 0; i < _cardsTarCount; i++) {
const idx = CARDS_TAR_ROLES.length + i;
const get = id => {
const el = document.getElementById('cards-ft-' + i + '-' + id);
const el = document.getElementById('cards-tr-' + idx + '-' + id);
return el ? String(el.value).trim() : '';
};
const e = { tar: get('tar').replace(/\s/g, '').toUpperCase(),
@@ -11905,32 +11963,25 @@ function cardsFreeTarsFromForm() {
return out;
}
// The TAR table as preset entries: the three role entries from their fixed
// inputs (the labels are the role descriptions) plus the free rows.
// The TAR table as preset entries: the three role entries from the first three
// rows (an emptied field keeps the pre-settled default) plus the free rows.
function cardsTarsFromForm() {
const roleInputs = { isd: 'cards-tar', uiccRfm: 'cards-uicc-tar', usimRfm: 'cards-usim-tar' };
const out = CARDS_TAR_ROLES.map(role => {
const tarEl = document.getElementById(roleInputs[role]);
const mslEl = document.getElementById(roleInputs[role] + '-msl');
const tar = (tarEl ? String(tarEl.value).trim() : '').replace(/\s/g, '').toUpperCase();
const msl = (mslEl ? String(mslEl.value).trim() : '').replace(/\s/g, '').toUpperCase();
// An emptied role field keeps the pre-settled default: the three role
// entries are mandatory with a non-empty TAR and MSL.
const out = CARDS_TAR_ROLES.map((role, i) => {
const get = id => {
const el = document.getElementById('cards-tr-' + i + '-' + id);
return el ? String(el.value).trim() : '';
};
const tar = get('tar').replace(/\s/g, '').toUpperCase();
const msl = get('msl').replace(/\s/g, '').toUpperCase();
return { role: role, tar: tar || cardsRoleDefault(role),
msl: msl || CARDS_MSL_DEFAULT, desc: '' };
});
return out.concat(cardsFreeTarsFromForm());
}
// The role inputs back to their pre-settled defaults (TAR + MSL 16).
// The role rows back to their pre-settled defaults (TAR + MSL 16) and no free
// rows.
function cardsTarsReset() {
const roleInputs = { isd: 'cards-tar', uiccRfm: 'cards-uicc-tar', usimRfm: 'cards-usim-tar' };
CARDS_TAR_ROLES.forEach(role => {
const tarEl = document.getElementById(roleInputs[role]);
if (tarEl) tarEl.value = cardsRoleDefault(role);
const mslEl = document.getElementById(roleInputs[role] + '-msl');
if (mslEl) mslEl.value = CARDS_MSL_DEFAULT;
});
cardsTarRowsRender([]);
}
@@ -12091,6 +12142,10 @@ function cardsFormValues() {
return {
name: val('name'),
iccid: val('iccid'),
pin1: val('pin1').replace(/\s/g, ''),
puk1: val('puk1').replace(/\s/g, ''),
pin2: val('pin2').replace(/\s/g, ''),
puk2: val('puk2').replace(/\s/g, ''),
adm: val('adm').replace(/\s/g, '').toUpperCase(),
tars: cardsTarsFromForm(),
pskIdentity: val('psk-id'),
@@ -12099,19 +12154,49 @@ function cardsFormValues() {
};
}
// Show the preset list (the Cards tab's default view).
function cardsShowList() {
const list = document.getElementById('cards-list-view');
const editor = document.getElementById('cards-editor-view');
if (list) list.classList.remove('hidden');
if (editor) editor.classList.add('hidden');
}
// Show the preset editor.
function cardsShowEditor() {
const list = document.getElementById('cards-list-view');
const editor = document.getElementById('cards-editor-view');
if (list) list.classList.add('hidden');
if (editor) editor.classList.remove('hidden');
}
function cardsSetFormMode() {
const title = document.getElementById('cards-editor-title');
if (title) {
title.textContent = _cardsEditIdx === null
? t('Add preset')
: t('Edit preset') + ': ' + ((cards[_cardsEditIdx] || {}).name || '');
}
const btn = document.getElementById('cards-add-btn');
if (btn) btn.textContent = _cardsEditIdx === null ? t('Add') : t('Save');
const cancel = document.getElementById('cards-cancel-btn');
if (cancel) cancel.classList.toggle('hidden', _cardsEditIdx === null);
}
// "Add preset" in the list view: reset the form and open the editor.
function cardsNew() {
_cardsEditIdx = null;
cardsClearForm();
cardsSetFormMode();
cardsShowEditor();
const el = document.getElementById('cards-name');
if (el) el.focus();
}
function cardsClearForm() {
['name','iccid','adm','psk-id','psk-key'].forEach(id => {
['name','iccid','pin1','puk1','pin2','puk2','adm','psk-id','psk-key'].forEach(id => {
const el = document.getElementById('cards-' + id);
if (el) el.value = '';
});
// The three role entries keep their pre-settled defaults (TAR + MSL 16)
// The three role rows keep their pre-settled defaults (TAR + MSL 16)
cardsTarsReset();
cardsKeysetRowsRender([]);
}
@@ -12119,6 +12204,14 @@ function cardsClearForm() {
async function cardsAdd() {
const v = cardsFormValues();
if (!v.name) { alert(t('Name is required')); return; }
// The optional card codes: PIN/PUK are 4-8 decimal digits; ADM stays
// free-form (hex or ASCII digits)
for (const k of ['pin1', 'puk1', 'pin2', 'puk2']) {
if (v[k] && !/^[0-9]{4,8}$/.test(v[k])) {
alert(t('PIN/PUK codes must be 4-8 decimal digits') + ': ' + k.toUpperCase());
return;
}
}
if (!v.keysets.length) {
alert(t('Define at least one keyset (KIc/KID and their keys)'));
return;
@@ -12191,24 +12284,24 @@ function cardsEdit(i) {
const el = document.getElementById('cards-' + id);
if (el) el.value = value == null ? '' : value;
};
set('name', c.name); set('iccid', c.iccid); set('adm', c.adm);
set('tar', cardsRoleTar(c, 'isd'));
set('tar-msl', cardsRoleMsl(c, 'isd'));
set('uicc-tar', cardsRoleTar(c, 'uiccRfm'));
set('uicc-tar-msl', cardsRoleMsl(c, 'uiccRfm'));
set('usim-tar', cardsRoleTar(c, 'usimRfm'));
set('usim-tar-msl', cardsRoleMsl(c, 'usimRfm'));
set('name', c.name); set('iccid', c.iccid);
set('pin1', c.pin1); set('puk1', c.puk1);
set('pin2', c.pin2); set('puk2', c.puk2);
set('adm', c.adm);
set('psk-id', c.pskIdentity); set('psk-key', c.pskKey);
cardsKeysetRowsRender(spKeysetList(c));
cardsTarRowsRender(cardsTarList(c).filter(e => !e.role));
cardsTarRowsRender(cardsTarList(c));
cardsSetFormMode();
document.getElementById('cards-name').focus();
cardsShowEditor();
const el = document.getElementById('cards-name');
if (el) el.focus();
}
function cardsCancelEdit() {
_cardsEditIdx = null;
cardsSetFormMode();
cardsClearForm();
cardsShowList();
}
function ramCardIdxAfterRemove(idx, removedIdx) {
@@ -20810,6 +20903,15 @@ const LANG_RU = {
'Duplicate TAR in the preset': 'Повторяющийся TAR в пресете',
'SPI1 is below the MSL of this TAR': 'SPI1 ниже MSL этого TAR',
'(empty)': '(пусто)',
'Add preset': 'Добавить пресет',
'Edit preset': 'Редактирование пресета',
'PIN/ADM': 'PIN/ADM',
'TAR': 'TAR',
'Description': 'Описание',
'Card presets are stored on the server. A preset carries the card codes (PIN/PUK/ADM), a TAR table (every TAR with its MSL) and several keysets, each with its own number and counter.': 'Пресеты карт хранятся на сервере. Пресет содержит коды карты (PIN/PUK/ADM), таблицу TAR (у каждого TAR свой MSL) и несколько keysets — у каждого свой номер и счётчик.',
'All codes are optional: PIN/PUK are 4-8 decimal digits (stored for later use by the card operations — not consumed yet); ADM is the administrator key.': 'Все коды необязательны: PIN/PUK — 4–8 десятичных цифр (сохраняются для будущего использования операциями карты — пока не используются); ADM — администраторский ключ.',
'The first three rows are the fixed role TARs; add further TARs as needed.': 'Первые три строки — фиксированные роли TAR; дополнительные TAR добавляются по необходимости.',
'PIN/PUK codes must be 4-8 decimal digits': 'Коды PIN/PUK должны состоять из 4–8 десятичных цифр',
'Keysets (number: KIc/KID — counter)': 'Keysets (номер: KIc/KID — счётчик)',
'must be one hex byte': 'должен быть одним hex-байтом',
'KIc and KID must carry the same keyset number (TS 102 225 A.2)': 'KIc и KID должны содержать один и тот же номер keyset (TS 102 225 A.2)',