diff --git a/frontend/index.html b/frontend/index.html
index b5c2c1f..60a82bc 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1559,7 +1559,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.5.5';
+const SIMPLE_VERSION = '3.5.6';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching =====
@@ -7710,19 +7710,26 @@ function stkMenuNaiSuffix(item) {
return (item && item.nai_name) ? '\u25b8 ' + item.nai_name : '';
}
+// The overlay's item list, shared by the cached top menu and the pending
+// SELECT ITEM items the card returns mid-dialogue: one row per item, each
+// with the card-provided next action suffix. `handler` is the click callback
+// (stkMenuItemClick / stkSubItemClick).
+function stkMenuItemsHtml(items, handler) {
+ let html = '
';
+ (items || []).forEach(item => {
+ const nai = stkMenuNaiSuffix(item);
+ html += '
' + item.id + '' + esc(item.text) + '' +
+ (nai ? '' + esc(nai) + '' : '') + '
';
+ });
+ return html + '
';
+}
+
function stkMenuRenderItems() {
const content = document.getElementById('stk-menu-panel-content');
content.innerHTML = 'Loading...';
pysimFetch('/api/menu').then(data => {
if (!data || !data.items) { content.innerHTML = 'No menu data'; return; }
- let html = '';
- data.items.forEach(item => {
- const nai = stkMenuNaiSuffix(item);
- html += '
' + item.id + '' + esc(item.text) + '' +
- (nai ? '' + esc(nai) + '' : '') + '
';
- });
- html += '
';
- content.innerHTML = html;
+ content.innerHTML = stkMenuItemsHtml(data.items, 'stkMenuItemClick');
}).catch(() => { content.innerHTML = 'Error loading menu'; });
}
@@ -7751,14 +7758,7 @@ function stkMenuHandleResponse(data) {
btns.classList.remove('hidden');
} else if (data.type === 'select_item') {
stkMenuStack.push(data.items);
- content.innerHTML = '';
- const items = data.items;
- let html = '';
- items.forEach(item => {
- html += '
' + item.id + '' + esc(item.text) + '
';
- });
- html += '
';
- content.innerHTML = html;
+ content.innerHTML = stkMenuItemsHtml(data.items, 'stkSubItemClick');
backBtn.style.display = '';
document.getElementById('stk-ok-btn').style.display = 'none';
btns.classList.remove('hidden');
diff --git a/frontend/sw.js b/frontend/sw.js
index 21b8e92..65aa7fb 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v258';
+const CACHE = 'simple-v259';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/stk_menu.test.js b/frontend/tests/stk_menu.test.js
index 709db1a..dcb7494 100644
--- a/frontend/tests/stk_menu.test.js
+++ b/frontend/tests/stk_menu.test.js
@@ -23,6 +23,7 @@ function extractFunc(src, name, asyncFn) {
let code = extractFunc(html, 'stkMenuRespond', true) + '\n';
code += extractFunc(html, 'stkMenuNaiSuffix') + '\n';
+code += extractFunc(html, 'stkMenuItemsHtml') + '\n';
code += 'globalThis.esc = s => s;\n';
eval(code);
@@ -47,6 +48,27 @@ test('the STK menu item suffix shows the item next action (8.24)', () => {
assert.strictEqual(stkMenuNaiSuffix(null), '');
});
+test('the overlay item list shows the next action and the row handler', () => {
+ const list = stkMenuItemsHtml([
+ { id: 1, text: 'Menu', nai: 0x25, nai_name: 'SET UP MENU' },
+ { id: 2, text: 'Info' },
+ ], 'stkSubItemClick');
+ assert.match(list, /onclick="stkSubItemClick\(1\)"/);
+ assert.match(list, /onclick="stkSubItemClick\(2\)"/);
+ assert.ok(list.includes('Menu'));
+ assert.ok(list.includes('\u25b8 SET UP MENU'));
+ // the second item has no indicator -> exactly one suffix in the list
+ assert.strictEqual((list.match(/\u25b8/g) || []).length, 1);
+ assert.strictEqual(stkMenuItemsHtml(null, 'stkMenuItemClick'), '');
+});
+
+test('both overlay lists render through the shared row helper', () => {
+ // the cached top menu and the pending SELECT ITEM items must use the same
+ // renderer, otherwise the latter silently loses the NAI suffix
+ assert.match(html, /stkMenuItemsHtml\(data\.items, 'stkMenuItemClick'\)/);
+ assert.match(html, /stkMenuItemsHtml\(data\.items, 'stkSubItemClick'\)/);
+});
+
test('back with a fetched SELECT ITEM continues the card dialogue', async () => {
const data = { type: 'select_item', items: [{ id: 1, text: 'Info' }] };
const calls = setup(data);
diff --git a/pyproject.toml b/pyproject.toml
index 231e854..1314574 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.5.5"
+version = "3.5.6"
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.
diff --git a/pysim_simple_server/server.py b/pysim_simple_server/server.py
index a2eb4d3..8266d39 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -29,7 +29,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.5.5'
+VERSION = '3.5.6'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE