defmodule ToweropsWeb.DeviceLive.Components.OverviewTab do @moduledoc """ LiveComponent for the device overview tab. Displays device metrics, charts (latency, CPU, memory, storage, traffic), and categorized sensors (temperature, voltage, power, etc.). """ use ToweropsWeb, :live_component alias Towerops.Devices.VersionComparator alias ToweropsWeb.DeviceLive.Helpers.ChartBuilders alias ToweropsWeb.DeviceLive.Helpers.DataLoaders alias ToweropsWeb.DeviceLive.Helpers.Formatters alias ToweropsWeb.DeviceLive.Helpers.SensorClassifiers @impl true def update(assigns, socket) do {:ok, socket |> assign(assigns) |> load_overview_data()} end defp load_overview_data(socket) do device = socket.assigns.device device_id = device.id snmp_device = socket.assigns.snmp_device sensors = socket.assigns.snmp_sensors # Load processors and storage processors = DataLoaders.load_processors(snmp_device) storage = DataLoaders.load_storage(snmp_device) # Build chart data latency_chart_data = ChartBuilders.load_latency_chart_data(device_id) processor_chart_data = ChartBuilders.load_processor_chart_data(processors) memory_chart_data = ChartBuilders.load_sensor_chart_data(snmp_device, ["memory_usage"]) storage_chart_data = ChartBuilders.load_sensor_chart_data(snmp_device, ["disk_usage"]) storage_volume_chart_data = ChartBuilders.load_storage_volume_chart_data(storage) traffic_chart_data = ChartBuilders.load_overall_traffic_chart_data(snmp_device) wireless_chart_data = ChartBuilders.load_sensor_chart_data(snmp_device, [ "frequency", "power", "rssi", "ccq", "clients", "distance", "noise-floor", "quality", "rate", "utilization" ]) # Classify sensors by type {transceiver_sensors, non_transceiver_sensors} = SensorClassifiers.split_transceiver_sensors(sensors) temperature_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.temperature_sensor?/1) voltage_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.voltage_sensor?/1) current_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.current_sensor?/1) power_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.power_sensor?/1) fan_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.fan_sensor?/1) load_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.load_sensor?/1) storage_sensors = Enum.filter(non_transceiver_sensors, &(&1.sensor_type in ["disk_usage"])) count_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.counter_sensor?/1) {firewall_counters, general_counters} = Enum.split_with(count_sensors, &SensorClassifiers.firewall_counter?/1) wireless_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.wireless_sensor?/1) signal_sensors = Enum.filter(non_transceiver_sensors, &SensorClassifiers.signal_sensor?/1) grouped_transceivers = SensorClassifiers.group_transceiver_sensors(transceiver_sensors) metrics = DataLoaders.calculate_metrics([], device) available_firmware = DataLoaders.get_available_firmware(snmp_device) connected_devices = DataLoaders.load_connected_devices(device_id) recent_config_changes = DataLoaders.load_recent_config_changes(device) socket |> assign(:metrics, metrics) |> assign(:processors, processors) |> assign(:storage, storage) |> assign(:latency_chart_data, latency_chart_data) |> assign(:processor_chart_data, processor_chart_data) |> assign(:memory_chart_data, memory_chart_data) |> assign(:storage_chart_data, storage_chart_data) |> assign(:storage_volume_chart_data, storage_volume_chart_data) |> assign(:traffic_chart_data, traffic_chart_data) |> assign(:wireless_chart_data, wireless_chart_data) |> assign(:temperature_sensors, temperature_sensors) |> assign(:voltage_sensors, voltage_sensors) |> assign(:current_sensors, current_sensors) |> assign(:power_sensors, power_sensors) |> assign(:fan_sensors, fan_sensors) |> assign(:load_sensors, load_sensors) |> assign(:storage_sensors, storage_sensors) |> assign(:count_sensors, count_sensors) |> assign(:general_counters, general_counters) |> assign(:firewall_counters, firewall_counters) |> assign(:wireless_sensors, wireless_sensors) |> assign(:signal_sensors, signal_sensors) |> assign(:grouped_transceivers, grouped_transceivers) |> assign(:available_firmware, available_firmware) |> assign(:connected_devices, connected_devices) |> assign(:recent_config_changes, recent_config_changes) end @impl true def render(assigns) do ~H"""

