ui: lay the EUICCInfo2 chip fields out in two columns

The short EUICCInfo2 fields (versions, card resources, category, PPRs, …)
were one per row in the wide right-hand box, wasting half of it.

- esimChipValueWide(): a row needs the full width when its value is a
  list or a long token (>24 chars or ', '), so short scalars pair up.
- esimChipBox() gained a twoColumns mode: rows in a responsive
  grid-cols-1 sm:grid-cols-2 grid with long values spanning both columns;
  the other boxes keep their single-column rows.
- esimRenderChip() renders the EUICCInfo2 box with two columns.
- style.css rebuilt (sm:grid-cols-2, sm:col-span-2).
- tests: the wide/short rule, the two-column markup and the render call;
  help EN/RU and AGENTS note the layout; sw.js simple-v233.
This commit is contained in:
2026-09-22 00:51:07 +03:00
parent cb63ff286d
commit bed1852110
6 changed files with 66 additions and 8 deletions
+1 -1
View File
@@ -497,7 +497,7 @@
<h3 id="esim" class="text-lg font-medium mb-2">8.8 eSIM / LPA (локальные операции)</h3>
<p class="text-sm mb-2">Для eUICC (SGP.22/SGP.32) подвкладка <strong>&laquo;eSIM&raquo;</strong> читает чип и управляет установленными профилями через локальный интерфейс ES10 (через pySim, без обращения к SM-DP+):</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Чип</strong> — EID, EUICCInfo1/2 со всеми декодированными полями (версия профиля, SVN, прошивка, расширенные ресурсы карты — число установленных приложений и свободная энергонезависимая/энергозависимая память —, битовые списки возможностей UICC и RSP, идентификаторы CI PKI, категория, запрещённые PPR, версия PP, номер аккредитации SS, данные сертификации), настроенные адреса SM-DP+ по умолчанию / корневого DS и <strong>таблица авторизации правил</strong> (ES10b GetRat: идентификаторы PPR, разрешённые операторы, флаг согласия). Неизвестные TLV сохраняются как сырой hex. Поля сгруппированы в блоки — EID, EUICCInfo1, адреса и таблица авторизации правил слева, более крупный EUICCInfo2 в правом блоке (на узких экранах — одна колонка).</li>
<li><strong>Чип</strong> — EID, EUICCInfo1/2 со всеми декодированными полями (версия профиля, SVN, прошивка, расширенные ресурсы карты — число установленных приложений и свободная энергонезависимая/энергозависимая память —, битовые списки возможностей UICC и RSP, идентификаторы CI PKI, категория, запрещённые PPR, версия PP, номер аккредитации SS, данные сертификации), настроенные адреса SM-DP+ по умолчанию / корневого DS и <strong>таблица авторизации правил</strong> (ES10b GetRat: идентификаторы PPR, разрешённые операторы, флаг согласия). Неизвестные TLV сохраняются как сырой hex. Поля сгруппированы в блоки — EID, EUICCInfo1, адреса и таблица авторизации правил слева, более крупный EUICCInfo2 в правом блоке (на узких экранах — одна колонка); короткие поля в блоке EUICCInfo2 располагаются в две колонки, длинные значения занимают всю ширину.</li>
<li><strong>Профили</strong> — все профили с состоянием (включён/отключён), ником, оператором, именем, ICCID, ISD-P AID, классом, владельцем и типом значка — сам значок показывается в натуральную величину рядом с метаданными, а строка <strong>Icon</strong> содержит его тип и размер данных в байтах. Кнопки <strong>&laquo;Включить&raquo;</strong>/<strong>&laquo;Отключить&raquo;</strong> переключают профиль; карта обычно сначала отправляет REFRESH (виден в журнале проактивных команд) — терминальный ответ завершает переключение, после чего сессия карты переинициализируется как при equip: ICCID, сетевое состояние и все кэшированные данные перечитываются, а запрошенное состояние проверяется по перечитанному списку профилей.</li>
<li><strong>Уведомления</strong> — список ожидающих уведомлений только для чтения (номер последовательности, операция, адрес, ICCID); ничего не обрабатывается и не удаляется.</li>
</ul>
+1 -1
View File
@@ -497,7 +497,7 @@
<h3 id="esim" class="text-lg font-medium mb-2">8.8 eSIM / LPA (local operations)</h3>
<p class="text-sm mb-2">For an eUICC (SGP.22/SGP.32) the <strong>eSIM</strong> pill reads the chip and manages the installed profiles through the local ES10 interface (via pySim, no SM-DP+ contact):</p>
<ul class="list-disc list-inside text-sm space-y-1 mb-3">
<li><strong>Chip</strong> — EID, EUICCInfo1/2 with every field decoded (profile version, SVN, firmware, extended card resource — installed applications and free non-volatile/volatile memory —, UICC and RSP capability bit lists, CI PKI identifiers, category, forbidden profile policy rules, PP version, SS accreditation number, certification data), the configured default SM-DP+ / root DS addresses, and the <strong>rules authorisation table</strong> (ES10b GetRat: PPR IDs, allowed operators, consent flag). Unknown TLVs are preserved as raw hex. The fields are grouped into boxes — EID, EUICCInfo1, Addresses and the rules authorisation table on the left, the larger EUICCInfo2 in the right-hand box (a single column on narrow screens).</li>
<li><strong>Chip</strong> — EID, EUICCInfo1/2 with every field decoded (profile version, SVN, firmware, extended card resource — installed applications and free non-volatile/volatile memory —, UICC and RSP capability bit lists, CI PKI identifiers, category, forbidden profile policy rules, PP version, SS accreditation number, certification data), the configured default SM-DP+ / root DS addresses, and the <strong>rules authorisation table</strong> (ES10b GetRat: PPR IDs, allowed operators, consent flag). Unknown TLVs are preserved as raw hex. The fields are grouped into boxes — EID, EUICCInfo1, Addresses and the rules authorisation table on the left, the larger EUICCInfo2 in the right-hand box (a single column on narrow screens); short fields in the EUICCInfo2 box share two columns, long values span the full width.</li>
<li><strong>Profiles</strong> — every profile with its state (enabled/disabled), nickname, provider, name, ICCID, ISD-P AID, class, owner and icon type — the icon image itself is shown at its natural size next to the metadata, and the <strong>Icon</strong> row carries its type and data size in bytes. <strong>Enable</strong>/<strong>Disable</strong> switches a profile; the card usually sends REFRESH first (logged in the proactive command log) — the terminal response completes the switch and the card session is then re-initialized like an equip, so the ICCID, network state and every cached card view are re-read, and the requested state is verified against the re-read profile list.</li>
<li><strong>Notifications</strong> — read-only list of pending notifications (sequence number, operation, address, ICCID); nothing is processed or removed.</li>
</ul>
+18 -4
View File
@@ -1678,16 +1678,30 @@ function esimChipSectionRows(data) {
return esimFieldRows(data, '');
}
// One bordered chip-info box; no rows -> no box.
function esimChipBox(title, rows) {
// A chip-info row needs the full box width when its value is a list or a
// long token (hex identifiers, capability lists); short scalars share a row.
function esimChipValueWide(label, value) {
const v = String(value);
return v.length > 24 || v.indexOf(', ') >= 0;
}
// One bordered chip-info box; no rows -> no box. With `twoColumns` the short
// fields are laid out in a 2-column grid (single column on narrow screens)
// and long values span the full width, which keeps the big EUICCInfo2 box
// compact.
function esimChipBox(title, rows, twoColumns) {
if (!rows || !rows.length) return '';
let html = '<div class="border border-gray-200 dark:border-slate-700 rounded p-2 min-w-0">';
html += '<div class="mb-1 text-xs font-semibold text-gray-600 dark:text-slate-300">' + esc(title) + '</div>';
if (twoColumns) html += '<div class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-1">';
for (const [k, v] of rows) {
html += '<div class="flex gap-2 mb-0.5">';
const cls = twoColumns ? 'flex gap-2 min-w-0' : 'flex gap-2 mb-0.5';
html += '<div class="' + cls +
(twoColumns && esimChipValueWide(k, v) ? ' sm:col-span-2' : '') + '">';
if (k) html += '<span class="w-36 shrink-0 text-right text-gray-500 dark:text-slate-400 break-words">' + esc(k) + '</span>';
html += '<span class="font-mono break-words min-w-0">' + esc(v) + '</span></div>';
}
if (twoColumns) html += '</div>';
return html + '</div>';
}
@@ -1701,7 +1715,7 @@ function esimRenderChip() {
esimChipBox(t('Addresses'), esimChipSectionRows(c.addresses)),
esimChipBox(t('Rules authorisation table'), esimChipSectionRows(c.rat)),
].filter(Boolean);
const right = esimChipBox('EUICCInfo2', esimChipSectionRows(c.info2));
const right = esimChipBox('EUICCInfo2', esimChipSectionRows(c.info2), true);
if (!left.length && !right) {
el.innerHTML = '<span class="text-gray-400">' + esc(t('No data')) + '</span>';
return;
+10
View File
@@ -1977,6 +1977,16 @@ video {
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
}
@media (min-width: 640px) {
.sm\:col-span-2 {
grid-column: span 2 / span 2;
}
.sm\:grid-cols-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (min-width: 1024px) {
.lg\:col-span-1 {
grid-column: span 1 / span 1;
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v232';
const CACHE = 'simple-v233';
const URLS = [
'index.html',
'help.html',
+35 -1
View File
@@ -24,7 +24,7 @@ function extractFunc(src, name) {
let code = '';
for (const fn of ['esimLabel', 'esimGroupEid', 'esimFieldRows', 'esimResultText',
'esimStateLabel', 'esimOperationsText', 'esimProfileRows', 'esimIconDataUrl',
'esimChipSectionRows', 'esimChipBox', 'esimSwitchStatus']) {
'esimChipSectionRows', 'esimChipValueWide', 'esimChipBox', 'esimSwitchStatus']) {
code += extractFunc(html, fn) + '\n';
}
for (const c of ['ESIM_CHIP_LABELS', 'ESIM_RESULT_KEYS']) {
@@ -179,6 +179,40 @@ test('the chip boxes prefer wrapping at spaces over breaking words', () => {
assert.doesNotMatch(render, /break-all/);
});
test('esimChipValueWide keeps short scalars in one column', () => {
assert.strictEqual(esimChipValueWide('Profile version', '2.3.1'), false);
assert.strictEqual(
esimChipValueWide('Card resource / Free non-volatile memory', '439084'), false);
assert.strictEqual(esimChipValueWide('SS accreditation number', 'ED-ZI-UP-0826'), false);
assert.strictEqual(
esimChipValueWide('RSP capability', 'additionalProfile, testProfileSupport'), true);
assert.strictEqual(
esimChipValueWide('CI PKI (verification) / Subject key identifier',
'81370F5125D0B1D408D4C3B232E6D25E795BEBFB'), true);
});
test('esimChipBox lays short fields out in two columns', () => {
const box = esimChipBox('EUICCInfo2', [
['Profile version', '2.3.1'], ['SVN', '2.2.2'],
['UICC capability', 'usimSupport, isimSupport'],
], true);
assert.match(box, /sm:grid-cols-2/);
const cells = box.match(/<div class="flex gap-2[^"]*"/g);
assert.deepStrictEqual(cells, [
'<div class="flex gap-2 min-w-0"',
'<div class="flex gap-2 min-w-0"',
'<div class="flex gap-2 min-w-0 sm:col-span-2"',
]);
const one = esimChipBox('EUICCInfo1', [['SVN', '2.2.2']]);
assert.doesNotMatch(one, /sm:grid-cols-2/);
assert.match(one, /flex gap-2 mb-0\.5/);
});
test('the chip view renders EUICCInfo2 in two columns', () => {
const fn = extractFunc(html, 'esimRenderChip');
assert.match(fn, /esimChipBox\('EUICCInfo2', esimChipSectionRows\(c\.info2\), true\)/);
});
test('the chip view groups the sections into a grid', () => {
const fn = extractFunc(html, 'esimRenderChip');
assert.match(fn, /lg:grid-cols-3/);