From 3b5a1495f569d485fc4a0cf8ba823a8f2a7685b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Mon, 28 Sep 2026 23:29:55 +0300 Subject: [PATCH] feat: table-align the explorer action buttons, Install green (v3.6.29) The explorer buttons sat inline after each AID/detail line, so their columns moved with the text length. Now every entry (app / ELF) and every module line is a row with a fixed-width action column (w-28): - left cell: the text (module rows are siblings of their ELF entry, indented pl-6, so their buttons share the same column as the entry buttons); - right cell: the buttons, full width of the column, so Delete / Delete All / Make selectable / Install line up vertically across rows. All buttons go through ramActionBtn(label, action, tone): Danger (red) for Delete / Delete All, Ok (emerald) for Make selectable and the **Install** buttons, which were blue. Test pins the column width and the Install tone. 660 frontend / 504 Python green; version 3.6.29; sw simple-v302. --- frontend/index.html | 73 ++++++++++++++++++++++------------- frontend/sw.js | 2 +- frontend/tests/ram.test.js | 23 ++++++++++- pyproject.toml | 2 +- pysim_simple_server/server.py | 2 +- 5 files changed, 72 insertions(+), 30 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index c58db3a..09835b3 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1713,7 +1713,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.6.28'; +const SIMPLE_VERSION = '3.6.29'; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; // ===== Tab switching ===== @@ -8811,6 +8811,19 @@ function ramFmtPrivileges(hex) { return decoded === 'None' ? t('(none)') : decoded; } +// One explorer action button. All buttons live in a fixed-width action +// column so Delete / Delete All / Make selectable / Install line up +// table-like across the entry and module rows (v3.6.29). +function ramActionBtn(label, action, tone) { + const tones = { + danger: 'bg-red-100 text-red-700 hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400', + ok: 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400', + }; + return ''; +} + // Render the full explorer result as structured HTML function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) { let html = ''; @@ -8840,21 +8853,23 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) { html += '
'; html += '
' + esc(t('Applications / Applet Instances')) + '
'; apps.forEach(o => { - html += '
'; - html += '
' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?'); - if (o.aid) { - html += ' '; - if ((o.lifecycle || '').toUpperCase() === '03') { - html += ' '; - } - } - html += '
'; + html += '
'; + html += '
'; + html += '
' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '
'; html += '
' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '
'; if (o.privileges) html += '
' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')
'; if (o.implicitSel) html += '
' + esc(t('Implicit sel:')) + ' ' + o.implicitSel + '
'; if (o.elfAid) html += '
' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '
'; if (o.sdAid) html += '
' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '
'; html += '
'; + html += '
'; + if (o.aid) { + html += ramActionBtn(t('Delete'), "ramDeleteFromExplorer('" + o.aid + "', false)", 'danger'); + if ((o.lifecycle || '').toUpperCase() === '03') { + html += ramActionBtn(t('Make selectable'), "ramMakeSelectableFromExplorer('" + o.aid + "')", 'ok'); + } + } + html += '
'; }); html += '
'; } @@ -8862,29 +8877,35 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) { html += '
'; html += '
' + esc(t('Executable Load Files (ELFs) / Packages')) + '
'; elfs.forEach(o => { - html += '
'; - html += '
' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?'); - if (o.aid) { - html += ' '; - html += ' '; - } - html += '
'; + html += '
'; + html += '
'; + html += '
' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '
'; html += '
' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '
'; if (o.version) html += '
' + esc(t('Version:')) + ' ' + o.version + '
'; if (o.moduleAids && o.moduleAids.length) { html += '
' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '
'; - o.moduleAids.forEach(m => { - html += '
- ' + jcAidHtml(m); - // A loaded applet class with no instance yet: one click to - // the single-APDU INSTALL [for install]. - if (!ramHasInstance(apps, m)) { - html += ' '; - } - html += '
'; - }); } if (o.sdAid) html += '
' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '
'; html += '
'; + html += '
'; + if (o.aid) { + html += ramActionBtn(t('Delete'), "ramDeleteFromExplorer('" + o.aid + "', false)", 'danger'); + html += ramActionBtn(t('Delete All'), "ramDeleteFromExplorer('" + o.aid + "', true)", 'danger'); + } + html += '
'; + // Module rows: emitted as sibling rows so their Install buttons share + // the same action column as the entry buttons. + (o.moduleAids || []).forEach(m => { + html += '
'; + html += '
- ' + jcAidHtml(m) + '
'; + html += '
'; + // A loaded applet class with no instance yet: one click to the + // single-APDU INSTALL [for install]. + if (!ramHasInstance(apps, m)) { + html += ramActionBtn(t('Install'), "ramInstallFromExplorer('" + o.aid + "','" + m + "')", 'ok'); + } + html += '
'; + }); }); html += '
'; } diff --git a/frontend/sw.js b/frontend/sw.js index 7bf971f..2af4142 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simple-v301'; +const CACHE = 'simple-v302'; const URLS = [ 'index.html', 'help.html', diff --git a/frontend/tests/ram.test.js b/frontend/tests/ram.test.js index 94e48ef..be4eb92 100644 --- a/frontend/tests/ram.test.js +++ b/frontend/tests/ram.test.js @@ -25,7 +25,7 @@ function extractFunc(src, name) { const FNS = ['berLenStr', 'buildApdu', 'escHtml', 'esc', 'chainInit', 'chainRamBuildRowHex', 'ramFmtLifecycle', 'ramFmtPrivileges', 'ramRenderExploreHtml', 'ramStepLine', 'ramGetStatusApdu', 'ramDeleteApdu', 'stkParamsBuild', 'ramRemoteSwOk', 'spPorAccepted', 'ramIncrementCntr', 'ramDeleteFromExplorer', 'ramListingSpi2', 'ramRemoveFromExplorer', 'ramHasInstance', 'ramExpandedQueryApdu', '_parseRawElfEntry', '_parseRawAppEntry', 'ramParseElfStatus', 'ramParseAppStatus', 'parseTLV', '_parseE3Entry', - 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', + 'ramCardIdxAfterRemove', 'ramClearResults', 'ramHideProgress', 'ramOpChanged', 'ramRender', 'ramApplyCard', 'ramExecute', 'decodePrivileges', 'ramActionBtn', 'jcAidNorm', 'jcAidName', 'jcAidSuffix', 'jcAidHtml']; let code = ''; for (const f of FNS) { @@ -192,6 +192,27 @@ test('ramRenderExploreHtml puts the command format after the resource lines', () assert.ok(iNv >= 0 && iV >= 0 && iFmt > iV, out); }); +test('explorer action buttons share a fixed-width column, Install is green', () => { + global.t = s => s; + const out = ramRenderExploreHtml( + { appCount: 1, freeNV: 10, freeV: 5 }, + [], + [{ aid: 'A000000151000000', lifecycle: '03', privileges: '' }], + [{ aid: 'ELF1', lifecycle: '01', version: '1.0', moduleAids: ['M1'] }] + ); + delete global.t; + // every action cell is the same fixed-width column + assert.ok(out.includes('w-28 shrink-0'), out); + // the Install button (module without an instance) is green, not blue + const inst = /