defmodule ToweropsWeb.DeviceLive.Index do @moduledoc false use ToweropsWeb, :live_view alias Towerops.Devices alias Towerops.Organizations.SubscriptionLimits alias Towerops.Sites alias Towerops.Snmp alias Towerops.Workers.DiscoveryWorker alias ToweropsWeb.Live.Helpers.AccessControl @impl true def mount(_params, _session, socket) do organization = socket.assigns.current_scope.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, t_equipment("Devices")) |> assign(:timezone, socket.assigns.current_scope.timezone) |> assign(:device, device) |> assign(:grouped_devices, grouped_devices) |> assign(:sites_enabled, organization.use_sites) |> 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_scope.organization case tab do "discovered" -> page = params |> Map.get("page", "1") |> String.to_integer() per_page = 20 # Load all discovered devices - this is temporarily kept in memory for slicing # For very large datasets, consider database-level pagination all_discovered = Snmp.list_discovered_devices_for_organization(organization.id) total_count = length(all_discovered) total_pages = ceil(total_count / per_page) # Ensure page is within valid range page = max(1, min(page, max(1, total_pages))) offset = (page - 1) * per_page # Only keep the current page in assigns - don't store all_discovered discovered_devices = Enum.slice(all_discovered, offset, per_page) socket |> assign(:active_tab, "discovered") # Use temporary assign - will be cleared after render |> assign_new(:discovered_devices, fn -> discovered_devices end) |> assign(:pagination, %{ page: page, per_page: per_page, total_count: total_count, total_pages: total_pages }) _ -> socket |> assign(:active_tab, "existing") |> assign(:discovered_devices, []) |> assign(:pagination, nil) 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, t_equipment("No SNMP-enabled devices found"))} else Enum.each(snmp_devices, fn device -> enqueue_discovery(device.id) end) count = length(snmp_devices) message = t_equipment("Discovery started for %{count} device", count: count) <> if count == 1, do: "", else: "s" {:noreply, put_flash(socket, :info, message)} end end @impl true def handle_event("reorder_site", %{"site_id" => site_id, "new_position" => new_position}, socket) do organization = socket.assigns.current_scope.organization # Verify site belongs to current organization case AccessControl.verify_site_access(site_id, organization.id) do {:ok, _site} -> perform_site_reorder(socket, site_id, new_position, organization.id) {:error, :not_found} -> {:noreply, put_flash(socket, :error, t_equipment("Site not found"))} {:error, :unauthorized} -> {:noreply, put_flash(socket, :error, t_equipment("You don't have access to this site"))} end end @impl true def handle_event("reorder_device", %{"device_id" => device_id, "new_position" => new_position}, socket) do organization = socket.assigns.current_scope.organization # Verify device belongs to current organization case AccessControl.verify_device_access(device_id, organization.id) do {:ok, _device} -> perform_device_reorder(socket, device_id, new_position, organization.id) {:error, :not_found} -> {:noreply, put_flash(socket, :error, t_equipment("Device not found"))} {:error, :unauthorized} -> {:noreply, put_flash(socket, :error, t_equipment("You don't have access to this device"))} 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_scope.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, t_equipment("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 perform_site_reorder(socket, site_id, new_position, organization_id) do position = if is_integer(new_position), do: new_position, else: String.to_integer(new_position) result = Sites.reorder_site(site_id, position) perform_reorder(socket, result, "site", organization_id) end defp perform_device_reorder(socket, device_id, new_position, organization_id) do position = if is_integer(new_position), do: new_position, else: String.to_integer(new_position) result = Devices.reorder_device(device_id, position) perform_reorder(socket, result, "device", organization_id) end defp perform_reorder(socket, reorder_result, resource_name, organization_id) do case reorder_result do {:ok, _} -> # Reload devices with updated order devices = Devices.list_organization_devices(organization_id) grouped_devices = group_devices_by_site(devices) success_message = case resource_name do "site" -> t_equipment("Site order updated") "device" -> t_equipment("Device order updated") end {:noreply, socket |> assign(:device, devices) |> assign(:grouped_devices, grouped_devices) |> put_flash(:info, success_message)} {:error, _changeset} -> error_message = case resource_name do "site" -> t_equipment("Failed to reorder site") "device" -> t_equipment("Failed to reorder device") end {:noreply, put_flash(socket, :error, error_message)} end 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} -> if site do {site.display_order || 999_999, site.name} else # Put devices without sites first (display_order 0) {0, ""} end 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 # Generate pagination range with ellipsis for large page counts # Shows: [1] ... [4] [5] [6] ... [20] when on page 5 of 20 defp pagination_range(current_page, total_pages) do cond do total_pages <= 7 -> # Show all pages if 7 or fewer Enum.to_list(1..total_pages) current_page <= 4 -> # Near the start [1, 2, 3, 4, 5, :ellipsis, total_pages] current_page >= total_pages - 3 -> # Near the end [1, :ellipsis, total_pages - 4, total_pages - 3, total_pages - 2, total_pages - 1, total_pages] true -> # In the middle [1, :ellipsis, current_page - 1, current_page, current_page + 1, :ellipsis, total_pages] end end end