towerops/lib/towerops_web/live/equipment_live/show.html.heex
Graham McIntire d6a9bb11d7
Redesign equipment view with compact LibreNMS-style dashboard
- 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
2026-01-03 15:11:47 -06:00

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>