towerops/lib/towerops_web/live/network_map_live.ex
Graham McIntire 7371ceb942
feat: add automatic network topology inference
Build a rich network topology from SNMP polling data using evidence-based
confidence scoring. LLDP/CDP neighbors, MAC address tables, and ARP data
are combined to infer device links with weighted confidence merging.

- Add DeviceLink and DeviceLinkEvidence schemas for persistent topology
- Implement evidence collectors: LLDP (0.95), CDP (0.95), MAC (0.7), ARP (0.6)
- Add device role inference from sysObjectID/sysDescr patterns
- Hook topology inference into DevicePollerWorker pipeline
- Add stale link cleanup (24h mark stale, 72h delete) via NeighborCleanupWorker
- Update NetworkMapLive with "Added" vs "All Devices" tabs
- Add connected devices section to device detail page
- Add device role selector to device edit form
- Update Cytoscape.js with role-based node shapes/colors and confidence edges
2026-02-12 13:28:01 -06:00

81 lines
2.2 KiB
Elixir

defmodule ToweropsWeb.NetworkMapLive do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Topology
@impl true
def mount(_params, _session, socket) do
organization = socket.assigns.current_scope.organization
# Subscribe to topology changes if connected
_ =
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "topology:#{organization.id}")
end
# Default empty topology
default_topology = %{
nodes: [],
edges: [],
stats: %{
total_devices: 0,
added_devices: 0,
discovered_devices: 0,
total_links: 0
},
last_updated: DateTime.utc_now()
}
{:ok,
socket
|> assign(:page_title, "Network Map")
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:loading, true)
|> assign(:topology, default_topology)
|> assign(:active_tab, "added")}
end
@impl true
def handle_params(params, _url, socket) do
tab = Map.get(params, "tab", "added")
organization = socket.assigns.current_scope.organization
{:noreply,
socket
|> assign(:active_tab, tab)
|> load_topology_data(organization.id, tab)}
end
@impl true
def handle_event("refresh_topology", _params, socket) do
organization = socket.assigns.current_scope.organization
{:noreply,
socket
|> assign(:loading, true)
|> load_topology_data(organization.id, socket.assigns.active_tab)
|> put_flash(:info, "Network map refreshed")}
end
@impl true
def handle_event("node_clicked", %{"node_id" => node_id}, socket) do
# Handle node click - could navigate to device detail page
{:noreply, assign(socket, :selected_node, node_id)}
end
@impl true
def handle_info({:topology_updated, _organization_id}, socket) do
# Real-time topology update
{:noreply, load_topology_data(socket, socket.assigns.current_scope.organization.id, socket.assigns.active_tab)}
end
defp load_topology_data(socket, organization_id, tab) do
topology = Topology.get_topology_for_map(organization_id, tab)
socket
|> assign(:topology, topology)
|> assign(:loading, false)
|> push_event("update_topology", %{topology: topology})
end
end