Show absolute wall-clock time in the APDU detail heading
Next to the relative elapsed time, the detail-view header now shows the absolute local time (YYYY-MM-DD HH:MM:SS.mmm) computed as session start + message elapsed offset. - STATE.sessionStarted populated from /api/capture/start (live capture), data.session.started (session view), and PCAP imports (sessions.started already derives from the first packet timestamp when real). - Graceful fallback to relative-only when the session start is unknown; applies to all detail header variants (gap, no-decoded, state change, Fi/Di, ATR/PPS, APDU). Version 1.17.0, SW cache simtrace-analyser-v24.
This commit is contained in:
1 parent
4541cc66c5
commit
07deab9c26
5 files changed
+31
-11
No files matched your search
+27
-7
@@ -372,6 +372,7 @@ const STATE = {
|
|||||||
currentSessionId: null,
|
currentSessionId: null,
|
||||||
currentSessionName: null,
|
currentSessionName: null,
|
||||||
sessionViewId: null,
|
sessionViewId: null,
|
||||||
|
sessionStarted: null,
|
||||||
livePoller: null,
|
livePoller: null,
|
||||||
messages: [],
|
messages: [],
|
||||||
typeCounts: [],
|
typeCounts: [],
|
||||||
@@ -467,6 +468,23 @@ function fmt_elapsed(e) {
|
|||||||
return `${String(s).padStart(4, '0')}.${String(ms).padStart(3, '0')}`;
|
return `${String(s).padStart(4, '0')}.${String(ms).padStart(3, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fmt_absolute(e) {
|
||||||
|
// Absolute wall-clock time of a message: session/capture start
|
||||||
|
// (sessions.started, local time) + message elapsed offset.
|
||||||
|
if (!STATE.sessionStarted) return null;
|
||||||
|
const t = new Date(STATE.sessionStarted);
|
||||||
|
if (isNaN(t)) return null;
|
||||||
|
t.setMilliseconds(t.getMilliseconds() + Math.round(e * 1000));
|
||||||
|
const p = (n, w) => String(n).padStart(w || 2, '0');
|
||||||
|
return `${t.getFullYear()}-${p(t.getMonth() + 1)}-${p(t.getDate())} ` +
|
||||||
|
`${p(t.getHours())}:${p(t.getMinutes())}:${p(t.getSeconds())}.${p(t.getMilliseconds(), 3)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detail_time_html(msg) {
|
||||||
|
const abs = fmt_absolute(msg.elapsed);
|
||||||
|
return abs ? `${fmt_elapsed(msg.elapsed)} · ${abs}` : fmt_elapsed(msg.elapsed);
|
||||||
|
}
|
||||||
|
|
||||||
function fmt_data(hex, limit) {
|
function fmt_data(hex, limit) {
|
||||||
limit = limit || 32;
|
limit = limit || 32;
|
||||||
const pairs = (hex || '').match(/.{1,2}/g) || [];
|
const pairs = (hex || '').match(/.{1,2}/g) || [];
|
||||||
@@ -920,19 +938,19 @@ function showDetail(msg, entry) {
|
|||||||
const decoded = msg.decoded;
|
const decoded = msg.decoded;
|
||||||
|
|
||||||
if (msg.type === 'gap') {
|
if (msg.type === 'gap') {
|
||||||
d.innerHTML = `<div class="detail-header"><strong>Capture Gap</strong><span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span></div>
|
d.innerHTML = `<div class="detail-header"><strong>Capture Gap</strong><span style="color:var(--text-muted)">${detail_time_html(msg)}</span></div>
|
||||||
<div class="detail-body" style="color:var(--warn);">⚠ ${t('The device disconnected and reconnected — some APDUs may be missing from this trace.')}</div>`;
|
<div class="detail-body" style="color:var(--warn);">⚠ ${t('The device disconnected and reconnected — some APDUs may be missing from this trace.')}</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
d.innerHTML = `<div class="detail-header"><strong>${ins_label(msg)}</strong><span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span></div>
|
d.innerHTML = `<div class="detail-header"><strong>${ins_label(msg)}</strong><span style="color:var(--text-muted)">${detail_time_html(msg)}</span></div>
|
||||||
<div class="detail-body" style="color:var(--text-muted);">No decoded data available for this message type.</div>`;
|
<div class="detail-body" style="color:var(--text-muted);">No decoded data available for this message type.</div>`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decoded.type === 'change') {
|
if (decoded.type === 'change') {
|
||||||
d.innerHTML = `<div class="detail-header"><strong>State Change</strong><span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span></div>
|
d.innerHTML = `<div class="detail-header"><strong>State Change</strong><span style="color:var(--text-muted)">${detail_time_html(msg)}</span></div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<table>
|
<table>
|
||||||
<tr><td class="label">Flags</td><td class="value">${decoded.flags.join(', ') || 'no changes'}</td></tr>
|
<tr><td class="label">Flags</td><td class="value">${decoded.flags.join(', ') || 'no changes'}</td></tr>
|
||||||
@@ -942,7 +960,7 @@ function showDetail(msg, entry) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (decoded.type === 'fidi') {
|
if (decoded.type === 'fidi') {
|
||||||
d.innerHTML = `<div class="detail-header"><strong>Fi/Di Change</strong><span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span></div>
|
d.innerHTML = `<div class="detail-header"><strong>Fi/Di Change</strong><span style="color:var(--text-muted)">${detail_time_html(msg)}</span></div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<table>
|
<table>
|
||||||
<tr><td class="label">Fi</td><td class="value">${decoded.fi_val} (index ${decoded.fi})</td></tr>
|
<tr><td class="label">Fi</td><td class="value">${decoded.fi_val} (index ${decoded.fi})</td></tr>
|
||||||
@@ -955,7 +973,7 @@ function showDetail(msg, entry) {
|
|||||||
|
|
||||||
if (decoded.type === 'atr' || decoded.type === 'pps') {
|
if (decoded.type === 'atr' || decoded.type === 'pps') {
|
||||||
const rows = atrPpsRows(decoded);
|
const rows = atrPpsRows(decoded);
|
||||||
d.innerHTML = `<div class="detail-header"><strong>${decoded.type.toUpperCase()}</strong><span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span></div>
|
d.innerHTML = `<div class="detail-header"><strong>${decoded.type.toUpperCase()}</strong><span style="color:var(--text-muted)">${detail_time_html(msg)}</span></div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<table>
|
<table>
|
||||||
${rows.map(([l, v]) => `<tr><td class="label">${l}</td><td class="hex">${v}</td></tr>`).join('')}
|
${rows.map(([l, v]) => `<tr><td class="label">${l}</td><td class="hex">${v}</td></tr>`).join('')}
|
||||||
@@ -1024,7 +1042,7 @@ function showDetail(msg, entry) {
|
|||||||
|
|
||||||
d.innerHTML = `<div class="detail-header">
|
d.innerHTML = `<div class="detail-header">
|
||||||
<strong>${decoded.cat_command || decoded.ins_name}</strong>
|
<strong>${decoded.cat_command || decoded.ins_name}</strong>
|
||||||
<span style="color:var(--text-muted)">${fmt_elapsed(msg.elapsed)}</span>
|
<span style="color:var(--text-muted)">${detail_time_html(msg)}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="detail-body">
|
<div class="detail-body">
|
||||||
<table>
|
<table>
|
||||||
@@ -1466,6 +1484,7 @@ async function startCapture() {
|
|||||||
const data = await API.startCapture();
|
const data = await API.startCapture();
|
||||||
STATE.captureActive = true;
|
STATE.captureActive = true;
|
||||||
STATE.currentSessionId = data.session_id;
|
STATE.currentSessionId = data.session_id;
|
||||||
|
STATE.sessionStarted = data.started;
|
||||||
STATE.messages = [];
|
STATE.messages = [];
|
||||||
_lastAfter = 0;
|
_lastAfter = 0;
|
||||||
STATE.mapExpanded.clear();
|
STATE.mapExpanded.clear();
|
||||||
@@ -1623,6 +1642,7 @@ async function loadSession(id) {
|
|||||||
|
|
||||||
const data = await API.getSession(id, 0);
|
const data = await API.getSession(id, 0);
|
||||||
STATE.sessionViewId = id;
|
STATE.sessionViewId = id;
|
||||||
|
STATE.sessionStarted = data.session.started;
|
||||||
STATE.captureActive = false;
|
STATE.captureActive = false;
|
||||||
STATE.currentSessionName = data.session.name || t('Session #') + id;
|
STATE.currentSessionName = data.session.name || t('Session #') + id;
|
||||||
STATE.messages = data.messages;
|
STATE.messages = data.messages;
|
||||||
@@ -1881,7 +1901,7 @@ function onFilterChange() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ──────────────────── Versioning ────────────────────
|
// ──────────────────── Versioning ────────────────────
|
||||||
const PWA_VERSION = '1.16.1';
|
const PWA_VERSION = '1.17.0';
|
||||||
const MIN_SERVER_VERSION = '1.0.0';
|
const MIN_SERVER_VERSION = '1.0.0';
|
||||||
|
|
||||||
function parseVersion(v) {
|
function parseVersion(v) {
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simtrace-analyser-v23';
|
const CACHE = 'simtrace-analyser-v24';
|
||||||
|
|
||||||
const PRECACHE = [
|
const PRECACHE = [
|
||||||
'index.html',
|
'index.html',
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "simtrace2-pysniff"
|
name = "simtrace2-pysniff"
|
||||||
version = "1.16.1"
|
version = "1.17.0"
|
||||||
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)"
|
||||||
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"}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver
|
|||||||
from .pcap import PcapWriter
|
from .pcap import PcapWriter
|
||||||
from .dump import FileDumper, format_message
|
from .dump import FileDumper, format_message
|
||||||
|
|
||||||
__version__ = '1.16.1'
|
__version__ = '1.17.0'
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'SniffSession',
|
'SniffSession',
|
||||||
'DeviceDisconnected',
|
'DeviceDisconnected',
|
||||||
|
|||||||
@@ -197,7 +197,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__, '1.16.1')
|
self.assertEqual(__version__, '1.17.0')
|
||||||
|
|
||||||
def test_status_includes_version(self):
|
def test_status_includes_version(self):
|
||||||
import os
|
import os
|
||||||
|
|||||||
Reference in new issue
Block a user