defmodule ToweropsWeb.WeathermapLive do @moduledoc """ Live view for real-time network utilization weathermap. Displays network topology with color-coded edges based on bandwidth utilization. """ 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 and capacity updates if connected _ = if connected?(socket) do Phoenix.PubSub.subscribe(Towerops.PubSub, "topology:#{organization.id}") Phoenix.PubSub.subscribe(Towerops.PubSub, "capacity:#{organization.id}") end # Default empty topology default_topology = %{ nodes: [], edges: [], stats: %{ total_devices: 0, added_devices: 0, discovered_devices: 0, total_links: 0, high_utilization_links: 0, overutilized_links: 0 }, last_updated: DateTime.utc_now() } # Schedule auto-refresh every 60 seconds if connected?(socket) do Process.send_after(self(), :refresh_data, 60_000) end {:ok, socket |> assign(:page_title, t("Network Weathermap")) |> assign(:timezone, socket.assigns.current_scope.timezone) |> assign(:loading, true) |> assign(:topology, default_topology) |> assign(:active_tab, "added") |> assign(:selected_node_detail, nil) |> assign(:filter, "all") |> assign(:search_query, "") |> assign(:layout_mode, "force") |> assign(:fullscreen, false)} end @impl true def handle_params(params, _url, socket) do tab = Map.get(params, "tab", "added") fullscreen = Map.get(params, "fullscreen") == "true" organization = socket.assigns.current_scope.organization socket = socket |> assign(:active_tab, tab) |> assign(:fullscreen, fullscreen) |> load_weathermap_data(organization.id, tab) # Handle ?node= param for deep-linking to a selected node socket = case Map.get(params, "node") do nil -> socket node_id -> load_node_detail(socket, node_id, organization.id) end {:noreply, socket} end @impl true def handle_event("node_clicked", %{"node_id" => node_id} = params, socket) do organization = socket.assigns.current_scope.organization node_type = Map.get(params, "node_type", "managed") socket = socket |> load_node_detail(node_id, organization.id) |> then(fn s -> if s.assigns.selected_node_detail do push_event(s, "highlight_node", %{node_id: node_id, node_type: node_type}) else s end end) {:noreply, socket} end @impl true def handle_event("close_detail_panel", _params, socket) do {:noreply, socket |> assign(:selected_node_detail, nil) |> push_event("clear_highlight", %{})} end @impl true def handle_event("set_filter", %{"filter" => filter}, socket) do {:noreply, socket |> assign(:filter, filter) |> push_event("apply_filter", %{filter: filter})} end @impl true def handle_event("search", %{"query" => query}, socket) do {:noreply, socket |> assign(:search_query, query) |> push_event("search_nodes", %{query: query})} end @impl true def handle_event("toggle_layout", %{"mode" => mode}, socket) do {:noreply, socket |> assign(:layout_mode, mode) |> push_event("change_layout", %{mode: mode})} end @impl true def handle_event("toggle_fullscreen", _params, socket) do new_fullscreen = not socket.assigns.fullscreen path_with_params = if new_fullscreen do ~p"/weathermap?#{%{tab: socket.assigns.active_tab, fullscreen: true}}" else ~p"/weathermap?#{%{tab: socket.assigns.active_tab}}" end {:noreply, push_navigate(socket, to: path_with_params)} end @impl true def handle_info(:refresh_data, socket) do organization = socket.assigns.current_scope.organization socket = load_weathermap_data(socket, organization.id, socket.assigns.active_tab) # Schedule next refresh Process.send_after(self(), :refresh_data, 60_000) {:noreply, socket} end @impl true def handle_info({:topology_updated, _organization_id}, socket) do # Real-time topology update {:noreply, load_weathermap_data( socket, socket.assigns.current_scope.organization.id, socket.assigns.active_tab )} end @impl true def handle_info({:capacity_updated, _organization_id}, socket) do # Real-time capacity/utilization update {:noreply, load_weathermap_data( socket, socket.assigns.current_scope.organization.id, socket.assigns.active_tab )} end defp load_weathermap_data(socket, organization_id, tab) do topology = Topology.get_topology_for_weathermap(organization_id, tab) socket |> assign(:topology, topology) |> assign(:loading, false) |> push_event("update_weathermap", %{topology: topology}) end defp load_node_detail(socket, node_id, organization_id) do case Topology.get_node_detail(node_id, organization_id) do nil -> assign(socket, :selected_node_detail, nil) detail -> assign(socket, :selected_node_detail, detail) end end end