diff --git a/frontend/help-ru.html b/frontend/help-ru.html index 35837ed..133eac2 100644 --- a/frontend/help-ru.html +++ b/frontend/help-ru.html @@ -497,7 +497,7 @@

8.8 eSIM / LPA (локальные операции)

Для eUICC (SGP.22/SGP.32) подвкладка «eSIM» читает чип и управляет установленными профилями через локальный интерфейс ES10 (через pySim, без обращения к SM-DP+):

diff --git a/frontend/help.html b/frontend/help.html index 6df5b62..ecb9762 100644 --- a/frontend/help.html +++ b/frontend/help.html @@ -497,7 +497,7 @@

8.8 eSIM / LPA (local operations)

For an eUICC (SGP.22/SGP.32) the eSIM pill reads the chip and manages the installed profiles through the local ES10 interface (via pySim, no SM-DP+ contact):

diff --git a/frontend/index.html b/frontend/index.html index 6d018c6..448ac5f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -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 = '
'; + html += '
' + esc(title) + '
'; + for (const [k, v] of rows) { + html += '
'; + if (k) html += '' + esc(k) + ''; + html += '' + esc(v) + '
'; + } + return html + '
'; +} + 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 += '
' + esc(k.slice(2)) + '
'; - continue; - } - html += '
' + esc(k) + '' + - '' + esc(v) + '
'; + 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 = '' + esc(t('No data')) + ''; + return; } + let html = '
'; + html += '
' + + left.join('') + '
'; + if (right) html += '
' + right + '
'; + html += '
'; const errs = Object.keys(c.errors || {}); if (errs.length) { html += '
' + esc(t('Not available')) + ': ' + esc(errs.join(', ')) + '
'; } - el.innerHTML = html || '' + esc(t('No data')) + ''; + el.innerHTML = html; } function esimRenderProfiles() { @@ -1724,8 +1740,8 @@ function esimRenderProfiles() { esc(t(enabled ? 'Disable' : 'Enable')) + ''; let rowsHtml = ''; for (const [k, v] of esimProfileRows(p)) { - rowsHtml += '
' + esc(k) + '' + - '' + esc(v) + '
'; + rowsHtml += '
' + esc(k) + '' + + '' + esc(v) + '
'; } const iconUrl = esimIconDataUrl(p); html += '
'; diff --git a/frontend/style.css b/frontend/style.css index 37880ca..1d31825 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -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 */ diff --git a/frontend/sw.js b/frontend/sw.js index 0ca8a2b..a434e5a 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v231'; +const CACHE = 'simple-v232'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/esim.test.js b/frontend/tests/esim.test.js index 4ecf8f9..c131064 100644 --- a/frontend/tests/esim.test.js +++ b/frontend/tests/esim.test.js @@ -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, / { + 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"'));