feat: live event notifications + dashboard status strip

The notification bell now receives event-driven alerts: new handshake
captures, new enterprise credentials, pineapd down/recovery and monitor
drops (15s poll, diffs against last-seen state). Dashboard gains an
Attacks / PineAPd / Recon live status row.
This commit is contained in:
2026-08-18 21:39:02 -05:00
parent 1327bec03c
commit 152178ce82
2 changed files with 87 additions and 1 deletions
@@ -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 = [