add gaiia subscriber context to device detail pages
Stage 4 of Gaiia integration: when a device is mapped to a Gaiia inventory item, show a Gaiia tab with inventory details, linked subscriber info, billing subscriptions, network site, and IP mismatch indicators.
This commit is contained in:
parent
2e9f1660d0
commit
ce85fb0465
3 changed files with 378 additions and 0 deletions
|
|
@ -132,6 +132,24 @@ defmodule Towerops.Gaiia do
|
|||
)
|
||||
end
|
||||
|
||||
# --- Reverse lookups (Towerops entity → Gaiia entity) ---
|
||||
|
||||
@doc "Find the Gaiia inventory item mapped to a Towerops device."
|
||||
def get_inventory_item_for_device(device_id) do
|
||||
InventoryItem
|
||||
|> where(device_id: ^device_id)
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
@doc "Find the Gaiia network site mapped to a Towerops site."
|
||||
def get_network_site_for_site(site_id) do
|
||||
NetworkSite
|
||||
|> where(site_id: ^site_id)
|
||||
|> limit(1)
|
||||
|> Repo.one()
|
||||
end
|
||||
|
||||
# --- Match Suggestions ---
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
alias Towerops.Devices.Firmware
|
||||
alias Towerops.Devices.MikrotikBackups
|
||||
alias Towerops.Devices.VersionComparator
|
||||
alias Towerops.Gaiia
|
||||
alias Towerops.Monitoring
|
||||
alias Towerops.Repo
|
||||
alias Towerops.Snmp
|
||||
|
|
@ -263,6 +264,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|> assign_backups_data(device_id)
|
||||
|> assign_checks_data(device_id)
|
||||
|> assign_preseem_data()
|
||||
|> assign_gaiia_data()
|
||||
end
|
||||
|
||||
# -- Selective reload helpers (used by handle_info handlers) --
|
||||
|
|
@ -303,6 +305,7 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
"backups" -> assign_backups_data(socket, device_id)
|
||||
"checks" -> assign_checks_data(socket, device_id)
|
||||
"preseem" -> assign_preseem_data(socket)
|
||||
"gaiia" -> assign_gaiia_data(socket)
|
||||
# neighbors, arp, mac, vlans, ip_addresses, debug use data from tab_nav/base
|
||||
_ -> socket
|
||||
end
|
||||
|
|
@ -477,6 +480,48 @@ defmodule ToweropsWeb.DeviceLive.Show do
|
|||
|> assign(:grouped_checks, grouped_checks)
|
||||
end
|
||||
|
||||
# Gaiia tab data.
|
||||
defp assign_gaiia_data(socket) do
|
||||
device = socket.assigns.device
|
||||
gaiia_item = Gaiia.get_inventory_item_for_device(device.id)
|
||||
|
||||
{gaiia_account, gaiia_subscriptions, gaiia_network_site} =
|
||||
if gaiia_item do
|
||||
account =
|
||||
if gaiia_item.assigned_account_gaiia_id do
|
||||
Gaiia.get_account(device.organization_id, gaiia_item.assigned_account_gaiia_id)
|
||||
end
|
||||
|
||||
subscriptions =
|
||||
if gaiia_item.assigned_account_gaiia_id do
|
||||
Gaiia.list_billing_subscriptions_for_account(
|
||||
device.organization_id,
|
||||
gaiia_item.assigned_account_gaiia_id
|
||||
)
|
||||
else
|
||||
[]
|
||||
end
|
||||
|
||||
network_site =
|
||||
if gaiia_item.assigned_network_site_gaiia_id do
|
||||
Gaiia.get_network_site(
|
||||
device.organization_id,
|
||||
gaiia_item.assigned_network_site_gaiia_id
|
||||
)
|
||||
end
|
||||
|
||||
{account, subscriptions, network_site}
|
||||
else
|
||||
{nil, [], nil}
|
||||
end
|
||||
|
||||
socket
|
||||
|> assign(:gaiia_item, gaiia_item)
|
||||
|> assign(:gaiia_account, gaiia_account)
|
||||
|> assign(:gaiia_subscriptions, gaiia_subscriptions)
|
||||
|> assign(:gaiia_network_site, gaiia_network_site)
|
||||
end
|
||||
|
||||
# Preseem tab data.
|
||||
defp assign_preseem_data(socket) do
|
||||
device = socket.assigns.device
|
||||
|
|
|
|||
|
|
@ -247,6 +247,27 @@
|
|||
</.link>
|
||||
<% end %>
|
||||
|
||||
<%= if @gaiia_item do %>
|
||||
<.link
|
||||
patch={~p"/devices/#{@device.id}?tab=gaiia"}
|
||||
class={[
|
||||
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
|
||||
if(@active_tab == "gaiia",
|
||||
do: "border-blue-500 text-blue-600 dark:text-blue-400",
|
||||
else:
|
||||
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
|
||||
)
|
||||
]}
|
||||
>
|
||||
<span class="flex items-center gap-1.5">
|
||||
<.icon
|
||||
name="hero-building-office"
|
||||
class="h-3.5 w-3.5 text-emerald-500 dark:text-emerald-400"
|
||||
/> Gaiia
|
||||
</span>
|
||||
</.link>
|
||||
<% end %>
|
||||
|
||||
<.link
|
||||
patch={~p"/devices/#{@device.id}?tab=checks"}
|
||||
class={[
|
||||
|
|
@ -2306,6 +2327,300 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% "gaiia" -> %>
|
||||
<%= 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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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" />
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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">
|
||||
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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
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"
|
||||
>
|
||||
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">
|
||||
No Gaiia inventory item linked to this device.
|
||||
</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<% "preseem" -> %>
|
||||
<%= if @preseem_access_point do %>
|
||||
<div class="space-y-6">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue