defmodule ToweropsWeb.EquipmentLive.Show do @moduledoc false use ToweropsWeb, :live_view alias Towerops.Equipment alias Towerops.Monitoring alias Towerops.Snmp @impl true def mount(_params, _session, socket) do {:ok, socket} end @impl true def handle_params(%{"id" => id}, _, socket) do if connected?(socket) do Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{id}") Process.send_after(self(), :refresh_data, 10_000) end {:noreply, load_equipment_data(socket, id)} end @impl true def handle_info(:refresh_data, socket) do Process.send_after(self(), :refresh_data, 10_000) {:noreply, load_equipment_data(socket, socket.assigns.equipment.id)} end @impl true def handle_info({:equipment_status_changed, _equipment_id, _new_status, _response_time}, socket) do {:noreply, load_equipment_data(socket, socket.assigns.equipment.id)} end @impl true def handle_info({:discovery_completed, _equipment_id}, socket) do {:noreply, socket |> load_equipment_data(socket.assigns.equipment.id) |> put_flash(:info, "Discovery completed")} end @impl true def handle_event("delete", _params, socket) do case Equipment.delete_equipment(socket.assigns.equipment) do {:ok, _} -> {:noreply, socket |> put_flash(:info, "Equipment deleted successfully") |> push_navigate(to: ~p"/orgs/#{socket.assigns.current_organization.slug}/equipment")} {:error, _} -> {:noreply, put_flash(socket, :error, "Unable to delete equipment")} end end @impl true def handle_event("trigger_check", _params, socket) do _ = Towerops.Monitoring.EquipmentMonitor.trigger_check(socket.assigns.equipment.id) {:noreply, put_flash(socket, :info, "Check triggered")} end @impl true def handle_event("trigger_discovery", _params, socket) do equipment = socket.assigns.equipment if equipment.snmp_enabled do # Run discovery in background Task.start(fn -> Snmp.discover_equipment(equipment) end) {:noreply, put_flash(socket, :info, "Discovery started...")} else {:noreply, put_flash(socket, :error, "SNMP is not enabled for this equipment")} end end # Private functions defp load_equipment_data(socket, equipment_id) do equipment = Equipment.get_equipment!(equipment_id) recent_checks = Monitoring.list_equipment_checks(equipment_id, 50) snmp_data = load_snmp_data(equipment_id) # Calculate metrics for dashboard metrics = calculate_metrics(recent_checks, equipment) socket |> assign(:page_title, equipment.name) |> assign(:equipment, equipment) |> assign(:recent_checks, recent_checks) |> assign(:metrics, metrics) |> assign(:snmp_device, snmp_data.device) |> assign(:snmp_interfaces, snmp_data.interfaces) |> assign(:snmp_sensors, snmp_data.sensors) end defp calculate_metrics(checks, _equipment) do total_checks = length(checks) if total_checks > 0 do successful_checks = Enum.count(checks, &(&1.status == :success)) uptime_percentage = Float.round(successful_checks / total_checks * 100, 1) avg_response_time = checks |> Enum.filter(&(&1.response_time_ms != nil)) |> case do [] -> 0 valid_checks -> sum = Enum.reduce(valid_checks, 0, &(&1.response_time_ms + &2)) Float.round(sum / length(valid_checks), 1) end %{ uptime_percentage: uptime_percentage, avg_response_time: avg_response_time, total_checks: total_checks, successful_checks: successful_checks } else %{ uptime_percentage: 0, avg_response_time: 0, total_checks: 0, successful_checks: 0 } end end defp format_speed(speed_bps) when is_integer(speed_bps) do cond do speed_bps >= 1_000_000_000 -> "#{Float.round(speed_bps / 1_000_000_000, 1)} Gbps" speed_bps >= 1_000_000 -> "#{Float.round(speed_bps / 1_000_000, 1)} Mbps" speed_bps >= 1_000 -> "#{Float.round(speed_bps / 1_000, 1)} Kbps" true -> "#{speed_bps} bps" end end defp format_speed(_), do: "-" defp format_sensor_value(value, divisor) when is_number(value) and is_number(divisor) do result = value / divisor Float.round(result, 1) end defp format_sensor_value(_, _), do: "-" defp time_ago(nil), do: "Never" defp time_ago(datetime) do diff = DateTime.diff(DateTime.utc_now(), datetime, :second) cond do diff < 60 -> "#{diff}s ago" diff < 3600 -> "#{div(diff, 60)}m ago" diff < 86_400 -> "#{div(diff, 3600)}h ago" true -> "#{div(diff, 86_400)}d ago" end end defp load_snmp_data(equipment_id) do case Snmp.get_device_with_associations(equipment_id) do nil -> %{device: nil, interfaces: [], sensors: []} device -> # Preload latest readings for sensors sensors_with_readings = Enum.map(device.sensors, fn sensor -> latest_reading = Snmp.get_latest_sensor_reading(sensor.id) Map.put(sensor, :latest_reading, latest_reading) end) # Preload latest stats for interfaces interfaces_with_stats = Enum.map(device.interfaces, fn interface -> latest_stat = Snmp.get_latest_interface_stat(interface.id) Map.put(interface, :latest_stat, latest_stat) end) %{ device: device, interfaces: interfaces_with_stats, sensors: sensors_with_readings } end end # SVG Chart helpers defp sparkline(checks, width, height) when is_list(checks) do if Enum.empty?(checks) or length(checks) < 2 do "" else # Get response times in reverse order (oldest to newest for left-to-right) data = checks |> Enum.reverse() |> Enum.map(fn check -> if check.response_time_ms, do: check.response_time_ms, else: 0 end) max_value = Enum.max(data, fn -> 1 end) max_value = if max_value == 0, do: 1, else: max_value data_count = length(data) points = data |> Enum.with_index() |> Enum.map_join(" ", fn {value, index} -> x = index * (width / (data_count - 1)) y = height - value / max_value * height "#{Float.round(x, 2)},#{Float.round(y, 2)}" end) assigns = %{points: points, width: width, height: height, data: data} ~H""" """ end end defp uptime_gauge(percentage) do assigns = %{percentage: percentage} color_class = cond do percentage >= 95 -> "bg-green-500" percentage >= 80 -> "bg-yellow-500" true -> "bg-red-500" end assigns = Map.put(assigns, :color_class, color_class) ~H"""