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 += '
';
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.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 = /