{t("Device Information")}

<%= if @snmp_device do %>
System Name
{@snmp_device.sys_name || @device.name}
{t("Resolved IP")}
{@device.ip_address} <.icon name="hero-clipboard-document" class="h-3.5 w-3.5 inline ml-1 opacity-0 group-hover/rip:opacity-100 transition-opacity text-gray-400" />
Hardware
{@snmp_device.model || "N/A"}
Operating System
{cond do @snmp_device.manufacturer && @snmp_device.firmware_version -> "#{@snmp_device.manufacturer} #{@snmp_device.firmware_version}" @snmp_device.manufacturer -> @snmp_device.manufacturer true -> @snmp_device.sys_descr || "N/A" end} <%= if firmware_update_available?(@snmp_device, @available_firmware) do %>
<.icon name="hero-arrow-up-circle" class="h-5 w-5 text-blue-500 cursor-help dark:text-blue-400" />
<% end %>
Serial Number
{@snmp_device.serial_number}
Object ID
{@snmp_device.sys_object_id || "N/A"}
Contact
{@snmp_device.sys_contact || "N/A"}
Location
{format_location(@snmp_device.sys_location)}
Uptime
{format_uptime(@snmp_device.sys_uptime)}
<% else %>
Name
{@device.name}
IP Address
{@device.ip_address}
<% end %>
Device Added
{format_device_age(@device.inserted_at)}
Last Discovered
{if @device.last_discovery_at do format_device_age(@device.last_discovery_at) else "Never" end}
Last Polled
{if @device.last_snmp_poll_at do format_device_age(@device.last_snmp_poll_at) else "Never" end}
<%= if @traffic_chart_data do %>
<.link navigate={~p"/devices/#{@device.id}/graph/traffic"} class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" >

{t("Overall Traffic")}

<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
{t("Loading chart...")}
<% end %> <%= if @latency_chart_data do %>
<.link navigate={~p"/devices/#{@device.id}/graph/latency"} class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" >

{t("ICMP Latency")}

<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
{t("Loading chart...")}
<% end %> <%!-- Recent Config Changes mini-card --%> <%= if false && @device.mikrotik_enabled && assigns[:recent_config_changes] && Enum.any?(@recent_config_changes) do %>

<.icon name="hero-clock" class="h-4 w-4 text-orange-500" /> {t("Recent Config Changes")}

<.link navigate={~p"/devices/#{@device.id}/config-timeline"} class="text-xs text-blue-500 hover:underline" > View Timeline →
<%= for event <- Enum.take(@recent_config_changes, 5) do %>
{Calendar.strftime(event.changed_at, "%b %d %H:%M")} {s}
{event.change_size} lines
<% end %>
<% end %>
<%= if @processor_chart_data || (@processors && length(@processors) > 0) do %> <% processor_count = if @processors, do: length(@processors), else: 0 avg_load = if processor_count > 0 do loads = @processors |> Enum.map(& &1.load_percent) |> Enum.reject(&is_nil/1) if length(loads) > 0, do: Enum.sum(loads) / length(loads), else: nil else nil end %>
<.link navigate={~p"/devices/#{@device.id}/graph/processors"} class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" >

<%= if processor_count > 0 do %> {processor_count} {if processor_count == 1, do: "Processor", else: "Processors"} <% else %> {t("Processors")} <% end %>

<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
<%= if @processor_chart_data do %>
<% end %> <%= if avg_load do %>
{t("Average Load")}
= 50 && avg_load < 80 && "bg-yellow-500", avg_load >= 80 && "bg-red-500" ]} style={"width: #{min(avg_load, 100)}%"} >
{Float.round(avg_load, 0)}%
<% end %>
<% end %> <%= if @memory_chart_data do %>
<.link navigate={~p"/devices/#{@device.id}/graph/memory"} class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors" >

