diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 35e29204b..0d9aefb90 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -436,6 +436,7 @@ jobs: BASE_URL=http://localhost:13581 node test-slideover-1056-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-1692-packets-init-parallel-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-147-packets-url-obs-e2e.js 2>&1 | tee -a e2e-output.txt + BASE_URL=http://localhost:13581 node test-issue-96-hide-control-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-issue-180-packets-url-modal-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-slideover-1168-munger-e2e.js 2>&1 | tee -a e2e-output.txt BASE_URL=http://localhost:13581 node test-logo-pulse-1173-e2e.js 2>&1 | tee -a e2e-output.txt diff --git a/public/packets.js b/public/packets.js index f501936ec..8cfa2824f 100644 --- a/public/packets.js +++ b/public/packets.js @@ -715,8 +715,58 @@ function _hashStripeStyle(hash) { return _isColorByHash() && hash && window.HashColor ? 'border-left:4px solid ' + HashColor.hashToHsl(hash, _currentTheme()) + ';' : ''; } let groupByHash = true; let filters = {}; - { const o = localStorage.getItem('meshcore-observer-filter'); if (o) filters.observer = o; - const t = localStorage.getItem('meshcore-type-filter'); if (t) filters.type = t; } + // Storage can throw (blocked, private mode); the filters then start empty. + try { + const o = localStorage.getItem('meshcore-observer-filter'); if (o) filters.observer = o; + const t = localStorage.getItem('meshcore-type-filter'); if (t) filters.type = t; + } catch (e) { /* storage unavailable */ } + // #96 — opt-in "Hide CONTROL packets" display filter, unchecked by default. + // Display only: CONTROL packets are still fetched and kept (also live ones), + // so unchecking shows them again without a reload, and a pinned hash (a + // direct link to one packet) bypasses it like every other client filter. + // Saved as '1' or '0' so an explicit unchecked choice survives a reload. + const PAYLOAD_TYPE_CONTROL = 11; + const HIDE_CONTROL_KEY = 'meshcore-hide-control'; + // The URL value ('1' or '0') wins over the saved choice; default off. + function readHideControlPref(urlValue, stored) { + if (urlValue === '1') return true; + if (urlValue === '0') return false; + return stored === '1'; + } + // The saved choice, or null when storage throws (blocked, private mode). + function readStoredHideControl() { + try { return localStorage.getItem(HIDE_CONTROL_KEY); } catch (e) { return null; } + } + // Saves the choice; false when storage throws (quota, blocked). Best effort: + // the checkbox still works for this view. + function saveHideControlPref(hide) { + try { localStorage.setItem(HIDE_CONTROL_KEY, hide ? '1' : '0'); return true; } catch (e) { return false; } + } + // One pass, order kept; the same array when off (no copy on the default path). + function filterHiddenControl(list, hide) { + return hide ? list.filter(p => p.payload_type !== PAYLOAD_TYPE_CONTROL) : list; + } + // Whether hiding CONTROL is what emptied the list: some CONTROL packet from + // before the CONTROL pass gets through the filters after it (laterFilters). + // Called only for an empty list, and runs the CONTROL packets only. + function controlHidingEmptiedList(beforeHide, laterFilters) { + const hidden = beforeHide.filter(p => p.payload_type === PAYLOAD_TYPE_CONTROL); + return hidden.length > 0 && laterFilters(hidden).length > 0; + } + // The checkbox's change: re-filters the loaded packets, no new request. The + // save comes first but cannot stop the filter or the URL update (#211). + function setHideControl(hide) { + hideControl = hide; + if (saveHideControlPref(hide)) savedHideControl = hide; + updatePacketsUrl(); + renderTableRows(); + } + // hideControl is what this view shows; savedHideControl is the saved choice + // as far as this page knows (it changes only when a save succeeds). + // buildPacketsQuery reads both, so a test sandbox that extracts it must + // declare both (test-issue-121/147-…, test-issue-96-hide-control.js). + let savedHideControl = readHideControlPref(null, readStoredHideControl()); + let hideControl = savedHideControl; let wsHandler = null; let packetsPaused = false; let pauseBuffer = []; @@ -779,6 +829,11 @@ if (filters.observer) parts.push('observer=' + encodeURIComponent(filters.observer)); if (filters.channel) parts.push('channel=' + encodeURIComponent(filters.channel)); if (filters._filterExpr) parts.push('filter=' + encodeURIComponent(filters._filterExpr)); + // #96: hideControl=1 while on. Off is the default and is omitted, except + // while the saved choice is on: an explicit hideControl=0 then stays, so a + // reload of this URL still shows CONTROL (#211). + if (hideControl) parts.push('hideControl=1'); + else if (savedHideControl) parts.push('hideControl=0'); // Sort state (#749) — encode as 'col[:asc]'; default 'time:desc' is omitted. if (_packetSortColumn) { var sortDefault = _packetSortColumn === 'time' && _packetSortDirection === 'desc'; @@ -1199,6 +1254,11 @@ if (_urlChannel) filters.channel = _urlChannel; var _urlFilterExpr = _initUrlParams.get('filter'); if (_urlFilterExpr) filters._filterExpr = _urlFilterExpr; + // #96 — ?hideControl=1|0 wins over the saved choice for this view; only + // the checkbox changes the saved choice. + var _storedHideControl = readStoredHideControl(); + savedHideControl = readHideControlPref(null, _storedHideControl); + hideControl = readHideControlPref(_initUrlParams.get('hideControl'), _storedHideControl); // #749 — restore sort state from URL (overrides localStorage). var _urlSort = _initUrlParams.get('sort'); if (_urlSort && window.URLState) { @@ -1671,6 +1731,7 @@
+