dashboard changes for Session tab
This commit is contained in:
@@ -11,6 +11,8 @@ let clientsData = [];
|
||||
let clientsGroupBy = null; // null | 'vendor' | 'ssid'
|
||||
let presenceData = {};
|
||||
let alertsData = {};
|
||||
let sessionsData = [];
|
||||
let sessionSort = { col: 'frame_count', dir: 'desc' };
|
||||
let chartBssid = null;
|
||||
let chartHours = 2;
|
||||
|
||||
@@ -570,6 +572,150 @@ function renderDeauthHeatmap(rows) {
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sessions view ─────────────────────────────────────────────────────────────
|
||||
function initSessionSort() {
|
||||
document.querySelectorAll('#sessions-table thead th.sortable').forEach(th => {
|
||||
th.style.cursor = 'pointer';
|
||||
th.addEventListener('click', () => {
|
||||
const col = th.dataset.col;
|
||||
if (sessionSort.col === col) {
|
||||
sessionSort.dir = sessionSort.dir === 'desc' ? 'asc' : 'desc';
|
||||
} else {
|
||||
sessionSort.col = col;
|
||||
sessionSort.dir = 'desc';
|
||||
}
|
||||
document.querySelectorAll('#sessions-table thead th.sortable').forEach(h => {
|
||||
h.classList.remove('sort-active');
|
||||
h.dataset.arrow = '';
|
||||
});
|
||||
th.classList.add('sort-active');
|
||||
th.dataset.arrow = sessionSort.dir === 'desc' ? '↓' : '↑';
|
||||
renderSessionsView();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchSessions() {
|
||||
sessionsData = await fetch('/api/sessions').then(r => r.json());
|
||||
renderSessionsView();
|
||||
}
|
||||
|
||||
function renderSessionsView() {
|
||||
const tbody = document.getElementById('sessions-tbody');
|
||||
const empty = document.getElementById('sessions-empty');
|
||||
const count = document.getElementById('sessions-count');
|
||||
|
||||
// Sort
|
||||
const { col, dir } = sessionSort;
|
||||
const sevOrder = { attack: 0, suspicious: 1, normal: 2 };
|
||||
const data = [...sessionsData].sort((a, b) => {
|
||||
let av, bv;
|
||||
if (col === 'bssids') { av = a.unique_bssids.length; bv = b.unique_bssids.length; }
|
||||
else if (col === 'targets') { av = a.unique_targets.length; bv = b.unique_targets.length; }
|
||||
else if (col === 'severity') { av = sevOrder[a.severity] ?? 9; bv = sevOrder[b.severity] ?? 9; }
|
||||
else if (col === 'peak_rssi') { av = a.peak_rssi ?? -200; bv = b.peak_rssi ?? -200; }
|
||||
else { av = a[col]; bv = b[col]; }
|
||||
if (av < bv) return dir === 'desc' ? 1 : -1;
|
||||
if (av > bv) return dir === 'desc' ? -1 : 1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
count.textContent = `${data.length} session${data.length !== 1 ? 's' : ''}`;
|
||||
|
||||
if (!data.length) {
|
||||
tbody.innerHTML = '';
|
||||
empty.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
empty.style.display = 'none';
|
||||
|
||||
tbody.innerHTML = '';
|
||||
|
||||
data.forEach((s, idx) => {
|
||||
const sevClass = `sev-${s.severity}`;
|
||||
const duration = s.duration_secs < 60
|
||||
? `${s.duration_secs}s`
|
||||
: `${Math.floor(s.duration_secs / 60)}m ${s.duration_secs % 60}s`;
|
||||
const vendor = s.src_randomized ? '<span class="muted">Randomized</span>' : (s.src_vendor || '<span class="muted">Unknown</span>');
|
||||
|
||||
const mainRow = document.createElement('tr');
|
||||
mainRow.className = `session-row ${sevClass}`;
|
||||
mainRow.dataset.idx = idx;
|
||||
mainRow.innerHTML = `
|
||||
<td class="expand-col"><span class="expand-btn">▶</span></td>
|
||||
<td class="mono">${s.src}</td>
|
||||
<td>${vendor}</td>
|
||||
<td>${s.start.replace('T', ' ')}</td>
|
||||
<td>${duration}</td>
|
||||
<td>${s.frame_count}</td>
|
||||
<td>${s.unique_bssids.length}</td>
|
||||
<td>${s.unique_targets.length}</td>
|
||||
<td>${s.node_count}</td>
|
||||
<td>${s.peak_rssi != null ? s.peak_rssi + ' dBm' : '—'}</td>
|
||||
<td><span class="sev-label ${sevClass}">${s.severity}</span></td>
|
||||
`;
|
||||
tbody.appendChild(mainRow);
|
||||
|
||||
// Build detail row
|
||||
const detailRow = document.createElement('tr');
|
||||
detailRow.className = 'session-detail';
|
||||
detailRow.dataset.sessionIdx = idx;
|
||||
|
||||
const bssidList = s.unique_bssids.join(', ') || '—';
|
||||
const targetList = s.unique_targets.join(', ') || '—';
|
||||
const nodeList = (s.nodes || []).join(', ') || '—';
|
||||
|
||||
let evRows = '';
|
||||
(s.events || []).forEach(ev => {
|
||||
evRows += `<tr>
|
||||
<td>${ev.received_at.replace('T', ' ')}</td>
|
||||
<td>${ev.node_id}</td>
|
||||
<td>${ev.subtype}</td>
|
||||
<td class="mono">${ev.src}</td>
|
||||
<td class="mono">${ev.dst}</td>
|
||||
<td class="mono">${ev.bssid}</td>
|
||||
<td>${ev.reason}</td>
|
||||
<td>${ev.rssi} dBm</td>
|
||||
</tr>`;
|
||||
});
|
||||
const truncNote = s.frame_count > 50
|
||||
? `<div class="muted" style="font-size:10px;margin-top:4px">Showing first 50 of ${s.frame_count} events</div>`
|
||||
: '';
|
||||
|
||||
detailRow.innerHTML = `
|
||||
<td colspan="11" class="session-detail-cell">
|
||||
<div class="session-detail-inner">
|
||||
<div class="session-meta">
|
||||
<span><strong>BSSIDs:</strong> ${bssidList}</span>
|
||||
<span><strong>Targets:</strong> ${targetList}</span>
|
||||
<span><strong>Nodes:</strong> ${nodeList}</span>
|
||||
<span><strong>Avg RSSI:</strong> ${s.avg_rssi != null ? s.avg_rssi + ' dBm' : '—'}</span>
|
||||
<span><strong>Reason:</strong> ${s.dominant_reason}</span>
|
||||
</div>
|
||||
<table class="session-events-table">
|
||||
<thead><tr>
|
||||
<th>Time</th><th>Node</th><th>Type</th><th>Src</th>
|
||||
<th>Dst</th><th>BSSID</th><th>Reason</th><th>RSSI</th>
|
||||
</tr></thead>
|
||||
<tbody>${evRows}</tbody>
|
||||
</table>
|
||||
${truncNote}
|
||||
</div>
|
||||
</td>
|
||||
`;
|
||||
|
||||
detailRow.style.display = 'none';
|
||||
|
||||
tbody.appendChild(detailRow);
|
||||
|
||||
mainRow.addEventListener('click', () => {
|
||||
const isOpen = detailRow.style.display !== 'none';
|
||||
detailRow.style.display = isOpen ? 'none' : '';
|
||||
mainRow.querySelector('.expand-btn').textContent = isOpen ? '▶' : '▼';
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ── Alerts view ───────────────────────────────────────────────────────────────
|
||||
async function fetchAlerts() {
|
||||
alertsData = await fetch('/api/alerts').then(r => r.json());
|
||||
@@ -1022,6 +1168,7 @@ function setView(view) {
|
||||
document.getElementById('view-detail').classList.toggle('active', view === 'detail');
|
||||
document.getElementById('view-presence').classList.toggle('active', view === 'presence');
|
||||
document.getElementById('view-alerts').classList.toggle('active', view === 'alerts');
|
||||
document.getElementById('view-sessions').classList.toggle('active', view === 'sessions');
|
||||
document.getElementById('view-analysis').classList.toggle('active', view === 'analysis');
|
||||
document.getElementById('view-search').classList.toggle('active', view === 'search');
|
||||
document.getElementById('tab-feed').classList.toggle('active', view === 'feed' || view === 'detail');
|
||||
@@ -1030,6 +1177,7 @@ function setView(view) {
|
||||
document.getElementById('tab-clients').classList.toggle('active', view === 'clients');
|
||||
document.getElementById('tab-presence').classList.toggle('active', view === 'presence');
|
||||
document.getElementById('tab-alerts').classList.toggle('active', view === 'alerts');
|
||||
document.getElementById('tab-sessions').classList.toggle('active', view === 'sessions');
|
||||
document.getElementById('tab-analysis').classList.toggle('active', view === 'analysis');
|
||||
document.getElementById('tab-search').classList.toggle('active', view === 'search');
|
||||
}
|
||||
@@ -1052,6 +1200,8 @@ async function showPresence() { setView('presence'); await fetchPresence(); }
|
||||
|
||||
async function showAlerts() { setView('alerts'); await fetchAlerts(); }
|
||||
|
||||
async function showSessions() { setView('sessions'); initSessionSort(); await fetchSessions(); }
|
||||
|
||||
async function showAnalysis() {
|
||||
setView('analysis');
|
||||
const data = await fetch('/api/alerts').then(r => r.json());
|
||||
|
||||
Reference in New Issue
Block a user