dashboard changes for Session tab
This commit is contained in:
@@ -450,6 +450,59 @@ th.group-active { color: var(--green-hi); cursor: pointer; }
|
||||
th.group-col { cursor: pointer; }
|
||||
th.group-col:hover { color: var(--text); }
|
||||
.nodes-multi { color: var(--green-hi); }
|
||||
.mono { font-family: monospace; font-size: 11px; }
|
||||
|
||||
/* ── Sessions ── */
|
||||
.sev-attack { border-left: 2px solid #c0392b; }
|
||||
.sev-suspicious { border-left: 2px solid #d4ac0d; }
|
||||
.sev-normal { border-left: 2px solid transparent; }
|
||||
|
||||
.sev-label { font-size: 10px; font-weight: 500; letter-spacing: 0.06em; text-transform: uppercase; }
|
||||
.sev-label.sev-attack { color: #e74c3c; }
|
||||
.sev-label.sev-suspicious { color: #f1c40f; }
|
||||
.sev-label.sev-normal { color: var(--text-muted); }
|
||||
|
||||
tr.session-row { cursor: pointer; }
|
||||
tr.session-row:hover td { background: var(--bg-hover); }
|
||||
|
||||
.expand-col { width: 24px; text-align: center; }
|
||||
.expand-btn { font-size: 9px; color: var(--text-muted); user-select: none; }
|
||||
|
||||
td.session-detail-cell { padding: 0; }
|
||||
.session-detail-inner {
|
||||
padding: 10px 16px 12px;
|
||||
background: var(--bg-panel);
|
||||
border-top: 1px solid var(--border);
|
||||
}
|
||||
.session-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
font-size: 11px;
|
||||
color: var(--text-dim);
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.session-meta strong { color: var(--text); }
|
||||
|
||||
.session-events-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 11px;
|
||||
}
|
||||
.session-events-table th {
|
||||
text-align: left;
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
padding: 3px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.session-events-table td {
|
||||
padding: 3px 8px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.session-events-table tr:last-child td { border-bottom: none; }
|
||||
|
||||
|
||||
/* ── Cross-node cells ── */
|
||||
.cn-best { color: var(--green-hi); font-weight: 500; }
|
||||
|
||||
@@ -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