defmodule ToweropsWeb.Org.PreseemDevicesLive do @moduledoc false use ToweropsWeb, :live_view alias Towerops.Devices alias Towerops.Preseem @impl true def mount(_params, _session, socket) do org = socket.assigns.current_scope.organization {:ok, socket |> assign(:page_title, t("Preseem Devices")) |> assign(:organization, org) |> assign(:filter, "all") |> assign(:linking_ap_id, nil) |> assign(:device_search, "") |> assign(:search_results, []) |> assign(:suggestions, %{}) |> load_access_points()} end @impl true def handle_params(params, _url, socket) do filter = case params["filter"] do f when f in ~w(all unmatched matched) -> f _ -> "all" end {:noreply, socket |> assign(:filter, filter) |> load_access_points()} end @impl true def handle_event("filter", %{"filter" => filter}, socket) do org = socket.assigns.organization {:noreply, push_patch(socket, to: ~p"/orgs/#{org.slug}/settings/integrations/preseem/devices?filter=#{filter}" )} end @impl true def handle_event("start_link", %{"ap-id" => ap_id}, socket) do {:noreply, socket |> assign(:linking_ap_id, ap_id) |> assign(:device_search, "") |> assign(:search_results, [])} end @impl true def handle_event("cancel_link", _params, socket) do {:noreply, socket |> assign(:linking_ap_id, nil) |> assign(:device_search, "") |> assign(:search_results, [])} end @impl true def handle_event("search_devices", %{"query" => query}, socket) do org = socket.assigns.organization results = if String.length(query) >= 2 do Devices.search_devices(org.id, query) else [] end {:noreply, socket |> assign(:device_search, query) |> assign(:search_results, results)} end @impl true def handle_event("link_device", %{"ap-id" => ap_id, "device-id" => device_id}, socket) do case Preseem.link_access_point(ap_id, device_id) do {:ok, _} -> {:noreply, socket |> assign(:linking_ap_id, nil) |> assign(:device_search, "") |> assign(:search_results, []) |> load_access_points() |> put_flash(:info, t("Device linked successfully"))} {:error, _} -> {:noreply, put_flash(socket, :error, t("Failed to link device"))} end end @impl true def handle_event("unlink_device", %{"ap-id" => ap_id}, socket) do case Preseem.unlink_access_point(ap_id) do {:ok, _} -> {:noreply, socket |> load_access_points() |> put_flash(:info, t("Device unlinked"))} {:error, _} -> {:noreply, put_flash(socket, :error, t("Failed to unlink device"))} end end defp match_confidence_classes(confidence) do case confidence do "unmatched" -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400" "ambiguous" -> "bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-400" "manual" -> "bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400" _ -> "bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400" end end defp humanize_confidence(confidence) do case confidence do "auto_mac" -> "Auto (MAC)" "auto_ip" -> "Auto (IP)" "auto_hostname" -> "Auto (Hostname)" "manual" -> "Manual" "ambiguous" -> "Ambiguous" "unmatched" -> "Unmatched" other -> String.capitalize(other || "unknown") end end defp load_access_points(socket) do org_id = socket.assigns.organization.id filter = socket.assigns.filter access_points = case filter do "unmatched" -> Preseem.list_unmatched_access_points(org_id) "matched" -> Preseem.list_matched_access_points(org_id) _ -> Preseem.list_access_points(org_id) end suggestions = compute_suggestions(access_points, org_id) sorted = Enum.sort_by(access_points, fn ap -> cond do Map.has_key?(suggestions, ap.id) -> 0 is_nil(ap.device_id) -> 1 true -> 2 end end) socket |> assign(:access_points, sorted) |> assign(:suggestions, suggestions) end defp compute_suggestions(access_points, org_id) do unmatched_with_ip = Enum.filter(access_points, &(is_nil(&1.device_id) and &1.ip_address not in [nil, ""])) if unmatched_with_ip == [] do %{} else linked_device_ids = access_points |> Enum.filter(&(not is_nil(&1.device_id))) |> MapSet.new(& &1.device_id) unlinked_by_ip = org_id |> Devices.list_organization_devices() |> Enum.filter(&(not MapSet.member?(linked_device_ids, &1.id) and &1.ip_address not in [nil, ""])) |> Enum.group_by(&to_string(&1.ip_address)) Enum.reduce(unmatched_with_ip, %{}, &add_ap_suggestion(&1, &2, unlinked_by_ip)) end end defp add_ap_suggestion(ap, acc, unlinked_by_ip) do case Map.get(unlinked_by_ip, ap.ip_address, []) do [] -> acc matches -> Map.put(acc, ap.id, Enum.map(matches, &%{device: &1, match_type: "ip"})) end end end