v3.11.0 editor polish: keyset editor as a header table, in sync with TAR

The keyset rows were a stack of flex divs with no column labels.  They are
now a fixed-layout table like the TAR table: a thead with # (keyset number,
with its tooltip), KIc, KID, KIc key, KID key, Counter and the action
column, and <tr> rows of py-1 px-2 cells with w-full inputs (the columns
drive the widths: 40/56/56 px, the two key columns split the remainder,
112 px for the 10-digit counter).  Ids and behaviour are unchanged (the
number span still toggles its red flag, the inputs keep their handlers).

Guards: html.test.js checks the keyset table headers and tbody;
cards_form.test.js checks the row markup.  sw cache simple-v366.

Tests: 665 Python + 788 frontend green; the whole inline PWA script
syntax-checked.
This commit is contained in:
2026-10-02 11:13:18 +03:00
parent f71b8ab73f
commit 84887d8518
4 changed files with 39 additions and 12 deletions
+22 -11
View File
@@ -1683,7 +1683,18 @@
<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>
<span class="block mb-1 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>
<div id="cards-keysets"></div>
<table class="w-full table-fixed text-sm mb-2">
<thead><tr>
<th class="w-10 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n-title="Keyset number (b8..b5 of KIc/KID)">#</th>
<th class="w-14 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KIc</th>
<th class="w-14 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KID</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KIc key</th>
<th class="text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700">KID key</th>
<th class="w-28 text-left py-1 px-2 border-b border-gray-200 dark:border-slate-700" data-l10n="Counter">Counter</th>
<th class="w-12 border-b border-gray-200 dark:border-slate-700"></th>
</tr></thead>
<tbody id="cards-keysets"></tbody>
</table>
<button type="button" onclick="cardsKeysetAdd()" class="mt-2 px-3 py-1.5 text-sm rounded bg-emerald-600 text-white hover:bg-emerald-700" data-l10n="Add keyset">Add keyset</button>
<p class="text-xs text-gray-500 dark:text-slate-400 mt-2" data-l10n="KIc and KID must carry the same keyset number (their high nibble) in the range 01-0F — TS 102 225 A.2; each keyset has its own counter on the card (Annex A.1).">KIc and KID must carry the same keyset number (their high nibble) in the range 01-0F — TS 102 225 A.2; each keyset has its own counter on the card (Annex A.1).</p>
</fieldset>
@@ -11815,17 +11826,17 @@ function spKeysetOptionsHtml(preset, kicHex, kidHex) {
let _cardsKeysetCount = 0;
function cardsKeysetRowHtml(ks, 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';
const cls = 'w-full border border-gray-300 dark:border-slate-600 text-sm rounded px-2 py-1.5 dark:bg-slate-800 font-mono';
const kvn = spKeysetKvnOf(ks.kic);
return '<div class="flex gap-2 items-center mb-1">' +
'<span id="cards-ks-' + i + '-kvn" class="w-10 text-xs font-mono text-center text-gray-500 dark:text-slate-400" title="' + esc(t('Keyset number (b8..b5 of KIc/KID)')) + '">' + (kvn === null ? '?' : kvn) + '</span>' +
'<input id="cards-ks-' + i + '-kic" class="w-12 ' + cls + ' text-center" maxlength="2" placeholder="KIc" value="' + esc(ks.kic || '') + '" oninput="cardsKeysetKvnUpdate(' + i + ')">' +
'<input id="cards-ks-' + i + '-kid" class="w-12 ' + cls + ' text-center" maxlength="2" placeholder="KID" value="' + esc(ks.kid || '') + '" oninput="cardsKeysetKvnUpdate(' + i + ')">' +
'<input id="cards-ks-' + i + '-kic-key" class="flex-1 ' + cls + '" placeholder="KIc key (hex)" value="' + esc(ks.kicKey || '') + '">' +
'<input id="cards-ks-' + i + '-kid-key" class="flex-1 ' + cls + '" placeholder="KID key (hex)" value="' + esc(ks.kidKey || '') + '">' +
'<input id="cards-ks-' + i + '-cntr" class="w-24 ' + cls + '" maxlength="10" placeholder="Counter" value="' + esc(ks.cntr || '') + '">' +
'<button type="button" onclick="cardsKeysetRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove keyset')) + '">\u2715</button>' +
'</div>';
return '<tr>' +
'<td class="py-1 px-2"><span id="cards-ks-' + i + '-kvn" class="block text-xs font-mono text-center text-gray-500 dark:text-slate-400" title="' + esc(t('Keyset number (b8..b5 of KIc/KID)')) + '">' + (kvn === null ? '?' : kvn) + '</span></td>' +
'<td class="py-1 px-2"><input id="cards-ks-' + i + '-kic" class="' + cls + ' text-center" maxlength="2" placeholder="KIc" value="' + esc(ks.kic || '') + '" oninput="cardsKeysetKvnUpdate(' + i + ')"></td>' +
'<td class="py-1 px-2"><input id="cards-ks-' + i + '-kid" class="' + cls + ' text-center" maxlength="2" placeholder="KID" value="' + esc(ks.kid || '') + '" oninput="cardsKeysetKvnUpdate(' + i + ')"></td>' +
'<td class="py-1 px-2"><input id="cards-ks-' + i + '-kic-key" class="' + cls + '" placeholder="KIc key (hex)" value="' + esc(ks.kicKey || '') + '"></td>' +
'<td class="py-1 px-2"><input id="cards-ks-' + i + '-kid-key" class="' + cls + '" placeholder="KID key (hex)" value="' + esc(ks.kidKey || '') + '"></td>' +
'<td class="py-1 px-2"><input id="cards-ks-' + i + '-cntr" class="' + cls + '" maxlength="10" placeholder="Counter" value="' + esc(ks.cntr || '') + '"></td>' +
'<td class="py-1 px-2"><button type="button" onclick="cardsKeysetRemove(' + i + ')" class="px-2 py-1 text-xs rounded bg-red-600 text-white hover:bg-red-700" title="' + esc(t('Remove keyset')) + '">\u2715</button></td>' +
'</tr>';
}
function cardsKeysetRowsRender(keysets) {
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v365';
const CACHE = 'simple-v366';
const URLS = [
'index.html',
'help.html',
+10
View File
@@ -539,6 +539,16 @@ test('the TAR row values are left-aligned like the headers', () => {
assert.match(out, /<td class="py-1 px-2"><input id="cards-tr-3-msl" class="w-full/);
});
test('the keyset row renders as a table row with full-width inputs', () => {
const out = cardsKeysetRowHtml({ kic: '15', kid: '15', kicKey: 'AA', kidKey: 'BB', cntr: '0000000001' }, 0);
assert.match(out, /^<tr>/);
assert.match(out, /<td class="py-1 px-2"><span id="cards-ks-0-kvn"/);
assert.match(out, /<td class="py-1 px-2"><input id="cards-ks-0-kic" class="w-full/);
assert.match(out, /<td class="py-1 px-2"><input id="cards-ks-0-kic-key" class="w-full/);
assert.match(out, /<td class="py-1 px-2"><input id="cards-ks-0-cntr" class="w-full/);
assert.match(out, /cardsKeysetRemove\(0\)/);
});
test('the TAR table renders the role rows and adds/removes free rows', () => {
const els = setup();
cardsTarRowsRender([{ tar: 'AF4D01', msl: '0A', desc: 'one' },
+6
View File
@@ -152,6 +152,12 @@ test('the card editor groups its fields into labelled fieldsets', () => {
// the keyset editor and the PSK pair stay as implemented
assert.match(scp80, /id="cards-keysets"/);
assert.match(scp80, /onclick="cardsKeysetAdd\(\)"/);
// ... and mirror the TAR table: a fixed-layout table with headers
assert.match(scp80, /<table class="w-full table-fixed text-sm mb-2">/);
assert.match(scp80, /<tbody id="cards-keysets">/);
for (const h of ['KIc', 'KID', 'KIc key', 'KID key', 'Counter']) {
assert.match(scp80, new RegExp('>' + h + '</th>'));
}
assert.match(scp81, /id="cards-psk-id"/);
assert.match(scp81, /id="cards-psk-key"/);
// the PSK explanation lives inside the SCP81 group, not outside it