ui: compact FCI rows in the file manager (v2.7.11)

- The short file identifier is merged into the File identifier row
  (same row, ' · ' separator) in profilerFciPreviewItems, everywhere the
  decoded FCI is rendered.
- New hideFileSize option: pysimFsInfoHtml drops the FCI File size row
  when its own header already shows Size (kept otherwise, e.g. DFs).
- tests: merged SFI row, hideFileSize, header-size precedence.
This commit is contained in:
2026-09-20 23:29:51 +03:00
parent e9f0a50ea5
commit 3cab9867be
5 changed files with 45 additions and 9 deletions
+20 -5
View File
@@ -1413,7 +1413,7 @@
// ===== Version ===== // ===== Version =====
// Single source of truth for the PWA version: shown in the header and used // Single source of truth for the PWA version: shown in the header and used
// by the server version check in pysimConnect(). // by the server version check in pysimConnect().
const SIMPLE_VERSION = '2.7.10'; const SIMPLE_VERSION = '2.7.11';
document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION; document.getElementById('app-version').textContent = 'v' + SIMPLE_VERSION;
// ===== Tab switching ===== // ===== Tab switching =====
@@ -7164,7 +7164,7 @@ function pysimFsInfoHtml(sel) {
let html = ''; let html = '';
if (attrs.length) html += '<div class="text-xs font-mono text-gray-500 dark:text-slate-400">' + attrs.join(' · ') + '</div>'; if (attrs.length) html += '<div class="text-xs font-mono text-gray-500 dark:text-slate-400">' + attrs.join(' · ') + '</div>';
if (sel.fci_hex) { if (sel.fci_hex) {
html += '<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all mt-0.5">' + profilerFciPreviewItems(sel.fci_hex) + '</div>'; html += '<div class="text-xs font-mono leading-relaxed text-gray-600 dark:text-slate-400 break-all mt-0.5">' + profilerFciPreviewItems(sel.fci_hex, { hideFileSize: num(sel.file_size) !== null }) + '</div>';
} }
return html; return html;
} }
@@ -11696,13 +11696,28 @@ function fcpDiffHtml(expectedHex, actualHex, labels) {
return html; return html;
} }
function profilerFciPreviewItems(hex) { // Decoded FCI rows. The short file identifier belongs to the file
// identifier, so it is merged into that row. `opts.hideFileSize` drops the
// File size DO where the caller already shows the size (file manager header).
function profilerFciPreviewItems(hex, opts) {
const d = fcpDecode(hex); const d = fcpDecode(hex);
if (!d.items.length && !d.error) return ''; if (!d.items.length && !d.error) return '';
let html = ''; opts = opts || {};
const rows = [];
for (const it of d.items) { for (const it of d.items) {
if (it.decoded === null) continue; if (it.decoded === null) continue;
html += '<div>' + esc(it.name) + ': <span class="text-gray-800 dark:text-slate-200">' + esc(it.decoded || it.value) + '</span></div>'; if (opts.hideFileSize && it.key === '80') continue;
rows.push({ key: it.key, name: it.name, text: it.decoded || it.value });
}
const fi = rows.find(r => r.key === '83');
const sfi = rows.find(r => r.key === '88');
if (fi && sfi) {
fi.text += ' · ' + sfi.name + ': ' + sfi.text;
rows.splice(rows.indexOf(sfi), 1);
}
let html = '';
for (const r of rows) {
html += '<div>' + esc(r.name) + ': <span class="text-gray-800 dark:text-slate-200">' + esc(r.text) + '</span></div>';
} }
if (d.error) { if (d.error) {
html += '<div class="text-red-600">' + esc(t('Decode failed')) + ': ' + esc(d.error) + '</div>'; html += '<div class="text-red-600">' + esc(t('Decode failed')) + ': ' + esc(d.error) + '</div>';
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simple-v213'; const CACHE = 'simple-v214';
const URLS = [ const URLS = [
'index.html', 'index.html',
'help.html', 'help.html',
+22 -1
View File
@@ -770,6 +770,21 @@ test('pysimFsInfoHtml shows FID, type, size and the decoded FCI', () => {
assert.ok(out.includes('Record count: 5'), out); assert.ok(out.includes('Record count: 5'), out);
assert.ok(!out.includes('Decoded FCI'), out); assert.ok(!out.includes('Decoded FCI'), out);
assert.ok(out.includes('File descriptor'), out); assert.ok(out.includes('File descriptor'), out);
// the header already shows the size -> the FCI File size row is dropped
assert.ok(!out.includes('File size:'), out);
assert.ok(!out.includes('75 bytes'), out);
// the short file identifier is merged into the file identifier row
assert.ok(out.includes('6F4F · Short file identifier: 22'), out);
assert.ok(!out.includes('<div>Short file identifier:'), out);
delete global.t;
});
test('pysimFsInfoHtml keeps the FCI File size when the header has none', () => {
global.t = s => s;
const hex = '621A82054221000F0583026F4F8A01058B036F06098002004B8801B0';
const out = pysimFsInfoHtml({ fid: '6f4f', file_type: 'linear_fixed', file_size: null, record_len: 15, num_of_rec: 5, fci_hex: hex });
assert.ok(!out.includes('Size:'), out);
assert.ok(out.includes('File size:'), out);
assert.ok(out.includes('75 bytes'), out); assert.ok(out.includes('75 bytes'), out);
delete global.t; delete global.t;
}); });
@@ -788,7 +803,13 @@ test('pysimFsInfoHtml omits the FCI block without fci_hex and skips null fields'
test('profilerFciPreviewItems renders decoded items and degrades gracefully', () => { test('profilerFciPreviewItems renders decoded items and degrades gracefully', () => {
global.t = s => s; global.t = s => s;
assert.ok(profilerFciPreviewItems(FCP_TRANSPARENT).includes('File size: ')); const full = profilerFciPreviewItems(FCP_TRANSPARENT);
assert.ok(full.includes('File size: '));
assert.ok(full.includes('6F07 · Short file identifier: 2'));
assert.ok(!full.includes('<div>Short file identifier:'), 'SFI is merged, not a separate row');
const noSize = profilerFciPreviewItems(FCP_TRANSPARENT, { hideFileSize: true });
assert.ok(!noSize.includes('File size: '));
assert.ok(noSize.includes('6F07 · Short file identifier: 2'));
assert.ok(profilerFciPreviewItems('not hex').includes('Decode failed')); assert.ok(profilerFciPreviewItems('not hex').includes('Decode failed'));
assert.strictEqual(profilerFciPreviewItems(''), ''); assert.strictEqual(profilerFciPreviewItems(''), '');
delete global.t; delete global.t;
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "pysim-simple-server" name = "pysim-simple-server"
version = "2.7.10" version = "2.7.11"
description = "HTTP REST server wrapping pysim for the SIMple PWA" description = "HTTP REST server wrapping pysim for the SIMple PWA"
requires-python = ">=3.8" requires-python = ">=3.8"
# pysim is a git-only dependency installed explicitly by setup.bat/setup.sh. # pysim is a git-only dependency installed explicitly by setup.bat/setup.sh.
+1 -1
View File
@@ -26,7 +26,7 @@ from osmocom.tlv import BER_TLV_IE
from osmocom.utils import rpad from osmocom.utils import rpad
VERSION = '2.7.10' VERSION = '2.7.11'
MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE MAX_ENVELOPE_SEGMENTS = 5 # max SMS segments for outgoing C-APDU in ENVELOPE