- 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
7.8 KiB
Elixir
276 lines
7.8 KiB
Elixir
defmodule ToweropsWeb.EquipmentLive.Show do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Equipment
|
|
alias Towerops.Monitoring
|
|
alias Towerops.Snmp
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(%{"id" => id}, _, socket) do
|
|
if connected?(socket) do
|
|
Phoenix.PubSub.subscribe(Towerops.PubSub, "equipment:#{id}")
|
|
Process.send_after(self(), :refresh_data, 10_000)
|
|
end
|
|
|
|
{:noreply, load_equipment_data(socket, id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info(:refresh_data, socket) do
|
|
Process.send_after(self(), :refresh_data, 10_000)
|
|
{:noreply, load_equipment_data(socket, socket.assigns.equipment.id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:equipment_status_changed, _equipment_id, _new_status, _response_time}, socket) do
|
|
{:noreply, load_equipment_data(socket, socket.assigns.equipment.id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:discovery_completed, _equipment_id}, socket) do
|
|
{:noreply,
|
|
socket
|
|
|> load_equipment_data(socket.assigns.equipment.id)
|
|
|> put_flash(:info, "Discovery completed")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("delete", _params, socket) do
|
|
case Equipment.delete_equipment(socket.assigns.equipment) do
|
|
{:ok, _} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, "Equipment deleted successfully")
|
|
|> push_navigate(to: ~p"/orgs/#{socket.assigns.current_organization.slug}/equipment")}
|
|
|
|
{:error, _} ->
|
|
{:noreply, put_flash(socket, :error, "Unable to delete equipment")}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("trigger_check", _params, socket) do
|
|
_ = Towerops.Monitoring.EquipmentMonitor.trigger_check(socket.assigns.equipment.id)
|
|
{:noreply, put_flash(socket, :info, "Check triggered")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("trigger_discovery", _params, socket) do
|
|
equipment = socket.assigns.equipment
|
|
|
|
if equipment.snmp_enabled do
|
|
# Run discovery in background
|
|
Task.start(fn ->
|
|
Snmp.discover_equipment(equipment)
|
|
end)
|
|
|
|
{:noreply, put_flash(socket, :info, "Discovery started...")}
|
|
else
|
|
{:noreply, put_flash(socket, :error, "SNMP is not enabled for this equipment")}
|
|
end
|
|
end
|
|
|
|
# Private functions
|
|
|
|
defp load_equipment_data(socket, equipment_id) do
|
|
equipment = Equipment.get_equipment!(equipment_id)
|
|
recent_checks = Monitoring.list_equipment_checks(equipment_id, 50)
|
|
snmp_data = load_snmp_data(equipment_id)
|
|
|
|
# Calculate metrics for dashboard
|
|
metrics = calculate_metrics(recent_checks, equipment)
|
|
|
|
socket
|
|
|> assign(:page_title, equipment.name)
|
|
|> assign(:equipment, equipment)
|
|
|> assign(:recent_checks, recent_checks)
|
|
|> assign(:metrics, metrics)
|
|
|> assign(:snmp_device, snmp_data.device)
|
|
|> assign(:snmp_interfaces, snmp_data.interfaces)
|
|
|> assign(:snmp_sensors, snmp_data.sensors)
|
|
end
|
|
|
|
defp calculate_metrics(checks, _equipment) do
|
|
total_checks = length(checks)
|
|
|
|
if total_checks > 0 do
|
|
successful_checks = Enum.count(checks, &(&1.status == :success))
|
|
uptime_percentage = Float.round(successful_checks / total_checks * 100, 1)
|
|
|
|
avg_response_time =
|
|
checks
|
|
|> Enum.filter(&(&1.response_time_ms != nil))
|
|
|> case do
|
|
[] ->
|
|
0
|
|
|
|
valid_checks ->
|
|
sum = Enum.reduce(valid_checks, 0, &(&1.response_time_ms + &2))
|
|
Float.round(sum / length(valid_checks), 1)
|
|
end
|
|
|
|
%{
|
|
uptime_percentage: uptime_percentage,
|
|
avg_response_time: avg_response_time,
|
|
total_checks: total_checks,
|
|
successful_checks: successful_checks
|
|
}
|
|
else
|
|
%{
|
|
uptime_percentage: 0,
|
|
avg_response_time: 0,
|
|
total_checks: 0,
|
|
successful_checks: 0
|
|
}
|
|
end
|
|
end
|
|
|
|
defp format_speed(speed_bps) when is_integer(speed_bps) do
|
|
cond do
|
|
speed_bps >= 1_000_000_000 ->
|
|
"#{Float.round(speed_bps / 1_000_000_000, 1)} Gbps"
|
|
|
|
speed_bps >= 1_000_000 ->
|
|
"#{Float.round(speed_bps / 1_000_000, 1)} Mbps"
|
|
|
|
speed_bps >= 1_000 ->
|
|
"#{Float.round(speed_bps / 1_000, 1)} Kbps"
|
|
|
|
true ->
|
|
"#{speed_bps} bps"
|
|
end
|
|
end
|
|
|
|
defp format_speed(_), do: "-"
|
|
|
|
defp format_sensor_value(value, divisor) when is_number(value) and is_number(divisor) do
|
|
result = value / divisor
|
|
Float.round(result, 1)
|
|
end
|
|
|
|
defp format_sensor_value(_, _), do: "-"
|
|
|
|
defp time_ago(nil), do: "Never"
|
|
|
|
defp time_ago(datetime) do
|
|
diff = DateTime.diff(DateTime.utc_now(), datetime, :second)
|
|
|
|
cond do
|
|
diff < 60 -> "#{diff}s ago"
|
|
diff < 3600 -> "#{div(diff, 60)}m ago"
|
|
diff < 86_400 -> "#{div(diff, 3600)}h ago"
|
|
true -> "#{div(diff, 86_400)}d ago"
|
|
end
|
|
end
|
|
|
|
defp load_snmp_data(equipment_id) do
|
|
case Snmp.get_device_with_associations(equipment_id) do
|
|
nil ->
|
|
%{device: nil, interfaces: [], sensors: []}
|
|
|
|
device ->
|
|
# Preload latest readings for sensors
|
|
sensors_with_readings =
|
|
Enum.map(device.sensors, fn sensor ->
|
|
latest_reading = Snmp.get_latest_sensor_reading(sensor.id)
|
|
Map.put(sensor, :latest_reading, latest_reading)
|
|
end)
|
|
|
|
# Preload latest stats for interfaces
|
|
interfaces_with_stats =
|
|
Enum.map(device.interfaces, fn interface ->
|
|
latest_stat = Snmp.get_latest_interface_stat(interface.id)
|
|
Map.put(interface, :latest_stat, latest_stat)
|
|
end)
|
|
|
|
%{
|
|
device: device,
|
|
interfaces: interfaces_with_stats,
|
|
sensors: sensors_with_readings
|
|
}
|
|
end
|
|
end
|
|
|
|
# SVG Chart helpers
|
|
|
|
defp sparkline(checks, width, height) when is_list(checks) do
|
|
if Enum.empty?(checks) or length(checks) < 2 do
|
|
""
|
|
else
|
|
# Get response times in reverse order (oldest to newest for left-to-right)
|
|
data =
|
|
checks
|
|
|> Enum.reverse()
|
|
|> Enum.map(fn check ->
|
|
if check.response_time_ms, do: check.response_time_ms, else: 0
|
|
end)
|
|
|
|
max_value = Enum.max(data, fn -> 1 end)
|
|
max_value = if max_value == 0, do: 1, else: max_value
|
|
data_count = length(data)
|
|
|
|
points =
|
|
data
|
|
|> Enum.with_index()
|
|
|> Enum.map_join(" ", fn {value, index} ->
|
|
x = index * (width / (data_count - 1))
|
|
y = height - value / max_value * height
|
|
"#{Float.round(x, 2)},#{Float.round(y, 2)}"
|
|
end)
|
|
|
|
assigns = %{points: points, width: width, height: height, data: data}
|
|
|
|
~H"""
|
|
<svg viewBox={"0 0 #{@width} #{@height}"} class="w-full h-full">
|
|
<polyline
|
|
points={@points}
|
|
fill="none"
|
|
stroke="currentColor"
|
|
stroke-width="2"
|
|
class="text-blue-500"
|
|
/>
|
|
<%= for {value, idx} <- Enum.with_index(@data) do %>
|
|
<circle
|
|
cx={idx * (@width / (length(@data) - 1))}
|
|
cy={@height - value / Enum.max(@data, fn -> 1 end) * @height}
|
|
r="2"
|
|
class={if value == 0, do: "fill-red-500", else: "fill-blue-500"}
|
|
/>
|
|
<% end %>
|
|
</svg>
|
|
"""
|
|
end
|
|
end
|
|
|
|
defp uptime_gauge(percentage) do
|
|
assigns = %{percentage: percentage}
|
|
|
|
color_class =
|
|
cond do
|
|
percentage >= 95 -> "bg-green-500"
|
|
percentage >= 80 -> "bg-yellow-500"
|
|
true -> "bg-red-500"
|
|
end
|
|
|
|
assigns = Map.put(assigns, :color_class, color_class)
|
|
|
|
~H"""
|
|
<div class="relative w-full h-6 bg-zinc-200 dark:bg-zinc-700 rounded-full overflow-hidden">
|
|
<div
|
|
class={"absolute inset-y-0 left-0 #{@color_class} transition-all duration-500"}
|
|
style={"width: #{@percentage}%"}
|
|
>
|
|
</div>
|
|
<div class="absolute inset-0 flex items-center justify-center text-xs font-medium text-zinc-900 dark:text-zinc-100">
|
|
{@percentage}%
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
end
|