const graphEl = document.getElementById("graph"); const deviceTableBody = document.querySelector("#deviceTable tbody"); const eventList = document.getElementById("eventList"); const refreshBtn = document.getElementById("refreshBtn"); const lastUpdated = document.getElementById("lastUpdated"); const NODE_RADIUS = 17; function kindColor(kind, status) { if (status === "offline") return "#94a3b8"; if (kind === "router") return "#0ea5e9"; if (kind === "switch") return "#f97316"; if (kind === "ap") return "#22c55e"; if (kind === "domain") return "#64748b"; return "#6366f1"; } function safe(v) { return v == null ? "" : String(v); } function formatTs(ts) { if (!ts) return "-"; return new Date(ts).toLocaleString(); } function renderGraph(data) { const width = 1200; const height = 700; graphEl.innerHTML = ""; const nodes = data.nodes || []; const edges = data.edges || []; const byId = new Map(nodes.map((n) => [n.id, n])); const router = nodes.find((n) => n.kind === "router"); const sw = nodes.find((n) => n.kind === "switch"); const aps = nodes.filter((n) => n.kind === "ap"); const domains = nodes.filter((n) => n.kind === "domain"); const clients = nodes.filter( (n) => !["router", "switch", "ap", "domain"].includes(n.kind) ); const pos = new Map(); if (router) pos.set(router.id, { x: 180, y: 120 }); if (sw) pos.set(sw.id, { x: 420, y: 120 }); aps.forEach((n, i) => { pos.set(n.id, { x: 240 + i * 160, y: 280 }); }); const cols = 5; clients.forEach((n, i) => { const row = Math.floor(i / cols); const col = i % cols; pos.set(n.id, { x: 120 + col * 180, y: 470 + row * 120 }); }); domains.forEach((n, i) => { pos.set(n.id, { x: 900, y: 120 + i * 70 }); }); nodes.forEach((n, i) => { if (!pos.has(n.id)) { pos.set(n.id, { x: 860 + (i % 3) * 80, y: 420 + Math.floor(i / 3) * 80 }); } }); const ns = "http://www.w3.org/2000/svg"; edges.forEach((e) => { const s = pos.get(e.source); const t = pos.get(e.target); if (!s || !t) return; const line = document.createElementNS(ns, "line"); line.setAttribute("x1", s.x); line.setAttribute("y1", s.y); line.setAttribute("x2", t.x); line.setAttribute("y2", t.y); line.setAttribute("class", `edge ${e.kind === "dns" ? "edge-dns" : ""}`); graphEl.appendChild(line); }); nodes.forEach((n) => { const p = pos.get(n.id); if (!p) return; const circle = document.createElementNS(ns, "circle"); circle.setAttribute("cx", p.x); circle.setAttribute("cy", p.y); circle.setAttribute("r", n.kind === "domain" ? NODE_RADIUS - 4 : NODE_RADIUS); circle.setAttribute("fill", kindColor(n.kind, n.status)); circle.setAttribute("class", "node"); circle.setAttribute( "title", `${safe(n.label)}\n${safe(n.ip)}\n${safe(n.kind)}\n${safe(n.status)}` ); graphEl.appendChild(circle); const text = document.createElementNS(ns, "text"); text.setAttribute("x", p.x + 24); text.setAttribute("y", p.y + 4); text.setAttribute("class", "node-label"); text.textContent = `${safe(n.label)}${n.ip ? ` (${n.ip})` : ""}`; graphEl.appendChild(text); }); const frame = document.createElementNS(ns, "rect"); frame.setAttribute("x", 1); frame.setAttribute("y", 1); frame.setAttribute("width", width - 2); frame.setAttribute("height", height - 2); frame.setAttribute("fill", "none"); frame.setAttribute("stroke", "#e2e8f0"); frame.setAttribute("rx", "10"); graphEl.insertBefore(frame, graphEl.firstChild); } function renderTable(devices) { deviceTableBody.innerHTML = ""; devices .slice() .sort((a, b) => String(a.ip).localeCompare(String(b.ip), undefined, { numeric: true })) .forEach((d) => { const tr = document.createElement("tr"); tr.innerHTML = `