222 lines
7.2 KiB
Elixir
222 lines
7.2 KiB
Elixir
defmodule ToweropsWeb.SiteLive.Show do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Alerts
|
|
alias Towerops.ConfigChanges
|
|
alias Towerops.Dashboard
|
|
alias Towerops.Devices
|
|
alias Towerops.Gaiia
|
|
alias Towerops.Monitoring
|
|
alias Towerops.Preseem
|
|
alias Towerops.Sites
|
|
alias Towerops.Snmp
|
|
alias Towerops.Workers.DiscoveryWorker
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(%{"id" => id}, _, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
site = Sites.get_organization_site!(organization.id, id)
|
|
devices = Devices.list_site_devices(site.id)
|
|
device_ids = Enum.map(devices, & &1.id)
|
|
latency_chart_data = load_site_latency_chart_data(devices)
|
|
site_summary = Dashboard.get_site_summary(site.id)
|
|
|
|
# QoE summary from Preseem
|
|
qoe_summary = Preseem.get_site_qoe_summary(site.id)
|
|
|
|
# Config changes in last 30 days
|
|
thirty_days_ago = DateTime.add(DateTime.utc_now(), -30, :day)
|
|
config_changes = ConfigChanges.list_site_changes(site.id, after: thirty_days_ago, limit: 10)
|
|
|
|
# Active alerts
|
|
active_alerts = Alerts.list_site_active_alerts(site.id)
|
|
|
|
# Insights
|
|
insights = Preseem.Insights.list_site_insights(site.id, limit: 10)
|
|
|
|
# Device response times for health grid
|
|
response_times = Monitoring.get_device_latest_response_times(device_ids)
|
|
|
|
# Subscriber impact per device from Gaiia
|
|
site_impact =
|
|
try do
|
|
Gaiia.get_site_impact(site.id)
|
|
rescue
|
|
_ -> %{subscriber_count: 0, mrr: nil, devices: []}
|
|
end
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:page_title, site.name)
|
|
|> assign(:site, site)
|
|
|> assign(:device, devices)
|
|
|> assign(:site_impact, site_impact)
|
|
|> assign(:latency_chart_data, latency_chart_data)
|
|
|> assign(:site_summary, site_summary)
|
|
|> assign(:qoe_summary, qoe_summary)
|
|
|> assign(:config_changes, config_changes)
|
|
|> assign(:active_alerts, active_alerts)
|
|
|> assign(:insights, insights)
|
|
|> assign(:response_times, response_times)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("force_rediscover_all", _params, socket) do
|
|
devices = socket.assigns.device
|
|
snmp_devices = Enum.filter(devices, & &1.snmp_enabled)
|
|
|
|
if Enum.empty?(snmp_devices) do
|
|
{:noreply, put_flash(socket, :error, t_equipment("No SNMP-enabled devices at this site"))}
|
|
else
|
|
# Enqueue discovery for all SNMP-enabled devices
|
|
Enum.each(snmp_devices, fn device ->
|
|
enqueue_discovery(device.id)
|
|
end)
|
|
|
|
count = length(snmp_devices)
|
|
|
|
message =
|
|
t_equipment("Discovery started for %{count} device", count: count) <>
|
|
if(count == 1, do: "", else: "s")
|
|
|
|
{:noreply, put_flash(socket, :info, message)}
|
|
end
|
|
end
|
|
|
|
defp load_site_latency_chart_data(devices) do
|
|
if Enum.empty?(devices) do
|
|
nil
|
|
else
|
|
twenty_four_hours_ago = DateTime.add(DateTime.utc_now(), -24, :hour)
|
|
|
|
datasets =
|
|
devices
|
|
|> Enum.map(fn device ->
|
|
checks =
|
|
device.id
|
|
|> Monitoring.get_latency_data(since: twenty_four_hours_ago, limit: 1000)
|
|
|> Enum.reverse()
|
|
|
|
%{
|
|
label: device.name,
|
|
data: Enum.map(checks, &latency_check_to_data_point/1)
|
|
}
|
|
end)
|
|
|> Enum.reject(fn dataset -> Enum.empty?(dataset.data) end)
|
|
|
|
if Enum.empty?(datasets) do
|
|
nil
|
|
else
|
|
Jason.encode!(%{datasets: datasets})
|
|
end
|
|
end
|
|
end
|
|
|
|
defp latency_check_to_data_point(check) do
|
|
%{
|
|
x: DateTime.to_unix(check.checked_at, :millisecond),
|
|
y: check.response_time_ms
|
|
}
|
|
end
|
|
|
|
defp format_mrr(nil), do: "$0"
|
|
|
|
defp format_mrr(%Decimal{} = amount) do
|
|
"$#{format_number(Decimal.to_integer(Decimal.round(amount, 0)))}"
|
|
end
|
|
|
|
defp format_mrr(amount) when is_number(amount), do: "$#{format_number(round(amount))}"
|
|
defp format_mrr(_), do: "$0"
|
|
|
|
defp format_number(number) when is_integer(number) do
|
|
number
|
|
|> Integer.to_string()
|
|
|> String.graphemes()
|
|
|> Enum.reverse()
|
|
|> Enum.chunk_every(3)
|
|
|> Enum.join(",")
|
|
|> String.reverse()
|
|
end
|
|
|
|
defp format_number(number), do: to_string(number)
|
|
|
|
defp qoe_color(nil), do: "text-gray-400 dark:text-gray-500"
|
|
defp qoe_color(score) when score >= 8.0, do: "text-green-600 dark:text-green-400"
|
|
defp qoe_color(score) when score >= 6.0, do: "text-yellow-600 dark:text-yellow-400"
|
|
defp qoe_color(_), do: "text-red-600 dark:text-red-400"
|
|
|
|
defp qoe_bg(nil), do: "bg-gray-50 border-gray-200 dark:bg-gray-800/50 dark:border-white/10"
|
|
defp qoe_bg(score) when score >= 8.0, do: "bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800/30"
|
|
|
|
defp qoe_bg(score) when score >= 6.0,
|
|
do: "bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800/30"
|
|
|
|
defp qoe_bg(_), do: "bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800/30"
|
|
|
|
defp capacity_bar_color(nil), do: "bg-gray-300"
|
|
defp capacity_bar_color(score) when score >= 70, do: "bg-green-500"
|
|
defp capacity_bar_color(score) when score >= 40, do: "bg-yellow-500"
|
|
defp capacity_bar_color(_), do: "bg-red-500"
|
|
|
|
defp status_dot_class(:up), do: "bg-green-500"
|
|
defp status_dot_class(:down), do: "bg-red-500"
|
|
defp status_dot_class(_), do: "bg-gray-400"
|
|
|
|
defp time_ago(nil), do: "—"
|
|
|
|
defp time_ago(datetime) do
|
|
seconds = DateTime.diff(DateTime.utc_now(), datetime, :second)
|
|
|
|
cond do
|
|
seconds < 60 -> "#{seconds}s ago"
|
|
seconds < 3600 -> "#{div(seconds, 60)}m ago"
|
|
seconds < 86_400 -> "#{div(seconds, 3600)}h ago"
|
|
true -> "#{div(seconds, 86_400)}d ago"
|
|
end
|
|
end
|
|
|
|
defp format_response_time(nil), do: "—"
|
|
defp format_response_time(ms) when is_number(ms) and ms < 1, do: "<1ms"
|
|
defp format_response_time(ms) when is_number(ms), do: "#{round(ms)}ms"
|
|
defp format_response_time(_), do: "—"
|
|
|
|
defp change_size_label(size) when size < 100, do: "Small"
|
|
defp change_size_label(size) when size < 500, do: "Medium"
|
|
defp change_size_label(_), do: "Large"
|
|
|
|
defp change_size_color(size) when size < 100, do: "bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-300"
|
|
|
|
defp change_size_color(size) when size < 500,
|
|
do: "bg-yellow-100 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
|
|
defp change_size_color(_), do: "bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-400"
|
|
|
|
defp insight_urgency_class("critical"), do: "text-red-600 dark:text-red-400"
|
|
defp insight_urgency_class("warning"), do: "text-yellow-600 dark:text-yellow-400"
|
|
defp insight_urgency_class(_), do: "text-blue-600 dark:text-blue-400"
|
|
|
|
defp insight_urgency_icon("critical"), do: "hero-exclamation-triangle"
|
|
defp insight_urgency_icon("warning"), do: "hero-exclamation-circle"
|
|
defp insight_urgency_icon(_), do: "hero-information-circle"
|
|
|
|
defp alert_severity(:device_down), do: "critical"
|
|
defp alert_severity(:device_up), do: "info"
|
|
defp alert_severity(_), do: "warning"
|
|
|
|
# Enqueue discovery job - safe to call in test environment
|
|
defp enqueue_discovery(device_id) do
|
|
if Application.get_env(:towerops, :env) == :test do
|
|
# In test, run synchronously
|
|
_ = Task.start(fn -> Snmp.discover_device(Devices.get_device!(device_id)) end)
|
|
else
|
|
# In dev/prod, enqueue to Oban
|
|
DiscoveryWorker.enqueue(device_id)
|
|
end
|
|
end
|
|
end
|