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.
This commit is contained in:
2026-09-28 23:29:55 +03:00
parent 90fe409dec
commit 3b5a1495f5
5 changed files with 72 additions and 30 deletions
+47 -26
View File
@@ -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 '<button onclick="' + action + '" data-needs="card" class="w-full px-2 py-0.5 text-xs text-center whitespace-nowrap rounded '
+ (tones[tone] || tones.ok)
+ ' disabled:opacity-40 disabled:cursor-not-allowed">' + esc(label) + '</button>';
}
// 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 += '<div class="mb-4">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Applications / Applet Instances')) + '</div>';
apps.forEach(o => {
html += '<div class="mb-1 pl-2 border-l-2 border-green-400">';
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?');
if (o.aid) {
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', false)" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete')) + '</button>';
if ((o.lifecycle || '').toUpperCase() === '03') {
html += ' <button onclick="ramMakeSelectableFromExplorer(\'' + o.aid + '\')" data-needs="card" class="ml-1 px-2 py-0.5 text-xs bg-emerald-100 text-emerald-700 rounded hover:bg-emerald-200 dark:bg-emerald-900/30 dark:text-emerald-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Make selectable')) + '</button>';
}
}
html += '</div>';
html += '<div class="mb-1 pl-2 border-l-2 border-green-400 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">';
html += '<div>' + esc(t('Application / Instance AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
if (o.privileges) html += '<div>' + esc(t('Privileges:')) + ' ' + ramFmtPrivileges(o.privileges) + ' (' + o.privileges + ')</div>';
if (o.implicitSel) html += '<div>' + esc(t('Implicit sel:')) + ' ' + o.implicitSel + '</div>';
if (o.elfAid) html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + jcAidHtml(o.elfAid) + '</div>';
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
html += '</div>';
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
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 += '</div></div>';
});
html += '</div>';
}
@@ -8862,29 +8877,35 @@ function ramRenderExploreHtml(mem, isd, apps, elfs, fmt) {
html += '<div class="mb-4">';
html += '<div class="font-semibold text-sm mb-1">' + esc(t('Executable Load Files (ELFs) / Packages')) + '</div>';
elfs.forEach(o => {
html += '<div class="mb-1 pl-2 border-l-2 border-purple-400">';
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?');
if (o.aid) {
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', false)" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete')) + '</button>';
html += ' <button onclick="ramDeleteFromExplorer(\'' + o.aid + '\', true)" data-needs="card" class="ml-1 px-2 py-0.5 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 dark:bg-red-900/30 dark:text-red-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Delete All')) + '</button>';
}
html += '</div>';
html += '<div class="mb-1 pl-2 border-l-2 border-purple-400 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">';
html += '<div>' + esc(t('Load File AID / Package AID:')) + ' ' + (o.aid ? jcAidHtml(o.aid) : '?') + '</div>';
html += '<div>' + esc(t('Lifecycle:')) + ' ' + ramFmtLifecycle(o.lifecycle || '') + '</div>';
if (o.version) html += '<div>' + esc(t('Version:')) + ' ' + o.version + '</div>';
if (o.moduleAids && o.moduleAids.length) {
html += '<div>' + esc(t('Executable Module AIDs / Applet Class AIDs:')) + '</div>';
o.moduleAids.forEach(m => {
html += '<div class="pl-4">- ' + jcAidHtml(m);
// A loaded applet class with no instance yet: one click to
// the single-APDU INSTALL [for install].
if (!ramHasInstance(apps, m)) {
html += ' <button onclick="ramInstallFromExplorer(\'' + o.aid + '\',\'' + m + '\')" data-needs="card" class="ml-2 px-2 py-0.5 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 dark:bg-blue-900/30 dark:text-blue-400 disabled:opacity-40 disabled:cursor-not-allowed">' + esc(t('Install')) + '</button>';
}
html += '</div>';
});
}
if (o.sdAid) html += '<div>' + esc(t('SD AID:')) + ' ' + jcAidHtml(o.sdAid) + '</div>';
html += '</div>';
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
if (o.aid) {
html += ramActionBtn(t('Delete'), "ramDeleteFromExplorer('" + o.aid + "', false)", 'danger');
html += ramActionBtn(t('Delete All'), "ramDeleteFromExplorer('" + o.aid + "', true)", 'danger');
}
html += '</div></div>';
// Module rows: emitted as sibling rows so their Install buttons share
// the same action column as the entry buttons.
(o.moduleAids || []).forEach(m => {
html += '<div class="mb-1 pl-6 flex items-start gap-3">';
html += '<div class="flex-1 min-w-0">- ' + jcAidHtml(m) + '</div>';
html += '<div class="w-28 shrink-0 flex flex-col gap-1">';
// 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 += '</div></div>';
});
});
html += '</div>';
}
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v301';
const CACHE = 'simple-v302';
const URLS = [
'index.html',
'help.html',
+22 -1
View File
@@ -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 = /<button onclick="ramInstallFromExplorer[^>]*class="([^"]*)"/.exec(out);
assert.ok(inst, out);
assert.ok(inst[1].includes('bg-emerald-100'), inst[1]);
assert.ok(!inst[1].includes('bg-blue-'), inst[1]);
// the delete buttons keep the danger tone
const del = /<button onclick="ramDeleteFromExplorer[^>]*class="([^"]*)"/.exec(out);
assert.ok(del && del[1].includes('bg-red-100'), del && del[1]);
});
test('ramRenderExploreHtml localizes every label and button', () => {
const seen = [];
global.t = s => { seen.push(s); return 'XX' + s; };
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
version = "3.6.28"
version = "3.6.29"
description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -31,7 +31,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
VERSION = '3.6.28'
VERSION = '3.6.29'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE