Transform the dashboard into a single-pane-of-glass command center that blends operational metrics with business context from Gaiia subscriber data. - Extend insight schema with multi-source support (snmp, gaiia, system) - Add Oban workers for automated insight generation (device health, system, gaiia) - New Dashboard context with health score computation and site summaries - Rewrite dashboard LiveView with health overview, alert/insight feeds, site grid - Add source filter pills for insights with URL state management - Add metrics bar to site detail pages (device count, alerts, subscribers, MRR) - Add subscriber/MRR context to alert list site links - Add subscriber badges to device list site headers - Real-time PubSub updates for alerts on dashboard
119 lines
3.3 KiB
Elixir
119 lines
3.3 KiB
Elixir
defmodule ToweropsWeb.AlertLive.Index do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.Alerts
|
|
alias Towerops.Gaiia
|
|
alias ToweropsWeb.Live.Helpers.AccessControl
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
|
|
# Subscribe to alert events
|
|
_ =
|
|
if connected?(socket) do
|
|
_ = Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:new")
|
|
Phoenix.PubSub.subscribe(Towerops.PubSub, "alerts:org:#{organization.id}:resolved")
|
|
end
|
|
|
|
{:ok,
|
|
socket
|
|
|> assign(:page_title, "Alerts")
|
|
|> assign(:filter, "active")
|
|
|> load_alerts(organization.id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
filter = Map.get(params, "filter", "active")
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:filter, filter)
|
|
|> load_alerts(socket.assigns.current_scope.organization.id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("acknowledge", %{"id" => id}, socket) do
|
|
organization = socket.assigns.current_scope.organization
|
|
user_id = socket.assigns.current_scope.user.id
|
|
|
|
case AccessControl.verify_alert_access(id, organization.id) do
|
|
{:ok, alert} ->
|
|
perform_acknowledge_alert(socket, alert, user_id, organization.id)
|
|
|
|
{:error, :not_found} ->
|
|
{:noreply, put_flash(socket, :error, t_equipment("Alert not found"))}
|
|
|
|
{:error, :unauthorized} ->
|
|
{:noreply, put_flash(socket, :error, t_equipment("You don't have access to this alert"))}
|
|
end
|
|
end
|
|
|
|
defp perform_acknowledge_alert(socket, alert, user_id, organization_id) do
|
|
case Alerts.acknowledge_alert(alert, user_id) do
|
|
{:ok, _alert} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t_equipment("Alert acknowledged"))
|
|
|> load_alerts(organization_id)}
|
|
|
|
{:error, _changeset} ->
|
|
{:noreply, put_flash(socket, :error, t_equipment("Unable to acknowledge alert"))}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:new_alert, _device_id, _alert_type}, socket) do
|
|
{:noreply, load_alerts(socket, socket.assigns.current_scope.organization.id)}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:alert_resolved, _device_id, _alert_type}, socket) do
|
|
{:noreply, load_alerts(socket, socket.assigns.current_scope.organization.id)}
|
|
end
|
|
|
|
defp load_alerts(socket, organization_id) do
|
|
alerts =
|
|
case socket.assigns.filter do
|
|
"active" ->
|
|
Alerts.list_organization_active_alerts(organization_id)
|
|
|
|
_ ->
|
|
Alerts.list_organization_alerts(organization_id, 100)
|
|
end
|
|
|
|
site_subscribers = load_site_subscribers(alerts)
|
|
|
|
socket
|
|
|> assign(:alerts, alerts)
|
|
|> assign(:site_subscribers, site_subscribers)
|
|
end
|
|
|
|
defp load_site_subscribers(alerts) do
|
|
alerts
|
|
|> Enum.map(& &1.device.site_id)
|
|
|> Enum.uniq()
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Map.new(fn site_id -> {site_id, Gaiia.get_site_subscriber_summary(site_id)} end)
|
|
end
|
|
|
|
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 format_mrr(%Decimal{} = amount) do
|
|
"$#{format_number(Decimal.to_integer(Decimal.round(amount, 0)))}"
|
|
end
|
|
|
|
defp format_mrr(_), do: nil
|
|
end
|