// ── State ────────────────────────────────────────────────────────────────────
const MAX_ROWS    = 100;
let generalEvents = [];
let currentNode   = null;
let currentView   = 'feed';
let nodeData      = {};
let networksData  = [];
let netSort       = { col: 'times_seen', dir: 'desc' };
let crossNodeData = { node_ids: [], networks: [] };
let clientsData   = [];
let presenceData  = {};
let alertsData    = {};
let chartBssid    = null;
let chartHours    = 2;

const NODE_COLORS = ['#4ec96a', '#e67e22', '#7eb3ff', '#c678dd'];

// ── Helpers ──────────────────────────────────────────────────────────────────
function fmt(val, fallback = '—') {
  return (val !== null && val !== undefined && val !== '') ? val : fallback;
}

function rssiClass(rssi) {
  if (rssi >= -50) return 'rssi-hi';
  if (rssi >= -80) return 'rssi-mid';
  return 'rssi-low';
}

function impBadge(imp) {
  const cls = ['high', 'normal', 'low'].includes(imp) ? imp : 'normal';
  return `<span class="badge ${cls}">${fmt(imp)}</span>`;
}

function shortTime(iso) {
  if (!iso) return '—';
  return iso.replace('T', ' ');
}

function fmtUptime(ms) {
  if (ms == null) return '—';
  const s = Math.floor(ms / 1000);
  const m = Math.floor(s / 60);
  const h = Math.floor(m / 60);
  const d = Math.floor(h / 24);
  if (d > 0)  return `${d}d ${h % 24}h`;
  if (h > 0)  return `${h}h ${m % 60}m`;
  if (m > 0)  return `${m}m`;
  return `${s}s`;
}

// ── Sidebar ──────────────────────────────────────────────────────────────────
function renderSidebar(nodes) {
  const online = nodes.filter(n => n.status === 'online').length;
  document.getElementById('sum-nodes').textContent  = nodes.length;
  document.getElementById('sum-online').textContent = online;

  document.getElementById('node-list').innerHTML = nodes.map(n => {
    const hbParts = [];
    if (n.uptime_ms  != null) hbParts.push(`↑${fmtUptime(n.uptime_ms)}`);
    if (n.free_heap  != null) hbParts.push(`${Math.round(n.free_heap / 1024)}K`);
    if (n.wifi_rssi  != null) hbParts.push(`${n.wifi_rssi}dBm`);
    const hbLine = hbParts.join(' · ');
    return `
    <div class="node-item ${currentNode === n.node_id ? 'active' : ''}"
         data-node-id="${n.node_id}"
         onclick="showDetail('${n.node_id}')">
      <div class="node-dot ${n.status}"></div>
      <div class="node-info">
        <div class="node-name">${n.node_id}</div>
        <div class="node-meta">${shortTime(n.last_seen)}</div>
        ${hbLine ? `<div class="node-hb">${hbLine}</div>` : ''}
      </div>
      <div class="node-count">${n.total_events}</div>
    </div>`;
  }).join('');
}

// ── Feed view ─────────────────────────────────────────────────────────────────
function renderGeneralTable(animate = false) {
  const tbody = document.getElementById('general-tbody');
  const empty = document.getElementById('general-empty');
  document.getElementById('general-count').textContent = `${generalEvents.length} events`;

  if (!generalEvents.length) {
    tbody.innerHTML = '';
    empty.style.display = 'block';
    return;
  }
  empty.style.display = 'none';

  if (animate) {
    const tr = document.createElement('tr');
    tr.className = 'new-row';
    tr.innerHTML = generalRowHTML(generalEvents[0]);
    tbody.prepend(tr);
    while (tbody.rows.length > MAX_ROWS) tbody.deleteRow(tbody.rows.length - 1);
  } else {
    tbody.innerHTML = generalEvents.map(generalRowHTML).join('');
  }
}

function generalRowHTML(r) {
  return `<tr>
    <td class="dim">${shortTime(r.received_at)}</td>
    <td>${fmt(r.node_id)}</td>
    <td>${fmt(r.ssid, '<hidden>')}</td>
    <td class="dim">${fmt(r.bssid)}</td>
    <td class="${rssiClass(r.rssi)}">${fmt(r.rssi)} dBm</td>
    <td class="muted">${fmt(r.channel)}</td>
    <td class="muted">${fmt(r.encryption)}</td>
    <td>${impBadge(r.importance)}</td>
  </tr>`;
}

