towerops/lib/towerops_web/live/equipment_live/show.ex
Graham McIntire 3fe3a01726
Remove 'Check Now' button and clean up equipment detail page
Removed the manual 'Check Now' functionality since polling happens
automatically on a schedule with distributed coordination. Manual
triggers would interfere with the coordinated polling system.

Changes:
- Removed 'Check Now' button from equipment detail page
- Removed trigger_check event handler
- Simplified Discover and Edit buttons to use standard button styling
- Removed custom classes in favor of default button component styling

The equipment is now polled automatically based on its configured
check_interval_seconds, coordinated across all pods.
2026-01-04 13:31:14 -06:00

256 lines
7.1 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("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