diff --git a/README.md b/README.md index dff4528..43c50d4 100644 --- a/README.md +++ b/README.md @@ -203,9 +203,10 @@ The Trace tab's side panel has a view selector (*Map* / *Stats* / **Files**; 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 -a row jumps straight to the APDU in the trace (the APDU list keeps the -reader's position: new messages pin the view to the bottom only while the -reader is already there, so a jump is never pulled away by the live poll). Rows that carry a recorded change (a +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 +there, so a jump is never pulled away or un-highlighted by the live poll). Rows that carry a recorded change (a card-side change, or an update that wrote or first observed bytes) also offer **Details**: a modal showing the full file path (`MF/EF_ICCID (2FE2)`, `ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — the DF diff --git a/README_RUS.md b/README_RUS.md index 7fb401c..a910e38 100644 --- a/README_RUS.md +++ b/README_RUS.md @@ -207,9 +207,10 @@ Application Layer* — два отдельных поля выбора, имен **Files** — выпадающий список, три подписи-вкладки уже не помещались). Вид Files — это таймлайн файловых операций по всей трассе: строки **Read**, **Update** и **Changed on card** показывают те же имена файлов, что и вид Map, -а щелчок по строке переходит прямо к APDU в трассе (список APDU сохраняет -позицию читателя: новые сообщения прижимают вид к низу, только пока читатель -уже внизу, поэтому переход не срывается живым опросом). У строк с записанным изменением +а щелчок по строке переходит прямо к APDU в трассе и оставляет его +выделенным, пока не выбран другой (список APDU сохраняет позицию читателя: +новые сообщения прижимают вид к низу, только пока читатель уже внизу, +поэтому переход не срывается живым опросом и не снимает выделение). У строк с записанным изменением (изменение со стороны карты либо запись, изменившая или впервые показавшая байты) есть кнопка **Details**: окно показывает полный путь файла (`MF/EF_ICCID (2FE2)`, `ADF/EF_IMSI (6F07)`, `DF_GSM_ACCESS/EF_KcGPRS (4F52)`, `AID …/4F09` — DF diff --git a/frontend/index.html b/frontend/index.html index bf1460d..a643c31 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -505,6 +505,7 @@ const STATE = { sessionStarted: null, livePoller: null, messages: [], + selectedIdx: null, // the focused APDU entry (persists across renders) typeCounts: [], traceMap: [], sideView: 'map', @@ -1754,9 +1755,13 @@ function atrPpsRows(decoded) { } function showDetail(msg, entry) { - // Highlight clicked entry + // Highlight the focused entry; the state keeps it marked across renders. document.querySelectorAll('.apdu-entry.selected').forEach(e => e.classList.remove('selected')); - if (entry) entry.classList.add('selected'); + if (entry) { + entry.classList.add('selected'); + const idx = Number(entry.dataset.idx); + if (!Number.isNaN(idx)) STATE.selectedIdx = idx; + } const d = el('apdu-detail'); const decoded = msg.decoded; @@ -2333,9 +2338,10 @@ function scrollToType(typeKey) { } } if (targetEl) { + STATE.selectedIdx = parseInt(targetEl.dataset.idx); targetEl.scrollIntoView({ block: 'center', behavior: 'smooth' }); + document.querySelectorAll('.apdu-entry.selected').forEach(e => e.classList.remove('selected')); targetEl.classList.add('selected'); - setTimeout(() => targetEl.classList.remove('selected'), 1500); } } @@ -2345,12 +2351,13 @@ function focusMessage(idx) { const needRender = filterEl.value !== '' || searchEl.value !== ''; filterEl.value = ''; searchEl.value = ''; + // The focused entry stays selected until another one is focused; the + // state lets a re-render (the live poll included) keep it marked. + STATE.selectedIdx = idx; if (needRender) renderMessages(true); const entry = el('apdu-list').querySelector(`.apdu-entry[data-idx="${idx}"]`); if (!entry) return; entry.scrollIntoView({ block: 'center', behavior: 'smooth' }); - entry.classList.add('selected'); - setTimeout(() => entry.classList.remove('selected'), 1500); showDetail(STATE.messages[idx], entry); } @@ -2432,9 +2439,11 @@ function renderMessages(skipScroll, forceStick) { return; } + const selectedIdx = STATE.selectedIdx; list.innerHTML = filtered.map(({ m, idx }) => { + const selCls = idx === selectedIdx ? ' selected' : ''; if (m.type === 'gap') { - return `