// ── Node detail view ──────────────────────────────────────────────────────────
function renderDetail(d) {
  document.getElementById('detail-node-id').textContent = d.node_id;
  document.getElementById('ds-status').textContent      = d.status.toUpperCase();
  document.getElementById('ds-status').style.color      = d.status === 'online' ? 'var(--green-live)' : 'var(--text-muted)';
  document.getElementById('ds-total').textContent       = d.total_events;
  document.getElementById('ds-ssids').textContent       = d.unique_ssids;
  document.getElementById('ds-bssids').textContent      = d.unique_bssids;
  document.getElementById('ds-rssi-avg').textContent    = d.avg_rssi !== null ? `${d.avg_rssi} dBm` : '—';
  document.getElementById('ds-rssi-range').textContent  = d.min_rssi !== null ? `${d.min_rssi} / ${d.max_rssi} dBm` : '';
  document.getElementById('ds-first').textContent       = shortTime(d.first_seen);
  document.getElementById('ds-last').textContent        = shortTime(d.last_seen);
  document.getElementById('ds-uptime').textContent      = d.uptime_ms  != null ? fmtUptime(d.uptime_ms) : '—';
  document.getElementById('ds-heap').textContent        = d.free_heap  != null ? Math.round(d.free_heap / 1024) + ' KB' : '—';
  document.getElementById('ds-ap-rssi').textContent     = d.wifi_rssi  != null ? d.wifi_rssi + ' dBm' : '—';

  document.getElementById('detail-count').textContent = `${d.events.length} events (last ${MAX_ROWS})`;
  document.getElementById('detail-tbody').innerHTML = d.events.map(r => `
    <tr>
      <td class="dim">${shortTime(r.received_at)}</td>
      <td>${fmt(r.ssid, '<hidden>')}</td>
      <td class="dim">${fmt(r.bssid)}</td>
      <td class="${rssiClass(r.rssi)}">${fmt(r.rssi)} dBm</td>
      <td class="muted">${fmt(r.channel)}</td>
      <td class="muted">${fmt(r.encryption)}</td>
      <td>${impBadge(r.importance)}</td>
      <td class="muted">${fmt(r.confidence)}</td>
    </tr>`).join('');
}

// ── Networks view ─────────────────────────────────────────────────────────────
async function fetchNetworks() {
  networksData = await fetch('/api/networks').then(r => r.json());
  renderNetworksTable();
}

function renderNetworksTable() {
  const tbody = document.getElementById('networks-tbody');
  const empty = document.getElementById('networks-empty');
  const count = document.getElementById('networks-count');

  if (!networksData.length) {
    tbody.innerHTML = '';
    empty.style.display = 'block';
    count.textContent = '';
    return;
  }
  empty.style.display = 'none';

  const sorted = [...networksData].sort((a, b) => {
    let av = a[netSort.col], bv = b[netSort.col];
    if (av == null) av = netSort.dir === 'asc' ? Infinity : -Infinity;
    if (bv == null) bv = netSort.dir === 'asc' ? Infinity : -Infinity;
    if (typeof av === 'string') av = av.toLowerCase();
    if (typeof bv === 'string') bv = bv.toLowerCase();
    if (av < bv) return netSort.dir === 'asc' ? -1 : 1;
    if (av > bv) return netSort.dir === 'asc' ?  1 : -1;
    return 0;
  });

  count.textContent = `${sorted.length} unique networks`;
  tbody.innerHTML = sorted.map(r => {
    const encCls  = (!r.encryption || r.encryption === 'OPEN' || r.encryption === 'None') ? 'enc-open' : 'muted';
    const nodeCls = r.node_count > 1 ? 'nodes-multi' : 'muted';
    return `<tr>
      <td>${fmt(r.ssid, '<hidden>')}</td>
      <td class="dim">${fmt(r.bssid)}</td>
      <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
      <td class="${rssiClass(r.avg_rssi)}">${fmt(r.avg_rssi)} dBm</td>
      <td class="muted">${fmt(r.channel)}</td>
      <td class="${encCls}">${r.encryption || 'OPEN'}</td>
      <td>${fmt(r.times_seen)}</td>
      <td class="${nodeCls}">${fmt(r.node_count)}</td>
      <td class="dim">${shortTime(r.last_seen)}</td>
      <td><button class="chart-btn" onclick="showNetworkChart('${r.bssid}')">&#9654;</button></td>
    </tr>`;
  }).join('');
}

function initNetworkSort() {
  document.querySelectorAll('#view-networks thead th.sortable').forEach(th => {
    th.addEventListener('click', () => {
      const col = th.dataset.col;
      if (netSort.col === col) {
        netSort.dir = netSort.dir === 'desc' ? 'asc' : 'desc';
      } else {
        netSort.col = col;
        netSort.dir = ['ssid', 'bssid', 'encryption'].includes(col) ? 'asc' : 'desc';
      }
      document.querySelectorAll('#view-networks thead th').forEach(h => {
        h.classList.remove('sort-active');
        h.dataset.arrow = '';
      });
      th.classList.add('sort-active');
      th.dataset.arrow = netSort.dir === 'desc' ? '↓' : '↑';
      renderNetworksTable();
    });
  });
}

// ── Cross-node view ───────────────────────────────────────────────────────────
async function fetchCrossNode() {
  crossNodeData = await fetch('/api/cross-node').then(r => r.json());
  renderCrossNodeTable();
}

