sligh changed to dashbaord
This commit is contained in:
@@ -438,6 +438,96 @@ function renderPresenceView() {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Deauth heatmap ────────────────────────────────────────────────────────────
|
||||
function renderDeauthHeatmap(rows) {
|
||||
const canvas = document.getElementById('deauth-heatmap');
|
||||
const empty = document.getElementById('heatmap-empty');
|
||||
const subtitle = document.getElementById('heatmap-subtitle');
|
||||
|
||||
if (!rows.length) {
|
||||
canvas.style.display = 'none';
|
||||
empty.style.display = 'block';
|
||||
subtitle.textContent = '';
|
||||
return;
|
||||
}
|
||||
canvas.style.display = 'block';
|
||||
empty.style.display = 'none';
|
||||
|
||||
// Build lookup
|
||||
const lookup = {};
|
||||
let maxFrames = 0;
|
||||
rows.forEach(r => {
|
||||
if (!lookup[r.day]) lookup[r.day] = {};
|
||||
lookup[r.day][r.hour] = r.frames;
|
||||
if (r.frames > maxFrames) maxFrames = r.frames;
|
||||
});
|
||||
const days = Object.keys(lookup).sort();
|
||||
subtitle.textContent = `${days.length} days`;
|
||||
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const padL = 100, padT = 36, padR = 16, padB = 12;
|
||||
const cellH = 42;
|
||||
|
||||
// Fill container width exactly
|
||||
const containerW = canvas.parentElement.clientWidth;
|
||||
const cellW = Math.floor((containerW - padL - padR) / 24);
|
||||
|
||||
const W = padL + 24 * cellW + padR;
|
||||
const H = padT + days.length * cellH + padB;
|
||||
|
||||
// Set physical pixel size, CSS display size
|
||||
canvas.width = W * dpr;
|
||||
canvas.height = H * dpr;
|
||||
canvas.style.width = W + 'px';
|
||||
canvas.style.height = H + 'px';
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
ctx.scale(dpr, dpr);
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
|
||||
// Hour labels
|
||||
ctx.font = '11px Roboto, sans-serif';
|
||||
ctx.fillStyle = '#5a7a60';
|
||||
ctx.textAlign = 'center';
|
||||
for (let h = 0; h < 24; h++) {
|
||||
ctx.fillText(String(h).padStart(2, '0'), padL + h * cellW + cellW / 2, padT - 10);
|
||||
}
|
||||
|
||||
// Day rows
|
||||
days.forEach((day, di) => {
|
||||
const y = padT + di * cellH;
|
||||
|
||||
// Day label
|
||||
ctx.font = '11px Roboto, sans-serif';
|
||||
ctx.fillStyle = '#5a7a60';
|
||||
ctx.textAlign = 'right';
|
||||
ctx.fillText(day.slice(5), padL - 8, y + cellH / 2 + 4);
|
||||
|
||||
for (let h = 0; h < 24; h++) {
|
||||
const frames = (lookup[day] && lookup[day][h]) || 0;
|
||||
const x = padL + h * cellW;
|
||||
|
||||
if (frames === 0) {
|
||||
ctx.fillStyle = '#111a14';
|
||||
} else {
|
||||
const t = Math.min(frames / maxFrames, 1);
|
||||
const r = Math.round(80 + t * 151);
|
||||
const g = Math.round(20 + t * 10);
|
||||
const b = Math.round(20 + t * 10);
|
||||
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||
}
|
||||
ctx.fillRect(x + 1, y + 1, cellW - 2, cellH - 2);
|
||||
|
||||
if (frames > 0) {
|
||||
ctx.font = '11px Roboto, sans-serif';
|
||||
ctx.fillStyle = frames > maxFrames * 0.4 ? '#fff' : '#e74c3c';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.fillText(frames > 999 ? '999+' : frames, x + cellW / 2, y + cellH / 2 + 4);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Alerts view ───────────────────────────────────────────────────────────────
|
||||
async function fetchAlerts() {
|
||||
alertsData = await fetch('/api/alerts').then(r => r.json());
|
||||
@@ -471,6 +561,33 @@ function renderAlertsView() {
|
||||
<span>unique sources</span>
|
||||
</div>`;
|
||||
|
||||
// Reason code breakdown
|
||||
const reasonsTbody = document.getElementById('reasons-tbody');
|
||||
const reasonsEmpty = document.getElementById('reasons-empty');
|
||||
const reasonsCount = document.getElementById('reasons-count');
|
||||
const reasons = a.reason_stats || [];
|
||||
reasonsCount.textContent = `${reasons.length} codes`;
|
||||
if (!reasons.length) {
|
||||
reasonsTbody.innerHTML = '';
|
||||
reasonsEmpty.style.display = 'block';
|
||||
} else {
|
||||
reasonsEmpty.style.display = 'none';
|
||||
reasonsTbody.innerHTML = reasons.map(r => {
|
||||
const cls = `reason-${r.classification}`;
|
||||
const label = r.classification === 'attack' ? 'ATTACK'
|
||||
: r.classification === 'suspicious' ? 'SUSPICIOUS' : 'normal';
|
||||
return `<tr>
|
||||
<td class="muted">${r.reason}</td>
|
||||
<td>${r.description}</td>
|
||||
<td class="${cls}">${label}</td>
|
||||
<td class="${r.classification === 'attack' ? cls : ''}" style="${r.classification === 'attack' ? 'font-weight:500' : ''}">${r.total_frames}</td>
|
||||
<td class="muted">${r.unique_bssids}</td>
|
||||
<td class="muted">${r.unique_targets}</td>
|
||||
<td class="dim">${shortTime(r.last_seen)}</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
// Most targeted networks
|
||||
const targetsTbody = document.getElementById('targets-tbody');
|
||||
const targetsEmpty = document.getElementById('targets-empty');
|
||||
@@ -863,6 +980,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-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');
|
||||
document.getElementById('tab-networks').classList.toggle('active', view === 'networks' || view === 'network-chart');
|
||||
@@ -870,6 +988,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-analysis').classList.toggle('active', view === 'analysis');
|
||||
document.getElementById('tab-search').classList.toggle('active', view === 'search');
|
||||
}
|
||||
|
||||
@@ -891,6 +1010,12 @@ async function showPresence() { setView('presence'); await fetchPresence(); }
|
||||
|
||||
async function showAlerts() { setView('alerts'); await fetchAlerts(); }
|
||||
|
||||
async function showAnalysis() {
|
||||
setView('analysis');
|
||||
const data = await fetch('/api/alerts').then(r => r.json());
|
||||
requestAnimationFrame(() => renderDeauthHeatmap(data.heatmap || []));
|
||||
}
|
||||
|
||||
function showSearch() { setView('search'); document.getElementById('search-input').focus(); }
|
||||
|
||||
async function showDetail(node_id) {
|
||||
@@ -946,6 +1071,7 @@ function startSSE() {
|
||||
else if (currentView === 'crossnode') await fetchCrossNode();
|
||||
else if (currentView === 'presence') await fetchPresence();
|
||||
else if (currentView === 'alerts') await fetchAlerts();
|
||||
else if (currentView === 'analysis') await showAnalysis();
|
||||
|
||||
const nodes = await fetch('/api/nodes').then(r => r.json());
|
||||
nodeData = Object.fromEntries(nodes.map(n => [n.node_id, n]));
|
||||
|
||||
Reference in New Issue
Block a user