Add --capture disabled mode + capture-mode status in PWA

Server:
- --capture accepts 'disabled'; no backend is created, capture_mode exposed
- /api/status returns capture_mode; capture start/stop return 403 when
  disabled; latest/status/delete_session are None-safe

PWA:
- captureMode state; conn-status shows server version
- new capture-mode line (Settings) explaining gsmtap/direct/disabled
- hide capture + New capture UI when disabled; trace empty-state shows
  'Capture unavailable'
- 7 new EN/RU i18n keys

Version 1.2.0 (server + PWA); tests: 111.
This commit is contained in:
catarrh committed 2026-08-14 23:49:10 +03:00
1 parent b900a8a3e1
commit 24d0decd51
7 files changed
+130 -22

No files matched your search

+50 -5
View File
@@ -171,6 +171,8 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-
.settings-view .conn-status.connected { background: var(--accent-bg); color: var(--green); }
.settings-view .conn-status.disconnected { background: var(--warn-bg); color: var(--error); }
.conn-hint { font-size: 12px; color: var(--text-muted); margin: 8px 0 0; line-height: 1.5; }
.capture-mode { font-size: 12px; color: var(--text-muted); margin: 8px 0 0; line-height: 1.5; }
.capture-mode:empty { display: none; }
.version-warn { font-size: 12px; color: var(--warn); margin: 8px 0 0; line-height: 1.5; }
.version-warn:empty { display: none; }
.conn-instructions { margin-top: 16px; font-size: 13px; color: var(--text-faint); line-height: 1.7; }
@@ -272,6 +274,7 @@ body { font: 14px/1.5 system-ui, sans-serif; background: var(--bg); color: var(-
<div class="conn-status disconnected" id="conn-status">
⬤ Not connected — ensure the server is running on the URL above
</div>
<div class="capture-mode" id="capture-mode"></div>
<div class="version-warn" id="version-warn"></div>
<div class="conn-hint" id="conn-hint"></div>
<div class="conn-instructions" id="conn-instructions"></div>
@@ -313,6 +316,7 @@ const STATE = {
typeCounts: [],
pendingDeleteId: null,
serverAvailable: false,
captureMode: '',
};
const API_BASE = () => STATE.serverUrl;
@@ -760,9 +764,14 @@ function renderMessages() {
populateFilterOptions();
if (!STATE.captureActive && !STATE.sessionViewId) {
list.innerHTML = `<div class="empty-state"><p>${t('No capture active')}</p>
<button id="trace-start-btn">${t('Start Capture')}</button></div>`;
$('#trace-start-btn', list).addEventListener('click', startCapture);
if (STATE.captureMode === 'disabled') {
list.innerHTML = `<div class="empty-state"><p>${t('Capture unavailable')}</p>
<p style="font-size:12px;">${t('Open a saved session from the Sessions tab.')}</p></div>`;
} else {
list.innerHTML = `<div class="empty-state"><p>${t('No capture active')}</p>
<button id="trace-start-btn">${t('Start Capture')}</button></div>`;
$('#trace-start-btn', list).addEventListener('click', startCapture);
}
updateStatus();
return;
}
@@ -837,6 +846,7 @@ function stopPolling() {
// ──────────────────── Capture ────────────────────
async function startCapture() {
if (STATE.captureMode === 'disabled') return;
try {
const data = await API.startCapture();
STATE.captureActive = true;
@@ -864,7 +874,14 @@ async function stopCapture() {
function updateCaptureButton() {
const btn = el('capture-btn');
const newCapture = el('new-capture-btn');
btn.classList.remove('stop', 'connect');
const captureOff = STATE.captureMode === 'disabled' && STATE.serverAvailable;
newCapture.style.display = captureOff ? 'none' : '';
if (captureOff) {
btn.style.display = 'none';
return;
}
if (STATE.sessionViewId && !STATE.captureActive) {
btn.style.display = 'none';
return;
@@ -1062,6 +1079,23 @@ function updateVersionWarn(serverVersion) {
elv.textContent = '';
}
function updateCaptureModeLine() {
const elm = el('capture-mode');
switch (STATE.captureMode) {
case 'gsmtap':
elm.textContent = t('Capture mode: GSMTAP — the server is listening for incoming UDP/GSMTAP traffic. Ensure a GSMTAP source (e.g. simtrace2-pysniff or simtrace2-sniff) is feeding the server.');
break;
case 'direct':
elm.textContent = t('Capture mode: direct — the server captures from SIMtrace2 hardware. Keep the device connected.');
break;
case 'disabled':
elm.textContent = t('Capture is disabled — only pre-captured traces (saved sessions / PCAP import) are available.');
break;
default:
elm.textContent = '';
}
}
function connectServer() {
if (_connTimer) return;
_connTimer = setInterval(checkConnection, 5000);
@@ -1076,6 +1110,7 @@ function disconnectServer() {
STATE.serverAvailable = false;
el('conn-status').textContent = '⬤ ' + t('Disconnected — click Connect to resume');
el('conn-status').className = 'conn-status disconnected';
el('capture-mode').textContent = '';
el('version-warn').textContent = '';
el('conn-hint').textContent = '';
el('conn-instructions').innerHTML = '';
@@ -1096,9 +1131,11 @@ async function checkConnection() {
try {
const data = await API.status();
STATE.serverAvailable = true;
el('conn-status').textContent = '⬤ ' + t('Connected') + ' — ' + data.server;
STATE.captureMode = data.capture_mode || '';
el('conn-status').textContent = '⬤ ' + t('Connected') + ' — ' + data.server + ' v' + (data.version || '?');
el('conn-status').className = 'conn-status connected';
updateVersionWarn(data.version);
updateCaptureModeLine();
el('conn-hint').textContent = '';
el('conn-instructions').innerHTML = '';
el('connect-btn').textContent = t('Disconnect');
@@ -1112,8 +1149,10 @@ async function checkConnection() {
updateCaptureButton();
} catch (e) {
STATE.serverAvailable = false;
STATE.captureMode = '';
el('conn-status').textContent = '⬤ ' + t('Not connected — ensure the server is running on the URL above');
el('conn-status').className = 'conn-status disconnected';
el('capture-mode').textContent = '';
el('version-warn').textContent = '';
const url = STATE.serverUrl || location.origin;
el('conn-hint').textContent = t(isLikelyBlocked() ? HINT_BLOCKED : HINT_UNREACHABLE).replace('{url}', url);
@@ -1167,7 +1206,7 @@ function onFilterChange() {
}
// ──────────────────── Versioning ────────────────────
const PWA_VERSION = '1.1.0';
const PWA_VERSION = '1.2.0';
const MIN_SERVER_VERSION = '1.0.0';
function parseVersion(v) {
@@ -1261,6 +1300,12 @@ const LANG_RU = {
'Server version unknown — update simtrace2-pysniff-server.': 'Версия сервера неизвестна — обновите simtrace2-pysniff-server.',
'Server version {v} is outdated. Please update simtrace2-pysniff-server.': 'Версия сервера {v} устарела. Обновите simtrace2-pysniff-server.',
'Server version {v} is newer than expected. Update simtrace2-pysniff for full compatibility.': 'Версия сервера {v} новее ожидаемой. Обновите simtrace2-pysniff для полной совместимости.',
'Capture mode: GSMTAP — the server is listening for incoming UDP/GSMTAP traffic. Ensure a GSMTAP source (e.g. simtrace2-pysniff or simtrace2-sniff) is feeding the server.': 'Режим захвата: GSMTAP — сервер ожидает входящий UDP/GSMTAP-трафик. Убедитесь, что источник GSMTAP (например, simtrace2-pysniff или simtrace2-sniff) передаёт данные серверу.',
'Capture mode: direct — the server captures from SIMtrace2 hardware. Keep the device connected.': 'Режим захвата: direct — сервер захватывает данные с устройства SIMtrace2. Не отключайте устройство.',
'Capture is disabled — only pre-captured traces (saved sessions / PCAP import) are available.': 'Захват отключён — доступны только ранее записанные трассировки (сохранённые сессии / импорт PCAP).',
'Capture unavailable': 'Захват недоступен',
'Open a saved session from the Sessions tab.': 'Откройте сохранённую сессию на вкладке «Сессии».',
'Capture disabled': 'Захват отключён',
};
function t(text) {