function renderCrossNodeTable() {
  const thead  = document.getElementById('crossnode-thead');
  const tbody  = document.getElementById('crossnode-tbody');
  const empty  = document.getElementById('crossnode-empty');
  const count  = document.getElementById('crossnode-count');
  const filter = document.getElementById('crossnode-filter').checked;
  const nodeIds = crossNodeData.node_ids || [];

  let networks = crossNodeData.networks || [];
  if (filter) networks = networks.filter(n => n.node_count > 1);

  if (!networks.length) {
    thead.innerHTML = tbody.innerHTML = '';
    empty.style.display = 'block';
    count.textContent = '';
    return;
  }
  empty.style.display = 'none';
  count.textContent = `${networks.length} networks`;

  thead.innerHTML = `
    <tr>
      <th>SSID</th><th>BSSID</th><th>Ch</th><th>Enc</th>
      ${nodeIds.map(id => `<th colspan="2" style="text-align:center;border-left:1px solid var(--border)">${id}</th>`).join('')}
      <th>Nodes</th>
    </tr>
    <tr>
      <th></th><th></th><th></th><th></th>
      ${nodeIds.map(() => `
        <th style="border-left:1px solid var(--border);font-size:8px">best</th>
        <th style="font-size:8px">avg</th>
      `).join('')}
      <th></th>
    </tr>`;

  tbody.innerHTML = networks.map(net => {
    const seenRssis = nodeIds.filter(id => net.nodes[id]).map(id => net.nodes[id].best_rssi);
    const globalBest = seenRssis.length ? Math.max(...seenRssis) : null;
    const encCls  = (!net.encryption || net.encryption === 'OPEN' || net.encryption === 'None') ? 'enc-open' : 'muted';
    const nodeCls = net.node_count > 1 ? 'nodes-multi' : 'muted';

    const nodeCells = nodeIds.map(id => {
      if (!net.nodes[id]) return `<td class="cn-none" style="border-left:1px solid var(--border)">—</td><td class="cn-none">—</td>`;
      const n = net.nodes[id];
      const isBest  = n.best_rssi === globalBest && net.node_count > 1;
      const bestCls = isBest ? 'cn-best' : rssiClass(n.best_rssi);
      return `<td class="${bestCls}" style="border-left:1px solid var(--border)">${n.best_rssi} dBm</td>
              <td class="${isBest ? 'cn-best' : 'dim'}">${n.avg_rssi} dBm</td>`;
    }).join('');

    return `<tr>
      <td>${fmt(net.ssid, '<hidden>')}</td>
      <td class="dim">${fmt(net.bssid)}</td>
      <td class="muted">${fmt(net.channel)}</td>
      <td class="${encCls}">${net.encryption || 'OPEN'}</td>
      ${nodeCells}
      <td class="${nodeCls}">${net.node_count} / ${nodeIds.length}</td>
    </tr>`;
  }).join('');
}

// ── Clients view ──────────────────────────────────────────────────────────────
async function fetchClients() {
  clientsData = await fetch('/api/clients').then(r => r.json());
  renderClientsTable();
}

function renderClientsTable() {
  const tbody = document.getElementById('clients-tbody');
  const empty = document.getElementById('clients-empty');
  const count = document.getElementById('clients-count');

  if (!clientsData.length) {
    tbody.innerHTML = '';
    empty.style.display = 'block';
    count.textContent = '';
    return;
  }
  empty.style.display = 'none';
  count.textContent = `${clientsData.length} unique MACs`;

  tbody.innerHTML = clientsData.map(r => {
    const probing    = r.probed_ssids.length
      ? r.probed_ssids.map(s => `<span style="color:var(--text-dim)">${s}</span>`).join(', ')
      : '<span style="color:var(--text-muted)">wildcard only</span>';
    const vendorCls  = r.randomized ? 'muted' : 'dim';
    const nodeCls    = r.node_count > 1 ? 'nodes-multi' : 'muted';
    return `<tr>
      <td>${fmt(r.src_mac)}</td>
      <td class="${vendorCls}">${r.vendor || '—'}</td>
      <td style="max-width:260px;white-space:normal;line-height:1.8">${probing}</td>
      <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
      <td class="${rssiClass(r.avg_rssi)}">${fmt(r.avg_rssi)} dBm</td>
      <td class="${nodeCls}">${fmt(r.node_count)}</td>
      <td>${fmt(r.times_seen)}</td>
      <td class="dim">${shortTime(r.last_seen)}</td>
    </tr>`;
  }).join('');
}

// ── Presence view ─────────────────────────────────────────────────────────────
async function fetchPresence() {
  presenceData = await fetch('/api/presence').then(r => r.json());
  renderPresenceView();
}