{t("Memory Usage")}

<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
<% end %> <%= if @storage_sensors && length(@storage_sensors) > 0 do %>

{t("Storage Usage")}

<%= for sensor <- @storage_sensors do %>
<.link navigate={~p"/devices/#{@device.id}/graph/storage?sensor_id=#{sensor.id}"} class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading do %> {format_sensor_value( sensor.latest_reading.value, sensor.sensor_divisor )} {sensor.sensor_unit} <% else %> {t("N/A")} <% end %>
<% end %>
<% end %> <%= if @storage && length(@storage) > 0 do %>

{t("Storage Volumes")} ({length(@storage)})

<%= if @storage_volume_chart_data do %>
<% end %>
<%= for storage <- @storage do %> <% usage_percent = if storage.total_bytes && storage.total_bytes > 0 do Float.round(storage.used_bytes / storage.total_bytes * 100, 1) else 0.0 end %> <.link navigate={ ~p"/devices/#{@device.id}/graph/storage_volume?storage_id=#{storage.id}" } class="block hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 py-1 rounded transition-colors" >
{storage.description || "Storage #{storage.storage_index}"} {format_bytes(storage.used_bytes)} / {format_bytes(storage.total_bytes)}
= 70 && usage_percent < 90 && "bg-yellow-500", usage_percent >= 90 && "bg-red-500" ]} style={"width: #{min(usage_percent, 100)}%"} >
{usage_percent}%
{String.replace(storage.storage_type || "other", "_", " ") |> String.capitalize()}
<% end %>
<% end %> <%= if @temperature_sensors && length(@temperature_sensors) > 0 do %>

{t("Temperature")}

<%= for sensor <- @temperature_sensors do %>
<.link navigate={ ~p"/devices/#{@device.id}/graph/temperature?sensor_id=#{sensor.id}" } class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading do %> {format_sensor_value( sensor.latest_reading.value, sensor.sensor_divisor )} {sensor.sensor_unit || "°C"} <% else %> {t("N/A")} <% end %>
<% end %>
<% end %> <%= if @voltage_sensors && length(@voltage_sensors) > 0 do %>

{t("Voltage")}

<%= for sensor <- @voltage_sensors do %>
<.link navigate={~p"/devices/#{@device.id}/graph/voltage?sensor_id=#{sensor.id}"} class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading do %> {format_sensor_value( sensor.latest_reading.value, sensor.sensor_divisor )} {sensor.sensor_unit} <% else %> {t("N/A")} <% end %>
<% end %>
<% end %> <%= if @grouped_transceivers && length(@grouped_transceivers) > 0 do %>

{t("Transceivers")}

<%= for {_transceiver_name, sensors} <- @grouped_transceivers do %> <%= for sensor <- sensors do %>
<.link navigate={~p"/devices/#{@device.id}/graph/dbm?sensor_id=#{sensor.id}"} class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading do %> {format_sensor_value( sensor.latest_reading.value, sensor.sensor_divisor )} {sensor.sensor_unit} <% else %> {t("N/A")} <% end %>
<% end %> <% end %>
<% end %> <%= if (@general_counters && length(@general_counters) > 0) || (@firewall_counters && length(@firewall_counters) > 0) do %>

{t("Counters")}

<%= if @general_counters && length(@general_counters) > 0 do %>
<%= for sensor <- @general_counters do %>
<.link navigate={ ~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}" } class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading && sensor.latest_reading.value do %> {trunc(sensor.latest_reading.value)} <%= if sensor.sensor_unit && sensor.sensor_unit != "" do %> {sensor.sensor_unit} <% end %> <% else %> {t("N/A")} <% end %>
<% end %>
<% end %> <%= if @firewall_counters && length(@firewall_counters) > 0 do %>

{t("Firewall")}

