From ef1ba00732536e7947e71c1ac3d305b152db65e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D0=BD=D1=82=D0=BE=D0=BD=20=D0=A2=D1=80=D0=BE=D1=88?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Thu, 20 Aug 2026 21:23:25 +0300 Subject: [PATCH] Attribute SFI ops in trace map; collapse long file lists --- frontend/index.html | 66 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 50 insertions(+), 16 deletions(-) diff --git a/frontend/index.html b/frontend/index.html index 5fa22d0..caa99fe 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -149,6 +149,8 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(- .side-tab.active { color: var(--accent); border-bottom-color: var(--accent); } .map-item { padding: 3px 8px; cursor: pointer; border-radius: 4px; font-size: 13px; color: var(--text); word-break: break-word; } .map-item:hover { background: var(--bg-input); } +.map-files-toggle { color: var(--accent); cursor: pointer; } +.map-files-toggle .map-arrow { color: var(--text-muted); } /* Status bar */ .status-bar { display: flex; padding: 8px 16px; background: var(--bg-panel); border-top: 1px solid var(--bg-input); font-size: 13px; gap: 16px; flex-shrink: 0; } @@ -359,6 +361,7 @@ const STATE = { serverVersion: '', captureMode: '', deviceConnected: null, + mapExpanded: new Set(), }; const API_BASE = () => STATE.serverUrl; @@ -1037,6 +1040,8 @@ function buildTraceMap() { if (ins === 'c0') { i++; continue; } // skip GET RESPONSE if (ins && MAP_RW_INS.has(ins)) { // Split the run into per-file records (delimited by SELECT). + // Each READ/UPDATE op contributes its own resolved file, so + // SFI-addressed files (never SELECTed) are attributed correctly. const access = []; let cur = null; let j = i; @@ -1046,47 +1051,50 @@ function buildTraceMap() { if (!jins || !MAP_RW_INS.has(jins)) break; if (jins === 'a4') { if (cur) access.push(cur); - cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; - } else if (is_write_ins(jins)) { - if (!cur) cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; - cur.hasWrite = true; - cur.end = j; - } else { // SEARCH / READ - if (!cur) cur = { file: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; - cur.hasRead = true; + cur = { files: [], selNote: map_file_label(msgs[j]), hasRead: false, hasWrite: false, start: j, end: j }; + } else { + if (!cur) cur = { files: [], selNote: null, hasRead: false, hasWrite: false, start: j, end: j }; + const f = map_file_label(msgs[j]); + if (f && !cur.files.includes(f)) cur.files.push(f); + if (is_write_ins(jins)) cur.hasWrite = true; else cur.hasRead = true; cur.end = j; } j++; } if (cur) access.push(cur); + // Records with no read/write ops (pure catalog SELECT) fall back to + // the selected file's note. + for (const rec of access) { + if (rec.files.length === 0 && rec.selNote) rec.files = [rec.selNote]; + } let k = 0; while (k < access.length) { const rec = access[k]; if (rec.hasRead && rec.hasWrite) { - groups.push({ label: t('Read/write:') + ' ' + (rec.file || '—'), start: rec.start, end: rec.end }); + groups.push({ kind: 'rw', files: rec.files, start: rec.start, end: rec.end }); k++; } else if (rec.hasWrite) { const files = []; let end = rec.end; let m = k; while (m < access.length && access[m].hasWrite && !access[m].hasRead) { - if (access[m].file && !files.includes(access[m].file)) files.push(access[m].file); + for (const f of access[m].files) if (f && !files.includes(f)) files.push(f); end = access[m].end; m++; } - groups.push({ label: t('Writes:') + ' ' + (files.join(', ') || '—'), start: rec.start, end }); + groups.push({ kind: 'write', files, start: rec.start, end }); k = m; } else { const files = []; let end = rec.end; let m = k; while (m < access.length && !access[m].hasWrite) { - if (access[m].file && !files.includes(access[m].file)) files.push(access[m].file); + for (const f of access[m].files) if (f && !files.includes(f)) files.push(f); end = access[m].end; m++; } - groups.push({ label: t('Reads:') + ' ' + (files.join(', ') || '—'), start: rec.start, end }); + groups.push({ kind: 'read', files, start: rec.start, end }); k = m; } } @@ -1113,9 +1121,13 @@ function renderSidePanel() { ).join('') || t('No commands'); const mapEl = el('type-map'); - mapEl.innerHTML = STATE.traceMap.map(g => - `
${g.label}
` - ).join('') || t('No commands'); + mapEl.innerHTML = STATE.traceMap.map(g => { + if (g.kind) { + const kind = g.kind === 'rw' ? t('Read/write:') : g.kind === 'write' ? t('Writes:') : t('Reads:'); + return `
${kind} ${mapFilesHtml(g)}
`; + } + return `
${g.label}
`; + }).join('') || t('No commands'); const showMap = STATE.sideView === 'map'; el('type-counts').style.display = showMap ? 'none' : ''; @@ -1125,6 +1137,15 @@ function renderSidePanel() { }); } +function mapFilesHtml(g) { + const files = g.files || []; + if (files.length === 0) return '—'; + if (files.length <= 3) return files.join(', '); + const expanded = STATE.mapExpanded.has(g.start); + const label = expanded ? files.join(', ') : t('{n} files').replace('{n}', files.length); + return `${label} ${expanded ? '▾' : '▸'}`; +} + function populateFilterOptions() { const sel = el('filter-select'); const counts = {}; @@ -1358,6 +1379,7 @@ async function startCapture() { STATE.currentSessionId = data.session_id; STATE.messages = []; _lastAfter = 0; + STATE.mapExpanded.clear(); buildTypeCounts(); buildTraceMap(); renderSidePanel(); @@ -1508,6 +1530,7 @@ async function loadSession(id) { STATE.captureActive = false; STATE.currentSessionName = data.session.name || t('Session #') + id; STATE.messages = data.messages; + STATE.mapExpanded.clear(); switchTab('trace'); updateCaptureButton(); renderMessages(); @@ -1684,6 +1707,7 @@ async function checkConnection() { STATE.currentSessionId = data.session_id; STATE.messages = []; _lastAfter = 0; + STATE.mapExpanded.clear(); } STATE.captureActive = true; STATE.currentSessionId = data.session_id; @@ -1800,6 +1824,7 @@ const LANG_RU = { 'Reads:': 'Чтение:', 'Writes:': 'Запись:', 'Read/write:': 'Чтение/запись:', + '{n} files': '{n} файлов', 'Not connected': 'Не подключено', 'Live': 'Захват', 'Session': 'Сессия', @@ -2057,6 +2082,15 @@ function init() { renderSidePanel(); return; } + const toggle = e.target.closest('.map-files-toggle'); + if (toggle) { + e.stopPropagation(); + const start = parseInt(toggle.dataset.start); + if (STATE.mapExpanded.has(start)) STATE.mapExpanded.delete(start); + else STATE.mapExpanded.add(start); + renderSidePanel(); + return; + } const mapItem = e.target.closest('.map-item'); if (mapItem) { focusMessage(parseInt(mapItem.dataset.start));