- Add real-time updates with 10-second refresh and PubSub integration - Implement pure SVG charts: response time sparkline and uptime gauge - Create compact dashboard layout with status cards, metrics, and graphs - Add relative time display (e.g., '5s ago') for last check - Redesign SNMP data display: compact device info, sensors grid, interfaces table - Calculate and display uptime percentage and average response time - Update tests to match new compact layout
276 lines
12 KiB
Text
276 lines
12 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_organization={@current_organization}
|
|
active_page="equipment"
|
|
>
|
|
<div class="mb-4 flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-zinc-900 dark:text-zinc-100">{@equipment.name}</h1>
|
|
<p class="text-sm text-zinc-600 dark:text-zinc-400 font-mono">{@equipment.ip_address}</p>
|
|
</div>
|
|
<div class="flex gap-2">
|
|
<.button
|
|
phx-click="trigger_check"
|
|
class="text-sm bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100"
|
|
>
|
|
<.icon name="hero-arrow-path" class="h-4 w-4" /> Check Now
|
|
</.button>
|
|
<%= if @equipment.snmp_enabled do %>
|
|
<.button
|
|
phx-click="trigger_discovery"
|
|
class="text-sm bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100"
|
|
>
|
|
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> Discover
|
|
</.button>
|
|
<% end %>
|
|
<.button
|
|
navigate={~p"/orgs/#{@current_organization.slug}/equipment/#{@equipment.id}/edit"}
|
|
class="text-sm bg-white dark:bg-zinc-800 border border-zinc-300 dark:border-zinc-700 text-zinc-900 dark:text-zinc-100"
|
|
>
|
|
<.icon name="hero-pencil" class="h-4 w-4" /> Edit
|
|
</.button>
|
|
<.button
|
|
phx-click="delete"
|
|
data-confirm="Are you sure?"
|
|
class="text-sm bg-red-600 text-white hover:bg-red-700"
|
|
>
|
|
<.icon name="hero-trash" class="h-4 w-4" /> Delete
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<!-- Status Overview Bar -->
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-3 mb-4">
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-3 border border-zinc-200 dark:border-zinc-700">
|
|
<div class="text-xs text-zinc-600 dark:text-zinc-400 mb-1">Status</div>
|
|
<div class="flex items-center gap-2">
|
|
<span class={[
|
|
"inline-flex h-3 w-3 rounded-full",
|
|
@equipment.status == :up && "bg-green-500",
|
|
@equipment.status == :down && "bg-red-500",
|
|
@equipment.status == :unknown && "bg-zinc-400"
|
|
]}>
|
|
</span>
|
|
<span class="text-sm font-semibold text-zinc-900 dark:text-zinc-100">
|
|
{@equipment.status |> to_string() |> String.upcase()}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-3 border border-zinc-200 dark:border-zinc-700">
|
|
<div class="text-xs text-zinc-600 dark:text-zinc-400 mb-1">Uptime</div>
|
|
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
|
{@metrics.uptime_percentage}%
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-3 border border-zinc-200 dark:border-zinc-700">
|
|
<div class="text-xs text-zinc-600 dark:text-zinc-400 mb-1">Avg Response</div>
|
|
<div class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
|
{@metrics.avg_response_time}ms
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-3 border border-zinc-200 dark:border-zinc-700">
|
|
<div class="text-xs text-zinc-600 dark:text-zinc-400 mb-1">Last Check</div>
|
|
<div class="text-sm font-medium text-zinc-900 dark:text-zinc-100">
|
|
{time_ago(@equipment.last_checked_at)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- Graphs Row -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-4">
|
|
<!-- Response Time Graph -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold mb-2 text-zinc-900 dark:text-zinc-100">Response Time</h3>
|
|
<div class="h-24">
|
|
{sparkline(@recent_checks, 400, 80)}
|
|
</div>
|
|
</div>
|
|
<!-- Uptime Gauge -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold mb-2 text-zinc-900 dark:text-zinc-100">
|
|
Availability (last {@metrics.total_checks} checks)
|
|
</h3>
|
|
<div class="mt-4">
|
|
{uptime_gauge(@metrics.uptime_percentage)}
|
|
</div>
|
|
<div class="mt-3 flex justify-between text-xs text-zinc-600 dark:text-zinc-400">
|
|
<span>{@metrics.successful_checks} successful</span>
|
|
<span>{@metrics.total_checks - @metrics.successful_checks} failed</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<%= if @snmp_device do %>
|
|
<!-- Device Info -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 mb-4">
|
|
<h3 class="text-sm font-semibold mb-3 text-zinc-900 dark:text-zinc-100">
|
|
Device Information
|
|
</h3>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-x-4 gap-y-2 text-sm">
|
|
<%= if @snmp_device.manufacturer do %>
|
|
<div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">Manufacturer:</span>
|
|
<span class="ml-2 text-zinc-900 dark:text-zinc-100 font-medium">
|
|
{@snmp_device.manufacturer}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
<%= if @snmp_device.model do %>
|
|
<div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">Model:</span>
|
|
<span class="ml-2 text-zinc-900 dark:text-zinc-100 font-medium">
|
|
{@snmp_device.model}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
<%= if @snmp_device.firmware_version do %>
|
|
<div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">Firmware:</span>
|
|
<span class="ml-2 text-zinc-900 dark:text-zinc-100 font-medium">
|
|
{@snmp_device.firmware_version}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
<%= if @equipment.last_discovery_at do %>
|
|
<div>
|
|
<span class="text-zinc-600 dark:text-zinc-400">Last Discovery:</span>
|
|
<span class="ml-2 text-zinc-900 dark:text-zinc-100 font-medium">
|
|
{Calendar.strftime(@equipment.last_discovery_at, "%Y-%m-%d %H:%M")}
|
|
</span>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<%= if @snmp_device.sys_descr do %>
|
|
<div class="mt-3 text-xs text-zinc-600 dark:text-zinc-400">
|
|
{@snmp_device.sys_descr}
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
<%= if @snmp_sensors && length(@snmp_sensors) > 0 do %>
|
|
<!-- Sensors -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 mb-4">
|
|
<h3 class="text-sm font-semibold mb-3 text-zinc-900 dark:text-zinc-100">Sensors</h3>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
<%= for sensor <- @snmp_sensors do %>
|
|
<div class="bg-zinc-50 dark:bg-zinc-900 rounded p-2 border border-zinc-200 dark:border-zinc-700">
|
|
<div
|
|
class="text-xs text-zinc-600 dark:text-zinc-400 truncate"
|
|
title={sensor.sensor_descr}
|
|
>
|
|
{sensor.sensor_descr}
|
|
</div>
|
|
<%= if sensor.latest_reading do %>
|
|
<div class="mt-1 flex items-baseline gap-1">
|
|
<span class="text-lg font-bold text-zinc-900 dark:text-zinc-100">
|
|
{format_sensor_value(sensor.latest_reading.value, sensor.sensor_divisor)}
|
|
</span>
|
|
<span class="text-xs text-zinc-600 dark:text-zinc-400">{sensor.sensor_unit}</span>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-1 text-xs text-zinc-500 dark:text-zinc-500">No data</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<%= if @snmp_interfaces && length(@snmp_interfaces) > 0 do %>
|
|
<!-- Interfaces -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700 mb-4">
|
|
<h3 class="text-sm font-semibold mb-3 text-zinc-900 dark:text-zinc-100">
|
|
Network Interfaces
|
|
</h3>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<thead>
|
|
<tr class="text-xs text-zinc-600 dark:text-zinc-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-left font-medium">Speed</th>
|
|
<th class="px-3 py-2 text-left font-medium">MAC</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<%= for interface <- @snmp_interfaces do %>
|
|
<tr class="text-sm">
|
|
<td class="px-3 py-2 text-zinc-900 dark:text-zinc-100">{interface.if_index}</td>
|
|
<td class="px-3 py-2 text-zinc-900 dark:text-zinc-100">
|
|
{interface.if_name || interface.if_descr}
|
|
</td>
|
|
<td class="px-3 py-2">
|
|
<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-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-200"
|
|
]}>
|
|
{String.upcase(interface.if_oper_status || "unknown")}
|
|
</span>
|
|
</td>
|
|
<td class="px-3 py-2 text-zinc-900 dark:text-zinc-100">
|
|
{format_speed(interface.if_speed)}
|
|
</td>
|
|
<td class="px-3 py-2 font-mono text-xs text-zinc-600 dark:text-zinc-400">
|
|
{interface.if_phys_address || "-"}
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
<!-- Recent Checks (Compact) -->
|
|
<div class="bg-white dark:bg-zinc-800 rounded-lg p-4 border border-zinc-200 dark:border-zinc-700">
|
|
<h3 class="text-sm font-semibold mb-3 text-zinc-900 dark:text-zinc-100">Recent Checks</h3>
|
|
<%= if Enum.empty?(Enum.take(@recent_checks, 10)) do %>
|
|
<div class="text-sm text-zinc-600 dark:text-zinc-400 text-center py-4">
|
|
No monitoring checks yet
|
|
</div>
|
|
<% else %>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<thead>
|
|
<tr class="text-xs text-zinc-600 dark:text-zinc-400">
|
|
<th class="px-3 py-2 text-left font-medium">Status</th>
|
|
<th class="px-3 py-2 text-left font-medium">Response</th>
|
|
<th class="px-3 py-2 text-left font-medium">Time</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-zinc-200 dark:divide-zinc-700">
|
|
<%= for check <- Enum.take(@recent_checks, 10) do %>
|
|
<tr class="text-sm">
|
|
<td class="px-3 py-2">
|
|
<span class={[
|
|
"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium",
|
|
check.status == :success &&
|
|
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
|
|
check.status == :failure &&
|
|
"bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"
|
|
]}>
|
|
{check.status |> to_string() |> String.upcase()}
|
|
</span>
|
|
</td>
|
|
<td class="px-3 py-2 text-zinc-900 dark:text-zinc-100">
|
|
<%= if check.response_time_ms do %>
|
|
{check.response_time_ms}ms
|
|
<% else %>
|
|
-
|
|
<% end %>
|
|
</td>
|
|
<td class="px-3 py-2 text-zinc-600 dark:text-zinc-400">
|
|
{Calendar.strftime(check.checked_at, "%H:%M:%S")}
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</Layouts.authenticated>
|