diff --git a/README.md b/README.md
index 9a6a518..c1b5599 100644
--- a/README.md
+++ b/README.md
@@ -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
while the panel scrolls (a drop-down, since three tab captions no longer
fit). The Files view is a
-trace-wide timeline of file operations — every **Read**, **Update** and
-**Changed on card** row shows the same file name as the Map view, and clicking
+trace-wide timeline of file operations — every **Read** and **Update** row
+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
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
diff --git a/README_RUS.md b/README_RUS.md
index 6dceb56..b380ccb 100644
--- a/README_RUS.md
+++ b/README_RUS.md
@@ -207,10 +207,11 @@ Application Layer* — два отдельных поля выбора, имен
**Files**) — он выровнен по правому краю над боковой панелью и имеет её
ширину, поэтому виден всё время, пока панель прокручивается (выпадающий
список, три подписи-вкладки уже не помещались). Вид
-Files — это таймлайн файловых операций по всей трассе: строки **Read**,
-**Update** и **Changed on card** показывают те же имена файлов, что и вид Map,
-а щелчок по строке переходит прямо к APDU в трассе и оставляет его
-выделенным, пока не выбран другой (список APDU сохраняет позицию читателя:
+Files — это таймлайн файловых операций по всей трассе: строки **Read** и
+**Update** показывают те же имена файлов, что и вид Map (изменение со стороны
+карты выглядит как чтение со ссылкой **Изменено на карте!**, открывающей
+подробности), а щелчок по строке переходит прямо к APDU в трассе и оставляет
+его выделенным, пока не выбран другой (список APDU сохраняет позицию читателя:
новые сообщения прижимают вид к низу, только пока читатель уже внизу,
поэтому переход не срывается живым опросом и не снимает выделение). У строк с записанным изменением
(изменение со стороны карты либо запись, изменившая или впервые показавшая
diff --git a/frontend/index.html b/frontend/index.html
index 69518dd..310aee3 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -2136,7 +2136,8 @@ function fileOpClass(kind) {
function fileOpVerb(kind) {
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');
}
@@ -2233,7 +2234,7 @@ async function renderFileView() {
}
view.innerHTML = timeline.map(op => {
const diff = op.change_ref
- ? ``
+ ? ``
: '';
return `
${escapeHtml(fileOpLabel(op))}${diff}
@@ -3225,7 +3226,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
-const PWA_VERSION = '2.6.3';
+const PWA_VERSION = '2.6.4';
const MIN_SERVER_VERSION = '2.3.0';
function parseVersion(v) {
@@ -3271,7 +3272,7 @@ const LANG_RU = {
'Files': 'Файлы',
'Read': 'Чтение',
'Update': 'Запись',
- 'Changed on card': 'Изменено картой',
+ 'Changed on card!': 'Изменено на карте!',
'Change details': 'Подробности изменения',
'Details': 'Подробнее',
'changed': 'изменено',
diff --git a/frontend/sw.js b/frontend/sw.js
index 412a149..04dcd8b 100644
--- a/frontend/sw.js
+++ b/frontend/sw.js
@@ -1,4 +1,4 @@
-const CACHE = 'simtrace-analyser-v77';
+const CACHE = 'simtrace-analyser-v78';
const PRECACHE = [
'index.html',
diff --git a/frontend/tests/comprion.test.js b/frontend/tests/comprion.test.js
index ba1ee68..b48c9cf 100644
--- a/frontend/tests/comprion.test.js
+++ b/frontend/tests/comprion.test.js
@@ -292,10 +292,12 @@ test('Files view: operation kinds map to classes and verbs', () => {
assert.equal(fileOpClass('card_change'), 'file-op-card');
assert.equal(fileOpVerb('read'), 'Read');
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: 'card_change', file: {name: 'EF_LOCI'}}),
- 'Changed on card EF_LOCI');
+ 'Read EF_LOCI');
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\('file-view', 'click'/);
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, /getSessionFileImage\(id, key, change\)/);
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)) {
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',
'changed', 'newly observed', 'never observed',
'{c} bytes changed, {n} bytes newly observed',
diff --git a/pyproject.toml b/pyproject.toml
index d0f8660..48a918e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
[project]
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)"
readme = {file = "README.md", content-type = "text/markdown"}
license = {text = "GPL-2.0-or-later"}
diff --git a/simtrace_analyser/version.py b/simtrace_analyser/version.py
index d522600..825542d 100644
--- a/simtrace_analyser/version.py
+++ b/simtrace_analyser/version.py
@@ -1,3 +1,3 @@
"""Single source of truth for the SIMtrace Analyser version."""
-__version__ = '2.6.3'
+__version__ = '2.6.4'
diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py
index af3417f..66909c7 100644
--- a/tests/test_pcap_import.py
+++ b/tests/test_pcap_import.py
@@ -224,7 +224,7 @@ class TestImportEndpoint(unittest.TestCase):
class TestVersion(unittest.TestCase):
def test_package_version(self):
from simtrace2_pysniff import __version__
- self.assertEqual(__version__, '2.6.3')
+ self.assertEqual(__version__, '2.6.4')
def test_status_includes_version(self):
import os