<%= for sensor <- @firewall_counters do %>
<.link navigate={ ~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}" } class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading && sensor.latest_reading.value do %> {trunc(sensor.latest_reading.value)} <%= if sensor.sensor_unit && sensor.sensor_unit != "" do %> {sensor.sensor_unit} <% end %> <% else %> {t("N/A")} <% end %>
<% end %>
<% end %>
<% end %> <%= if @wireless_sensors && length(@wireless_sensors) > 0 do %>

{t("Wireless")}

<%= for sensor <- @wireless_sensors do %>
<.link navigate={~p"/devices/#{@device.id}/graph/wireless?sensor_id=#{sensor.id}"} class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline" > {sensor.sensor_descr}
<%= if sensor.latest_reading && sensor.latest_reading.value do %> <%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %> {trunc(sensor.latest_reading.value)} <% else %> {:erlang.float_to_binary(sensor.latest_reading.value * 1.0, decimals: 1 )} <% end %> <%= if sensor.sensor_unit && sensor.sensor_unit != "" do %> {sensor.sensor_unit} <% end %> <% else %> {t("N/A")} <% end %>
<% end %>
<% end %>
<%!-- Connected Devices --%> <%= if @connected_devices != [] do %>

{t("Connected Devices")} ({length(@connected_devices)} links)

<%= for link <- @connected_devices do %> <% peer_device = cond do link.target_device && link.target_device.id != @device.id -> link.target_device link.source_device && link.source_device.id != @device.id -> link.source_device link.target_device -> link.target_device true -> nil end %> <% end %>
Device IP Address Interface Link Type Confidence Status
<%= if peer_device do %> <.link navigate={~p"/devices/#{peer_device.id}"} class="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300" > {peer_device.name} <% else %>
{link.discovered_remote_name || link.discovered_remote_mac || "Unknown"}
<% end %>
<%= if peer_device do %> {peer_device.ip_address} <% else %> {link.discovered_remote_ip || "-"} <% end %> <%= if link.source_interface do %> {link.source_interface.if_name} <% else %> - <% end %> "bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200" link.link_type == "mac_match" -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200" link.link_type == "arp_inference" -> "bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200" true -> "bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200" end ]}> {link.link_type}
= 0.9 -> "bg-green-500" link.confidence >= 0.7 -> "bg-blue-500" link.confidence >= 0.5 -> "bg-yellow-500" true -> "bg-red-500" end ]} style={"width: #{trunc(link.confidence * 100)}%"} >
{trunc(link.confidence * 100)}%
<%= if peer_device do %> {t("Managed")} <% else %> {t("Discovered")} <% end %>
<% end %>
""" end # Helper functions - delegate to Formatters or implement locally defp format_date(date), do: Formatters.format_date(date) defp format_sensor_value(value, divisor), do: Formatters.format_sensor_value(value, divisor) defp format_location(location), do: Formatters.format_location(location) defp format_uptime(timeticks), do: Formatters.format_uptime(timeticks) defp format_device_age(datetime), do: Formatters.format_device_age(datetime) defp format_bytes(bytes), do: Formatters.format_bytes(bytes) defp config_change_dot_color(event) do cond do event.change_size > 50 -> "bg-red-500" event.change_size > 20 -> "bg-yellow-500" true -> "bg-green-500" end end defp firmware_update_available?(nil, _), do: false defp firmware_update_available?(_, nil), do: false defp firmware_update_available?(snmp_device, available_firmware) do current = snmp_device.firmware_version available = available_firmware.version # Extract clean version numbers for comparison current_clean = extract_version_number(current) available_clean = extract_version_number(available) current_clean && available_clean && VersionComparator.newer(current_clean, available_clean) end defp extract_version_number(nil), do: nil defp extract_version_number(""), do: nil defp extract_version_number(version_string) when is_binary(version_string) do # Match semantic version pattern: X.Y or X.Y.Z case Regex.run(~r/\d+\.\d+(?:\.\d+)?/, version_string) do [version] -> version _ -> nil end end end