function renderPresenceView() {
  const p = presenceData;

  // Present Now
  const presentTbody = document.getElementById('present-tbody');
  const presentEmpty = document.getElementById('present-empty');
  const presentCount = document.getElementById('present-count');
  const present = p.present || [];
  presentCount.textContent = `${present.length} devices`;
  if (!present.length) {
    presentTbody.innerHTML = '';
    presentEmpty.style.display = 'block';
  } else {
    presentEmpty.style.display = 'none';
    presentTbody.innerHTML = present.map(r => {
      const nodeCls = r.node_count > 1 ? 'nodes-multi' : 'muted';
      return `<tr>
        <td>${fmt(r.src_mac)}</td>
        <td class="dim">${r.vendor || '—'}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td class="${rssiClass(r.avg_rssi)}">${fmt(r.avg_rssi)} dBm</td>
        <td>${fmt(r.times_seen)}</td>
        <td class="${nodeCls}">${fmt(r.node_count)}</td>
        <td class="dim">${shortTime(r.last_seen)}</td>
      </tr>`;
    }).join('');
  }

  // New Arrivals — devices
  const arrMacsTbody = document.getElementById('arrivals-macs-tbody');
  const arrMacsEmpty = document.getElementById('arrivals-macs-empty');
  const arrCount     = document.getElementById('arrivals-count');
  const newMacs = p.new_macs || [];
  const newNets = p.new_networks || [];
  arrCount.textContent = `${newMacs.length} devices · ${newNets.length} networks`;
  if (!newMacs.length) {
    arrMacsTbody.innerHTML = '';
    arrMacsEmpty.style.display = 'block';
  } else {
    arrMacsEmpty.style.display = 'none';
    arrMacsTbody.innerHTML = newMacs.map(r => `<tr>
      <td>${fmt(r.src_mac)}</td>
      <td class="dim">${r.vendor || '—'}</td>
      <td class="dim">${shortTime(r.first_seen)}</td>
      <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
      <td>${fmt(r.times_seen)}</td>
    </tr>`).join('');
  }

  // New Arrivals — networks
  const arrNetsTbody = document.getElementById('arrivals-nets-tbody');
  const arrNetsEmpty = document.getElementById('arrivals-nets-empty');
  if (!newNets.length) {
    arrNetsTbody.innerHTML = '';
    arrNetsEmpty.style.display = 'block';
  } else {
    arrNetsEmpty.style.display = 'none';
    arrNetsTbody.innerHTML = newNets.map(r => {
      const encCls = (!r.encryption || r.encryption === 'OPEN' || r.encryption === 'None') ? 'enc-open' : 'muted';
      return `<tr>
        <td>${fmt(r.ssid, '<hidden>')}</td>
        <td class="dim">${fmt(r.bssid)}</td>
        <td class="dim">${shortTime(r.first_seen)}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td class="muted">${fmt(r.channel)}</td>
        <td class="${encCls}">${r.encryption || 'OPEN'}</td>
      </tr>`;
    }).join('');
  }

  // Regulars — devices
  const regMacsTbody = document.getElementById('regulars-macs-tbody');
  const regMacsEmpty = document.getElementById('regulars-macs-empty');
  const regCount     = document.getElementById('regulars-count');
  const regMacs = p.regular_macs || [];
  const regNets = p.regular_networks || [];
  regCount.textContent = `${regMacs.length} devices · ${regNets.length} networks`;
  if (!regMacs.length) {
    regMacsTbody.innerHTML = '';
    regMacsEmpty.style.display = 'block';
  } else {
    regMacsEmpty.style.display = 'none';
    regMacsTbody.innerHTML = regMacs.map(r => `<tr>
      <td>${fmt(r.src_mac)}</td>
      <td class="dim">${r.vendor || '—'}</td>
      <td>${fmt(r.days_seen)}</td>
      <td class="dim">${shortTime(r.first_seen)}</td>
      <td class="dim">${shortTime(r.last_seen)}</td>
      <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
    </tr>`).join('');
  }

  // Regulars — networks
  const regNetsTbody = document.getElementById('regulars-nets-tbody');
  const regNetsEmpty = document.getElementById('regulars-nets-empty');
  if (!regNets.length) {
    regNetsTbody.innerHTML = '';
    regNetsEmpty.style.display = 'block';
  } else {
    regNetsEmpty.style.display = 'none';
    regNetsTbody.innerHTML = regNets.map(r => {
      const encCls = (!r.encryption || r.encryption === 'OPEN' || r.encryption === 'None') ? 'enc-open' : 'muted';
      return `<tr>
        <td>${fmt(r.ssid, '<hidden>')}</td>
        <td class="dim">${fmt(r.bssid)}</td>
        <td>${fmt(r.days_seen)}</td>
        <td class="dim">${shortTime(r.first_seen)}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td class="muted">${fmt(r.channel)}</td>
      </tr>`;
    }).join('');
  }
}

// ── 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());
  renderAlertsView();
}

