diff --git a/frontend/index.html b/frontend/index.html
index 070cf92..f35d232 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.25.4';
+const SIMPLE_VERSION = '3.25.5';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== view history (the browser Back/Forward buttons) =====
diff --git a/frontend/sw.js b/frontend/sw.js
index 6c68cc2..4918ca2 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simple-v428';
+const CACHE = 'simple-v429';
const URLS = [
'index.html',
'help.html',
diff --git a/pyproject.toml b/pyproject.toml
index e7ff9c8..f6b5d7e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "pysim-simple-server"
-version = "3.25.4"
+version = "3.25.5"
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 2f72860..941857f 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.25.4'
+VERSION = '3.25.5'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE
@@ -2669,7 +2669,13 @@ PLI_DEFAULTS = {
0x0E: '3F02' + '0308',
}
-_PLI_DATA = {q: '' for q in PLI_QUALIFIER_NAMES} # the operator overrides
+# The operator's overrides only, keyed by qualifier code - the built-in
+# PLI_DEFAULTS above are the base layer and stay the fallback; a field cleared
+# through /api/pli-dict removes its entry (v3.25.5). Keeping this dict free
+# of placeholder keys is deliberate: the POST's guard tests the known
+# qualifiers, never "an override already exists" (a full-dict Save posts every
+# field - an empty one must not make later overrides no-ops).
+_PLI_DATA = {}
_BIP = httpota.BipTerminal()
# Active BIP session, shared by the SCP81 listener and the Simulator's
@@ -9102,7 +9108,10 @@ class PysimHandler(BaseHTTPRequestHandler):
if isinstance(v, str):
try:
code = int(k, 16)
- if code in _PLI_DATA:
+ # a known qualifier - never "an override already
+ # exists": a new override must stick, and a clear
+ # must not poison a later set (v3.25.5, live report)
+ if code in PLI_QUALIFIER_NAMES:
if v:
bytes.fromhex(v) # valid hex only
_PLI_DATA[code] = v
diff --git a/tests/test_fetch_routes.py b/tests/test_fetch_routes.py
index 3625877..ec587ce 100644
--- a/tests/test_fetch_routes.py
+++ b/tests/test_fetch_routes.py
@@ -142,6 +142,7 @@ class PliDictHttpTests(unittest.TestCase):
def test_defaults_overrides_and_clearing(self):
import pysim_simple_server.server as srv
+ saved = dict(srv._PLI_DATA)
try:
status, resp = self._req('GET', '/api/pli-dict')
self.assertEqual(status, 200, resp)
@@ -162,8 +163,46 @@ class PliDictHttpTests(unittest.TestCase):
# a bad value is ignored
status, resp = self._req('POST', '/api/pli-dict', {'01': 'ZZ'})
self.assertEqual(resp['01'], srv.PLI_DEFAULTS[0x01])
+ self.assertNotIn(0x01, srv._PLI_DATA)
finally:
- srv._PLI_DATA.pop(0x01, None)
+ srv._PLI_DATA.clear()
+ srv._PLI_DATA.update(saved)
+
+ def test_a_new_override_after_a_full_save_sticks(self):
+ # The PWA's Save posts every field; the empty ones used to pop the
+ # dictionary's placeholder keys, after which the old `code in
+ # _PLI_DATA` guard silently skipped every later override for those
+ # qualifiers - POST answered 200 with the unchanged/default dict
+ # (live report 2026-10-11). The guard now tests the known qualifiers.
+ import pysim_simple_server.server as srv
+ saved = dict(srv._PLI_DATA)
+ try:
+ full = {('%02X' % q): '' for q in srv.PLI_QUALIFIER_NAMES}
+ status, resp = self._req('POST', '/api/pli-dict', full)
+ self.assertEqual(status, 200, resp)
+ self.assertEqual(srv._PLI_DATA, {})
+ # a new override for a qualifier with a default sticks
+ status, resp = self._req('POST', '/api/pli-dict', {'0A': '630102'})
+ self.assertEqual(resp['0A'], '630102')
+ # and one for a qualifier without a default
+ status, resp = self._req('POST', '/api/pli-dict', {'02': 'AABBCC'})
+ self.assertEqual(resp['02'], 'AABBCC')
+ status, resp = self._req('GET', '/api/pli-dict')
+ self.assertEqual(resp['0A'], '630102')
+ self.assertEqual(resp['02'], 'AABBCC')
+ # clearing restores the default and a later set still works
+ status, resp = self._req('POST', '/api/pli-dict', {'0A': ''})
+ self.assertEqual(resp['0A'], srv.PLI_DEFAULTS[0x0A])
+ status, resp = self._req('POST', '/api/pli-dict', {'0A': '630103'})
+ self.assertEqual(resp['0A'], '630103')
+ # an unknown qualifier is ignored
+ status, resp = self._req('POST', '/api/pli-dict', {'18': 'AABB'})
+ self.assertEqual(status, 200, resp)
+ self.assertNotIn(0x18, srv._PLI_DATA)
+ self.assertEqual(resp['0A'], '630103')
+ finally:
+ srv._PLI_DATA.clear()
+ srv._PLI_DATA.update(saved)
class CardFreeGetTests(unittest.TestCase):