From a04319dfc8d05064832ef473d28853d34d4762d5 Mon Sep 17 00:00:00 2001 From: c4ch3c4d3 Date: Sun, 23 Aug 2026 09:29:10 -0600 Subject: [PATCH] feat(webui,recon): drop PineAP mode card, sort recon report APs by signal - Remove the Passive/Active/Advanced mode card from the PineAP dashboard and move the RF Role (radio1) card into its slot, along with the now dead mode state machinery and mode-card-only CSS. - Sort recon HTML report access points by signal strength (strongest first). --- .../user/remote_access/pager-webui/server.py | 5 +- .../remote_access/pager-webui/www/css/app.css | 7 +- .../remote_access/pager-webui/www/js/views.js | 109 ++---------------- tests/test_recon.py | 10 ++ 4 files changed, 23 insertions(+), 108 deletions(-) diff --git a/payload/user/remote_access/pager-webui/server.py b/payload/user/remote_access/pager-webui/server.py index ea452c3..11993f9 100644 --- a/payload/user/remote_access/pager-webui/server.py +++ b/payload/user/remote_access/pager-webui/server.py @@ -2281,9 +2281,10 @@ def _recon_html_download(scan_id, data, client_count, archive=None): body_parts.append(_html_table(['Band', 'Channel', 'Access Points'], chan_rows)) else: body_parts.append('

No access points with a known channel.

') - # AP table with color-coded signal. + # AP table with color-coded signal, strongest signal first. ap_rows = [] - for a in aps: + for a in sorted(aps, key=lambda row: row.get('signal') + if row.get('signal') is not None else -200, reverse=True): ap_rows.append([a.get('ssid') or '(hidden)', a.get('bssid'), a.get('band') or '--', a.get('channel') if a.get('channel') is not None else '--', diff --git a/payload/user/remote_access/pager-webui/www/css/app.css b/payload/user/remote_access/pager-webui/www/css/app.css index 8f90270..686c798 100644 --- a/payload/user/remote_access/pager-webui/www/css/app.css +++ b/payload/user/remote_access/pager-webui/www/css/app.css @@ -494,7 +494,7 @@ html.dark .recon-pill.on { background: #1b3a23; color: #81c784; } .hs-settings-value { font-family: Consolas, Menlo, monospace; word-break: break-all; } html.dark .modal { background: #303030; } -/* ---- PineAP overview segmented control ---- */ +/* ---- PineAP segmented control ---- */ .seg { display: inline-flex; margin-top: 8px; border: 1px solid var(--border, #e0e0e0); border-radius: 4px; overflow: hidden; } .seg-btn { background: transparent; border: none; padding: 5px 14px; font-size: 12px; cursor: pointer; color: var(--muted, #666); } .seg-btn + .seg-btn { border-left: 1px solid var(--border, #e0e0e0); } @@ -510,11 +510,6 @@ html.dark .modal { background: #303030; } .pineap-card-title-link:visited { color: inherit; } .pineap-card-title-link:hover { text-decoration: underline; } .pineap-card-title-content { display: flex; justify-content: center; align-items: center; font-size: 24px; } -.pineap-card-button-group { width: 100%; height: 30px; display: flex; } -.pineap-card-button-group .seg { flex: 1; height: 100%; margin-top: 0; } -.pineap-card-button-group .seg-btn { flex: 1; } -.pineap-mode-save { display: flex; justify-content: flex-end; margin-top: 10px; } -.pineap-mode-features { margin: 6px 0 0; padding-left: 20px; } .pineap-card-settings, .pineap-card-pool, .pineap-card-handshakes, .pineap-card-inject { flex: 1; } .pineap-handshakes-none { display: flex; justify-content: center; font-style: italic; color: var(--muted); } diff --git a/payload/user/remote_access/pager-webui/www/js/views.js b/payload/user/remote_access/pager-webui/www/js/views.js index b5e968e..4fed3a3 100644 --- a/payload/user/remote_access/pager-webui/www/js/views.js +++ b/payload/user/remote_access/pager-webui/www/js/views.js @@ -389,7 +389,7 @@ const PINEAP_TABS = [ // The Pager daemon can change these states but cannot read them back. Keep // them explicitly unknown until this WebUI successfully changes them. -const PINEAP_SESSION = { mode: null, karma: null, advertise: null, collect: null }; +const PINEAP_SESSION = { karma: null, advertise: null, collect: null }; function setKnownCheckbox(cb, value) { cb.indeterminate = value == null; @@ -430,27 +430,6 @@ views.pineap = (root) => { }); box.appendChild(statWrap); - const mode = h('span', { class: 'badge', text: '—' }); - let selectedMode = 'unknown'; - let modeDirty = false; - let modePending = false; - const segBtns = {}; - const modeBar = h('div', { class: 'seg' }); - ['passive', 'active', 'advanced'].forEach((m) => { - const b = h('button', { class: 'seg-btn', text: m[0].toUpperCase() + m.slice(1) }); - b.addEventListener('click', () => selectMode(m, true)); - modeBar.appendChild(b); - segBtns[m] = b; - }); - const modeInfo = h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' }); - const saveModeBtn = btn('Save Mode', saveMode, 'ghost'); - saveModeBtn.disabled = true; - const modeCard = h('div', { class: 'pineap-title-card' }, - h('div', { class: 'pineap-card-title-flex' }, mode), - h('div', { class: 'pineap-card-button-group' }, modeBar), - modeInfo, - h('div', { class: 'pineap-mode-save' }, saveModeBtn)); - const quick = { collect: h('input', { type: 'checkbox', id: 'po-collect' }), advertise: h('input', { type: 'checkbox', id: 'po-advertise' }) @@ -465,11 +444,6 @@ views.pineap = (root) => { quickCard.appendChild(h('div', { class: 'muted', style: 'margin-top:8px;font-size:12px' }, 'Client connect/disconnect notifications are handled by the Pager alert payload system.')); - const modeRow = h('div', { class: 'pineap-title-card-container' }); - modeRow.appendChild(modeCard); - modeRow.appendChild(quickCard); - box.appendChild(modeRow); - const rfSel = h('select', {}, h('option', { value: 'uplink', text: 'Uplink (station)' }), h('option', { value: 'attack', text: 'Attack' }), @@ -523,9 +497,11 @@ views.pineap = (root) => { h('label', {}, 'Uplink Password', rfPsk))); rfCard.appendChild(rfApply); rfCard.appendChild(rfResult); - const rfRow = h('div', { class: 'pineap-title-card-container' }); - rfRow.appendChild(rfCard); - box.appendChild(rfRow); + + const modeRow = h('div', { class: 'pineap-title-card-container' }); + modeRow.appendChild(rfCard); + modeRow.appendChild(quickCard); + box.appendChild(modeRow); const cards = { karma: {}, open: {}, wpa: {} }; const cardWrap = h('div', { class: 'pineap-title-card-container' }); @@ -559,9 +535,6 @@ views.pineap = (root) => { } function rememberAdvanced(key, value) { PINEAP_SESSION[key] = value; - PINEAP_SESSION.mode = 'advanced'; - modeDirty = false; - selectMode('advanced', false); } bind(quick.collect, (v) => PagerAPI.post('/api/pineap/ssidpool/collect', { enable: v }), (v) => rememberAdvanced('collect', v)); @@ -569,80 +542,16 @@ views.pineap = (root) => { (v) => rememberAdvanced('advertise', v)); setKnownCheckbox(quick.advertise, PINEAP_SESSION.advertise); - function renderModeInfo(m) { - modeInfo.innerHTML = ''; - if (m === 'unknown') { - modeInfo.textContent = 'Select a mode to establish the Pager\'s PineAP preset.'; - return; - } - const descriptions = { - passive: ['Capture SSIDs to the impersonation pool', 'Do not broadcast the pool', 'Keep the PineAP response engine disabled'], - active: ['Capture SSIDs to the impersonation pool', 'Enable the PineAP response engine', 'Pool broadcast stays disabled (firmware crash fix)'] - }; - if (m === 'advanced') { - modeInfo.textContent = 'All supported PineAP features are individually customizable from Quick Settings and the PineAP tabs.'; - return; - } - modeInfo.appendChild(h('div', { text: 'In ' + m[0].toUpperCase() + m.slice(1) + ' Mode:' })); - const list = h('ul', { class: 'pineap-mode-features' }); - descriptions[m].forEach((text) => list.appendChild(h('li', { text }))); - modeInfo.appendChild(list); - } - - function selectMode(m, dirty) { - selectedMode = m; - if (dirty) modeDirty = true; - Object.keys(segBtns).forEach((k) => segBtns[k].classList.toggle('active', k === m)); - mode.textContent = m === 'unknown' ? 'Unknown' : m[0].toUpperCase() + m.slice(1); - mode.className = 'badge ' + (m === 'unknown' ? 'unknown' : 'on'); - renderModeInfo(m); - saveModeBtn.disabled = !modeDirty || modePending || m === 'unknown'; - } - - function saveMode() { - if (!modeDirty || modePending || selectedMode === 'unknown') return; - modePending = true; - saveModeBtn.disabled = true; - saveModeBtn.classList.add('busy'); - saveModeBtn.setAttribute('aria-busy', 'true'); - PagerAPI.post('/api/pineap/mode', { mode: selectedMode }).then((r) => { - const state = r.data || {}; - PINEAP_SESSION.mode = state.mode || selectedMode; - ['karma', 'advertise', 'collect'].forEach((key) => { - if (typeof state[key] === 'boolean') PINEAP_SESSION[key] = state[key]; - }); - modeDirty = false; - selectMode(PINEAP_SESSION.mode, false); - App.toast('Mode: ' + PINEAP_SESSION.mode[0].toUpperCase() + PINEAP_SESSION.mode.slice(1)); - load(); - }).catch(() => { App.toast('Failed to save PineAP mode', 'error'); }) - .finally(() => { - modePending = false; - saveModeBtn.classList.remove('busy'); - saveModeBtn.removeAttribute('aria-busy'); - saveModeBtn.disabled = !modeDirty; - }); - } - let loadPending = false; function load() { if (loadPending) return; loadPending = true; const stateRequest = Promise.all([ PagerAPI.get('/api/pineap/get_config').catch(() => ({ data: {} })), - PagerAPI.get('/api/pineap/hostapd').catch(() => ({ data: {} })), - PagerAPI.post('/api/pineap/wifi/get_ap').catch(() => ({ data: {} })), - PagerAPI.get('/api/pineap/mode').catch(() => ({ data: {} })) - ]).then(([cfg, host, ap, preset]) => { - const c = cfg.data || {}, hh = host.data || {}, a = ap.data || {}, p = preset.data || {}; - const disabled = Object.prototype.hasOwnProperty.call(hh, 'pineap_disabled') ? !!hh.pineap_disabled : null; + PagerAPI.post('/api/pineap/wifi/get_ap').catch(() => ({ data: {} })) + ]).then(([cfg, ap]) => { + const c = cfg.data || {}, a = ap.data || {}; const wpa = a.wpa || {}; - PINEAP_SESSION.mode = ['passive', 'active', 'advanced'].indexOf(p.mode) !== -1 ? p.mode : 'unknown'; - ['karma', 'advertise', 'collect'].forEach((key) => { - if (typeof p[key] === 'boolean') PINEAP_SESSION[key] = p[key]; - }); - if (!modeDirty) selectMode(PINEAP_SESSION.mode, false); - mode.className = 'badge ' + (disabled === true ? 'off' : disabled === false ? 'on' : 'unknown'); const collect = typeof PINEAP_SESSION.collect === 'boolean' ? PINEAP_SESSION.collect : Object.prototype.hasOwnProperty.call(c, 'autossidpool') ? !!c.autossidpool : null; diff --git a/tests/test_recon.py b/tests/test_recon.py index 2113e7d..9b7a269 100644 --- a/tests/test_recon.py +++ b/tests/test_recon.py @@ -1164,6 +1164,16 @@ class ReconReportTest(unittest.TestCase): self.assertIn('AE:77:C0:EB:31:41', text) self.assertIn('handshake', text) self.assertNotIn('ProbeOnlySSID', text) + def test_html_download_orders_strongest_signal_first(self): + with mock.patch.object(server, '_gps_status_data', return_value={'lock': False}): + status, payload = server.h_recon_scan_download_html(self._ctx(('1',))) + self.assertEqual(status, 200) + text = payload.data.decode('utf-8') + # Fixture: hidden AP at -64 dBm is stronger than Anderson-5 at -76 dBm. + strong = '50:6F:9A:01:00:00' + weak = 'C8:9E:43:64:80:80' + self.assertLess(text.index(strong), text.index(weak)) + def test_html_report_includes_gps_when_locked(self): with mock.patch.object(server, '_gps_status_data', return_value={'lock': True, 'lat': 37.7,