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

1589 lines
79 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="devices"
>
<div class="mb-6 -mt-2">
<!-- Breadcrumbs -->
<nav class="flex mb-4" aria-label="Breadcrumb">
<ol class="inline-flex items-center space-x-1 text-sm text-gray-600 dark:text-gray-400">
<li class="inline-flex items-center">
<.link
navigate={~p"/sites/#{@device.site.id}"}
class="hover:text-gray-900 dark:hover:text-white"
>
{@device.site.name}
</.link>
</li>
<li aria-current="page">
<div class="flex items-center">
<.icon name="hero-chevron-right" class="h-4 w-4 mx-1" />
<span class="text-gray-900 dark:text-white font-medium">{@device.name}</span>
</div>
</li>
</ol>
</nav>
<div class="flex items-center justify-between mb-4">
<div>
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@device.name}</h1>
<div class="flex items-center gap-3 mt-0.5">
<span class="text-sm text-gray-600 dark:text-gray-400 font-mono">
{@device.ip_address}
</span>
<span class="text-gray-300 dark:text-gray-600">•</span>
<span class="text-xs text-gray-500 dark:text-gray-400">
Polled by:
</span>
<%= if @agent_info.type == :cloud do %>
<span class="inline-flex items-center gap-1 text-xs text-blue-600 dark:text-blue-400">
<.icon name="hero-cloud" class="h-3.5 w-3.5" /> Cloud
</span>
<% else %>
<span class="inline-flex items-center gap-1 text-xs text-green-600 dark:text-green-400">
<.icon name="hero-server" class="h-3.5 w-3.5" />
{@agent_info.name}
</span>
<%= if @agent_info.last_seen_at do %>
<span class="text-xs text-gray-400 dark:text-gray-500">
({format_relative_time(@agent_info.last_seen_at)})
</span>
<% end %>
<% end %>
</div>
</div>
<div class="flex gap-2">
<.button navigate={~p"/devices/#{@device.id}/edit"}>
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
</.button>
</div>
</div>
<!-- Tabs -->
<div class="border-b border-gray-200 dark:border-white/10">
<nav class="-mb-px flex space-x-8">
<.link
patch={~p"/devices/#{@device.id}?tab=overview"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "overview" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
Overview
</.link>
<%= if @snmp_device do %>
<.link
patch={~p"/devices/#{@device.id}?tab=ports"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "ports" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
Ports
</.link>
<% end %>
<.link
patch={~p"/devices/#{@device.id}?tab=neighbors"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "neighbors" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
Neighbors
</.link>
<%= 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 font-medium text-sm",
if @active_tab == "arp" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
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 font-medium text-sm",
if @active_tab == "mac" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
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 font-medium text-sm",
if @active_tab == "vlans" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
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 font-medium text-sm",
if @active_tab == "ip_addresses" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
IP Addresses
</.link>
<% end %>
<.link
patch={~p"/devices/#{@device.id}?tab=logs"}
class={[
"whitespace-nowrap py-4 px-1 border-b-2 font-medium text-sm",
if @active_tab == "logs" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
Logs
</.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 font-medium text-sm",
if @active_tab == "debug" do
"border-blue-500 text-blue-600 dark:text-blue-400"
else
"border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 dark:text-gray-400 dark:hover:text-gray-300"
end
]}
>
Debug
</.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">
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="text-sm text-gray-600 dark:text-gray-400">System Name</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Resolved IP</dt>
<dd class="text-sm font-medium text-gray-900 dark:text-white font-mono">
{@device.ip_address}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="text-sm text-gray-600 dark:text-gray-400">Hardware</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Operating System</dt>
<dd class="text-sm font-medium text-gray-900 dark:text-white">
{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}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="text-sm text-gray-600 dark:text-gray-400">Object ID</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Contact</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Location</dt>
<dd class="text-sm font-medium text-gray-900 dark:text-white">
{@snmp_device.sys_location || "N/A"}
</dd>
</div>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="text-sm text-gray-600 dark:text-gray-400">Uptime</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Name</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">IP Address</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Device Added</dt>
<dd class="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="text-sm text-gray-600 dark:text-gray-400">Last Discovered</dt>
<dd class="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>
</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">
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"
data-chart={@traffic_chart_data}
data-unit="bps"
data-auto-scale="true"
data-show-zero-line="true"
style="height: 300px;"
>
<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">
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"
data-chart={@latency_chart_data}
data-unit="ms"
data-auto-scale="true"
style="height: 300px;"
>
<canvas></canvas>
</div>
</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 %>
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"
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">
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">
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"
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">
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="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="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 %>
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">
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"
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">
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="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="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 %>
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">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="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="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 %>
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">
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="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="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 %>
N/A
<% end %>
</dd>
</div>
<% end %>
<% end %>
</dl>
</div>
</div>
<% end %>
<!-- Count Sensors (DHCP Leases, Connections, etc.) -->
<%= if @count_sensors && length(@count_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">Counters</h3>
</div>
<div class="p-4">
<dl class="space-y-1">
<%= for sensor <- @count_sensors do %>
<div class="flex justify-between py-1.5 border-b border-gray-100 dark:border-white/5">
<dt class="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="text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
{trunc(sensor.latest_reading.value)}
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
{sensor.sensor_unit}
<% end %>
<% else %>
N/A
<% end %>
</dd>
</div>
<% end %>
</dl>
</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">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="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="text-sm font-medium text-gray-900 dark:text-white">
<%= if sensor.latest_reading do %>
<%= if sensor.sensor_type in ["clients", "ccq", "utilization"] do %>
{trunc(sensor.latest_reading.value)}
<% else %>
{Float.round(sensor.latest_reading.value, 1)}
<% end %>
<%= if sensor.sensor_unit && sensor.sensor_unit != "" do %>
{sensor.sensor_unit}
<% end %>
<% else %>
N/A
<% end %>
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<% end %>
</div>
</div>
<% "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-4 py-3 text-left font-medium">#</th>
<th class="px-4 py-3 text-left font-medium">Name</th>
<th class="px-4 py-3 text-left font-medium">Status</th>
<th class="px-4 py-3 text-left font-medium">Speed</th>
<th class="px-4 py-3 text-left font-medium">IP Address</th>
<th class="px-4 py-3 text-left font-medium">MAC</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">
<td class="px-4 py-3 text-gray-900 dark:text-white">
{interface.if_index}
</td>
<td class="px-4 py-3">
<.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">
{interface.if_name || interface.if_descr}
</div>
<%= if interface.if_alias do %>
<div class="text-xs text-gray-500 dark:text-gray-400">
{interface.if_alias}
</div>
<% end %>
</.link>
</td>
<td class="px-4 py-3">
<span class={[
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
interface.if_oper_status == "up" &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
interface.if_oper_status == "down" &&
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
interface.if_oper_status not in ["up", "down"] &&
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
]}>
{String.upcase(interface.if_oper_status || "unknown")}
</span>
</td>
<td class="px-4 py-3 text-gray-900 dark:text-white">
{format_speed(interface.if_speed)}
</td>
<td class="px-4 py-3 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>
{ip.ip_address}
<%= if ip.prefix_length do %>
/{ip.prefix_length}
<% end %>
</div>
<% end %>
</div>
<% end %>
</td>
<td class="px-4 py-3 font-mono text-xs text-gray-600 dark:text-gray-400">
{interface.if_phys_address || "-"}
</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 %>
<% "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">
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 %>
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">
No neighbors discovered
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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">
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">
No ARP entries discovered
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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">
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">
No MAC addresses discovered
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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">
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">
No VLANs discovered
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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">
IPv4
</span>
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">
Primary
</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">-</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">
IPv6
</span>
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">
No IP addresses discovered
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
This device doesn't have any IP addresses discovered, or IP discovery hasn't run yet.
</p>
</div>
</div>
<% end %>
</div>
<% "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">Event Logs</h3>
<p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
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">
No events yet
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
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">
Raw Discovery Data
</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500 dark:text-gray-400">
<p>
Raw SNMP OID values collected during the last discovery run at
<%= if @snmp_device.last_discovery_at do %>
{Calendar.strftime(@snmp_device.last_discovery_at, "%Y-%m-%d %H:%M:%S UTC")}
<% 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">
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">
Field
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
OID
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
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">
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">
OID
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
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">
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">
Type
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Description
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
OID
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
Value
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
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">
Copyable Debug 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!(@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">
No raw discovery data available. Run discovery again to collect debug data.
</p>
</div>
<% end %>
</div>
</div>
<% _ -> %>
<div class="text-center py-12">
<p class="text-gray-500 dark:text-gray-400">Tab not found</p>
</div>
<% end %>
</div>
</Layouts.authenticated>