diff --git a/frontend/index.html b/frontend/index.html index 50d15eb..7061f8f 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -211,6 +211,16 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(- .session-download:hover { border-color: var(--accent); color: var(--accent); } .session-delete { padding: 4px 12px; background: none; border: 1px solid var(--accent); color: var(--accent); border-radius: 4px; cursor: pointer; font-size: 13px; } .session-delete:hover { background: var(--accent); color: #fff; } +.session-view-toggle { display: flex; gap: 2px; } +.session-view-toggle button { padding: 4px 10px; background: var(--bg-input); border: 1px solid var(--border); color: var(--text-muted); cursor: pointer; font-size: 13px; } +.session-view-toggle button.active { background: var(--accent); color: #fff; border-color: var(--accent); } +.sessions-list { padding: 0 16px 16px; overflow-y: auto; } +.sessions-list table { width: 100%; border-collapse: collapse; } +.sessions-list th { text-align: left; padding: 8px 12px; color: var(--text-muted); font-size: 12px; border-bottom: 1px solid var(--border); } +.sessions-list td { padding: 8px 12px; border-bottom: 1px solid var(--bg-input); font-size: 13px; } +.sessions-list tr[data-id] { cursor: pointer; } +.sessions-list tr[data-id]:hover { background: var(--bg-panel); } +.sessions-list td .session-actions { display: flex; gap: 8px; align-items: center; margin-top: 0; } /* Settings */ .settings-view { padding: 24px; max-width: 600px; } @@ -321,6 +331,10 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(- +
+ + +
@@ -328,6 +342,21 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-

Start a capture from the Trace tab to create sessions.

+ @@ -2162,10 +2191,103 @@ function updateCaptureButton() { } // ──────────────────── Sessions ──────────────────── +let SESSION_VIEW = localStorage.getItem('session-view') || 'grid'; + +function toggleSessionView(mode) { + SESSION_VIEW = mode; + localStorage.setItem('session-view', mode); + el('view-grid-btn').classList.toggle('active', mode === 'grid'); + el('view-list-btn').classList.toggle('active', mode === 'list'); + el('sessions-grid').style.display = mode === 'grid' ? '' : 'none'; + el('sessions-list').style.display = mode === 'list' ? '' : 'none'; + loadSessions(); +} + +function buildGridHTML(sessions) { + return sessions.map(s => { + const name = s.name || t('Session #') + s.id; + const dStart = s.started ? s.started.replace('T', ' ') : '?'; + const duration = s.ended ? formatDuration(s.started, s.ended) : t('active'); + return `
+
+ +
+
+ ${dStart} + ${duration} + ${s.message_count} ${t('msgs')} + ${s.mode} +
+
+ ${t('Download PCAP')} + ${is_loopback() ? '' : ``} + +
+
`; + }).join(''); +} + +function buildListHTML(sessions) { + return sessions.map(s => { + const name = s.name || t('Session #') + s.id; + const dStart = s.started ? s.started.replace('T', ' ') : '?'; + const duration = s.ended ? formatDuration(s.started, s.ended) : t('active'); + return ` + + ${dStart} + ${duration} + ${s.message_count} ${t('msgs')} + ${s.mode} + + ${t('Download PCAP')} + ${is_loopback() ? '' : ``} + + + `; + }).join(''); +} + +function attachSessionListeners(container) { + container.querySelectorAll('[data-id]').forEach(el => { + el.addEventListener('click', e => { + if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'A') return; + loadSession(parseInt(el.dataset.id)); + }); + }); + container.querySelectorAll('input[data-sid]').forEach(input => { + const originalName = input.value; + input.addEventListener('blur', async () => { + const sid = parseInt(input.dataset.sid); + const name = input.value.trim(); + try { + await API.renameSession(sid, name); + } catch (e) { + input.value = originalName; + } + }); + input.addEventListener('keydown', e => { + if (e.key === 'Enter') input.blur(); + }); + }); + container.querySelectorAll('.session-delete').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + showDeleteModal(parseInt(btn.dataset.sid)); + }); + }); + container.querySelectorAll('.session-copy').forEach(btn => { + btn.addEventListener('click', e => { + e.stopPropagation(); + copySessionLink(parseInt(btn.dataset.sid)); + }); + }); +} + async function loadSessions() { try { const data = await API.listSessions(); const grid = el('sessions-grid'); + const list = el('sessions-list'); const total = data.total_sessions != null ? data.total_sessions : data.sessions.length; const dbSize = data.db_size != null ? fmtSize(data.db_size) : '?'; @@ -2174,68 +2296,27 @@ async function loadSessions() { : t('{n} session(s) · {size}').replace('{n}', total).replace('{size}', dbSize); el('sessions-stats').textContent = sessionsLabel; + grid.style.display = SESSION_VIEW === 'grid' ? '' : 'none'; + list.style.display = SESSION_VIEW === 'list' ? '' : 'none'; + el('view-grid-btn').classList.toggle('active', SESSION_VIEW === 'grid'); + el('view-list-btn').classList.toggle('active', SESSION_VIEW === 'list'); + if (!data.sessions.length) { - grid.innerHTML = `
+ const emptyHTML = `

${t('No saved sessions')}

`; + grid.innerHTML = emptyHTML; + list.innerHTML = emptyHTML; return; } - grid.innerHTML = data.sessions.map(s => { - const name = s.name || t('Session #') + s.id; - const dStart = s.started ? s.started.replace('T', ' ') : '?'; - const duration = s.ended ? formatDuration(s.started, s.ended) : t('active'); - return `
-
- -
-
- ${dStart} - ${duration} - ${s.message_count} ${t('msgs')} - ${s.mode} -
-
- ${t('Download PCAP')} - ${is_loopback() ? '' : ``} - -
-
`; - }).join(''); - grid.querySelectorAll('.session-card').forEach(card => { - card.addEventListener('click', e => { - if (e.target.tagName === 'INPUT' || e.target.tagName === 'BUTTON' || e.target.tagName === 'A') return; - loadSession(parseInt(card.dataset.id)); - }); - }); - grid.querySelectorAll('.session-name input').forEach(input => { - const originalName = input.value; - input.addEventListener('blur', async () => { - const sid = parseInt(input.dataset.sid); - const name = input.value.trim(); - try { - await API.renameSession(sid, name); - } catch (e) { - input.value = originalName; - } - }); - input.addEventListener('keydown', e => { - if (e.key === 'Enter') input.blur(); - }); - }); - grid.querySelectorAll('.session-delete').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - const sid = parseInt(btn.dataset.sid); - showDeleteModal(sid); - }); - }); - grid.querySelectorAll('.session-copy').forEach(btn => { - btn.addEventListener('click', e => { - e.stopPropagation(); - copySessionLink(parseInt(btn.dataset.sid)); - }); - }); + if (SESSION_VIEW === 'grid') { + grid.innerHTML = buildGridHTML(data.sessions); + } else { + list.querySelector('tbody').innerHTML = buildListHTML(data.sessions); + } + + attachSessionListeners(SESSION_VIEW === 'grid' ? grid : list); } catch (e) {} } @@ -2526,7 +2607,7 @@ function onFilterChange() { } // ──────────────────── Versioning ──────────────────── -const PWA_VERSION = '1.22.5'; +const PWA_VERSION = '1.22.6'; const MIN_SERVER_VERSION = '1.0.0'; function parseVersion(v) { @@ -2584,6 +2665,14 @@ const LANG_RU = { 'Start a capture from the Trace tab to create sessions.': 'Начните захват на вкладке «Трассировка», чтобы создать сессии.', 'Import PCAP': 'Импорт PCAP', 'New capture': 'Новый захват', + 'Tiles': 'Плитки', + 'List': 'Список', + 'Name': 'Имя', + 'Date': 'Дата', + 'Duration': 'Длительность', + 'Messages': 'Сообщения', + 'Mode': 'Режим', + 'Actions': 'Действия', 'Server': 'Сервер', 'device disconnected — reconnecting…': 'устройство отключено — переподключение…', 'device disconnected — messages may be missing': 'устройство отключалось — сообщения могли быть потеряны', @@ -2788,6 +2877,9 @@ function init() { el('import-pcap-file').value = ''; }); + $on('view-grid-btn', 'click', () => toggleSessionView('grid')); + $on('view-list-btn', 'click', () => toggleSessionView('list')); + $on('search-input', 'input', onSearchChange); $on('filter-select', 'change', onFilterChange); diff --git a/frontend/sw.js b/frontend/sw.js index 6f0b0fa..4e98098 100644 --- a/frontend/sw.js +++ b/frontend/sw.js @@ -1,4 +1,4 @@ -const CACHE = 'simtrace-analyser-v37'; +const CACHE = 'simtrace-analyser-v38'; const PRECACHE = [ 'index.html', diff --git a/pyproject.toml b/pyproject.toml index 5c40c9f..4b2cf0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.backends._legacy:_Backend" [project] name = "simtrace2-pysniff" -version = "1.22.5" +version = "1.22.6" description = "Python-based sniffing tool for SIMtrace2 hardware (SIM card communication sniffer)" readme = {file = "README.md", content-type = "text/markdown"} license = {text = "GPL-2.0-or-later"} diff --git a/simtrace2_pysniff/__init__.py b/simtrace2_pysniff/__init__.py index b0b4375..9e0ccc6 100644 --- a/simtrace2_pysniff/__init__.py +++ b/simtrace2_pysniff/__init__.py @@ -6,7 +6,7 @@ from .gsmtap import GsmtapSender, GsmtapReceiver from .pcap import PcapWriter from .dump import FileDumper, format_message -__version__ = '1.22.5' +__version__ = '1.22.6' __all__ = [ 'SniffSession', 'DeviceDisconnected', diff --git a/tests/test_pcap_import.py b/tests/test_pcap_import.py index 01b77b5..56b46c6 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__, '1.22.5') + self.assertEqual(__version__, '1.22.6') def test_status_includes_version(self): import os