towerops/lib/towerops_web/live/device_live/show.html.heex

3664 lines
185 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="devices"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<div class="mb-6 -mt-2">
<.breadcrumb items={
[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "Devices", navigate: ~p"/devices"}
] ++
if(@device.site,
do: [%{label: @device.site.name, navigate: ~p"/sites/#{@device.site.id}"}],
else: []
) ++
[%{label: @device.name}]
} />
<div class="sticky top-0 z-30 -mx-4 px-4 sm:-mx-6 sm:px-6 lg:-ml-4 lg:-mr-8 lg:pl-4 lg:pr-8 py-3 bg-gray-50/95 dark:bg-gray-950/95 backdrop-blur-sm border-b border-gray-200/50 dark:border-white/5 mb-4">
<div class="flex items-start justify-between gap-2">
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<h1 class="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white truncate">
{@device.name}
</h1>
<div class={[
"inline-flex shrink-0 items-center gap-2 px-2.5 py-1 rounded-lg font-semibold text-sm transition-all",
case @device.status do
:up ->
"bg-green-100 text-green-800 dark:bg-green-950/50 dark:text-green-400 ring-1 ring-green-600/20 dark:ring-green-500/30"
:down ->
"bg-red-100 text-red-800 dark:bg-red-950/50 dark:text-red-400 ring-1 ring-red-600/20 dark:ring-red-500/30"
:unknown ->
"bg-gray-100 text-gray-800 dark:bg-gray-800/50 dark:text-gray-400 ring-1 ring-gray-600/20 dark:ring-gray-500/30"
end
]}>
<span class={[
"h-2 w-2 rounded-full animate-pulse",
case @device.status do
:up -> "bg-green-600 dark:bg-green-500"
:down -> "bg-red-600 dark:bg-red-500"
:unknown -> "bg-gray-500 dark:bg-gray-400"
end
]}>
</span>
<span>
{case @device.status do
:up -> "Online"
:down -> "Offline"
:unknown -> "Unknown"
end}
</span>
</div>
</div>
<div class="flex flex-wrap items-center gap-1.5 mt-1 text-xs text-gray-500 dark:text-gray-400">
<span
class="font-mono text-gray-600 dark:text-gray-400 group/ip cursor-pointer"
phx-click={JS.dispatch("phx:copy", detail: %{text: @device.ip_address})}
title={t("Click to copy")}
>
{@device.ip_address}
<.icon
name="hero-clipboard-document"
class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/ip:opacity-100 transition-opacity text-gray-400"
/>
</span>
<span class="text-gray-300 dark:text-gray-600">·</span>
<span class="inline-flex items-center gap-1.5">
<span class="text-xs text-gray-400 dark:text-gray-500 font-medium">
{t("Agent:")}
</span>
<%= if @agent_info.type == :cloud do %>
<span class="inline-flex items-center gap-1 text-blue-600 dark:text-blue-400">
<.icon name="hero-cloud" class="h-3 w-3" />
<%= if @agent_info.agent_token_id do %>
{t("Cloud")} ({@agent_info.name})
<% else %>
{t("Cloud")}
<% end %>
</span>
<% else %>
<%= if @agent_info.is_offline do %>
<span
class="inline-flex items-center gap-1 px-1.5 py-0.5 font-medium text-amber-700 bg-amber-100 dark:text-amber-400 dark:bg-amber-900/30 rounded"
title="Agent has not checked in for over 5 minutes."
>
<.icon name="hero-exclamation-triangle" class="h-3 w-3" />
{@agent_info.name} · Offline
</span>
<% else %>
<span class="inline-flex items-center gap-1 text-green-600 dark:text-green-400">
<.icon name="hero-server" class="h-3 w-3" />
{@agent_info.name}
</span>
<% end %>
<% end %>
</span>
</div>
</div>
<div class="shrink-0 mt-0.5">
<.button navigate={~p"/devices/#{@device.id}/edit"}>
<.icon name="hero-pencil" class="h-4 w-4" />
<span class="hidden sm:inline">{t("Edit")}</span>
</.button>
</div>
</div>
</div>
<!-- Subscriber Impact Card -->
<%= if @subscriber_impact && @subscriber_impact.subscriber_count > 0 do %>
<div class="mb-4 rounded-lg border border-indigo-200 bg-indigo-50 dark:border-indigo-800/50 dark:bg-indigo-950/30 px-4 py-3">
<div class="flex items-center gap-6">
<div class="flex items-center gap-2">
<.icon name="hero-users" class="h-5 w-5 text-indigo-600 dark:text-indigo-400" />
<span class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">
{@subscriber_impact.subscriber_count} {if @subscriber_impact.subscriber_count == 1,
do: "subscriber",
else: "subscribers"}
</span>
</div>
<%= if @can_view_financials && @subscriber_impact.mrr do %>
<div class="flex items-center gap-2">
<.icon
name="hero-currency-dollar"
class="h-5 w-5 text-indigo-600 dark:text-indigo-400"
/>
<span class="text-sm font-semibold text-indigo-900 dark:text-indigo-200">
{format_mrr(@subscriber_impact.mrr)}/mo
</span>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- Tabs -->
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8 overflow-x-auto">
<.link
patch={~p"/devices/#{@device.id}?tab=overview"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "overview" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Overview")}
</.link>
<%= if @snmp_device do %>
<.link
patch={~p"/devices/#{@device.id}?tab=ports"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "ports" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Ports")}
</.link>
<% end %>
<%= if assigns[:wireless_clients_available] do %>
<.link
patch={~p"/devices/#{@device.id}?tab=wireless"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors inline-flex items-center gap-1.5",
if @active_tab == "wireless" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Wireless")}
<span class="inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 px-2 py-0.5 text-xs font-medium text-gray-600 dark:text-gray-300">
{@wireless_client_count}
</span>
</.link>
<% end %>
<%= if assigns[:transceivers_available] do %>
<.link
patch={~p"/devices/#{@device.id}?tab=transceivers"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "transceivers" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Transceivers")}
</.link>
<% end %>
<%= if assigns[:printer_supplies_available] do %>
<.link
patch={~p"/devices/#{@device.id}?tab=printer_supplies"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "printer_supplies" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Printer Supplies")}
</.link>
<% end %>
<%= if assigns[:hardware_inventory_available] do %>
<.link
patch={~p"/devices/#{@device.id}?tab=hardware"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "hardware" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Hardware Inventory")}
</.link>
<% end %>
<%= if @device.snmp_enabled do %>
<.link
patch={~p"/devices/#{@device.id}?tab=neighbors"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "neighbors" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Neighbors")}
</.link>
<% end %>
<%= if @arp_entries && length(@arp_entries) > 0 do %>
<.link
patch={~p"/devices/#{@device.id}?tab=arp"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "arp" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("ARP Table")}
</.link>
<% end %>
<%= if @mac_addresses && length(@mac_addresses) > 0 do %>
<.link
patch={~p"/devices/#{@device.id}?tab=mac"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "mac" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("MAC Table")}
</.link>
<% end %>
<%= if @vlans && length(@vlans) > 0 do %>
<.link
patch={~p"/devices/#{@device.id}?tab=vlans"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "vlans" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("VLANs")}
</.link>
<% end %>
<%= if length(@ipv4_addresses) > 0 || length(@ipv6_addresses) > 0 do %>
<.link
patch={~p"/devices/#{@device.id}?tab=ip_addresses"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "ip_addresses" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("IP Addresses")}
</.link>
<% end %>
<%= if false and @device.mikrotik_enabled do %>
<.link
patch={~p"/devices/#{@device.id}?tab=backups"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "backups" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-beaker" class="h-3.5 w-3.5 text-blue-500 dark:text-blue-400" />
{t("Backups")}
</span>
</.link>
<% end %>
<%= if false and @device.mikrotik_enabled do %>
<.link
navigate={~p"/devices/#{@device.id}/config-timeline"}
class="whitespace-nowrap py-4 px-1 border-b-2 border-transparent font-medium text-sm 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-clock" class="h-3.5 w-3.5 text-orange-500 dark:text-orange-400" />
{t("Config Timeline")}
</span>
</.link>
<% end %>
<%= if @preseem_access_point do %>
<.link
patch={~p"/devices/#{@device.id}?tab=preseem"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "preseem",
do: "border-blue-500 text-blue-600 dark:text-blue-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<span class="flex items-center gap-1.5">
<.icon name="hero-signal" class="h-3.5 w-3.5 text-purple-500 dark:text-purple-400" />
{t("Preseem")}
</span>
</.link>
<% end %>
<%= if @gaiia_item do %>
<.link
patch={~p"/devices/#{@device.id}?tab=gaiia"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if(@active_tab == "gaiia",
do: "border-blue-500 text-blue-600 dark:text-blue-400 font-semibold",
else:
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
)
]}
>
<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={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "checks" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Checks")}
</.link>
<.link
patch={~p"/devices/#{@device.id}?tab=logs"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "logs" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Events")}
</.link>
<%= if @snmp_device && @snmp_device.raw_discovery_data do %>
<.link
patch={~p"/devices/#{@device.id}?tab=debug"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 text-sm transition-colors",
if @active_tab == "debug" do
"border-blue-500 text-blue-600 dark:text-blue-400 font-semibold"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300 font-medium"
end
]}
>
{t("Raw Data")}
</.link>
<% end %>
</nav>
</div>
</div>
<!-- Tab Content -->
<div class="mt-6">
<%= case @active_tab do %>
<% "overview" -> %>
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 items-start">
<!-- Left Column: Device Information & Traffic -->
<div class="space-y-6">
<!-- Device Information -->
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Device Information")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= if @snmp_device do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
System Name
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{@snmp_device.sys_name || @device.name}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
{t("Resolved IP")}
</dt>
<dd
class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono group/rip cursor-pointer"
phx-click={JS.dispatch("phx:copy", detail: %{text: @device.ip_address})}
title={t("Click to copy")}
>
{@device.ip_address}
<.icon
name="hero-clipboard-document"
class="h-3.5 w-3.5 inline ml-1 opacity-0 group-hover/rip:opacity-100 transition-opacity text-gray-400"
/>
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Hardware</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{@snmp_device.model || "N/A"}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
Operating System
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<div class="flex items-center justify-end gap-2">
<span>
{cond do
@snmp_device.manufacturer && @snmp_device.firmware_version ->
"#{@snmp_device.manufacturer} #{@snmp_device.firmware_version}"
@snmp_device.manufacturer ->
@snmp_device.manufacturer
true ->
@snmp_device.sys_descr || "N/A"
end}
</span>
<%= if firmware_update_available?(@snmp_device, @available_firmware) do %>
<div class="group/firmware relative inline-flex items-center">
<.icon
name="hero-arrow-up-circle"
class="h-5 w-5 text-blue-500 cursor-help dark:text-blue-400"
/>
<!-- Invisible bridge to prevent gap -->
<div class="absolute left-0 top-0 w-full h-8 z-40"></div>
<!-- Tooltip popup -->
<div class="invisible group-hover/firmware:visible hover:visible absolute left-0 top-7 z-50 w-80 rounded-lg bg-white dark:bg-gray-800 shadow-xl ring-1 ring-black ring-opacity-5 dark:ring-white dark:ring-opacity-10 p-4">
<div class="flex items-start gap-3">
<.icon
name="hero-arrow-up-circle"
class="h-5 w-5 text-blue-500 dark:text-blue-400 flex-shrink-0 mt-0.5"
/>
<div class="flex-1 min-w-0">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white mb-2">
{t("Firmware Update Available")}
</h4>
<div class="space-y-1.5 text-xs text-gray-600 dark:text-gray-400">
<div>
<span class="font-medium">Current:</span>
<span class="font-mono ml-1">
{@snmp_device.firmware_version}
</span>
</div>
<div>
<span class="font-medium">Latest:</span>
<span class="font-mono ml-1 text-blue-600 dark:text-blue-400">
{@available_firmware.version}
</span>
<%= if @available_firmware.release_date do %>
<span class="text-gray-500 dark:text-gray-400 dark:text-gray-500 dark:text-gray-400">
(released {format_date(
@available_firmware.release_date
)})
</span>
<% end %>
</div>
</div>
<div class="mt-3 flex gap-2">
<a
href={@available_firmware.download_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium bg-blue-600 text-white hover:bg-blue-700"
>
{t("Download")}
<.icon
name="hero-arrow-top-right-on-square"
class="h-3 w-3"
/>
</a>
<%= if @available_firmware.changelog_url do %>
<a
href={@available_firmware.changelog_url}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 rounded px-2 py-1 text-xs font-medium text-blue-600 dark:text-blue-400 hover:bg-blue-50 dark:hover:bg-blue-900/20"
>
{t("Release Notes")}
<.icon
name="hero-arrow-top-right-on-square"
class="h-3 w-3"
/>
</a>
<% end %>
</div>
</div>
</div>
</div>
</div>
<% end %>
</div>
</dd>
</div>
<div
:if={@snmp_device.serial_number}
class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5"
>
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
Serial Number
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
{@snmp_device.serial_number}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Object ID</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
{@snmp_device.sys_object_id || "N/A"}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Contact</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{@snmp_device.sys_contact || "N/A"}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Location</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{format_location(@snmp_device.sys_location)}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Uptime</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{format_uptime(@snmp_device.sys_uptime)}
</dd>
</div>
<% else %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Name</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{@device.name}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
IP Address
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
{@device.ip_address}
</dd>
</div>
<% end %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
Device Added
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{format_device_age(@device.inserted_at)}
</dd>
</div>
<div class="flex justify-between py-1.5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
Last Discovered
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{if @device.last_discovery_at do
format_device_age(@device.last_discovery_at)
else
"Never"
end}
</dd>
</div>
<div class="flex justify-between py-1.5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">Last Polled</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
{if @device.last_snmp_poll_at do
format_device_age(@device.last_snmp_poll_at)
else
"Never"
end}
</dd>
</div>
</dl>
</div>
</div>
<!-- Overall Traffic Chart -->
<%= if @traffic_chart_data do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.link
navigate={~p"/devices/#{@device.id}/graph/traffic"}
class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Overall Traffic")}
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
</.link>
<div class="p-4">
<div
id="traffic-chart"
phx-hook="SensorChart"
phx-update="ignore"
data-chart={@traffic_chart_data}
data-unit="bps"
data-auto-scale="true"
data-show-zero-line="true"
class="relative"
style="height: 300px;"
>
<div class="absolute inset-0 flex items-center justify-center" data-loading>
<div class="flex flex-col items-center gap-2">
<div class="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 dark:border-gray-700 border-t-blue-500">
</div>
<span class="text-xs text-gray-400">{t("Loading chart...")}</span>
</div>
</div>
<canvas></canvas>
</div>
</div>
</div>
<% end %>
<!-- ICMP Latency Chart -->
<%= if @latency_chart_data do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.link
navigate={~p"/devices/#{@device.id}/graph/latency"}
class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("ICMP Latency")}
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
</.link>
<div class="p-4">
<div
id="latency-chart"
phx-hook="SensorChart"
phx-update="ignore"
data-chart={@latency_chart_data}
data-unit="ms"
data-auto-scale="true"
class="relative"
style="height: 300px;"
>
<div class="absolute inset-0 flex items-center justify-center" data-loading>
<div class="flex flex-col items-center gap-2">
<div class="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 dark:border-gray-700 border-t-blue-500">
</div>
<span class="text-xs text-gray-400">{t("Loading chart...")}</span>
</div>
</div>
<canvas></canvas>
</div>
</div>
</div>
<% end %>
<%!-- Recent Config Changes mini-card --%>
<%= if false && @device.mikrotik_enabled && assigns[:recent_config_changes] && Enum.any?(@recent_config_changes) do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-clock" class="h-4 w-4 text-orange-500" />
{t("Recent Config Changes")}
</h3>
<.link
navigate={~p"/devices/#{@device.id}/config-timeline"}
class="text-xs text-blue-500 hover:underline"
>
View Timeline →
</.link>
</div>
<div class="p-3 space-y-2">
<%= for event <- Enum.take(@recent_config_changes, 5) do %>
<div class="flex items-center justify-between text-sm">
<div class="flex items-center gap-2">
<span class={"w-2 h-2 rounded-full #{config_change_dot_color(event)}"} />
<span class="text-gray-600 dark:text-gray-400">
{Calendar.strftime(event.changed_at, "%b %d %H:%M")}
</span>
<span
:for={s <- Enum.take(event.sections_changed, 3)}
class="badge text-xs !px-2 !py-0 text-gray-700 dark:text-gray-300"
>
{s}
</span>
</div>
<span class="text-gray-500 dark:text-gray-400 text-xs">
{event.change_size} lines
</span>
</div>
<% end %>
</div>
</div>
<% end %>
</div>
<!-- Right Column: Graphs and Metrics -->
<div class="space-y-6">
<!-- CPU / Processors -->
<%= if @processor_chart_data || (@processors && length(@processors) > 0) do %>
<% processor_count = if @processors, do: length(@processors), else: 0
avg_load =
if processor_count > 0 do
loads = @processors |> Enum.map(& &1.load_percent) |> Enum.reject(&is_nil/1)
if length(loads) > 0, do: Enum.sum(loads) / length(loads), else: nil
else
nil
end %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.link
navigate={~p"/devices/#{@device.id}/graph/processors"}
class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
<%= if processor_count > 0 do %>
{processor_count} {if processor_count == 1,
do: "Processor",
else: "Processors"}
<% else %>
{t("Processors")}
<% end %>
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
</.link>
<!-- Show chart if we have time-series data -->
<%= if @processor_chart_data do %>
<div class="p-4">
<div
id="processor-chart"
phx-hook="SensorChart"
phx-update="ignore"
data-chart={@processor_chart_data}
data-unit="%"
style="height: 200px;"
>
<canvas></canvas>
</div>
</div>
<% end %>
<!-- Show combined processor load -->
<%= if avg_load do %>
<div class={[
"p-4",
@processor_chart_data && "border-t border-gray-200 dark:border-white/10"
]}>
<div class="flex items-center justify-between gap-4">
<span class="text-sm text-gray-700 dark:text-gray-300">
{t("Average Load")}
</span>
<div class="flex items-center gap-3 flex-1 max-w-md">
<div class="flex-1 bg-gray-200 rounded-full h-3 dark:bg-gray-700">
<div
class={[
"h-3 rounded-full transition-all",
avg_load < 50 && "bg-green-500",
avg_load >= 50 && avg_load < 80 && "bg-yellow-500",
avg_load >= 80 && "bg-red-500"
]}
style={"width: #{min(avg_load, 100)}%"}
>
</div>
</div>
<span class="text-sm font-medium text-gray-900 dark:text-white w-12 text-right">
{Float.round(avg_load, 0)}%
</span>
</div>
</div>
</div>
<% end %>
</div>
<% end %>
<!-- Memory Usage Chart -->
<%= if @memory_chart_data do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<.link
navigate={~p"/devices/#{@device.id}/graph/memory"}
class="block px-4 py-3 border-b border-gray-200 dark:border-white/10 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors"
>
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Memory Usage")}
</h3>
<.icon name="hero-arrow-right" class="h-4 w-4 text-gray-400" />
</div>
</.link>
<div class="p-4">
<div
id="memory-chart"
phx-hook="SensorChart"
phx-update="ignore"
data-chart={@memory_chart_data}
style="height: 300px;"
>
<canvas></canvas>
</div>
</div>
</div>
<% end %>
<!-- Storage Usage -->
<%= if @storage_sensors && length(@storage_sensors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Storage Usage")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= for sensor <- @storage_sensors do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/storage?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit}
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- Physical Storage Volumes (from HOST-RESOURCES-MIB) -->
<%= if @storage && length(@storage) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Storage Volumes")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@storage)})
</span>
</h3>
</div>
<!-- Storage Volume Chart -->
<%= if @storage_volume_chart_data do %>
<div class="p-4 border-b border-gray-200 dark:border-white/10">
<div
id="storage-volume-chart"
phx-hook="SensorChart"
phx-update="ignore"
data-chart={@storage_volume_chart_data}
data-unit="%"
style="height: 200px;"
>
<canvas></canvas>
</div>
</div>
<% end %>
<div class="p-4 space-y-4">
<%= for storage <- @storage do %>
<% usage_percent =
if storage.total_bytes && storage.total_bytes > 0 do
Float.round(storage.used_bytes / storage.total_bytes * 100, 1)
else
0.0
end %>
<.link
navigate={
~p"/devices/#{@device.id}/graph/storage_volume?storage_id=#{storage.id}"
}
class="block hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 py-1 rounded transition-colors"
>
<div class="flex items-center justify-between mb-1">
<span class="text-sm text-gray-700 dark:text-gray-300 hover:text-blue-600 dark:hover:text-blue-400 hover:underline">
{storage.description || "Storage #{storage.storage_index}"}
</span>
<span class="text-sm font-medium text-gray-900 dark:text-white">
{format_bytes(storage.used_bytes)} / {format_bytes(storage.total_bytes)}
</span>
</div>
<div class="flex items-center gap-3">
<div class="flex-1 bg-gray-200 rounded-full h-2 dark:bg-gray-700">
<div
class={[
"h-2 rounded-full transition-all",
usage_percent < 70 && "bg-green-500",
usage_percent >= 70 && usage_percent < 90 && "bg-yellow-500",
usage_percent >= 90 && "bg-red-500"
]}
style={"width: #{min(usage_percent, 100)}%"}
>
</div>
</div>
<span class="text-xs font-medium text-gray-600 dark:text-gray-400 w-12 text-right">
{usage_percent}%
</span>
</div>
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{String.replace(storage.storage_type || "other", "_", " ")
|> String.capitalize()}
</div>
</.link>
<% end %>
</div>
</div>
<% end %>
<!-- Temperature Sensors -->
<%= if @temperature_sensors && length(@temperature_sensors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Temperature")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= for sensor <- @temperature_sensors do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/temperature?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit || "°C"}
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- Voltage Sensors -->
<%= if @voltage_sensors && length(@voltage_sensors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Voltage")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= for sensor <- @voltage_sensors do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/voltage?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit}
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- SFP Transceivers -->
<%= if @grouped_transceivers && length(@grouped_transceivers) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Transceivers")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= for {_transceiver_name, sensors} <- @grouped_transceivers do %>
<%= for sensor <- sensors do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/dbm?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white font-mono">
<%= if sensor.latest_reading do %>
{format_sensor_value(
sensor.latest_reading.value,
sensor.sensor_divisor
)}
{sensor.sensor_unit}
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- Count Sensors (DHCP Leases, Connections, etc.) -->
<%= if (@general_counters && length(@general_counters) > 0) || (@firewall_counters && length(@firewall_counters) > 0) do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Counters")}
</h3>
</div>
<div class="p-4 space-y-4">
<!-- General Counters -->
<%= if @general_counters && length(@general_counters) > 0 do %>
<dl class="space-y-1">
<%= for sensor <- @general_counters do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
{trunc(sensor.latest_reading.value)}
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
{sensor.sensor_unit}
<% end %>
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
</dl>
<% end %>
<!-- Firewall Counters -->
<%= if @firewall_counters && length(@firewall_counters) > 0 do %>
<div>
<h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 mb-2 uppercase tracking-wider">
{t("Firewall")}
</h4>
<dl class="space-y-1">
<%= for sensor <- @firewall_counters do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/#{sensor.sensor_type}?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
{trunc(sensor.latest_reading.value)}
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
{sensor.sensor_unit}
<% end %>
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
<% end %>
</div>
</div>
<% end %>
<!-- Wireless Sensors (Frequency, Power, CCQ, etc.) -->
<%= if @wireless_sensors && length(@wireless_sensors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Wireless")}
</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= for sensor <- @wireless_sensors do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="shrink-0 text-sm text-gray-600 dark:text-gray-400">
<.link
navigate={
~p"/devices/#{@device.id}/graph/wireless?sensor_id=#{sensor.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{sensor.sensor_descr}
</.link>
</dt>
<dd class="flex-1 text-right ml-4 text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading && sensor.latest_reading.value do %>
<%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %>
{trunc(sensor.latest_reading.value)}
<% else %>
{:erlang.float_to_binary(sensor.latest_reading.value * 1.0,
decimals: 1
)}
<% end %>
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
{sensor.sensor_unit}
<% end %>
<% else %>
{t("N/A")}
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
</div>
</div>
<%!-- Connected Devices --%>
<%= if @connected_devices != [] do %>
<div class="mt-6 bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Connected Devices")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@connected_devices)} links)
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">Device</th>
<th class="px-4 py-3 text-left font-medium">IP Address</th>
<th class="px-4 py-3 text-left font-medium">Interface</th>
<th class="px-4 py-3 text-left font-medium">Link Type</th>
<th class="px-4 py-3 text-left font-medium">Confidence</th>
<th class="px-4 py-3 text-left font-medium">Status</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for link <- @connected_devices do %>
<% peer_device =
cond do
link.target_device && link.target_device.id != @device.id ->
link.target_device
link.source_device && link.source_device.id != @device.id ->
link.source_device
link.target_device ->
link.target_device
true ->
nil
end %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3">
<%= if peer_device do %>
<.link
navigate={~p"/devices/#{peer_device.id}"}
class="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{peer_device.name}
</.link>
<% else %>
<div class="text-gray-900 dark:text-white">
{link.discovered_remote_name || link.discovered_remote_mac ||
"Unknown"}
</div>
<% end %>
</td>
<td class="px-4 py-3 font-mono text-sm text-gray-900 dark:text-white">
<%= if peer_device do %>
{peer_device.ip_address}
<% else %>
{link.discovered_remote_ip || "-"}
<% end %>
</td>
<td class="px-4 py-3 text-gray-900 dark:text-white">
<%= if link.source_interface do %>
{link.source_interface.if_name}
<% else %>
-
<% end %>
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium uppercase",
cond do
link.link_type in ["lldp", "cdp"] ->
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200"
link.link_type == "mac_match" ->
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
link.link_type == "arp_inference" ->
"bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200"
true ->
"bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200"
end
]}>
{link.link_type}
</span>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-16 bg-gray-200 dark:bg-gray-700 rounded-full h-1.5">
<div
class={[
"h-1.5 rounded-full",
cond do
link.confidence >= 0.9 -> "bg-green-500"
link.confidence >= 0.7 -> "bg-blue-500"
link.confidence >= 0.5 -> "bg-yellow-500"
true -> "bg-red-500"
end
]}
style={"width: #{trunc(link.confidence * 100)}%"}
>
</div>
</div>
<span class="text-xs text-gray-600 dark:text-gray-400">
{trunc(link.confidence * 100)}%
</span>
</div>
</td>
<td class="px-4 py-3">
<%= if peer_device do %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
{t("Managed")}
</span>
<% else %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300">
{t("Discovered")}
</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
<% "ports" -> %>
<%= if @snmp_interfaces && length(@snmp_interfaces) > 0 do %>
<div class="space-y-6">
<%= for {category, interfaces} <- @interfaces_by_type do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{category} Interfaces
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(interfaces)})
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-3 py-2 text-left font-medium">#</th>
<th class="px-3 py-2 text-left font-medium">Name</th>
<th class="px-3 py-2 text-left font-medium">Status</th>
<th class="px-3 py-2 text-right font-medium">Speed</th>
<th class="px-3 py-2 text-left font-medium">IP Address</th>
<th class="px-3 py-2 text-left font-medium">MAC</th>
<th class="px-3 py-2 text-left font-medium">Capacity</th>
<th class="px-3 py-2 text-left font-medium">Utilization</th>
<th class="px-3 py-2 text-right font-medium">In</th>
<th class="px-3 py-2 text-right font-medium">Out</th>
<th class="px-3 py-2 text-right font-medium">Errors</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for interface <- interfaces do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<td class="px-3 py-2 text-gray-500 dark:text-gray-400 font-mono text-xs">
{interface.if_index}
</td>
<td class="px-3 py-2">
<.link
navigate={
~p"/devices/#{@device.id}/graph/traffic?interface_id=#{interface.id}"
}
class="hover:text-blue-600 dark:hover:text-blue-400"
>
<div class="font-medium text-gray-900 dark:text-white hover:underline text-sm">
{interface.if_name || interface.if_descr}
</div>
<%= if interface.if_alias do %>
<div class="text-xs text-gray-500 dark:text-gray-400 truncate max-w-[200px]">
{interface.if_alias}
</div>
<% end %>
</.link>
</td>
<td class="px-3 py-2">
<span class="inline-flex items-center gap-1.5 text-xs">
<span class={[
"h-2 w-2 rounded-full flex-shrink-0",
interface.if_oper_status == "up" && "bg-green-500",
interface.if_oper_status == "down" && "bg-red-500",
interface.if_oper_status not in ["up", "down"] && "bg-gray-400"
]} />
<span class={[
"font-medium",
interface.if_oper_status == "up" &&
"text-green-700 dark:text-green-400",
interface.if_oper_status == "down" &&
"text-red-700 dark:text-red-400",
interface.if_oper_status not in ["up", "down"] &&
"text-gray-600 dark:text-gray-400"
]}>
{String.upcase(interface.if_oper_status || "unknown")}
</span>
</span>
</td>
<td class="px-3 py-2 text-right text-gray-900 dark:text-white text-xs font-mono">
{format_speed(interface.if_speed)}
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">
<% interface_ips =
Map.get(@ip_addresses_by_interface, interface.id, []) %>
<%= if Enum.empty?(interface_ips) do %>
<span class="text-gray-400">-</span>
<% else %>
<div class="space-y-0.5">
<%= for ip <- interface_ips do %>
<div
class="cursor-pointer group/iip"
phx-click={
JS.dispatch("phx:copy", detail: %{text: ip.ip_address})
}
title={t("Click to copy")}
>
{ip.ip_address}
<%= if ip.prefix_length do %>
/{ip.prefix_length}
<% end %>
<.icon
name="hero-clipboard-document"
class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/iip:opacity-100 transition-opacity text-gray-400"
/>
</div>
<% end %>
</div>
<% end %>
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">
<span
:if={interface.if_phys_address}
class="cursor-pointer group/mac"
phx-click={
JS.dispatch("phx:copy",
detail: %{text: interface.if_phys_address}
)
}
title={t("Click to copy")}
>
{interface.if_phys_address}
<.icon
name="hero-clipboard-document"
class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/mac:opacity-100 transition-opacity text-gray-400"
/>
</span>
<span :if={!interface.if_phys_address} class="text-gray-400">-</span>
</td>
<td class="px-3 py-2 text-xs">
<%= if interface.configured_capacity_bps do %>
<div class="flex items-center gap-1">
<span class="font-mono text-gray-900 dark:text-white">
{format_speed(interface.configured_capacity_bps)}
</span>
<span class={[
"px-1 py-0.5 rounded text-[10px] font-medium",
capacity_source_class(interface.capacity_source)
]}>
{capacity_source_label(interface.capacity_source)}
</span>
<button
id={"clear-capacity-#{interface.id}"}
phx-click="clear_capacity"
phx-value-interface_id={interface.id}
class="text-gray-400 hover:text-red-500 transition-colors ml-0.5"
title={t("Clear capacity")}
>
<.icon name="hero-x-mark" class="h-3 w-3" />
</button>
</div>
<% else %>
<button
id={"set-capacity-#{interface.id}"}
phx-click={
JS.push("set_capacity",
value: %{
interface_id: interface.id,
capacity_mbps:
if(interface.if_speed,
do: to_string(div(interface.if_speed, 1_000_000)),
else: ""
)
}
)
}
class="text-xs text-blue-600 dark:text-blue-400 hover:underline"
>
{t("Set")}
</button>
<% end %>
</td>
<td class="px-3 py-2 text-xs">
<%= if interface.utilization do %>
<div class="flex items-center gap-2 min-w-[100px]">
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
<div
class={[
"h-full rounded-full transition-all",
utilization_color(interface.utilization.utilization_pct)
]}
style={"width: #{min(interface.utilization.utilization_pct, 100)}%"}
>
</div>
</div>
<span class={[
"font-mono whitespace-nowrap",
utilization_text_color(interface.utilization.utilization_pct)
]}>
{Float.round(interface.utilization.utilization_pct, 1)}%
</span>
</div>
<% else %>
<span class="text-gray-400">-</span>
<% end %>
</td>
<td class="px-3 py-2 text-right font-mono text-xs text-gray-600 dark:text-gray-400">
{if interface.latest_stat && interface.latest_stat.if_in_octets,
do: format_bytes(interface.latest_stat.if_in_octets),
else: "-"}
</td>
<td class="px-3 py-2 text-right font-mono text-xs text-gray-600 dark:text-gray-400">
{if interface.latest_stat && interface.latest_stat.if_out_octets,
do: format_bytes(interface.latest_stat.if_out_octets),
else: "-"}
</td>
<td class="px-3 py-2 text-right font-mono text-xs">
<% total_errors =
interface.latest_stat &&
(interface.latest_stat.if_in_errors || 0) +
(interface.latest_stat.if_out_errors || 0) +
(interface.latest_stat.if_in_discards || 0) +
(interface.latest_stat.if_out_discards || 0) %>
<%= if total_errors && total_errors > 0 do %>
<.link
navigate={
~p"/devices/#{@device.id}/graph/interface_errors?interface_id=#{interface.id}"
}
class="text-red-600 dark:text-red-400 hover:underline"
>
{total_errors}
</.link>
<% else %>
<span class="text-gray-400">-</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="text-center py-12">
<p class="text-gray-500 dark:text-gray-400">No interfaces discovered</p>
</div>
<% end %>
<% "wireless" -> %>
<%= if @wireless_clients && length(@wireless_clients) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 flex items-center justify-between">
<div>
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Connected Wireless Clients")}
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{length(@wireless_clients)} clients — {@wireless_matched_count} subscribers matched
<%= if @wireless_last_updated do %>
· Last updated {format_relative_time(@wireless_last_updated)}
<% end %>
</p>
</div>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-3 py-2 text-left font-medium">MAC Address</th>
<th class="px-3 py-2 text-left font-medium">IP Address</th>
<th class="px-3 py-2 text-left font-medium">Subscriber</th>
<th class="px-3 py-2 text-left font-medium">Signal</th>
<th class="px-3 py-2 text-left font-medium">SNR</th>
<th class="px-3 py-2 text-right font-medium">Distance</th>
<th class="px-3 py-2 text-right font-medium">TX Rate</th>
<th class="px-3 py-2 text-right font-medium">RX Rate</th>
<th class="px-3 py-2 text-right font-medium">Uptime</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for client <- @wireless_clients do %>
<% subscriber_link =
Map.get(@wireless_client_subscribers, String.downcase(client.mac_address)) %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<td class="px-3 py-2 font-mono text-xs">
<span
class="cursor-pointer group/mac text-gray-900 dark:text-white"
phx-click={JS.dispatch("phx:copy", detail: %{text: client.mac_address})}
title={t("Click to copy")}
>
{client.mac_address}
<.icon
name="hero-clipboard-document"
class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/mac:opacity-100 transition-opacity text-gray-400"
/>
</span>
</td>
<td class="px-3 py-2 font-mono text-xs text-gray-600 dark:text-gray-400">
<%= if client.ip_address do %>
<span
class="cursor-pointer group/ip"
phx-click={
JS.dispatch("phx:copy", detail: %{text: client.ip_address})
}
title={t("Click to copy")}
>
{client.ip_address}
<.icon
name="hero-clipboard-document"
class="h-3 w-3 inline ml-0.5 opacity-0 group-hover/ip:opacity-100 transition-opacity text-gray-400"
/>
</span>
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
<td class="px-3 py-2 text-xs">
<%= if subscriber_link && subscriber_link.gaiia_account do %>
<span class="text-gray-900 dark:text-white font-medium">
{subscriber_link.gaiia_account.account_name}
</span>
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
<td class="px-3 py-2 text-xs">
<%= if client.signal_strength do %>
<.signal_badge value={client.signal_strength} />
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
<td class="px-3 py-2 text-xs">
<%= if client.snr do %>
<.snr_badge value={client.snr} />
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
<td class="px-3 py-2 text-right font-mono text-xs text-gray-900 dark:text-white">
<%= if client.distance do %>
{client.distance}m
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
<td class="px-3 py-2 text-right font-mono text-xs text-gray-900 dark:text-white">
{format_rate(client.tx_rate)}
</td>
<td class="px-3 py-2 text-right font-mono text-xs text-gray-900 dark:text-white">
{format_rate(client.rx_rate)}
</td>
<td class="px-3 py-2 text-right font-mono text-xs text-gray-600 dark:text-gray-400">
<%= if client.uptime_seconds do %>
{format_uptime(client.uptime_seconds * 100)}
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="text-center py-12">
<.icon
name="hero-signal"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600"
/>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{t("No wireless clients")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("No clients are currently connected to this access point.")}
</p>
</div>
<% end %>
<% "transceivers" -> %>
<%= if @transceivers && length(@transceivers) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Optical Transceivers")}
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{length(@transceivers)} {if length(@transceivers) == 1,
do: "transceiver",
else: "transceivers"} installed
</p>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">Port</th>
<th class="px-4 py-3 text-left font-medium">Type</th>
<th class="px-4 py-3 text-left font-medium">Vendor</th>
<th class="px-4 py-3 text-left font-medium">Part Number</th>
<th class="px-4 py-3 text-left font-medium">Serial Number</th>
<th class="px-4 py-3 text-right font-medium">Wavelength</th>
<th class="px-4 py-3 text-center font-medium">DOM</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for transceiver <- @transceivers do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<td class="px-4 py-3 font-mono text-xs text-gray-900 dark:text-white">
{transceiver.port_index}
</td>
<td class="px-4 py-3">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
{transceiver.transceiver_type || "Unknown"}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
{transceiver.vendor_name || "—"}
</td>
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">
{transceiver.vendor_part_number || "—"}
</td>
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">
{transceiver.vendor_serial_number || "—"}
</td>
<td class="px-4 py-3 text-right font-mono text-xs text-gray-900 dark:text-white">
<%= if transceiver.wavelength_nm do %>
{transceiver.wavelength_nm} nm
<% else %>
<span class="text-gray-400">—</span>
<% end %>
</td>
<td class="px-4 py-3 text-center">
<%= if transceiver.supports_dom do %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-400">
DOM
</span>
<% else %>
<span class="text-gray-400 text-xs">—</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="text-center py-12">
<.icon
name="hero-cpu-chip"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600"
/>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{t("No transceivers found")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("No optical transceivers detected on this device.")}
</p>
</div>
<% end %>
<% "printer_supplies" -> %>
<%= if @printer_supplies && length(@printer_supplies) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Printer Supplies")}
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{length(@printer_supplies)} {if length(@printer_supplies) == 1,
do: "supply",
else: "supplies"} monitored
</p>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">Type</th>
<th class="px-4 py-3 text-left font-medium">Description</th>
<th class="px-4 py-3 text-left font-medium">Color</th>
<th class="px-4 py-3 text-left font-medium">Part Number</th>
<th class="px-4 py-3 text-right font-medium">Level</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for supply <- @printer_supplies do %>
<% percent =
if supply.max_capacity && supply.max_capacity > 0 do
round(supply.current_level * 100 / supply.max_capacity)
else
nil
end %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
case supply.supply_type do
"toner" ->
"bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400"
"ink" ->
"bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400"
"drum" ->
"bg-purple-100 text-purple-800 dark:bg-purple-900/30 dark:text-purple-400"
_ ->
"bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400"
end
]}>
{supply.supply_type || "Unknown"}
</span>
</td>
<td class="px-4 py-3 text-sm text-gray-900 dark:text-white">
{supply.supply_description || "—"}
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{supply.color_name || "—"}
</td>
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">
{supply.part_number || "—"}
</td>
<td class="px-4 py-3 text-right">
<%= if percent do %>
<div class="flex items-center justify-end gap-2">
<div class="w-24 h-2 bg-gray-200 dark:bg-gray-700 rounded-full overflow-hidden">
<div
class={[
"h-full transition-all",
cond do
percent < 20 -> "bg-red-500"
percent < 40 -> "bg-yellow-500"
true -> "bg-green-500"
end
]}
style={"width: #{percent}%"}
>
</div>
</div>
<span class={[
"text-xs font-medium",
cond do
percent < 20 -> "text-red-600 dark:text-red-400"
percent < 40 -> "text-yellow-600 dark:text-yellow-400"
true -> "text-green-600 dark:text-green-400"
end
]}>
{percent}%
</span>
</div>
<% else %>
<span class="text-gray-400 text-xs">—</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="text-center py-12">
<.icon name="hero-printer" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600" />
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{t("No printer supplies found")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("No printer supplies detected on this device.")}
</p>
</div>
<% end %>
<% "hardware" -> %>
<%= if @hardware_inventory && length(@hardware_inventory) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Hardware Inventory")}
</h3>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{length(@hardware_inventory)} {if length(@hardware_inventory) == 1,
do: "component",
else: "components"} detected
</p>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">Class</th>
<th class="px-4 py-3 text-left font-medium">Name</th>
<th class="px-4 py-3 text-left font-medium">Description</th>
<th class="px-4 py-3 text-left font-medium">Serial Number</th>
<th class="px-4 py-3 text-left font-medium">Model</th>
<th class="px-4 py-3 text-left font-medium">Manufacturer</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for entity <- @hardware_inventory do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
<td class="px-4 py-3">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-800 dark:bg-gray-900/30 dark:text-gray-400">
{entity.entity_class || "Unknown"}
</span>
</td>
<td class="px-4 py-3 text-sm font-medium text-gray-900 dark:text-white">
{entity.name || "—"}
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entity.description || "—"}
</td>
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">
{entity.serial_number || "—"}
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entity.model || "—"}
</td>
<td class="px-4 py-3 text-sm text-gray-600 dark:text-gray-400">
{entity.manufacturer || "—"}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="text-center py-12">
<.icon
name="hero-cpu-chip"
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-600"
/>
<h3 class="mt-2 text-sm font-medium text-gray-900 dark:text-white">
{t("No hardware inventory found")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("No hardware components detected on this device.")}
</p>
</div>
<% end %>
<% "neighbors" -> %>
<%= if @neighbors && length(@neighbors) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Discovered Neighbors")}
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">Protocol</th>
<th class="px-4 py-3 text-left font-medium">Local Port</th>
<th class="px-4 py-3 text-left font-medium">Remote Device</th>
<th class="px-4 py-3 text-left font-medium">Remote Port</th>
<th class="px-4 py-3 text-left font-medium">IP Address</th>
<th class="px-4 py-3 text-left font-medium">Platform</th>
<th class="px-4 py-3 text-left font-medium">Capabilities</th>
<th class="px-4 py-3 text-left font-medium">Last Seen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for neighbor <- @neighbors do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium uppercase",
neighbor.protocol == "lldp" &&
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
neighbor.protocol == "cdp" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"
]}>
{neighbor.protocol}
</span>
</td>
<td class="px-4 py-3">
<.link
navigate={
~p"/devices/#{@device.id}/graph/traffic?interface_id=#{neighbor.interface.id}"
}
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{neighbor.interface.if_name || neighbor.interface.if_descr}
</.link>
</td>
<td class="px-4 py-3">
<%= if neighbor.matched_device do %>
<.link
navigate={~p"/devices/#{neighbor.matched_device.id}"}
class="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{neighbor.matched_device.name}
</.link>
<% else %>
<div class="text-gray-900 dark:text-white">
<%= if neighbor.remote_system_name do %>
{neighbor.remote_system_name |> String.split("/") |> List.first()}
<% else %>
{t("Unknown")}
<% end %>
</div>
<% end %>
</td>
<td class="px-4 py-3">
<div class="text-gray-900 dark:text-white">
{neighbor.remote_port_id || "-"}
</div>
<%= if !neighbor.matched_device && neighbor.remote_chassis_id do %>
<div class="text-xs font-mono text-gray-500 dark:text-gray-400">
{neighbor.remote_chassis_id}
</div>
<% end %>
</td>
<td class="px-4 py-3 font-mono text-sm text-gray-900 dark:text-white">
{neighbor.remote_address || "-"}
</td>
<td class="px-4 py-3 text-xs text-gray-600 dark:text-gray-400">
{neighbor.remote_platform || "-"}
</td>
<td class="px-4 py-3 text-xs text-gray-600 dark:text-gray-400">
<%= if neighbor.remote_capabilities && length(neighbor.remote_capabilities) > 0 do %>
{Enum.join(neighbor.remote_capabilities, ", ")}
<% else %>
-
<% end %>
</td>
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
{time_ago(neighbor.last_discovered_at)}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-server-stack" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No neighbors discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"This device doesn't have any LLDP or CDP neighbors, or neighbor discovery hasn't run yet."
)}
</p>
</div>
</div>
<% end %>
<% "arp" -> %>
<%= if @arp_entries && length(@arp_entries) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("ARP Table")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@arp_entries)} entries)
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">IP Address</th>
<th class="px-4 py-3 text-left font-medium">MAC Address</th>
<th class="px-4 py-3 text-left font-medium">Interface</th>
<th class="px-4 py-3 text-left font-medium">Type</th>
<th class="px-4 py-3 text-left font-medium">Last Seen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for arp <- @arp_entries do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 font-mono text-gray-900 dark:text-white">
{arp.ip_address}
</td>
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">
{arp.mac_address}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
<%= if arp.interface do %>
<.link
navigate={
~p"/devices/#{@device.id}/graph/traffic?interface_id=#{arp.interface.id}"
}
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{arp.interface.if_name || arp.interface.if_descr}
</.link>
<% else %>
<span class="text-gray-400">ifIndex {arp.if_index}</span>
<% end %>
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
arp.entry_type == "dynamic" &&
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
arp.entry_type == "static" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
arp.entry_type not in ["dynamic", "static"] &&
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
]}>
{arp.entry_type || "unknown"}
</span>
</td>
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
{time_ago(arp.last_seen_at)}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-table-cells" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No ARP entries discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("This device doesn't have any ARP entries, or ARP discovery hasn't run yet.")}
</p>
</div>
</div>
<% end %>
<% "mac" -> %>
<%= if @mac_addresses && length(@mac_addresses) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("MAC Address Table")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@mac_addresses)} entries)
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">MAC Address</th>
<th class="px-4 py-3 text-left font-medium">VLAN</th>
<th class="px-4 py-3 text-left font-medium">Interface</th>
<th class="px-4 py-3 text-left font-medium">Status</th>
<th class="px-4 py-3 text-left font-medium">Last Seen</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for mac <- @mac_addresses do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 font-mono text-xs text-gray-900 dark:text-white">
{mac.mac_address}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
<%= if mac.vlan_id do %>
{mac.vlan_id}
<% else %>
<span class="text-gray-400">-</span>
<% end %>
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
<%= if mac.interface do %>
<.link
navigate={
~p"/devices/#{@device.id}/graph/traffic?interface_id=#{mac.interface.id}"
}
class="text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
{mac.interface.if_name || mac.interface.if_descr}
</.link>
<% else %>
<span class="text-gray-400">Port {mac.port_index}</span>
<% end %>
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
mac.entry_status == "learned" &&
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
mac.entry_status == "self" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
mac.entry_status == "mgmt" &&
"bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200",
mac.entry_status not in ["learned", "self", "mgmt"] &&
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
]}>
{mac.entry_status || "unknown"}
</span>
</td>
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
{time_ago(mac.last_seen_at)}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-table-cells" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No MAC addresses discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"This device doesn't have any MAC forwarding entries, or MAC discovery hasn't run yet."
)}
</p>
</div>
</div>
<% end %>
<% "vlans" -> %>
<%= if @vlans && length(@vlans) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Configured VLANs")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@vlans)})
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">VLAN ID</th>
<th class="px-4 py-3 text-left font-medium">Name</th>
<th class="px-4 py-3 text-left font-medium">Type</th>
<th class="px-4 py-3 text-left font-medium">Status</th>
<th class="px-4 py-3 text-left font-medium">Last Checked</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for vlan <- @vlans do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 text-gray-900 dark:text-white font-mono">
{vlan.vlan_id}
</td>
<td class="px-4 py-3 font-medium text-gray-900 dark:text-white">
{vlan.vlan_name || "-"}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{vlan.vlan_type || "ethernet"}
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
vlan.status == "active" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
vlan.status == "suspended" &&
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200",
vlan.status not in ["active", "suspended"] &&
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
]}>
{String.upcase(vlan.status || "unknown")}
</span>
</td>
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
{time_ago(vlan.last_checked_at)}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-square-3-stack-3d" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No VLANs discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"This device doesn't have any VLANs configured, or VLAN discovery hasn't run yet."
)}
</p>
</div>
</div>
<% end %>
<% "ip_addresses" -> %>
<div class="space-y-6">
<!-- IPv4 Addresses -->
<%= if length(@ipv4_addresses) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200 mr-2">
{t("IPv4")}
</span>
{t("Addresses")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@ipv4_addresses)})
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">IP Address</th>
<th class="px-4 py-3 text-left font-medium">Subnet</th>
<th class="px-4 py-3 text-left font-medium">Interface</th>
<th class="px-4 py-3 text-left font-medium">Primary</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for ip <- @ipv4_addresses do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 text-gray-900 dark:text-white font-mono">
{ip.ip_address}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 font-mono">
<%= if ip.prefix_length do %>
/{ip.prefix_length}
<% else %>
{ip.subnet_mask || "-"}
<% end %>
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{(ip.snmp_interface &&
(ip.snmp_interface.if_name || ip.snmp_interface.if_descr)) || "-"}
</td>
<td class="px-4 py-3">
<%= if ip.is_primary do %>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200">
{t("Primary")}
</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500 dark:text-gray-400">
-
</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
<!-- IPv6 Addresses -->
<%= if length(@ipv6_addresses) > 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200 mr-2">
{t("IPv6")}
</span>
{t("Addresses")}
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@ipv6_addresses)})
</span>
</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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-3 text-left font-medium">IP Address</th>
<th class="px-4 py-3 text-left font-medium">Prefix</th>
<th class="px-4 py-3 text-left font-medium">Interface</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for ip <- @ipv6_addresses do %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 text-gray-900 dark:text-white font-mono text-xs">
{ip.ip_address}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 font-mono">
/{ip.prefix_length || "-"}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{(ip.snmp_interface &&
(ip.snmp_interface.if_name || ip.snmp_interface.if_descr)) || "-"}
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% end %>
<!-- No IP addresses found -->
<%= if length(@ipv4_addresses) == 0 && length(@ipv6_addresses) == 0 do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-globe-alt" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No IP addresses discovered")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"This device doesn't have any IP addresses discovered, or IP discovery hasn't run yet."
)}
</p>
</div>
</div>
<% end %>
</div>
<% "backups" -> %>
<%= if false and @device.mikrotik_enabled do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<div class="flex items-center justify-between">
<div class="flex items-center gap-2">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Configuration Backups")}
<%= if Enum.any?(@mikrotik_backups) do %>
<span class="ml-2 text-xs font-normal text-gray-500 dark:text-gray-400">
({length(@mikrotik_backups)})
</span>
<% end %>
</h3>
<span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-blue-700 bg-blue-100 rounded dark:bg-blue-900 dark:text-blue-200">
<.icon name="hero-beaker" class="h-3 w-3" /> Experimental
</span>
</div>
<%= if @agent_info.agent_token_id do %>
<button
phx-click="backup_now"
phx-throttle="2000"
class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
>
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> Backup Now
</button>
<% else %>
<div class="text-sm text-amber-600 dark:text-amber-400">
{t("Assign an agent to enable backups")}
</div>
<% end %>
</div>
</div>
<%= if Enum.any?(@mikrotik_backups) do %>
<%!-- Instructions for comparing backups --%>
<%= if length(@mikrotik_backups) > 1 do %>
<div class="px-4 py-3 bg-blue-50 dark:bg-blue-950/30 border-b border-blue-100 dark:border-blue-900/50">
<div class="flex items-start gap-2">
<.icon
name="hero-information-circle"
class="h-5 w-5 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-0.5"
/>
<div class="text-sm text-blue-900 dark:text-blue-200">
<span class="font-medium">Compare configurations:</span>
{t("Select any 2 backups using the checkboxes to compare their differences")}
</div>
</div>
</div>
<% end %>
<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-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th
class="px-4 py-3 text-center font-medium w-12"
title="Select backups to compare"
>
<.icon name="hero-check-circle" class="h-4 w-4 mx-auto" />
</th>
<th class="px-4 py-3 text-left font-medium">Date</th>
<th class="px-4 py-3 text-left font-medium">Original Size</th>
<th class="px-4 py-3 text-left font-medium">Compressed Size</th>
<th class="px-4 py-3 text-left font-medium">Ratio</th>
<th class="px-4 py-3 text-left font-medium">Source</th>
<th class="px-4 py-3 text-left font-medium">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for backup <- @mikrotik_backups do %>
<% compression_ratio =
if backup.config_size_bytes && backup.config_size_bytes > 0 do
Float.round(
(1 - backup.compressed_size_bytes / backup.config_size_bytes) * 100,
1
)
else
0.0
end %>
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800">
<td class="px-4 py-3 text-center">
<input
type="checkbox"
phx-click="toggle_backup_selection"
phx-value-id={backup.id}
checked={MapSet.member?(@selected_backup_ids, backup.id)}
disabled={
!MapSet.member?(@selected_backup_ids, backup.id) &&
MapSet.size(@selected_backup_ids) >= 2
}
title={
if MapSet.member?(@selected_backup_ids, backup.id),
do: "Selected for comparison",
else:
if(MapSet.size(@selected_backup_ids) >= 2,
do: "Maximum 2 backups can be selected",
else: "Select to compare"
)
}
class="h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-600 disabled:opacity-50 disabled:cursor-not-allowed dark:border-gray-600 dark:bg-gray-700"
/>
</td>
<td class="px-4 py-3 text-gray-900 dark:text-white whitespace-nowrap">
{ToweropsWeb.TimeHelpers.format_iso8601(backup.backed_up_at, @timezone)}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 font-mono">
{format_bytes(backup.config_size_bytes)}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400 font-mono">
{format_bytes(backup.compressed_size_bytes)}
</td>
<td class="px-4 py-3 text-gray-600 dark:text-gray-400">
{compression_ratio}%
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
backup.trigger_source == "daily_cron" &&
"bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200",
backup.trigger_source == "manual" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
backup.trigger_source == "pre_change" &&
"bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200"
]}>
{String.replace(backup.trigger_source, "_", " ")
|> String.capitalize()}
</span>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<button
phx-click="download_backup"
phx-value-id={backup.id}
class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-blue-600 hover:text-blue-800 dark:text-blue-400 dark:hover:text-blue-300"
>
<.icon name="hero-arrow-down-tray" class="h-4 w-4" /> Download
</button>
<%= if ToweropsWeb.Permissions.owner?(@current_scope) do %>
<.button
phx-click="delete_backup"
phx-value-id={backup.id}
data-confirm={
t(
"Are you sure you want to delete this backup? This action cannot be undone."
)
}
variant="danger"
>
<.icon name="hero-trash" class="h-4 w-4" /> Delete
</.button>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%!-- Pagination --%>
<%= if assigns[:pagination] do %>
<div class="px-4 py-3 border-t border-gray-200 dark:border-white/10">
<.pagination
meta={@pagination}
path={~p"/devices/#{@device.id}"}
params={%{"tab" => "backups"}}
/>
</div>
<% end %>
<%!-- Floating Compare Button --%>
<%= if MapSet.size(@selected_backup_ids) > 0 do %>
<div class="fixed bottom-8 right-8 z-50">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-white/10 p-4">
<div class="flex flex-col gap-2">
<%!-- Selection status --%>
<div class="flex items-center gap-2 text-sm">
<.icon
name="hero-check-circle"
class="h-5 w-5 text-blue-600 dark:text-blue-400"
/>
<div class="text-gray-700 dark:text-gray-300">
<span class="font-medium">{MapSet.size(@selected_backup_ids)}</span>
of 2 backups selected
</div>
</div>
<%!-- Instructions when only 1 selected --%>
<%= if MapSet.size(@selected_backup_ids) == 1 do %>
<div class="text-xs text-gray-500 dark:text-gray-400 pl-7">
{t("Select one more backup to compare")}
</div>
<% end %>
<%!-- Action buttons --%>
<div class="flex items-center gap-2 mt-1">
<button
phx-click="clear_selection"
class="inline-flex items-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 border border-gray-300 rounded-lg transition-colors dark:bg-gray-700 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-600"
>
{t("Clear")}
</button>
<button
phx-click="compare_selected"
disabled={MapSet.size(@selected_backup_ids) != 2}
class="inline-flex items-center gap-1.5 px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed rounded-lg transition-colors shadow-sm"
>
<.icon name="hero-arrows-right-left" class="h-4 w-4" />
{if MapSet.size(@selected_backup_ids) == 2,
do: "Compare Selected",
else: "Compare"}
</button>
</div>
</div>
</div>
</div>
<% end %>
<% else %>
<div class="p-8 text-center">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No backups yet")}
</h3>
<%= if @agent_info.agent_token_id do %>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
Automatic backups run daily at {ToweropsWeb.TimeHelpers.format_utc_hour(
7,
@timezone
)}.
</p>
<% else %>
<p class="mt-1 text-sm text-amber-600 dark:text-amber-400">
{t("Assign an agent to enable automatic backups.")}
</p>
<% end %>
</div>
<% end %>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12">
<div class="text-center">
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("MikroTik API not enabled")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("Enable MikroTik API in device settings to enable configuration backups.")}
</p>
<div class="mt-6">
<.button navigate={~p"/devices/#{@device.id}/edit"}>
{t("Edit Device Settings")}
</.button>
</div>
</div>
</div>
<% end %>
<% "logs" -> %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">{t("Event Logs")}</h3>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
{t("Showing the most recent 100 events")}
</p>
</div>
<%= if Enum.any?(@events) do %>
<div class="divide-y divide-gray-200 dark:divide-white/10">
<%= for event <- @events do %>
<div class="px-4 py-3 hover:bg-gray-50 dark:hover:bg-gray-800">
<div class="flex items-start gap-3">
<!-- Severity Icon -->
<div class={[
"flex-shrink-0 mt-0.5",
event.severity == "info" && "text-blue-500",
event.severity == "warning" && "text-yellow-500",
event.severity == "critical" && "text-red-500"
]}>
<%= if event.severity == "info" do %>
<.icon name="hero-information-circle" class="w-5 h-5" />
<% end %>
<%= if event.severity == "warning" do %>
<.icon name="hero-exclamation-triangle" class="w-5 h-5" />
<% end %>
<%= if event.severity == "critical" do %>
<.icon name="hero-x-circle" class="w-5 h-5" />
<% end %>
</div>
<!-- Event Details -->
<div class="flex-1 min-w-0">
<div class="flex items-start justify-between gap-2">
<div class="flex-1">
<p class="text-sm font-medium text-gray-900 dark:text-white">
{event.message}
</p>
<div class="mt-1 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
<span>{format_event_type(event.event_type)}</span>
<span>•</span>
<span class="capitalize">{event.severity}</span>
</div>
</div>
<time class="flex-shrink-0 text-xs text-gray-500 dark:text-gray-400">
{time_ago(event.occurred_at)}
</time>
</div>
</div>
</div>
</div>
<% end %>
</div>
<% else %>
<div class="p-8 text-center">
<.icon name="hero-document-text" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No events yet")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"Events will appear here as interface states change, speeds change, or other monitored changes occur."
)}
</p>
</div>
<% end %>
</div>
<% "debug" -> %>
<div class="bg-white dark:bg-gray-800/50 shadow sm:rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-white">
{t("Raw Discovery Data")}
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>
{t("Raw SNMP OID values collected during the last discovery run at")}
<%= if @snmp_device.last_discovery_at do %>
{ToweropsWeb.TimeHelpers.format_iso8601(
@snmp_device.last_discovery_at,
@timezone
)}
<% else %>
unknown time
<% end %>.
</p>
</div>
<%= if @snmp_device.raw_discovery_data do %>
<div class="mt-5">
<h4 class="text-sm font-medium text-gray-900 dark:text-white mb-3">
{t("System OIDs")}
</h4>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Field")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("OID")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Value")}
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800/50 divide-y divide-gray-200 dark:divide-white/10">
<%= for {key, data} <- Map.get(@snmp_device.raw_discovery_data, "system_oids", %{}) do %>
<tr>
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{key}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">
{data["oid"]}
</td>
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
<code class="text-xs">{inspect(data["value"])}</code>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<%= if Map.get(@snmp_device.raw_discovery_data, "health_sensor_table") do %>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mt-6 mb-3">
{t("Health Sensor Table Walk")}
</h4>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("OID")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Value")}
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800/50 divide-y divide-gray-200 dark:divide-white/10">
<%= for {oid, value} <- Map.get(@snmp_device.raw_discovery_data, "health_sensor_table", %{}) do %>
<tr>
<td class="px-3 py-4 whitespace-nowrap text-sm font-mono text-gray-500 dark:text-gray-400">
{oid}
</td>
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
<code class="text-xs">{inspect(value)}</code>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<%= if Map.get(@snmp_device.raw_discovery_data, "discovered_sensors") do %>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mt-6 mb-3">
{t("Discovered Sensors")}
</h4>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Type")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Description")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("OID")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Value")}
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("State")}
</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800/50 divide-y divide-gray-200 dark:divide-white/10">
<%= for sensor <- Map.get(@snmp_device.raw_discovery_data, "discovered_sensors", []) do %>
<tr>
<td class="px-3 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{sensor["type"]}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-white">
{sensor["descr"]}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 font-mono">
{sensor["oid"]}
</td>
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
<code class="text-xs">{sensor["value"]}</code>
<%= if sensor["unit"] && sensor["unit"] != "" do %>
<span class="text-gray-500 dark:text-gray-400 ml-1">
{sensor["unit"]}
</span>
<% end %>
</td>
<td class="px-3 py-4 text-sm text-gray-900 dark:text-white">
<%= if sensor["state_descr"] do %>
<span class="inline-flex items-center rounded-full px-2 py-1 text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300">
{sensor["state_descr"]}
</span>
<% else %>
<span class="text-gray-400">-</span>
<% end %>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
<% end %>
<h4 class="text-sm font-medium text-gray-900 dark:text-white mt-6 mb-3">
{t("Copyable Raw Data")}
</h4>
<div class="rounded-lg border border-gray-200 dark:border-white/10">
<textarea
readonly
rows="10"
class="w-full p-3 font-mono text-xs bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-white border-0 rounded-lg focus:ring-2 focus:ring-blue-500"
onclick="this.select()"
><%= Jason.encode!(%{device_id: @device.id, raw_discovery_data: @snmp_device.raw_discovery_data}, pretty: true) %></textarea>
</div>
<p class="mt-2 text-xs text-gray-500 dark:text-gray-400">
Click the text box to select all data, then copy with Cmd+C or Ctrl+C
</p>
</div>
<% else %>
<div class="mt-5 p-4 bg-gray-50 dark:bg-gray-900 rounded-md">
<p class="text-sm text-gray-500 dark:text-gray-400">
{t(
"No raw discovery data available. Run discovery again to collect debug data."
)}
</p>
</div>
<% end %>
</div>
</div>
<% "checks" -> %>
<%= if Enum.empty?(@checks) do %>
<!-- Empty State -->
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-12 text-center">
<.icon name="hero-clipboard-document-check" class="mx-auto h-12 w-12 text-gray-400" />
<h3 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
{t("No checks configured")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t(
"Run discovery to automatically detect sensors, interfaces, and other monitorable items, or add a service check manually."
)}
</p>
<div class="mt-6 flex justify-center gap-3">
<%= if @device.snmp_enabled do %>
<.button type="button" phx-click="run_discovery">
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Run Discovery
</.button>
<% end %>
<.button type="button" phx-click="add_check">
<.icon name="hero-plus" class="h-4 w-4" /> Add Check
</.button>
</div>
</div>
</div>
<% else %>
<!-- Checks List -->
<div class="space-y-6">
<!-- Header with count and Add button -->
<div class="flex items-center justify-between">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
Checks ({length(@checks)})
</h2>
<.button type="button" phx-click="add_check">
<.icon name="hero-plus" class="h-4 w-4" /> Add Check
</.button>
</div>
<%= for {group_key, group_checks} <- @grouped_checks do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 overflow-hidden">
<!-- Group Header -->
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10 bg-gray-50 dark:bg-gray-900/50">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{group_title(group_key)} ({length(group_checks)})
</h3>
</div>
<!-- Checks Table -->
<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-gray-900/30">
<tr>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
{t("Name")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
{t("Status")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
{t("Value")}
</th>
<th
scope="col"
class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"
>
{t("Last Checked")}
</th>
<th scope="col" class="relative px-4 py-3">
<span class="sr-only">Actions</span>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for check <- group_checks do %>
<tr class="hover:bg-gray-50 dark:hover:bg-gray-900/30">
<td class="px-4 py-3 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-white">
{check.name}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-left">
{render_status_badge(check.current_state)}
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= if check.last_check_at do %>
{get_latest_value(check)}
<% else %>
<span class="text-gray-400 dark:text-gray-500 dark:text-gray-400">
Pending
</span>
<% end %>
</td>
<td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<%= if check.last_check_at do %>
{format_relative_time(check.last_check_at)}
<% else %>
<span class="text-gray-400 dark:text-gray-500 dark:text-gray-400">
Never
</span>
<% end %>
</td>
<td class="px-4 py-3 whitespace-nowrap text-right text-sm font-medium">
<div class="flex items-center justify-end gap-3">
<.link
navigate={
~p"/devices/#{@device.id}/graph/check?check_id=#{check.id}&range=24h"
}
class="text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-300 text-sm font-medium"
>
{t("Graph")}
</.link>
<%= if check.check_type in ["http", "tcp", "dns", "ssl"] do %>
<button
type="button"
phx-click="edit_check"
phx-value-id={check.id}
class="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
>
<.icon name="hero-pencil-square" class="h-4 w-4" />
</button>
<button
type="button"
phx-click="delete_check"
phx-value-id={check.id}
data-confirm="Are you sure you want to delete this check?"
class="text-red-500 hover:text-red-700 dark:text-red-400 dark:hover:text-red-300"
>
<.icon name="hero-trash" class="h-4 w-4" />
</button>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
</div>
</div>
<% 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">
{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 %>
<% "preseem" -> %>
<%= if @preseem_access_point do %>
<div class="space-y-6">
<%!-- Insights section --%>
<%= if @preseem_insights != [] do %>
<div class="space-y-3">
<%= for insight <- @preseem_insights do %>
<div class={[
"rounded-lg border p-4 flex items-start justify-between",
case insight.urgency do
"critical" ->
"bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800"
"warning" ->
"bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800"
_ ->
"bg-blue-50 border-blue-200 dark:bg-blue-900/20 dark:border-blue-800"
end
]}>
<div class="flex items-start gap-3">
<div class={[
"mt-0.5",
case insight.urgency do
"critical" -> "text-red-500"
"warning" -> "text-yellow-500"
_ -> "text-blue-500"
end
]}>
<%= case insight.urgency do %>
<% "critical" -> %>
<.icon name="hero-exclamation-triangle" class="h-5 w-5" />
<% "warning" -> %>
<.icon name="hero-exclamation-circle" class="h-5 w-5" />
<% _ -> %>
<.icon name="hero-information-circle" class="h-5 w-5" />
<% end %>
</div>
<div>
<p class="text-sm font-medium text-gray-900 dark:text-white">
{insight.title}
</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-300">
{insight.description}
</p>
</div>
</div>
<button
phx-click="dismiss_insight"
phx-value-id={insight.id}
class="ml-4 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 flex-shrink-0"
title="Dismiss"
>
<.icon name="hero-x-mark" class="h-4 w-4" />
</button>
</div>
<% end %>
</div>
<% end %>
<%!-- Score cards --%>
<div class="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">QoE Score</div>
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
{if @preseem_access_point.qoe_score,
do: Float.round(@preseem_access_point.qoe_score, 1),
else: "---"}
</div>
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">
{t("Capacity Score")}
</div>
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
{if @preseem_access_point.capacity_score,
do: Float.round(@preseem_access_point.capacity_score, 1),
else: "---"}
</div>
</div>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="text-sm font-medium text-gray-500 dark:text-gray-400">RF Score</div>
<div class="mt-1 text-2xl font-semibold text-gray-900 dark:text-white">
{if @preseem_access_point.rf_score,
do: Float.round(@preseem_access_point.rf_score, 1),
else: "---"}
</div>
</div>
</div>
<%!-- Details card --%>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Access Point Details")}
</h3>
</div>
<div class="px-4 py-3">
<dl class="grid grid-cols-1 sm:grid-cols-2 gap-x-4 gap-y-3">
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">Name</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.name || "---"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{t("Preseem ID")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.preseem_id}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{t("Subscribers")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.subscriber_count || "---"}
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{t("Busy Hours")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
<%= if @preseem_access_point.busy_hours do %>
{@preseem_access_point.busy_hours} hrs/day
<% else %>
---
<% end %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{t("Airtime Utilization")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
<%= if @preseem_access_point.airtime_utilization do %>
{Float.round(@preseem_access_point.airtime_utilization, 1)}%
<% else %>
---
<% end %>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500 dark:text-gray-400">
{t("Match Type")}
</dt>
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">
{@preseem_access_point.match_confidence}
</dd>
</div>
</dl>
</div>
</div>
<%!-- Recent metrics table --%>
<%= if @preseem_metrics != [] do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("Recent QoE Metrics")}
</h3>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead>
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("Time")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("Latency (ms)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("P95 Latency")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("Jitter (ms)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("Loss (%)")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("Throughput")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
{t("Subscribers")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for metric <- @preseem_metrics do %>
<tr>
<td class="px-4 py-2 text-sm text-gray-900 dark:text-white whitespace-nowrap">
{ToweropsWeb.TimeHelpers.format_iso8601(
metric.recorded_at,
@current_scope.timezone,
@current_scope.time_format
)}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-white">
{if metric.avg_latency,
do: Float.round(metric.avg_latency, 1),
else: "---"}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-white">
{if metric.p95_latency,
do: Float.round(metric.p95_latency, 1),
else: "---"}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-white">
{if metric.avg_jitter,
do: Float.round(metric.avg_jitter, 1),
else: "---"}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-white">
{if metric.avg_loss, do: Float.round(metric.avg_loss, 2), else: "---"}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-white">
{if metric.avg_throughput,
do: "#{Float.round(metric.avg_throughput, 1)} Mbps",
else: "---"}
</td>
<td class="px-4 py-2 text-sm text-right text-gray-900 dark:text-white">
{metric.subscriber_count || "---"}
</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 Preseem data linked to this device.")}
</p>
</div>
<% end %>
<% _ -> %>
<div class="text-center py-12">
<p class="text-gray-500 dark:text-gray-400">Tab not found</p>
</div>
<% end %>
</div>
<%= if @show_check_form do %>
<.live_component
module={FormComponent}
id={if @edit_check, do: @edit_check.id, else: :new}
action={if @edit_check, do: :edit, else: :new}
check={@edit_check}
device={@device}
/>
<% end %>
</Layouts.authenticated>