towerops/lib/towerops_web/live/device_live/components/gaiia_tab.ex
Graham McIntire c4e301a84c fix: address code review findings - memory leaks and redundant queries (#192)
Fixed 5 critical issues from code review:

1. **GlobalSearchTrigger hook listener leak**
   - Added destroyed() callback to remove click listener
   - Prevents memory leak on LiveView unmount

2. **NetworkMap/WeathermapViewer hook listener leaks**
   - Store zoom/fit button handlers as properties
   - Remove DOM event listeners in destroyed() callback
   - Fixes leak on every LiveView remount

3. **SitesMap uses undocumented window.liveSocket.execJS**
   - Changed to proper LiveView pattern using pushEvent
   - Attach listener via Leaflet's popupopen event

4. **handle_info(:refresh_data) double DB fetch**
   - Removed redundant Devices.get_device call
   - Reuse device from assign_base_data

5. **Redundant DB queries in DeviceLive.Show overview**
   - Changed load_sensor_chart_data to accept snmp_device
   - Changed load_overall_traffic_chart_data to accept snmp_device
   - Eliminated 4 redundant Snmp.get_device_with_associations calls

Also removed unused functions:
- load_equipment_data/2
- reload_active_tab_data/1
- assign_subscriber_impact/1

Note: DeviceLive.Show module size (2252 lines) is acknowledged but
deferred as it requires larger architectural refactoring.

Reviewed-on: graham/towerops-web#192
2026-03-27 16:34:42 -05:00

331 lines
14 KiB
Elixir

defmodule ToweropsWeb.DeviceLive.Components.GaiiaTab do
@moduledoc """
LiveComponent for the device Gaiia integration tab.
Displays Gaiia inventory item data, assigned account, subscriptions,
and network site information.
"""
use ToweropsWeb, :live_component
alias ToweropsWeb.DeviceLive.Helpers.DataLoaders
@impl true
def update(assigns, socket) do
{:ok,
socket
|> assign(assigns)
|> load_gaiia_data()}
end
defp load_gaiia_data(socket) do
device = socket.assigns.device
gaiia_data = DataLoaders.load_gaiia_data(device)
socket
|> assign(:gaiia_item, gaiia_data.gaiia_item)
|> assign(:gaiia_account, gaiia_data.gaiia_account)
|> assign(:gaiia_subscriptions, gaiia_data.gaiia_subscriptions)
|> assign(:gaiia_network_site, gaiia_data.gaiia_network_site)
end
@impl true
def render(assigns) do
~H"""
<div id="gaiia-tab">
<%= if @gaiia_item do %>
<div class="space-y-6">
<%!-- Inventory Item Details --%>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("Gaiia Inventory Item")}
</h3>
</div>
<div class="px-4 py-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_item.name || "---"}
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Gaiia ID</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.gaiia_id}
</dd>
</div>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Status</dt>
<dd class="mt-1">
<span class={[
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
case @gaiia_item.status do
"active" ->
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
"decommissioned" ->
"bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400"
_ ->
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
end
]}>
{@gaiia_item.status || "unknown"}
</span>
</dd>
</div>
<%= if @gaiia_item.model_name do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Model</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_item.model_name}
</dd>
</div>
<% end %>
<%= if @gaiia_item.manufacturer_name do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Manufacturer")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_item.manufacturer_name}
</dd>
</div>
<% end %>
<%= if @gaiia_item.category do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Category")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_item.category}
</dd>
</div>
<% end %>
<%= if @gaiia_item.serial_number do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Serial Number")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.serial_number}
</dd>
</div>
<% end %>
<%= if @gaiia_item.mac_address do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("MAC Address")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.mac_address}
</dd>
</div>
<% end %>
<%= if @gaiia_item.ip_address do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("IP Address (Gaiia)")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_item.ip_address}
<%= if @device.ip_address && @gaiia_item.ip_address != @device.ip_address do %>
<span class="ml-2 inline-flex items-center rounded-full bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
<.icon name="hero-exclamation-triangle-mini" class="mr-0.5 h-3 w-3" />
{t("Mismatch")}
</span>
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<%!-- Network Site --%>
<%= if @gaiia_network_site do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("Gaiia Network Site")}
</h3>
</div>
<div class="px-4 py-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Site Name")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_network_site.name}
</dd>
</div>
<%= if @gaiia_network_site.account_count do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Subscribers")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_network_site.account_count}
</dd>
</div>
<% end %>
<%= if @gaiia_network_site.total_mrr do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Monthly Revenue")}
</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
${Decimal.round(@gaiia_network_site.total_mrr, 2)}
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<%!-- Linked Subscriber --%>
<%= if @gaiia_account do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("Linked Subscriber")}
</h3>
</div>
<div class="px-4 py-4">
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2 lg:grid-cols-3">
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
{@gaiia_account.name}
</dd>
</div>
<%= if @gaiia_account.readable_id do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">
{t("Account ID")}
</dt>
<dd class="mt-1 text-sm font-mono text-gray-900 dark:text-white">
{@gaiia_account.readable_id}
</dd>
</div>
<% end %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">Status</dt>
<dd class="mt-1">
<span class={[
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
case @gaiia_account.status do
"active" ->
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
"suspended" ->
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
_ ->
"bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400"
end
]}>
{@gaiia_account.status || "unknown"}
</span>
</dd>
</div>
<%= if @gaiia_account.mrr do %>
<div>
<dt class="text-xs font-medium text-gray-500 dark:text-gray-400">MRR</dt>
<dd class="mt-1 text-sm text-gray-900 dark:text-white">
${Decimal.round(@gaiia_account.mrr, 2)}
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<%!-- Billing Subscriptions --%>
<%= if @gaiia_subscriptions != [] do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-white/5">
<h3 class="text-sm font-medium text-gray-900 dark:text-white">
{t("Billing Subscriptions")}
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-white/5">
<tr>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Plan")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("Status")}
</th>
<th
scope="col"
class="px-4 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500 dark:text-gray-400"
>
{t("MRR")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white dark:divide-white/5 dark:bg-transparent">
<%= for sub <- @gaiia_subscriptions do %>
<tr class="hover:bg-gray-50 dark:hover:bg-white/5">
<td class="whitespace-nowrap px-4 py-3 text-sm text-gray-900 dark:text-white">
{sub.product_name || "---"}
</td>
<td class="whitespace-nowrap px-4 py-3 text-sm">
<span class={[
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
case sub.status do
"active" ->
"bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400"
"suspended" ->
"bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
_ ->
"bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400"
end
]}>
{sub.status || "unknown"}
</span>
</td>
<td class="whitespace-nowrap px-4 py-3 text-right text-sm text-gray-900 dark:text-white">
<%= if sub.mrr_amount do %>
${Decimal.round(sub.mrr_amount, 2)}
<span class="text-gray-400">
{sub.currency || ""}
</span>
<% else %>
---
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-8 text-center">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t("No Gaiia inventory item linked to this device.")}
</p>
</div>
<% end %>
</div>
"""
end
end