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:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user