diff --git a/frontend/index.html b/frontend/index.html
index 661bab9..12224c1 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -1905,7 +1905,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.23.3';
+const SIMPLE_VERSION = '3.23.4';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
@@ -16157,8 +16157,8 @@ function eventFormFieldsHtml(cfg, fields, values, prefix, withOnchange) {
html += '';
} else {
diff --git a/frontend/sw.js b/frontend/sw.js
index 5f41cd3..d7e7bbc 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v416';
+const CACHE = 'simple-v417';
const URLS = [
'index.html',
'help.html',
diff --git a/frontend/tests/event_forms.test.js b/frontend/tests/event_forms.test.js
index dc40a91..2b56e4a 100644
--- a/frontend/tests/event_forms.test.js
+++ b/frontend/tests/event_forms.test.js
@@ -38,6 +38,8 @@ eval(extractBlock('const EVENT_INNER_MAX = ', 'function pysimEventSendForm').rep
eval(extractFunc(html, 'eventFieldsForRelease'));
eval(extractFunc(html, 'encPlmn'));
eval(extractFunc(html, 'bytesToHex'));
+eval(extractFunc(html, 'esc'));
+eval(extractFunc(html, 'eventFormFieldsHtml'));
test('CMD_NAMES decodes timer management and the BIP commands', () => {
assert.strictEqual(CMD_NAMES['01'], 'REFRESH');
@@ -344,3 +346,32 @@ test('the shared event vectors match the PWA builders', () => {
'scts ' + JSON.stringify(v));
}
});
+
+test('every event form renders - numeric option values are stringified (v3.23.4)', () => {
+ // esc() is string-only and most EVENT_FORMS option values are numbers
+ // (e.g. {v:0,l:'Normal service'} and the ACCESSTECH list): rendering the
+ // form with esc(o.v) crashed with 'str.replace is not a function' for the
+ // Phone tab and the test-script step editor alike (v3.22.0 regression).
+ globalThis.t = s => s;
+ const bad = [];
+ for (const [ev, cfg] of Object.entries(EVENT_FORMS)) {
+ if (!cfg) continue; // the unimplemented events carry a null form
+ for (const withOnchange of [true, false]) {
+ try {
+ eventFormFieldsHtml(cfg, cfg.fields, {}, 'x-', withOnchange);
+ } catch (e) {
+ bad.push(ev + (withOnchange ? '/phone' : '/step') + ': ' + e.message);
+ }
+ }
+ }
+ assert.deepStrictEqual(bad, [], 'every event form must render');
+ // a numeric option value lands as its string form and prefills selected
+ const cfg = EVENT_FORMS[0x00];
+ const sel = cfg.fields.find(f => f.type === 'select' && f.opts.some(o => typeof o.v === 'number'));
+ assert.ok(sel, 'a numeric-option select must exist');
+ const num = sel.opts.find(o => typeof o.v === 'number');
+ const out = eventFormFieldsHtml(cfg, cfg.fields, {[sel.id]: num.v}, 'x-', true);
+ assert.ok(out.includes('value="' + String(num.v) + '"'), out);
+ assert.ok(out.includes('value="' + String(num.v) + '" selected'), out);
+ delete globalThis.t;
+});
diff --git a/pyproject.toml b/pyproject.toml
index c5644df..cba09e4 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.23.3"
+version = "3.23.4"
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 e2db4fe..b587fd6 100644
--- a/pysim_simple_server/server.py
+++ b/pysim_simple_server/server.py
@@ -36,7 +36,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad
-VERSION = '3.23.3'
+VERSION = '3.23.4'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE