towerops/lib/towerops_web/live/weathermap_live.ex
Graham McIntire 91e3181bbc dialyzer: fix all unmatched_return warnings (154 → 0)
Prefix fire-and-forget calls (PubSub.broadcast, Oban enqueue, Logger,
update_*, etc.) with `_ =` so dialyzer knows the return value is
intentionally discarded. Wrap a few if/case expressions whose
branches produce mixed types the same way.

No behavior changes — only explicit acknowledgement of discarded
returns.

Warnings: 242 → 88.
2026-04-21 10:03:55 -05:00

196 lines
5.2 KiB
Elixir

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