diff --git a/payload/user/remote_access/pager-webui/www/js/app.js b/payload/user/remote_access/pager-webui/www/js/app.js index 75cc2f8..c41d95a 100644 --- a/payload/user/remote_access/pager-webui/www/js/app.js +++ b/payload/user/remote_access/pager-webui/www/js/app.js @@ -435,6 +435,9 @@ const Live = (() => { let ever = false; let poll = null; let pollHealthTimer = null; + let pollEventsTimer = null; + const lastEvents = { hsSeen: {}, hsPrimed: false, creds: null, credsPrimed: false, + pineapUp: null, mon0: null, mon1: null }; const subs = []; let timer = null; function stopPoll() { @@ -447,6 +450,10 @@ const Live = (() => { pollHealthTimer = setInterval(pollHealth, 15000); pollHealth(); } + if (!pollEventsTimer) { + pollEventsTimer = setInterval(pollEvents, 15000); + pollEvents(); + } try { ws = new WebSocket(App.wsUrl('/api/ws')); } catch (e) { fallback(); return; } ws.onopen = () => { ever = true; }; @@ -509,8 +516,43 @@ const Live = (() => { el.textContent = 'PINEAP OK'; el.className = 'health-chip good'; } + const prev = lastEvents; + if (prev.pineapUp === false && h.pineap_up) { + toast('PineAPd recovered', 'success'); + } else if (prev.pineapUp === true && h.pineap_up === false) { + toast('PineAPd is down — health monitor is repairing it', 'error'); + } + lastEvents.pineapUp = !!h.pineap_up; + const monState = [h.wlan0mon_up, h.wlan1mon_up]; + if (prev.mon0 === true && monState[0] === false) toast('wlan0mon went down', 'error'); + if (prev.mon1 === true && monState[1] === false) toast('wlan1mon went down', 'error'); + lastEvents.mon0 = !!monState[0]; + lastEvents.mon1 = !!monState[1]; }).catch(() => {}); } + function pollEvents() { + Promise.all([ + fetch(App.apiBase + '/api/pineap/handshakes', { credentials: 'include' }).then((r) => r.json()).catch(() => ({})), + fetch(App.apiBase + '/api/attacks/status', { credentials: 'include' }).then((r) => r.json()).catch(() => ({})) + ]).then(([hs, atk]) => { + const files = (hs && hs.files) || []; + const fresh = files.filter((f) => !lastEvents.hsSeen[f.name]); + if (lastEvents.hsPrimed && fresh.length) { + fresh.forEach((f) => { + const bssid = (f.name.match(/^[0-9]+_([0-9A-F]+)_/) || [])[1] || ''; + toast('Handshake captured: ' + (bssid || f.name), 'success'); + }); + } + files.forEach((f) => { lastEvents.hsSeen[f.name] = true; }); + lastEvents.hsPrimed = true; + const creds = ((atk.enterprise || {}).creds) == null ? null : atk.enterprise.creds; + if (lastEvents.credsPrimed && creds !== null && creds > lastEvents.creds) { + toast('Enterprise credential captured (' + (creds - lastEvents.creds) + ' new)', 'success'); + } + if (creds !== null) lastEvents.creds = creds; + lastEvents.credsPrimed = true; + }).catch(() => {}); + } function onTick(fn) { subs.push(fn); return () => { 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 5a958ee..3f7e856 100644 --- a/payload/user/remote_access/pager-webui/www/js/views.js +++ b/payload/user/remote_access/pager-webui/www/js/views.js @@ -127,6 +127,50 @@ views.dashboard = (root) => { cards[k] = card.querySelector('.card-value'); }); + const live = h('div', { class: 'cards', style: 'margin-top:8px' }); + root.appendChild(live); + const liveCards = {}; + [['attacks', 'Attacks'], ['health', 'PineAPd / Monitors'], ['recon', 'Recon']] + .forEach(([k, label]) => { + const card = h('div', { class: 'card' }, + h('div', { class: 'card-label', text: label }), + h('div', { class: 'card-value', style: 'font-size:12px;line-height:1.8', text: 'Loading…' })); + live.appendChild(card); + liveCards[k] = card.querySelector('.card-value'); + }); + function loadLive() { + PagerAPI.get('/api/attacks/status').then((r) => { + const s = r.data || {}; + const st = (x) => (x && x.enabled ? (x.live ? 'LIVE' : 'ON') : 'off'); + const wpa = st(s.wpa && s.wpa.radio0) === 'LIVE' || st(s.wpa && s.wpa.radio1) === 'LIVE' + ? 'LIVE' : (st(s.wpa && s.wpa.radio0) !== 'off' || st(s.wpa && s.wpa.radio1) !== 'off' ? 'ON' : 'OFF'); + const open = st(s.open && s.open.radio0) === 'LIVE' || st(s.open && s.open.radio1) === 'LIVE' + ? 'LIVE' : (st(s.open && s.open.radio0) !== 'off' || st(s.open && s.open.radio1) !== 'off' ? 'ON' : 'OFF'); + const ent = s.enterprise && s.enterprise.ap + ? (s.enterprise.ap.live ? 'LIVE' : s.enterprise.ap.enabled ? 'ON' : 'OFF') : 'OFF'; + liveCards.attacks.textContent = 'WPA ' + wpa + ' · Open ' + open + ' · Ent ' + ent + + ' · HS ' + (s.handshakes || 0) + ' · creds ' + ((s.enterprise || {}).creds || 0); + liveCards.attacks.style.color = wpa === 'LIVE' || open === 'LIVE' || ent === 'LIVE' + ? 'var(--primary)' : ''; + }).catch(() => {}); + PagerAPI.get('/api/health').then((r) => { + const h2 = r.data || {}; + liveCards.health.textContent = (h2.pineap_up ? 'pineapd up' : 'pineapd DOWN') + + ' · wlan0mon ' + (h2.wlan0mon_up ? 'up' : 'down') + + ' · wlan1mon ' + (h2.wlan1mon_up ? 'up' : 'down') + + (h2.pool_disabled ? ' · pool off' : ''); + liveCards.health.style.color = h2.pineap_up ? '' : '#b71c1c'; + }).catch(() => {}); + PagerAPI.get('/api/recon/status').then((r) => { + const s = r.data || {}; + const last = s.last_activity || s.last_scan; + liveCards.recon.textContent = (s.scanning ? 'scanning' : 'idle') + + (last ? ' · last activity ' + fmtTime(last) : ''); + }).catch(() => {}); + } + loadLive(); + const liveIv = setInterval(loadLive, 10000); + const chartBox = h('div', { class: 'section' }, h('h2', {}, 'Clients'), h('canvas', { id: 'dash-chart', style: 'width:100%;height:140px' })); @@ -192,7 +236,7 @@ views.dashboard = (root) => { loadClients(); loadHandshakes(); const iv = setInterval(loadClients, 10000); - return { destroy: () => { clearInterval(iv); unsubscribe(); } }; + return { destroy: () => { clearInterval(iv); clearInterval(liveIv); unsubscribe(); } }; }; const PINEAP_TABS = [