defmodule ToweropsWeb.DeviceLive.Index do @moduledoc false use ToweropsWeb, :live_view alias Towerops.Devices alias Towerops.Organizations.SubscriptionLimits alias Towerops.Repo alias Towerops.Sites alias Towerops.Snmp alias Towerops.Workers.DiscoveryWorker @impl true def mount(_params, _session, socket) do organization = socket.assigns.current_organization device = Devices.list_organization_devices(organization.id) sites = Sites.list_organization_sites(organization.id) grouped_devices = group_devices_by_site(device) # Load device quota {current, limit} = SubscriptionLimits.device_quota(organization) {:ok, socket |> assign(:page_title, "Devices") |> assign(:device, device) |> assign(:grouped_devices, grouped_devices) |> assign(:has_sites, sites != []) |> assign(:reorder_mode, false) |> assign(:device_quota, %{current: current, limit: limit})} end @impl true def handle_params(params, _url, socket) do tab = Map.get(params, "tab", "existing") {:noreply, apply_action(socket, socket.assigns.live_action, params, tab)} end defp apply_action(socket, :index, _params, tab) do organization = socket.assigns.current_organization case tab do "discovered" -> discovered = Snmp.list_discovered_devices_for_organization(organization.id) socket |> assign(:active_tab, "discovered") |> assign(:discovered_devices, discovered) _ -> socket |> assign(:active_tab, "existing") |> assign(:discovered_devices, []) end end @impl true def handle_event("force_rediscover_all", _params, socket) do devices = socket.assigns.device snmp_devices = Enum.filter(devices, & &1.snmp_enabled) if Enum.empty?(snmp_devices) do {:noreply, put_flash(socket, :error, "No SNMP-enabled devices found")} else Enum.each(snmp_devices, fn device -> enqueue_discovery(device.id) end) count = length(snmp_devices) {:noreply, put_flash(socket, :info, "Discovery started for #{count} device#{if count == 1, do: "", else: "s"}")} end end @impl true def handle_event("reorder_site", %{"site_id" => site_id, "new_position" => new_position}, socket) do organization = socket.assigns.current_organization # Verify site belongs to current organization case Sites.get_site(site_id) do nil -> {:noreply, put_flash(socket, :error, "Site not found")} site -> if site.organization_id == organization.id do # Handle both integer and string input position = if is_integer(new_position), do: new_position, else: String.to_integer(new_position) case Sites.reorder_site(site_id, position) do {:ok, _site} -> # Reload devices with updated order devices = Devices.list_organization_devices(organization.id) grouped_devices = group_devices_by_site(devices) {:noreply, socket |> assign(:device, devices) |> assign(:grouped_devices, grouped_devices) |> put_flash(:info, "Site order updated")} {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Failed to reorder site")} end else {:noreply, put_flash(socket, :error, "You don't have access to this site")} end end end @impl true def handle_event("reorder_device", %{"device_id" => device_id, "new_position" => new_position}, socket) do organization = socket.assigns.current_organization # Verify device belongs to current organization case Devices.get_device(device_id) do nil -> {:noreply, put_flash(socket, :error, "Device not found")} device -> # Preload site to check organization access device = Repo.preload(device, site: :organization) if device.site.organization_id == organization.id do # Handle both integer and string input position = if is_integer(new_position), do: new_position, else: String.to_integer(new_position) case Devices.reorder_device(device_id, position) do {:ok, _device} -> # Reload devices with updated order devices = Devices.list_organization_devices(organization.id) grouped_devices = group_devices_by_site(devices) {:noreply, socket |> assign(:device, devices) |> assign(:grouped_devices, grouped_devices) |> put_flash(:info, "Device order updated")} {:error, _changeset} -> {:noreply, put_flash(socket, :error, "Failed to reorder device")} end else {:noreply, put_flash(socket, :error, "You don't have access to this device")} end end end @impl true def handle_event("toggle_reorder_mode", _params, socket) do {:noreply, assign(socket, :reorder_mode, !socket.assigns.reorder_mode)} end @impl true def handle_event("reset_order", _params, socket) do organization_id = socket.assigns.current_organization.id Sites.reset_site_order(organization_id) Devices.reset_organization_device_order(organization_id) # Reload devices with alphabetical order devices = Devices.list_organization_devices(organization_id) grouped_devices = group_devices_by_site(devices) {:noreply, socket |> assign(:device, devices) |> assign(:grouped_devices, grouped_devices) |> put_flash(:info, "Order reset to alphabetical")} end def handle_event("add_discovered_device", params, socket) do identifier = Jason.decode!(params["identifier"]) discovered = Enum.find(socket.assigns.discovered_devices, &(&1.identifier == identifier)) prefill_params = %{ "name" => discovered.hostname || "", "ip_address" => List.first(discovered.ip_addresses) || "", "snmp_enabled" => "true" } {:noreply, push_navigate(socket, to: ~p"/devices/new?#{prefill_params}")} end defp enqueue_discovery(device_id) do if Application.get_env(:towerops, :env) == :test do _ = Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end) else DiscoveryWorker.enqueue(device_id) end end # Group devices by site with statistics # Sites and devices are sorted by display_order (with alphabetical fallback) defp group_devices_by_site(devices) do devices |> Enum.group_by(& &1.site) |> Enum.map(fn {site, site_devices} -> {site, site_devices, calculate_site_stats(site_devices)} end) |> Enum.sort_by( fn {site, _devices, _stats} -> {site.display_order || 999_999, site.name} end, :asc ) end defp calculate_site_stats(devices) do status_counts = Enum.frequencies_by(devices, & &1.status) %{ total: length(devices), up: Map.get(status_counts, :up, 0), down: Map.get(status_counts, :down, 0), unknown: Map.get(status_counts, :unknown, 0) } end defp device_type_icon(:router), do: "hero-signal" defp device_type_icon(:switch), do: "hero-squares-2x2" defp device_type_icon(:wireless), do: "hero-wifi" defp device_type_icon(:server), do: "hero-server" defp device_type_icon(:workstation), do: "hero-computer-desktop" defp device_type_icon(_), do: "hero-question-mark-circle" defp device_type_label(type) do type |> to_string() |> String.capitalize() end end