sligh changed to dashbaord

This commit is contained in:
bot
2026-04-05 11:29:59 +03:00
parent 5c0ec8cc5a
commit 2636ea1e11
10 changed files with 364 additions and 42 deletions
+126
View File
@@ -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]));