Fix auto_match return tuple order (mac/ip/site were swapped). Add ghost devices tab for cleaning up stale mappings to deleted devices. Add "Link" button on untracked devices to manually link to existing Gaiia items. Add manual refresh button for on-demand reconciliation.
275 lines
7.9 KiB
Elixir
275 lines
7.9 KiB
Elixir
defmodule ToweropsWeb.Org.GaiiaReconciliationLive do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Gaiia
|
|
alias Towerops.Gaiia.InventoryCreator
|
|
alias Towerops.Gaiia.Reconciliation
|
|
|
|
require Logger
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
org = socket.assigns.current_scope.organization
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:organization, org)
|
|
|> assign(:page_title, t("Gaiia Inventory Reconciliation"))
|
|
|> assign(:open_create_for, nil)
|
|
|> assign(:open_link_for, nil)
|
|
|> assign(:link_search_query, "")
|
|
|> assign(:link_search_results, [])
|
|
|> assign(:manufacturers, [])
|
|
|> assign(:models, %{})
|
|
|> assign(:selected_manufacturer, nil)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
tab = Map.get(params, "tab", "summary")
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:active_tab, tab)
|
|
|> load_reconciliation()}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("auto_match", _params, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, match_flash(Gaiia.auto_match_untracked(org_id)))
|
|
|> load_reconciliation()}
|
|
end
|
|
|
|
def handle_event("refresh", _params, socket) do
|
|
{:noreply, load_reconciliation(socket)}
|
|
end
|
|
|
|
def handle_event("open_create_form", %{"device_id" => device_id}, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
|
|
socket =
|
|
if socket.assigns.manufacturers == [] do
|
|
case InventoryCreator.list_manufacturers(org_id) do
|
|
{:ok, manufacturers} -> assign(socket, :manufacturers, manufacturers)
|
|
{:error, _} -> socket
|
|
end
|
|
else
|
|
socket
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:open_create_for, device_id)
|
|
|> assign(:open_link_for, nil)
|
|
|> assign(:selected_manufacturer, nil)}
|
|
end
|
|
|
|
def handle_event("cancel_create", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:open_create_for, nil)
|
|
|> assign(:selected_manufacturer, nil)}
|
|
end
|
|
|
|
def handle_event("select_manufacturer", %{"manufacturer_id" => ""}, socket) do
|
|
{:noreply, assign(socket, :selected_manufacturer, nil)}
|
|
end
|
|
|
|
def handle_event("select_manufacturer", %{"manufacturer_id" => manufacturer_id}, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
existing = socket.assigns.models
|
|
|
|
socket =
|
|
if Map.has_key?(existing, manufacturer_id) do
|
|
assign(socket, :selected_manufacturer, manufacturer_id)
|
|
else
|
|
case InventoryCreator.list_models(org_id, manufacturer_id) do
|
|
{:ok, []} ->
|
|
Logger.warning("Gaiia: no models for manufacturer #{manufacturer_id}")
|
|
|
|
socket
|
|
|> assign(:selected_manufacturer, manufacturer_id)
|
|
|> put_flash(:warning, t("No models found for this manufacturer."))
|
|
|
|
{:ok, models} ->
|
|
socket
|
|
|> assign(:models, Map.put(existing, manufacturer_id, models))
|
|
|> assign(:selected_manufacturer, manufacturer_id)
|
|
|
|
{:error, reason} ->
|
|
Logger.error("Gaiia: failed to fetch models: #{inspect(reason)}")
|
|
|
|
put_flash(socket, :error, t("Failed to load models. Check server logs."))
|
|
end
|
|
end
|
|
|
|
{:noreply, socket}
|
|
end
|
|
|
|
def handle_event("create_gaiia_item", %{"device_id" => _, "model_id" => ""}, socket) do
|
|
{:noreply, put_flash(socket, :warning, t("Please select a model."))}
|
|
end
|
|
|
|
def handle_event("create_gaiia_item", %{"device_id" => device_id, "model_id" => model_id}, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
|
|
result =
|
|
with {:ok, device} <- get_device(device_id),
|
|
{:ok, _job_id} <- InventoryCreator.create_inventory_item(org_id, device, model_id) do
|
|
{:ok, device.name}
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:open_create_for, nil)
|
|
|> assign(:selected_manufacturer, nil)
|
|
|> put_flash(:info, create_flash(result))
|
|
|> load_reconciliation()}
|
|
end
|
|
|
|
# -- Link to existing Gaiia item from untracked tab --
|
|
|
|
def handle_event("start_link_search", %{"device_id" => device_id}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:open_link_for, device_id)
|
|
|> assign(:open_create_for, nil)
|
|
|> assign(:link_search_query, "")
|
|
|> assign(:link_search_results, [])}
|
|
end
|
|
|
|
def handle_event("cancel_link_search", _params, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> assign(:open_link_for, nil)
|
|
|> assign(:link_search_query, "")
|
|
|> assign(:link_search_results, [])}
|
|
end
|
|
|
|
def handle_event("search_gaiia_items", %{"query" => query}, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
|
|
results =
|
|
if String.length(query) >= 2 do
|
|
org_id
|
|
|> Gaiia.search_inventory_items(query)
|
|
|> Enum.filter(&is_nil(&1.device_id))
|
|
else
|
|
[]
|
|
end
|
|
|
|
{:noreply, socket |> assign(:link_search_query, query) |> assign(:link_search_results, results)}
|
|
end
|
|
|
|
def handle_event("link_to_gaiia", %{"device_id" => device_id, "gaiia_id" => gaiia_id}, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
|
|
result =
|
|
case Gaiia.get_inventory_item(org_id, gaiia_id) do
|
|
nil -> {:error, :not_found}
|
|
item -> Gaiia.update_inventory_item_mapping(item, %{device_id: device_id})
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:open_link_for, nil)
|
|
|> assign(:link_search_query, "")
|
|
|> assign(:link_search_results, [])
|
|
|> put_flash(:info, link_flash(result))
|
|
|> load_reconciliation()}
|
|
end
|
|
|
|
# -- Ghost device cleanup --
|
|
|
|
def handle_event("unlink_ghost", %{"gaiia_id" => gaiia_id}, socket) do
|
|
org_id = socket.assigns.organization.id
|
|
|
|
case Gaiia.get_inventory_item(org_id, gaiia_id) do
|
|
nil ->
|
|
{:noreply, put_flash(socket, :error, t("Gaiia inventory item not found"))}
|
|
|
|
item ->
|
|
case Gaiia.update_inventory_item_mapping(item, %{device_id: nil}) do
|
|
{:ok, _} ->
|
|
{:noreply,
|
|
socket
|
|
|> load_reconciliation()
|
|
|> put_flash(:info, t("Unlinked stale device mapping."))}
|
|
|
|
{:error, _} ->
|
|
{:noreply, put_flash(socket, :error, t("Failed to unlink stale mapping."))}
|
|
end
|
|
end
|
|
end
|
|
|
|
def handle_event("select_tab", %{"tab" => tab}, socket) do
|
|
org = socket.assigns.organization
|
|
|
|
{:noreply,
|
|
push_patch(socket,
|
|
to: ~p"/orgs/#{org.slug}/settings/integrations/gaiia/reconciliation?tab=#{tab}"
|
|
)}
|
|
end
|
|
|
|
# -- Flash helpers --
|
|
|
|
defp match_flash({:ok, 0, 0, 0, _remaining}) do
|
|
t("No matches found. Link devices manually on the mapping page.")
|
|
end
|
|
|
|
defp match_flash({:ok, mac_matched, ip_matched, site_matched, remaining}) do
|
|
total = mac_matched + ip_matched + site_matched
|
|
|
|
ngettext(
|
|
"Auto-matched %{total} device (%{mac} by MAC, %{ip} by IP, %{site} by site). %{remaining} remaining.",
|
|
"Auto-matched %{total} devices (%{mac} by MAC, %{ip} by IP, %{site} by site). %{remaining} remaining.",
|
|
total,
|
|
total: total,
|
|
mac: mac_matched,
|
|
ip: ip_matched,
|
|
site: site_matched,
|
|
remaining: remaining
|
|
)
|
|
end
|
|
|
|
defp get_device(device_id) do
|
|
case Towerops.Devices.get_device(device_id) do
|
|
nil -> {:error, :not_found}
|
|
device -> {:ok, device}
|
|
end
|
|
end
|
|
|
|
defp create_flash({:ok, device_name}) do
|
|
t("Created Gaiia inventory item for %{device}.", device: device_name)
|
|
end
|
|
|
|
defp create_flash({:error, :no_gaiia_integration}) do
|
|
t("Gaiia integration not configured.")
|
|
end
|
|
|
|
defp create_flash({:error, reason}) do
|
|
t("Failed to create Gaiia inventory item: %{reason}", reason: inspect(reason))
|
|
end
|
|
|
|
defp link_flash({:ok, _item}) do
|
|
t("Device linked to Gaiia inventory item.")
|
|
end
|
|
|
|
defp link_flash({:error, :not_found}) do
|
|
t("Gaiia inventory item not found.")
|
|
end
|
|
|
|
defp link_flash({:error, _reason}) do
|
|
t("Failed to link device.")
|
|
end
|
|
|
|
defp load_reconciliation(socket) do
|
|
report = Reconciliation.reconcile(socket.assigns.organization.id)
|
|
assign(socket, :report, report)
|
|
end
|
|
end
|