Files view: a card-side change reads as its read, with a "Changed on card!" link (v2.6.4)

- a card-side change is discovered by a read operation, so the timeline row
  now reads "Read EF_IMSI" — the row keeps its warn colour and layout — with
  "Changed on card!" as the trailing underlined link that opens the change
  modal; an update row keeps the generic "Details" link
- the 'Changed on card' verb string is gone; the new 'Changed on card!' key
  carries the Russian «Изменено на карте!»
- tests: the verb/label cases updated (card_change -> Read, "Read EF_LOCI")
  and the Files-view wiring test pins the kind-dependent link label;
  527 Python + 61 Node tests
- README/README_RUS describe the new row wording; PWA_VERSION/pyproject/
  version.py 2.6.4, SW cache simtrace-analyser-v78
This commit is contained in:
catarrh committed 2026-10-17 23:37:30 +03:00
1 parent 475c136749
commit be75be9ed2
8 files changed
+25 -18

No files matched your search

+3 -2
View File
@@ -203,8 +203,9 @@ The Trace tab has a view selector (*Map* / *Stats* / **Files**) in its topbar,
right-aligned over the side panel with the panel's width, so it stays visible right-aligned over the side panel with the panel's width, so it stays visible
while the panel scrolls (a drop-down, since three tab captions no longer while the panel scrolls (a drop-down, since three tab captions no longer
fit). The Files view is a fit). The Files view is a
trace-wide timeline of file operations — every **Read**, **Update** and trace-wide timeline of file operations — every **Read** and **Update** row
**Changed on card** row shows the same file name as the Map view, and clicking shows the same file name as the Map view (a card-side change appears as a
read with a **Changed on card!** link that opens its details), and clicking
a row jumps straight to the APDU in the trace and leaves it highlighted a row jumps straight to the APDU in the trace and leaves it highlighted
until another one is focused (the APDU list keeps the reader's position: until another one is focused (the APDU list keeps the reader's position:
new messages pin the view to the bottom only while the reader is already new messages pin the view to the bottom only while the reader is already
+5 -4
View File
@@ -207,10 +207,11 @@ Application Layer* — два отдельных поля выбора, имен
**Files**) — он выровнен по правому краю над боковой панелью и имеет её **Files**) — он выровнен по правому краю над боковой панелью и имеет её
ширину, поэтому виден всё время, пока панель прокручивается (выпадающий ширину, поэтому виден всё время, пока панель прокручивается (выпадающий
список, три подписи-вкладки уже не помещались). Вид список, три подписи-вкладки уже не помещались). Вид
Files — это таймлайн файловых операций по всей трассе: строки **Read**, Files — это таймлайн файловых операций по всей трассе: строки **Read** и
**Update** и **Changed on card** показывают те же имена файлов, что и вид Map, **Update** показывают те же имена файлов, что и вид Map (изменение со стороны
а щелчок по строке переходит прямо к APDU в трассе и оставляет его карты выглядит как чтение со ссылкой **Изменено на карте!**, открывающей
выделенным, пока не выбран другой (список APDU сохраняет позицию читателя: подробности), а щелчок по строке переходит прямо к APDU в трассе и оставляет
его выделенным, пока не выбран другой (список APDU сохраняет позицию читателя:
новые сообщения прижимают вид к низу, только пока читатель уже внизу, новые сообщения прижимают вид к низу, только пока читатель уже внизу,
поэтому переход не срывается живым опросом и не снимает выделение). У строк с записанным изменением поэтому переход не срывается живым опросом и не снимает выделение). У строк с записанным изменением
(изменение со стороны карты либо запись, изменившая или впервые показавшая (изменение со стороны карты либо запись, изменившая или впервые показавшая
+5 -4
View File
@@ -2136,7 +2136,8 @@ function fileOpClass(kind) {
function fileOpVerb(kind) { function fileOpVerb(kind) {
if (kind === 'update') return t('Update'); if (kind === 'update') return t('Update');
if (kind === 'card_change') return t('Changed on card'); // A card-side change is discovered by a read operation, so the row reads
// as one ("Read EF_IMSI") with the change offered as a trailing link.
return t('Read'); return t('Read');
} }
@@ -2233,7 +2234,7 @@ async function renderFileView() {
} }
view.innerHTML = timeline.map(op => { view.innerHTML = timeline.map(op => {
const diff = op.change_ref const diff = op.change_ref
? `<button class="file-op-diff" data-diff="${escapeHtml(op.change_ref.file)}|${op.change_ref.idx}" title="${t('Show the change')}">${t('Details')}</button>` ? `<button class="file-op-diff" data-diff="${escapeHtml(op.change_ref.file)}|${op.change_ref.idx}" title="${t('Show the change')}">${op.kind === 'card_change' ? t('Changed on card!') : t('Details')}</button>`
: ''; : '';
return `<div class="file-op ${fileOpClass(op.kind)}" data-idx="${op.index}"> return `<div class="file-op ${fileOpClass(op.kind)}" data-idx="${op.index}">
<span>${escapeHtml(fileOpLabel(op))}</span>${diff} <span>${escapeHtml(fileOpLabel(op))}</span>${diff}
@@ -3225,7 +3226,7 @@ function onFilterChange() {
} }
// ──────────────────── Versioning ──────────────────── // ──────────────────── Versioning ────────────────────
const PWA_VERSION = '2.6.3'; const PWA_VERSION = '2.6.4';
const MIN_SERVER_VERSION = '2.3.0'; const MIN_SERVER_VERSION = '2.3.0';
function parseVersion(v) { function parseVersion(v) {
@@ -3271,7 +3272,7 @@ const LANG_RU = {
'Files': 'Файлы', 'Files': 'Файлы',
'Read': 'Чтение', 'Read': 'Чтение',
'Update': 'Запись', 'Update': 'Запись',
'Changed on card': 'Изменено картой', 'Changed on card!': 'Изменено на карте!',
'Change details': 'Подробности изменения', 'Change details': 'Подробности изменения',
'Details': 'Подробнее', 'Details': 'Подробнее',
'changed': 'изменено', 'changed': 'изменено',
+1 -1
View File
@@ -1,4 +1,4 @@
const CACHE = 'simtrace-analyser-v77'; const CACHE = 'simtrace-analyser-v78';
const PRECACHE = [ const PRECACHE = [
'index.html', 'index.html',
+8 -4
View File
@@ -292,10 +292,12 @@ test('Files view: operation kinds map to classes and verbs', () => {
assert.equal(fileOpClass('card_change'), 'file-op-card'); assert.equal(fileOpClass('card_change'), 'file-op-card');
assert.equal(fileOpVerb('read'), 'Read'); assert.equal(fileOpVerb('read'), 'Read');
assert.equal(fileOpVerb('update'), 'Update'); assert.equal(fileOpVerb('update'), 'Update');
assert.equal(fileOpVerb('card_change'), 'Changed on card'); // A card-side change is discovered by a read, so the row reads as a read
// operation ("Read EF_LOCI") with the change as a trailing link.
assert.equal(fileOpVerb('card_change'), 'Read');
assert.equal(fileOpLabel({kind: 'read', file: {name: 'EF_ICCID'}}), 'Read EF_ICCID'); assert.equal(fileOpLabel({kind: 'read', file: {name: 'EF_ICCID'}}), 'Read EF_ICCID');
assert.equal(fileOpLabel({kind: 'card_change', file: {name: 'EF_LOCI'}}), assert.equal(fileOpLabel({kind: 'card_change', file: {name: 'EF_LOCI'}}),
'Changed on card EF_LOCI'); 'Read EF_LOCI');
assert.equal(fileOpLabel({kind: 'read', file: null}), 'Read ?'); assert.equal(fileOpLabel({kind: 'read', file: null}), 'Read ?');
}); });
@@ -340,7 +342,9 @@ test('Files view: the view selector (topbar) and the timeline are wired', () =>
assert.match(src, /\$on\('side-view-select', 'change'/); assert.match(src, /\$on\('side-view-select', 'change'/);
assert.match(src, /\$on\('file-view', 'click'/); assert.match(src, /\$on\('file-view', 'click'/);
assert.match(src, /API\.getSessionFiles\(sid\)/); assert.match(src, /API\.getSessionFiles\(sid\)/);
assert.match(src, />\$\{t\('Details'\)\}<\/button>/); // The trailing link: "Changed on card!" for a card-side change (it opens
// the change modal), the generic "Details" for an update row.
assert.match(src, />\$\{op\.kind === 'card_change' \? t\('Changed on card!'\) : t\('Details'\)\}<\/button>/);
assert.match(src, /API\.getSessionFileImage\(STATE\.sessionViewId, fileKey, idx\)/); assert.match(src, /API\.getSessionFileImage\(STATE\.sessionViewId, fileKey, idx\)/);
assert.match(src, /getSessionFileImage\(id, key, change\)/); assert.match(src, /getSessionFileImage\(id, key, change\)/);
assert.match(src, /files\/image\?key=/); assert.match(src, /files\/image\?key=/);
@@ -354,7 +358,7 @@ test('Files view: strings carry Russian translations', () => {
for (const m of lang.matchAll(/'((?:[^'\\]|\\.)*)'\s*:/g)) { for (const m of lang.matchAll(/'((?:[^'\\]|\\.)*)'\s*:/g)) {
keys.add(m[1].replace(/\\(['\\])/g, '$1')); keys.add(m[1].replace(/\\(['\\])/g, '$1'));
} }
for (const s of ['Files', 'Read', 'Update', 'Changed on card', 'Change details', for (const s of ['Files', 'Read', 'Update', 'Changed on card!', 'Change details',
'Details', 'Close', 'Offset', 'Record', 'Before', 'After', 'Show the change', 'Details', 'Close', 'Offset', 'Record', 'Before', 'After', 'Show the change',
'changed', 'newly observed', 'never observed', 'changed', 'newly observed', 'never observed',
'{c} bytes changed, {n} bytes newly observed', '{c} bytes changed, {n} bytes newly observed',
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project] [project]
name = "simtrace-analyser" name = "simtrace-analyser"
version = "2.6.3" version = "2.6.4"
description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)" description = "SIMtrace Analyser — APDU trace analyser for GSMTAP/PCAP captures (bundles the simtrace2-pysniff utility)"
readme = {file = "README.md", content-type = "text/markdown"} readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"} license = {text = "GPL-2.0-or-later"}
+1 -1
View File
@@ -1,3 +1,3 @@
"""Single source of truth for the SIMtrace Analyser version.""" """Single source of truth for the SIMtrace Analyser version."""
__version__ = '2.6.3' __version__ = '2.6.4'
+1 -1
View File
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase): class TestVersion(unittest.TestCase):
def test_package_version(self): def test_package_version(self):
from simtrace2_pysniff import __version__ from simtrace2_pysniff import __version__
self.assertEqual(__version__, '2.6.3') self.assertEqual(__version__, '2.6.4')
def test_status_includes_version(self): def test_status_includes_version(self):
import os import os