towerops/lib/towerops_web/live/org/gaiia_reconciliation_live.ex
Graham McIntire 3267bb134f feat(gaiia): create inventory items for unmatched devices
Adds `Towerops.Gaiia.InventoryCreator` which:
- Matches a device's SNMP-discovered manufacturer/model against Gaiia's
  inventory model library to find the right modelId
- Calls `startAddInventoryItemsJob` to create the item, assigned to the
  correct Gaiia network site with MAC address from SNMP interfaces

A "Create in Gaiia" button now appears next to each untracked device on
the reconciliation page.
2026-05-11 14:58:37 -05:00

120 lines
3.3 KiB
Elixir

defmodule ToweropsWeb.Org.GaiiaReconciliationLive do
@moduledoc false
use ToweropsWeb, :live_view
alias Towerops.Gaiia
alias Towerops.Gaiia.InventoryCreator
alias Towerops.Gaiia.Reconciliation
@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"))}
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("create_gaiia_item", %{"device_id" => device_id}, socket) do
org_id = socket.assigns.organization.id
result =
with {:ok, device} <- get_device(device_id),
{:ok, model_id, model_name} <- InventoryCreator.find_matching_model(org_id, device),
{:ok, _job_id} <- InventoryCreator.create_inventory_item(org_id, device, model_id) do
{:ok, device.name, model_name}
end
{:noreply,
socket
|> put_flash(:info, create_flash(result))
|> load_reconciliation()}
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
defp match_flash({:ok, 0, 0, _remaining}) do
t("No matches found. Link devices manually on the mapping page.")
end
defp match_flash({:ok, ip_matched, site_matched, remaining}) do
total = ip_matched + site_matched
"Auto-matched %{total} device (%{ip} by IP, %{site} by site). %{remaining} remaining."
|> ngettext(
"Auto-matched %{total} devices (%{ip} by IP, %{site} by site). %{remaining} remaining.",
total
)
|> then(
&Gettext.dngettext(ToweropsWeb.Gettext, "", &1, &1,
total: total,
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, model_name}) do
t("Created Gaiia inventory item for %{device} (model: %{model}).",
device: device_name,
model: model_name
)
end
defp create_flash({:error, :no_snmp_model_info}) do
t("Device has no SNMP manufacturer/model data. Run SNMP discovery first.")
end
defp create_flash({:error, :no_matching_model}) do
t("No matching Gaiia inventory model found for this device.")
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 load_reconciliation(socket) do
report = Reconciliation.reconcile(socket.assigns.organization.id)
assign(socket, :report, report)
end
end