diff --git a/frontend/index.html b/frontend/index.html
index 677b96c..17b7f78 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -111,6 +111,10 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-
.apdu-type.pps { color: var(--yellow); }
.apdu-data { font-family: monospace; color: var(--text-dim); word-break: break-all; flex: 1; }
.apdu-sw { font-family: monospace; color: var(--text-muted); white-space: nowrap; min-width: 52px; }
+.apdu-sw.sw-ok { color: var(--green); }
+.apdu-sw.sw-pending { color: var(--accent); }
+.apdu-sw.sw-warn { color: var(--warn); }
+.apdu-sw.sw-error { color: var(--error); }
/* APDU detail panel */
.apdu-detail { display: flex; flex-direction: column; max-height: 40%; overflow-y: auto; border-top: 2px solid var(--accent); background: var(--bg-panel); flex-shrink: 0; }
@@ -453,6 +457,17 @@ function sw_text(m) {
return d.sw.sw1 + ' ' + d.sw.sw2;
}
+function sw_class(m) {
+ const d = m.decoded;
+ if (!d || !d.sw) return '';
+ const s = d.sw.sw1;
+ if (s === '90' || (s >= '92' && s <= '9f')) return 'sw-ok';
+ if (s === '61' || s === '91' || s === '6c') return 'sw-pending';
+ if (s === '62' || s === '63') return 'sw-warn';
+ if (s >= '64' && s <= '6f') return 'sw-error';
+ return '';
+}
+
// ──────────────────── API ────────────────────
const API = {
async _fetch(method, path, body) {
@@ -974,7 +989,7 @@ function renderMessages() {
return `
${fmt_elapsed(m.elapsed)}
${label}
- ${sw_text(m)}
+ ${sw_text(m)}
${cmd_description(m)}
`;
}).join('');
diff --git a/simtrace2_pysniff/server/decode.py b/simtrace2_pysniff/server/decode.py
index f22f10b..deed7c9 100644
--- a/simtrace2_pysniff/server/decode.py
+++ b/simtrace2_pysniff/server/decode.py
@@ -2213,6 +2213,8 @@ def _decode_phase(raw, p1=None):
def _decode_nai(raw, p1=None):
"""ISIM identity files (EF_IMPI/DOMAIN/IMPU): BER-TLV tag 0x80 + ASCII value."""
+ if not raw or all(b == 0xff for b in raw):
+ return {'empty': True}
texts = []
for tag, _length, value in parse_tlv(raw):
if tag == 0x80:
@@ -2221,7 +2223,7 @@ def _decode_nai(raw, p1=None):
texts.append(txt)
if texts:
return {'text': texts[0] if len(texts) == 1 else ', '.join(texts)}
- txt = raw.decode('ascii', 'replace').replace('\xff', '').strip()
+ txt = raw.rstrip(b'\xff').decode('ascii', 'replace').strip()
return {'text': txt} if txt else {'raw': raw.hex().upper()}
diff --git a/tests/test_decode.py b/tests/test_decode.py
index 8c2e5ed..569deb0 100644
--- a/tests/test_decode.py
+++ b/tests/test_decode.py
@@ -901,6 +901,20 @@ class TestFileDecoders(unittest.TestCase):
self.assertEqual(f['applications'][0]['label'], 'MegaFon')
self.assertEqual(f['applications'][0]['aid'], 'A0000000871002FFFFF00189000001FF')
+ def test_nai_empty_record(self):
+ from simtrace2_pysniff.server.decode import _decode_file_data
+ # Unused ISIM record (all FF) must be 'empty', not garbage text.
+ f = _decode_file_data('6f04', b'\xff' * 75)
+ self.assertTrue(f['empty'])
+ self.assertNotIn('text', f)
+
+ def test_nai(self):
+ from simtrace2_pysniff.server.decode import _decode_file_data
+ f = _decode_file_data('6f04', bytes.fromhex(
+ '80357369703a32353030323639333537373333363840696d732e6d6e63'
+ '3030322e6d63633235302e336770706e6574776f726b2e6f7267'))
+ self.assertEqual(f['text'], 'sip:250026935773368@ims.mnc002.mcc250.3gppnetwork.org')
+
def test_plmn_wact(self):
from simtrace2_pysniff.server.decode import _decode_file_data
# TS 51.011 §10.3.35: 5-byte entries (3 PLMN + 2 access tech).