function renderAlertsView() {
  const a = alertsData;
  const sum = a.summary || {};

  // Summary strip
  document.getElementById('alerts-summary').innerHTML = `
    <div class="stat ${sum.total > 0 ? 'alert' : ''}">
      <span class="val">${sum.total ?? 0}</span>
      <span>events (last 1h)</span>
    </div>
    <div class="stat">
      <span class="val">${sum.deauth_count ?? 0}</span>
      <span>deauth</span>
    </div>
    <div class="stat">
      <span class="val">${sum.disassoc_count ?? 0}</span>
      <span>disassoc</span>
    </div>
    <div class="stat">
      <span class="val">${sum.unique_bssids ?? 0}</span>
      <span>unique BSSIDs</span>
    </div>
    <div class="stat">
      <span class="val">${sum.unique_srcs ?? 0}</span>
      <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');
  const targetsCount = document.getElementById('targets-count');
  const topTargets   = a.top_targets || [];
  targetsCount.textContent = `${topTargets.length}`;
  if (!topTargets.length) {
    targetsTbody.innerHTML = '';
    targetsEmpty.style.display = 'block';
  } else {
    targetsEmpty.style.display = 'none';
    targetsTbody.innerHTML = topTargets.map((t, i) => `<tr>
      <td class="dim">${fmt(t.bssid)}</td>
      <td>${fmt(t.ssid, '<hidden>')}</td>
      <td style="color:#e74c3c;font-weight:500">${t.total_frames}</td>
      <td class="muted">${t.unique_srcs}</td>
      <td class="${t.node_count > 1 ? 'nodes-multi' : 'muted'}">${t.node_count}</td>
      <td class="dim">${shortTime(t.last_seen)}</td>
    </tr>`).join('');
  }

  // Most active attackers
  const attackersTbody = document.getElementById('attackers-tbody');
  const attackersEmpty = document.getElementById('attackers-empty');
  const attackersCount = document.getElementById('attackers-count');
  const topTargetedDevices = a.top_targeted_devices || [];
  attackersCount.textContent = `${topTargetedDevices.length}`;
  if (!topTargetedDevices.length) {
    attackersTbody.innerHTML = '';
    attackersEmpty.style.display = 'block';
  } else {
    attackersEmpty.style.display = 'none';
    attackersTbody.innerHTML = topTargetedDevices.map(d => {
      const vendorCls = d.randomized ? 'muted' : 'dim';
      return `<tr>
        <td class="dim">${fmt(d.dst)}</td>
        <td class="${vendorCls}">${d.vendor || '—'}</td>
        <td style="color:#e74c3c;font-weight:500">${d.total_frames}</td>
        <td class="muted">${d.networks_used}</td>
        <td class="${d.node_count > 1 ? 'nodes-multi' : 'muted'}">${d.node_count}</td>
        <td class="dim">${shortTime(d.last_seen)}</td>
      </tr>`;
    }).join('');
  }

  // Bursts
  const burstsTbody = document.getElementById('bursts-tbody');
  const burstsEmpty = document.getElementById('bursts-empty');
  const burstsCount = document.getElementById('bursts-count');
  const bursts = a.bursts || [];
  burstsCount.textContent = bursts.length ? `${bursts.length} active` : '';
  if (!bursts.length) {
    burstsTbody.innerHTML = '';
    burstsEmpty.style.display = 'block';
  } else {
    burstsEmpty.style.display = 'none';
    burstsTbody.innerHTML = bursts.map(b => {
      const multiNode = b.node_count > 1;
      const rowCls    = multiNode ? 'burst-row burst-multi' : 'burst-row';
      const nodeCls   = multiNode ? 'burst-multi' : 'muted';
      const typeCls   = b.subtype === 'deauth' ? 'deauth-type' : 'disassoc-type';
      return `<tr class="${rowCls}">
        <td class="dim">${fmt(b.bssid)}</td>
        <td>${fmt(b.ssid, '<hidden>')}</td>
        <td class="${typeCls}">${b.subtype}</td>
        <td style="color:#e74c3c;font-weight:500">${b.count}</td>
        <td class="muted">${b.unique_srcs}</td>
        <td class="${nodeCls}">${b.node_count}</td>
        <td class="dim">${shortTime(b.first_seen)}</td>
        <td class="dim">${shortTime(b.last_seen)}</td>
      </tr>`;
    }).join('');
  }

  // Raw deauth feed
  const deauthTbody = document.getElementById('deauth-tbody');
  const deauthEmpty = document.getElementById('deauth-empty');
  const deauthCount = document.getElementById('deauth-count');
  const recent = a.recent || [];
  deauthCount.textContent = `${recent.length} events`;
  if (!recent.length) {
    deauthTbody.innerHTML = '';
    deauthEmpty.style.display = 'block';
  } else {
    deauthEmpty.style.display = 'none';
    deauthTbody.innerHTML = recent.map(r => {
      const typeCls = r.subtype === 'deauth' ? 'deauth-type' : 'disassoc-type';
      return `<tr>
        <td class="dim">${shortTime(r.received_at)}</td>
        <td class="muted">${fmt(r.node_id)}</td>
        <td class="${typeCls}">${r.subtype}</td>
        <td class="dim">${fmt(r.src)}</td>
        <td class="dim">${fmt(r.dst)}</td>
        <td class="dim">${fmt(r.bssid)}</td>
        <td class="muted">${fmt(r.reason)}</td>
        <td class="${rssiClass(r.rssi)}">${fmt(r.rssi)} dBm</td>
      </tr>`;
    }).join('');
  }
}

// ── RSSI chart ────────────────────────────────────────────────────────────────
async function showNetworkChart(bssid) {
  chartBssid = bssid;
  const net = networksData.find(n => n.bssid === bssid);
  document.getElementById('chart-title').textContent    = fmt(net?.ssid, '<hidden>');
  document.getElementById('chart-subtitle').textContent = bssid;
  setView('network-chart');
  // requestAnimationFrame ensures the view is laid out before we read canvas.offsetWidth
  requestAnimationFrame(() => loadAndRenderChart());
}

async function setChartRange(hours) {
  chartHours = hours;
  document.querySelectorAll('.range-btn').forEach(b => {
    b.classList.toggle('active', parseInt(b.dataset.hours) === hours);
  });
  await loadAndRenderChart();
}

async function loadAndRenderChart() {
  const data = await fetch(`/api/rssi-history?bssid=${encodeURIComponent(chartBssid)}&hours=${chartHours}`).then(r => r.json());
  renderRssiChart(data);
}

function renderRssiChart(series) {
  const canvas = document.getElementById('rssi-chart');
  const empty  = document.getElementById('chart-empty');
  const legend = document.getElementById('chart-legend');
  const nodeIds = Object.keys(series).sort();
  const allPts  = nodeIds.flatMap(id => series[id]);

  if (!allPts.length) {
    canvas.style.display = 'none';
    empty.style.display  = 'block';
    legend.innerHTML     = '';
    return;
  }
  canvas.style.display = 'block';
  empty.style.display  = 'none';

  const W = canvas.offsetWidth;
  const H = canvas.height;
  canvas.width = W;
  const ctx = canvas.getContext('2d');

  const pad = { top: 20, right: 16, bottom: 36, left: 52 };
  const pw  = W - pad.left - pad.right;
  const ph  = H - pad.top  - pad.bottom;

  const now   = Date.now();
  const tStart = now - chartHours * 3600000;

  const rssis = allPts.map(p => p.rssi);
  const minR  = Math.floor((Math.min(...rssis) - 5) / 5) * 5;
  const maxR  = Math.ceil ((Math.max(...rssis) + 5) / 5) * 5;
  const rng   = maxR - minR || 1;

  const xMap = t => pad.left + (new Date(t + 'Z').getTime() - tStart) / (now - tStart) * pw;
  const yMap = r => pad.top  + (1 - (r - minR) / rng) * ph;

  ctx.clearRect(0, 0, W, H);
  ctx.font = '10px IBM Plex Mono';

  // Horizontal gridlines + Y labels
  const step = rng <= 20 ? 5 : rng <= 50 ? 10 : 20;
  for (let r = minR; r <= maxR; r += step) {
    const y = yMap(r);
    ctx.strokeStyle = '#1e3024'; ctx.lineWidth = 1;
    ctx.setLineDash([3, 4]);
    ctx.beginPath(); ctx.moveTo(pad.left, y); ctx.lineTo(pad.left + pw, y); ctx.stroke();
    ctx.setLineDash([]);
    ctx.fillStyle = '#3a5040'; ctx.textAlign = 'right';
    ctx.fillText(`${r}`, pad.left - 6, y + 3);
  }

  // X axis time ticks
  const ticks = Math.min(6, Math.floor(pw / 80));
  ctx.fillStyle = '#3a5040'; ctx.textAlign = 'center';
  for (let i = 0; i <= ticks; i++) {
    const t = tStart + (i / ticks) * (now - tStart);
    const x = pad.left + (i / ticks) * pw;
    ctx.fillText(new Date(t).toTimeString().slice(0, 5), x, H - 8);
    ctx.strokeStyle = '#1e3024'; ctx.lineWidth = 1;
    ctx.beginPath(); ctx.moveTo(x, pad.top + ph); ctx.lineTo(x, pad.top + ph + 4); ctx.stroke();
  }

  // Plot border
  ctx.strokeStyle = '#1e3024'; ctx.lineWidth = 1; ctx.setLineDash([]);
  ctx.beginPath();
  ctx.moveTo(pad.left, pad.top);
  ctx.lineTo(pad.left, pad.top + ph);
  ctx.lineTo(pad.left + pw, pad.top + ph);
  ctx.stroke();

  // Series
  nodeIds.forEach((nid, i) => {
    const pts   = series[nid];
    const color = NODE_COLORS[i % NODE_COLORS.length];
    ctx.strokeStyle = color; ctx.lineWidth = 1.5; ctx.setLineDash([]);
    ctx.beginPath();
    pts.forEach((p, j) => {
      const x = xMap(p.t), y = yMap(p.rssi);
      j === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
    });
    ctx.stroke();
    ctx.fillStyle = color;
    pts.forEach(p => {
      ctx.beginPath();
      ctx.arc(xMap(p.t), yMap(p.rssi), 2.5, 0, Math.PI * 2);
      ctx.fill();
    });
  });

  // Legend
  legend.innerHTML = nodeIds.map((nid, i) => `
    <div class="legend-item">
      <div class="legend-swatch" style="background:${NODE_COLORS[i % NODE_COLORS.length]}"></div>
      <span>${nid}</span>
    </div>`).join('');
}

// ── Search view ───────────────────────────────────────────────────────────────
async function doSearch() {
  const q = document.getElementById('search-input').value.trim();
  if (q.length < 2) return;
  const data = await fetch(`/api/search?q=${encodeURIComponent(q)}`).then(r => r.json());
  renderSearchResults(data, q);
}

function renderSearchResults(data, q) {
  const container = document.getElementById('search-results');
  const empty     = document.getElementById('search-empty');
  container.querySelectorAll('.search-section').forEach(el => el.remove());

  if (!Object.keys(data).length) {
    empty.textContent    = `No data found for "${q}".`;
    empty.style.display  = 'block';
    return;
  }
  empty.style.display = 'none';

  // Helper to append a section
  function addSection(title, html) {
    const div = document.createElement('div');
    div.className = 'search-section';
    div.innerHTML = `<div class="search-section-label">${title}</div>${html}`;
    container.appendChild(div);
  }

  // As a network
  if (data.as_network) {
    const rows = data.as_network.map(r => {
      const encCls = (!r.encryption || r.encryption === 'OPEN') ? 'enc-open' : 'muted';
      return `<tr>
        <td>${fmt(r.ssid, '<hidden>')}</td>
        <td class="dim">${fmt(r.bssid)}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td class="muted">${fmt(r.channel)}</td>
        <td class="${encCls}">${r.encryption || 'OPEN'}</td>
        <td>${fmt(r.times_seen)}</td>
        <td class="${r.node_count > 1 ? 'nodes-multi' : 'muted'}">${r.node_count}</td>
        <td class="dim">${shortTime(r.first_seen)}</td>
        <td class="dim">${shortTime(r.last_seen)}</td>
      </tr>`;
    }).join('');
    addSection('As a Network', `<div class="tbl-wrap"><table>
      <thead><tr><th>SSID</th><th>BSSID</th><th>Best RSSI</th><th>Ch</th><th>Enc</th><th>Times Seen</th><th>Nodes</th><th>First Seen</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }

  // As a client device
  if (data.as_client) {
    const rows = data.as_client.map(r => {
      const probing = r.probed_ssids.length
        ? r.probed_ssids.join(', ')
        : '<span style="color:var(--text-muted)">wildcard only</span>';
      return `<tr>
        <td>${fmt(r.src_mac)}</td>
        <td class="${r.randomized ? 'muted' : 'dim'}">${r.vendor || '—'}</td>
        <td style="white-space:normal;max-width:300px;line-height:1.8">${probing}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td>${fmt(r.times_seen)}</td>
        <td class="dim">${shortTime(r.first_seen)}</td>
        <td class="dim">${shortTime(r.last_seen)}</td>
      </tr>`;
    }).join('');
    addSection('As a Client Device (Probe Requests)', `<div class="tbl-wrap"><table>
      <thead><tr><th>MAC</th><th>Vendor</th><th>Probing For</th><th>Best RSSI</th><th>Times Seen</th><th>First Seen</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }

  // Devices probing for this SSID
  if (data.probing_for) {
    const rows = data.probing_for.map(r => `<tr>
      <td>${fmt(r.src_mac)}</td>
      <td class="${r.randomized ? 'muted' : 'dim'}">${r.vendor || '—'}</td>
      <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
      <td>${fmt(r.times_seen)}</td>
      <td class="dim">${shortTime(r.first_seen)}</td>
      <td class="dim">${shortTime(r.last_seen)}</td>
    </tr>`).join('');
    addSection('Devices Probing for This SSID', `<div class="tbl-wrap"><table>
      <thead><tr><th>MAC</th><th>Vendor</th><th>Best RSSI</th><th>Times Seen</th><th>First Seen</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }

  // As a deauth victim
  if (data.as_victim) {
    const rows = data.as_victim.map(r => `<tr>
      <td>${fmt(r.dst)}</td>
      <td class="${r.randomized ? 'muted' : 'dim'}">${r.vendor || '—'}</td>
      <td style="color:#e74c3c;font-weight:500">${r.total_frames}</td>
      <td class="muted">${r.networks_used}</td>
      <td class="${r.node_count > 1 ? 'nodes-multi' : 'muted'}">${r.node_count}</td>
      <td class="dim">${shortTime(r.first_seen)}</td>
      <td class="dim">${shortTime(r.last_seen)}</td>
    </tr>`).join('');
    addSection('As a Deauth Victim', `<div class="tbl-wrap"><table>
      <thead><tr><th>MAC</th><th>Vendor</th><th>Frames Received</th><th>Networks Used</th><th>Nodes</th><th>First Seen</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }

  // Vendor — client devices
  if (data.vendor_clients) {
    const label = data.vendor_name || q;
    const rows = data.vendor_clients.map(r => {
      const probing = r.probed_ssids.length
        ? r.probed_ssids.join(', ')
        : '<span style="color:var(--text-muted)">wildcard only</span>';
      return `<tr>
        <td>${fmt(r.src_mac)}</td>
        <td class="${r.randomized ? 'muted' : 'dim'}">${r.vendor || '—'}</td>
        <td style="white-space:normal;max-width:300px;line-height:1.8">${probing}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td>${fmt(r.times_seen)}</td>
        <td class="dim">${shortTime(r.last_seen)}</td>
      </tr>`;
    }).join('');
    addSection(`Vendor Match — Client Devices (${label})`, `<div class="tbl-wrap"><table>
      <thead><tr><th>MAC</th><th>Vendor</th><th>Probing For</th><th>Best RSSI</th><th>Times Seen</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }

  // Vendor — networks / APs
  if (data.vendor_networks) {
    const label = data.vendor_name || q;
    const rows = data.vendor_networks.map(r => {
      const encCls = (!r.encryption || r.encryption === 'OPEN') ? 'enc-open' : 'muted';
      return `<tr>
        <td>${fmt(r.ssid, '<hidden>')}</td>
        <td class="dim">${fmt(r.bssid)}</td>
        <td class="${rssiClass(r.best_rssi)}">${fmt(r.best_rssi)} dBm</td>
        <td class="muted">${fmt(r.channel)}</td>
        <td class="${encCls}">${r.encryption || 'OPEN'}</td>
        <td>${fmt(r.times_seen)}</td>
        <td class="${r.node_count > 1 ? 'nodes-multi' : 'muted'}">${r.node_count}</td>
        <td class="dim">${shortTime(r.last_seen)}</td>
      </tr>`;
    }).join('');
    addSection(`Vendor Match — Networks / APs (${label})`, `<div class="tbl-wrap"><table>
      <thead><tr><th>SSID</th><th>BSSID</th><th>Best RSSI</th><th>Ch</th><th>Enc</th><th>Times Seen</th><th>Nodes</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }

  // As an impersonated AP
  if (data.as_impersonated) {
    const rows = data.as_impersonated.map(r => `<tr>
      <td class="dim">${fmt(r.bssid)}</td>
      <td style="color:#e74c3c;font-weight:500">${r.total_frames}</td>
      <td class="muted">${r.unique_targets}</td>
      <td class="${r.node_count > 1 ? 'nodes-multi' : 'muted'}">${r.node_count}</td>
      <td class="dim">${shortTime(r.first_seen)}</td>
      <td class="dim">${shortTime(r.last_seen)}</td>
    </tr>`).join('');
    addSection('As an Impersonated AP (Deauth Source)', `<div class="tbl-wrap"><table>
      <thead><tr><th>BSSID</th><th>Frames Sent</th><th>Unique Targets</th><th>Nodes</th><th>First Seen</th><th>Last Seen</th></tr></thead>
      <tbody>${rows}</tbody></table></div>`);
  }
}

// ── View switching ────────────────────────────────────────────────────────────
function setView(view) {
  currentView = view;
  document.getElementById('view-general').classList.toggle('active',         view === 'feed');
  document.getElementById('view-networks').classList.toggle('active',        view === 'networks');
  document.getElementById('view-network-chart').classList.toggle('active',   view === 'network-chart');
  document.getElementById('view-crossnode').classList.toggle('active',       view === 'crossnode');
  document.getElementById('view-clients').classList.toggle('active',         view === 'clients');
  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');
  document.getElementById('tab-crossnode').classList.toggle('active',  view === 'crossnode');
  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');
}

function showFeed() {
  currentNode = null;
  setView('feed');
  document.querySelectorAll('.node-item').forEach(el => el.classList.remove('active'));
}

function showGeneral() { showFeed(); }

function showNetworks() { setView('networks'); fetchNetworks(); }

async function showCrossNode() { setView('crossnode'); await fetchCrossNode(); }

async function showClients() { setView('clients'); await fetchClients(); }

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) {
  currentNode = node_id;
  setView('detail');
  document.querySelectorAll('.node-item').forEach(el => {
    el.classList.toggle('active', el.dataset.nodeId === node_id);
  });
  const data = await fetch(`/api/nodes/${node_id}`).then(r => r.json());
  renderDetail(data);
}

// ── Init ─────────────────────────────────────────────────────────────────────
async function init() {
  const [nodes, events] = await Promise.all([
    fetch('/api/nodes').then(r => r.json()),
    fetch(`/api/events?limit=${MAX_ROWS}`).then(r => r.json()),
  ]);
  generalEvents = events;
  nodeData = Object.fromEntries(nodes.map(n => [n.node_id, n]));
  document.getElementById('sum-events').textContent = generalEvents.length;
  renderSidebar(nodes);
  renderGeneralTable(false);
}

// ── SSE ───────────────────────────────────────────────────────────────────────
function startSSE() {
  const es  = new EventSource('/api/stream');
  const dot = document.getElementById('live-dot');
  let sseTimer = null;

  es.onopen = () => dot.classList.add('alive');

  es.onmessage = (e) => {
    dot.classList.remove('alive');
    requestAnimationFrame(() => dot.classList.add('alive'));

    // Update the feed array immediately — this is just JS, no API call needed
    const ev = JSON.parse(e.data);
    generalEvents.unshift(ev);
    if (generalEvents.length > MAX_ROWS) generalEvents.pop();
    document.getElementById('sum-events').textContent = generalEvents.length;
    if (currentView === 'feed') renderGeneralTable(true);

    // Debounce all API-backed refreshes: a scan fires 20+ events in rapid
    // succession — only act on the trailing edge, at most once per 2 seconds.
    if (sseTimer) return;
    sseTimer = setTimeout(async () => {
      sseTimer = null;

      if      (currentView === 'networks')      await fetchNetworks();
      else if (currentView === 'network-chart') await loadAndRenderChart();
      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]));
      renderSidebar(nodes);

      if (currentView === 'detail' && currentNode) {
        const data = await fetch(`/api/nodes/${currentNode}`).then(r => r.json());
        renderDetail(data);
      }
    }, 2000);
  };

  es.onerror = () => { dot.classList.remove('alive'); setTimeout(startSSE, 3000); };
}

// ── Boot ──────────────────────────────────────────────────────────────────────
initNetworkSort();
init().then(startSSE);
