towerops/lib/towerops_web/live/graph_live/show.ex
Graham McIntire 139a42531f
Refactor credo issues: reduce complexity and nesting depth
- Refactor check_threshold_violation: extract threshold checks into separate functions (complexity 10→~3)
- Refactor load_sensor_chart_data: extract helper functions to reduce nesting
- Refactor save_equipment: extract SNMP discovery logic into helpers
- Refactor poll_sensors: extract error handling and result processing
- Refactor handle_status_change: extract equipment down/up handlers
- Refactor mount_current_scope: extract session scope building
- Refactor load_traffic_chart_data: extract aggregation and calculation helpers
- Refactor load_overall_traffic_chart_data: similar pattern to traffic chart
- Refactor load_sensor_chart_data in equipment_live/show: extract dataset builders
- Refactor discover_storage_sensors: extract storage sensor building
- Refactor perform_poll: extract device polling logic into separate functions

All 254 tests passing. Credo reports no issues.
2026-01-06 13:55:16 -06:00

234 lines
7.3 KiB
Elixir

defmodule ToweropsWeb.GraphLive.Show do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Equipment
alias Towerops.Snmp
@impl true
def mount(_params, _session, socket) do
{:ok, socket}
end
@impl true
def handle_params(%{"id" => equipment_id, "sensor_type" => sensor_type} = params, _, socket) do
if connected?(socket) do
Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{equipment_id}")
end
range = Map.get(params, "range", "24h")
socket =
socket
|> assign(:equipment_id, equipment_id)
|> assign(:sensor_type, sensor_type)
|> assign(:range, range)
|> load_graph_data()
{:noreply, socket}
end
@impl true
def handle_event("change_range", %{"range" => range}, socket) do
{:noreply,
push_patch(socket,
to:
~p"/orgs/#{socket.assigns.current_organization.slug}/equipment/#{socket.assigns.equipment_id}/graph/#{socket.assigns.sensor_type}?range=#{range}"
)}
end
# Private functions
defp load_graph_data(socket) do
equipment_id = socket.assigns.equipment_id
sensor_type = socket.assigns.sensor_type
range = socket.assigns.range
equipment = Equipment.get_equipment!(equipment_id)
chart_data =
if sensor_type == "traffic" do
load_traffic_chart_data(equipment_id, range)
else
sensor_types = get_sensor_types_for_chart(sensor_type)
load_sensor_chart_data(equipment_id, sensor_types, range)
end
{title, unit, auto_scale} = get_chart_config(sensor_type)
show_zero_line = sensor_type == "traffic"
socket
|> assign(:equipment, equipment)
|> assign(:page_title, "#{equipment.name} - #{title}")
|> assign(:chart_title, title)
|> assign(:chart_data, chart_data)
|> assign(:unit, unit)
|> assign(:auto_scale, auto_scale)
|> assign(:show_zero_line, show_zero_line)
end
defp get_sensor_types_for_chart("processors"), do: ["cpu_load"]
defp get_sensor_types_for_chart("memory"), do: ["memory_usage"]
defp get_sensor_types_for_chart("storage"), do: ["disk_usage"]
defp get_sensor_types_for_chart("temperature"), do: ["temperature", "cpu_temperature", "celsius"]
defp get_sensor_types_for_chart("voltage"), do: ["voltage", "volts"]
defp get_sensor_types_for_chart("traffic"), do: nil
defp get_sensor_types_for_chart(_), do: []
defp get_chart_config("processors"), do: {"Processor Usage", "%", false}
defp get_chart_config("memory"), do: {"Memory Usage", "%", false}
defp get_chart_config("storage"), do: {"Storage Usage", "%", false}
defp get_chart_config("temperature"), do: {"Temperature", "°C", true}
defp get_chart_config("voltage"), do: {"Voltage", "V", true}
defp get_chart_config("traffic"), do: {"Overall Traffic", "bps", true}
defp get_chart_config(_), do: {"Sensor Data", "", false}
defp load_sensor_chart_data(equipment_id, sensor_types, range) do
case Snmp.get_device_with_associations(equipment_id) do
nil ->
nil
device ->
build_sensor_chart_json(device, sensor_types, range)
end
end
defp build_sensor_chart_json(device, sensor_types, range) do
sensors =
device.sensors
|> Enum.filter(&(&1.sensor_type in sensor_types))
|> Enum.sort_by(& &1.sensor_index)
if Enum.empty?(sensors) do
nil
else
since = get_datetime_from_range(range)
limit = get_limit_for_range(range)
datasets = Enum.map(sensors, &sensor_to_dataset(&1, since, limit))
Jason.encode!(%{datasets: datasets})
end
end
defp sensor_to_dataset(sensor, since, limit) do
readings =
sensor.id
|> Snmp.get_sensor_readings(since: since, limit: limit)
|> Enum.reverse()
%{
label: sensor.sensor_descr,
data: Enum.map(readings, &reading_to_chart_point/1)
}
end
defp reading_to_chart_point(reading) do
%{
x: DateTime.to_unix(reading.checked_at, :millisecond),
y: if(reading.value, do: Float.round(reading.value, 1))
}
end
defp get_datetime_from_range("1h"), do: DateTime.add(DateTime.utc_now(), -1, :hour)
defp get_datetime_from_range("6h"), do: DateTime.add(DateTime.utc_now(), -6, :hour)
defp get_datetime_from_range("12h"), do: DateTime.add(DateTime.utc_now(), -12, :hour)
defp get_datetime_from_range("24h"), do: DateTime.add(DateTime.utc_now(), -24, :hour)
defp get_datetime_from_range("7d"), do: DateTime.add(DateTime.utc_now(), -7, :day)
defp get_datetime_from_range("30d"), do: DateTime.add(DateTime.utc_now(), -30, :day)
defp get_datetime_from_range(_), do: DateTime.add(DateTime.utc_now(), -24, :hour)
defp get_limit_for_range("1h"), do: 360
defp get_limit_for_range("6h"), do: 720
defp get_limit_for_range("12h"), do: 1440
defp get_limit_for_range("24h"), do: 2880
defp get_limit_for_range("7d"), do: 10_080
defp get_limit_for_range("30d"), do: 43_200
defp get_limit_for_range(_), do: 2880
defp load_traffic_chart_data(equipment_id, range) do
case Snmp.get_device_with_associations(equipment_id) do
nil -> nil
device -> build_traffic_chart_json(device, range)
end
end
defp build_traffic_chart_json(device, range) do
if Enum.empty?(device.interfaces) do
nil
else
since = get_datetime_from_range(range)
limit = get_limit_for_range(range)
all_stats = aggregate_interface_stats(device.interfaces, since, limit)
if Enum.empty?(all_stats) do
nil
else
build_traffic_datasets(all_stats)
end
end
end
defp aggregate_interface_stats(interfaces, since, limit) do
interfaces
|> Enum.flat_map(fn interface ->
stats =
interface.id
|> Snmp.get_interface_stats(since: since, limit: limit)
|> Enum.reverse()
Enum.map(stats, &stat_to_tuple/1)
end)
|> Enum.group_by(&elem(&1, 0))
|> Enum.map(&sum_stats_by_timestamp/1)
|> Enum.sort_by(&elem(&1, 0), DateTime)
end
defp stat_to_tuple(stat) do
{stat.checked_at, stat.if_in_octets, stat.if_out_octets}
end
defp sum_stats_by_timestamp({timestamp, stats}) do
total_in = Enum.reduce(stats, 0, fn {_, in_octets, _}, acc -> acc + (in_octets || 0) end)
total_out = Enum.reduce(stats, 0, fn {_, _, out_octets}, acc -> acc + (out_octets || 0) end)
{timestamp, total_in, total_out}
end
defp build_traffic_datasets(all_stats) do
in_data = calculate_traffic_data(all_stats, :inbound)
out_data = calculate_traffic_data(all_stats, :outbound)
datasets = [
%{label: "Outbound", data: out_data},
%{label: "Inbound", data: in_data}
]
Jason.encode!(%{datasets: datasets})
end
defp calculate_traffic_data(all_stats, :inbound) do
all_stats
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [{t1, in1, _out1}, {t2, in2, _out2}] ->
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
bps = ((in2 - in1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
%{
x: DateTime.to_unix(t2, :millisecond),
y: -bps
}
end)
end
defp calculate_traffic_data(all_stats, :outbound) do
all_stats
|> Enum.chunk_every(2, 1, :discard)
|> Enum.map(fn [{t1, _in1, out1}, {t2, _in2, out2}] ->
time_diff = t2 |> DateTime.diff(t1, :second) |> max(1)
bps = ((out2 - out1) * 8 / time_diff) |> max(0) |> :erlang.float() |> Float.round(2)
%{
x: DateTime.to_unix(t2, :millisecond),
y: bps
}
end)
end
end