ui: group the eSIM chip info into boxes and wrap values nicely
The chip view was one long flat list spread over the full height. - esimRenderChip split into esimChipSectionRows (object or RAT rule list -> rows) and esimChipBox (titled box, skipped when empty); the boxes are laid out on a grid: EID, EUICCInfo1, Addresses and the rules authorisation table in the left column, the larger EUICCInfo2 in the wider right-hand box (single column on narrow screens). Rows use a narrower label column (w-36). - Values wrap with break-words + min-w-0 instead of break-all: text breaks at spaces first and long hex/AID tokens only when they cannot fit (same for the profile metadata rows). - style.css rebuilt (grid-cols-1, lg:grid-cols-3, lg:col-span-1/2/3, break-words). - tests: the new helpers, the grid/box layout and the wrapping check; help EN/RU note the grouping; sw.js simple-v232.
This commit is contained in:
@@ -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>«eSIM»</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.</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 в правом блоке (на узких экранах — одна колонка).</li>
|
||||
<li><strong>Профили</strong> — все профили с состоянием (включён/отключён), ником, оператором, именем, ICCID, ISD-P AID, классом, владельцем и типом значка — сам значок показывается в натуральную величину рядом с метаданными, а строка <strong>Icon</strong> содержит его тип и размер данных в байтах. Кнопки <strong>«Включить»</strong>/<strong>«Отключить»</strong> переключают профиль; карта обычно сначала отправляет REFRESH (виден в журнале проактивных команд) — терминальный ответ завершает переключение, после чего сессия карты переинициализируется как при equip: ICCID, сетевое состояние и все кэшированные данные перечитываются, а запрошенное состояние проверяется по перечитанному списку профилей.</li>
|
||||
<li><strong>Уведомления</strong> — список ожидающих уведомлений только для чтения (номер последовательности, операция, адрес, ICCID); ничего не обрабатывается и не удаляется.</li>
|
||||
</ul>
|
||||
|
||||
+1
-1
@@ -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.</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).</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>
|
||||
|
||||
+44
-28
@@ -1665,41 +1665,57 @@ function esimIconDataUrl(p) {
|
||||
return 'data:' + mime + ';base64,' + btoa(bin);
|
||||
}
|
||||
|
||||
// One chip-info section -> rows (the RAT is a list of rules).
|
||||
function esimChipSectionRows(data) {
|
||||
if (!data) return [];
|
||||
if (Array.isArray(data)) {
|
||||
const rows = [];
|
||||
data.forEach((item, i) => {
|
||||
rows.push(...esimFieldRows(item, t('Rule') + ' ' + (i + 1)));
|
||||
});
|
||||
return rows;
|
||||
}
|
||||
return esimFieldRows(data, '');
|
||||
}
|
||||
|
||||
// One bordered chip-info box; no rows -> no box.
|
||||
function esimChipBox(title, rows) {
|
||||
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>';
|
||||
for (const [k, v] of rows) {
|
||||
html += '<div class="flex gap-2 mb-0.5">';
|
||||
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>';
|
||||
}
|
||||
return html + '</div>';
|
||||
}
|
||||
|
||||
function esimRenderChip() {
|
||||
const el = document.getElementById('esim-chip');
|
||||
if (!el) return;
|
||||
const c = _esimChip || {};
|
||||
const rows = [];
|
||||
if (c.eid) rows.push(['EID', esimGroupEid(c.eid)]);
|
||||
const sections = [
|
||||
['EUICCInfo1', c.info1], ['EUICCInfo2', c.info2],
|
||||
['Addresses', c.addresses], ['Rules authorisation table', c.rat],
|
||||
];
|
||||
for (const [name, data] of sections) {
|
||||
if (!data || (Array.isArray(data) && !data.length)) continue;
|
||||
rows.push(['§ ' + t(name), '']);
|
||||
if (Array.isArray(data)) {
|
||||
data.forEach((item, i) => {
|
||||
rows.push(...esimFieldRows(item, t('Rule') + ' ' + (i + 1)));
|
||||
});
|
||||
} else {
|
||||
rows.push(...esimFieldRows(data, ''));
|
||||
}
|
||||
}
|
||||
let html = '';
|
||||
for (const [k, v] of rows) {
|
||||
if (k.startsWith('§ ')) {
|
||||
html += '<div class="mt-2 mb-0.5 font-semibold text-gray-600 dark:text-slate-300">' + esc(k.slice(2)) + '</div>';
|
||||
continue;
|
||||
}
|
||||
html += '<div class="flex gap-2 mb-0.5"><span class="w-56 shrink-0 text-right text-gray-500 dark:text-slate-400 break-all">' + esc(k) + '</span>' +
|
||||
'<span class="font-mono break-all">' + esc(v) + '</span></div>';
|
||||
const left = [
|
||||
c.eid ? esimChipBox('EID', [['', esimGroupEid(c.eid)]]) : '',
|
||||
esimChipBox('EUICCInfo1', esimChipSectionRows(c.info1)),
|
||||
esimChipBox(t('Addresses'), esimChipSectionRows(c.addresses)),
|
||||
esimChipBox(t('Rules authorisation table'), esimChipSectionRows(c.rat)),
|
||||
].filter(Boolean);
|
||||
const right = esimChipBox('EUICCInfo2', esimChipSectionRows(c.info2));
|
||||
if (!left.length && !right) {
|
||||
el.innerHTML = '<span class="text-gray-400">' + esc(t('No data')) + '</span>';
|
||||
return;
|
||||
}
|
||||
let html = '<div class="grid grid-cols-1 lg:grid-cols-3 gap-3 items-start">';
|
||||
html += '<div class="' + (right ? 'lg:col-span-1' : 'lg:col-span-3') + ' space-y-3 min-w-0">' +
|
||||
left.join('') + '</div>';
|
||||
if (right) html += '<div class="lg:col-span-2 min-w-0">' + right + '</div>';
|
||||
html += '</div>';
|
||||
const errs = Object.keys(c.errors || {});
|
||||
if (errs.length) {
|
||||
html += '<div class="text-xs text-yellow-600 dark:text-yellow-500 mt-1">' + esc(t('Not available')) + ': ' + esc(errs.join(', ')) + '</div>';
|
||||
}
|
||||
el.innerHTML = html || '<span class="text-gray-400">' + esc(t('No data')) + '</span>';
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function esimRenderProfiles() {
|
||||
@@ -1724,8 +1740,8 @@ function esimRenderProfiles() {
|
||||
esc(t(enabled ? 'Disable' : 'Enable')) + '</button></div>';
|
||||
let rowsHtml = '';
|
||||
for (const [k, v] of esimProfileRows(p)) {
|
||||
rowsHtml += '<div class="flex gap-2"><span class="w-40 shrink-0 text-right text-gray-500 dark:text-slate-400 break-all">' + esc(k) + '</span>' +
|
||||
'<span class="font-mono break-all">' + esc(v) + '</span></div>';
|
||||
rowsHtml += '<div class="flex gap-2"><span class="w-40 shrink-0 text-right text-gray-500 dark:text-slate-400 break-words">' + esc(k) + '</span>' +
|
||||
'<span class="font-mono break-words min-w-0">' + esc(v) + '</span></div>';
|
||||
}
|
||||
const iconUrl = esimIconDataUrl(p);
|
||||
html += '<div class="flex gap-3 items-start">';
|
||||
|
||||
@@ -612,6 +612,10 @@ video {
|
||||
}
|
||||
}
|
||||
|
||||
.pointer-events-none {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.visible {
|
||||
visibility: visible;
|
||||
}
|
||||
@@ -1036,6 +1040,10 @@ video {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.grid-cols-1 {
|
||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid-cols-2 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
@@ -1159,6 +1167,12 @@ video {
|
||||
margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.space-y-4 > :not([hidden]) ~ :not([hidden]) {
|
||||
--tw-space-y-reverse: 0;
|
||||
margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
|
||||
margin-bottom: calc(1rem * var(--tw-space-y-reverse));
|
||||
}
|
||||
|
||||
.overflow-auto {
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -1185,6 +1199,10 @@ video {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.break-words {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
.break-all {
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -1850,6 +1868,11 @@ video {
|
||||
color: rgb(251 191 36 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:text-amber-500:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(245 158 11 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:text-blue-400:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(96 165 250 / var(--tw-text-opacity, 1));
|
||||
@@ -1905,6 +1928,11 @@ video {
|
||||
color: rgb(100 116 139 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:text-slate-600:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(71 85 105 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
.dark\:text-white:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(255 255 255 / var(--tw-text-opacity, 1));
|
||||
@@ -1947,6 +1975,24 @@ video {
|
||||
.dark\:hover\:text-slate-300:hover:is(.dark *) {
|
||||
--tw-text-opacity: 1;
|
||||
color: rgb(203 213 225 / var(--tw-text-opacity, 1));
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\:col-span-1 {
|
||||
grid-column: span 1 / span 1;
|
||||
}
|
||||
|
||||
.lg\:col-span-2 {
|
||||
grid-column: span 2 / span 2;
|
||||
}
|
||||
|
||||
.lg\:col-span-3 {
|
||||
grid-column: span 3 / span 3;
|
||||
}
|
||||
|
||||
.lg\:grid-cols-3 {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}/* ===== Theme contrast adjustments ===== */
|
||||
|
||||
/* Light theme: darken softer (muted) text one step for higher contrast */
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
const CACHE = 'simple-v231';
|
||||
const CACHE = 'simple-v232';
|
||||
const URLS = [
|
||||
'index.html',
|
||||
'help.html',
|
||||
|
||||
@@ -24,7 +24,7 @@ function extractFunc(src, name) {
|
||||
let code = '';
|
||||
for (const fn of ['esimLabel', 'esimGroupEid', 'esimFieldRows', 'esimResultText',
|
||||
'esimStateLabel', 'esimOperationsText', 'esimProfileRows', 'esimIconDataUrl',
|
||||
'esimSwitchStatus']) {
|
||||
'esimChipSectionRows', 'esimChipBox', 'esimSwitchStatus']) {
|
||||
code += extractFunc(html, fn) + '\n';
|
||||
}
|
||||
for (const c of ['ESIM_CHIP_LABELS', 'ESIM_RESULT_KEYS']) {
|
||||
@@ -32,6 +32,7 @@ for (const c of ['ESIM_CHIP_LABELS', 'ESIM_RESULT_KEYS']) {
|
||||
.replace('const ', 'var ') + '\n';
|
||||
}
|
||||
code += 'function t(s){return s;}\n';
|
||||
code += 'function esc(s){return String(s);}\n';
|
||||
eval(code);
|
||||
|
||||
test('esimGroupEid groups the hex digits in fours', () => {
|
||||
@@ -153,6 +154,39 @@ test('the profile card renders the icon image next to the rows', () => {
|
||||
assert.match(fn, /<img src=/);
|
||||
});
|
||||
|
||||
test('esimChipSectionRows builds rows for objects and RAT rule lists', () => {
|
||||
assert.deepStrictEqual(esimChipSectionRows({ svn: '2.2.2' }), [['SVN', '2.2.2']]);
|
||||
assert.deepStrictEqual(esimChipSectionRows(null), []);
|
||||
assert.deepStrictEqual(esimChipSectionRows([{ ppr_ids: ['ppr1'] }]),
|
||||
[['Rule 1 / PPR IDs', 'ppr1']]);
|
||||
});
|
||||
|
||||
test('esimChipBox renders a titled box and skips empty sections', () => {
|
||||
assert.strictEqual(esimChipBox('EUICCInfo1', []), '');
|
||||
assert.strictEqual(esimChipBox('EUICCInfo1', null), '');
|
||||
const box = esimChipBox('EUICCInfo1', [['SVN', '2.2.2'], ['', 'raw-value']]);
|
||||
assert.match(box, /EUICCInfo1/);
|
||||
assert.match(box, /SVN/);
|
||||
assert.match(box, /2\.2\.2/);
|
||||
assert.match(box, /raw-value/);
|
||||
});
|
||||
|
||||
test('the chip boxes prefer wrapping at spaces over breaking words', () => {
|
||||
const box = esimChipBox('EUICCInfo1', [['SVN', '2.2.2']]);
|
||||
assert.match(box, /break-words/);
|
||||
assert.doesNotMatch(box, /break-all/);
|
||||
const render = extractFunc(html, 'esimRenderProfiles');
|
||||
assert.doesNotMatch(render, /break-all/);
|
||||
});
|
||||
|
||||
test('the chip view groups the sections into a grid', () => {
|
||||
const fn = extractFunc(html, 'esimRenderChip');
|
||||
assert.match(fn, /lg:grid-cols-3/);
|
||||
assert.match(fn, /lg:col-span-2/);
|
||||
assert.match(fn, /esimChipBox\('EUICCInfo2'/);
|
||||
assert.match(fn, /esimChipSectionRows/);
|
||||
});
|
||||
|
||||
test('the eSIM pill is wired into the Phone simulator tab', () => {
|
||||
assert.ok(html.includes('data-phone-sub="esim"'));
|
||||
assert.ok(html.includes('id="phone-sub-esim"'));
|
||||
|
||||
Reference in New Issue
Block a user