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
404 lines
11 KiB
Elixir
404 lines
11 KiB
Elixir
defmodule ToweropsWeb.DashboardLiveTest do
|
|
use ToweropsWeb.ConnCase
|
|
|
|
import Phoenix.LiveViewTest
|
|
|
|
alias Towerops.Preseem.Insights
|
|
|
|
setup :register_and_log_in_user
|
|
|
|
setup %{conn: conn, user: user} do
|
|
{:ok, organization} =
|
|
Towerops.Organizations.create_organization(%{name: "Test Org", use_sites: true}, user.id)
|
|
|
|
{:ok, site} =
|
|
Towerops.Sites.create_site(%{
|
|
name: "Test Site",
|
|
organization_id: organization.id
|
|
})
|
|
|
|
conn = Plug.Conn.put_session(conn, :current_organization_id, organization.id)
|
|
|
|
%{conn: conn, organization: organization, site: site}
|
|
end
|
|
|
|
describe "empty state" do
|
|
test "shows onboarding when no devices exist", %{conn: conn} do
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Let's get started!"
|
|
assert html =~ "Create Your First Site"
|
|
end
|
|
|
|
test "requires authentication" do
|
|
conn = build_conn()
|
|
{:error, redirect} = live(conn, ~p"/dashboard")
|
|
|
|
assert {:redirect, %{to: path}} = redirect
|
|
assert path == ~p"/users/log-in"
|
|
end
|
|
end
|
|
|
|
describe "health overview" do
|
|
test "displays health score", %{conn: conn, organization: organization, site: site} do
|
|
{:ok, device} = create_device(organization, site)
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Health Score"
|
|
assert html =~ "100"
|
|
end
|
|
|
|
test "displays device counts with up/down/unknown", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, d1} = create_device(organization, site, "Router 1", "192.168.1.1")
|
|
{:ok, d2} = create_device(organization, site, "Router 2", "192.168.1.2")
|
|
Towerops.Devices.update_device_status(d1, :up)
|
|
Towerops.Devices.update_device_status(d2, :down)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Devices"
|
|
assert html =~ "1 Up"
|
|
assert html =~ "1 Down"
|
|
end
|
|
|
|
test "displays active alert count", %{conn: conn, organization: organization, site: site} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, _alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: :device_down,
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device is down"
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Active Alerts"
|
|
assert html =~ "1"
|
|
end
|
|
|
|
test "displays insight counts by urgency", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, _} =
|
|
Insights.insert_insight_if_new(%{
|
|
organization_id: organization.id,
|
|
device_id: device.id,
|
|
type: "device_poll_gap",
|
|
source: "snmp",
|
|
urgency: "warning",
|
|
channel: "proactive",
|
|
title: "Device not polled"
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Insights"
|
|
end
|
|
|
|
test "hides subscriber section when no Gaiia integration", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, _device} = create_device(organization, site)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
refute html =~ "Subscribers"
|
|
refute html =~ "MRR"
|
|
end
|
|
|
|
test "shows subscriber section when Gaiia data exists", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, _device} = create_device(organization, site)
|
|
|
|
{:ok, _} =
|
|
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
|
gaiia_id: "ns-dash",
|
|
name: "Tower",
|
|
account_count: 100,
|
|
total_mrr: Decimal.new("5000.00")
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Subscribers"
|
|
assert html =~ "100"
|
|
assert html =~ "5,000"
|
|
end
|
|
end
|
|
|
|
describe "alert feed" do
|
|
test "shows alerts with device links", %{conn: conn, organization: organization, site: site} do
|
|
{:ok, device} = create_device(organization, site, "Main Router", "10.0.0.1")
|
|
Towerops.Devices.update_device_status(device, :down)
|
|
|
|
{:ok, _alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: :device_down,
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device is unreachable"
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Main Router"
|
|
assert html =~ "Device is unreachable"
|
|
assert html =~ ~p"/devices/#{device.id}"
|
|
end
|
|
|
|
test "shows View All link when alerts exist", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, _alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: :device_down,
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device is down"
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "View All"
|
|
end
|
|
|
|
test "shows acknowledge button for unacknowledged alerts", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, _alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: :device_down,
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device is down"
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Acknowledge"
|
|
end
|
|
end
|
|
|
|
describe "insight feed" do
|
|
test "shows insights from multiple sources", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, _} =
|
|
Insights.insert_insight_if_new(%{
|
|
organization_id: organization.id,
|
|
device_id: device.id,
|
|
type: "device_poll_gap",
|
|
source: "snmp",
|
|
urgency: "warning",
|
|
channel: "proactive",
|
|
title: "Device not polled in 24h"
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Device not polled in 24h"
|
|
assert html =~ "snmp"
|
|
end
|
|
|
|
test "dismiss insight removes it from feed", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, insight} =
|
|
Insights.insert_insight_if_new(%{
|
|
organization_id: organization.id,
|
|
device_id: device.id,
|
|
type: "device_poll_gap",
|
|
source: "snmp",
|
|
urgency: "warning",
|
|
channel: "proactive",
|
|
title: "Poll gap detected"
|
|
})
|
|
|
|
{:ok, view, html} = live(conn, ~p"/dashboard")
|
|
assert html =~ "Poll gap detected"
|
|
|
|
view |> element("#dismiss-insight-#{insight.id}") |> render_click()
|
|
|
|
html = render(view)
|
|
refute html =~ "Poll gap detected"
|
|
end
|
|
|
|
test "source filter filters insights via URL", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, _} =
|
|
Insights.insert_insight_if_new(%{
|
|
organization_id: organization.id,
|
|
device_id: device.id,
|
|
type: "device_poll_gap",
|
|
source: "snmp",
|
|
urgency: "warning",
|
|
channel: "proactive",
|
|
title: "SNMP insight"
|
|
})
|
|
|
|
{:ok, _} =
|
|
Insights.insert_insight_if_new(%{
|
|
organization_id: organization.id,
|
|
device_id: device.id,
|
|
type: "reconciliation_finding",
|
|
source: "gaiia",
|
|
urgency: "info",
|
|
channel: "proactive",
|
|
title: "Gaiia insight",
|
|
dedup_key: "test_dedup",
|
|
metadata: %{"dedup_key" => "test_dedup"}
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/dashboard?insight_source=snmp")
|
|
|
|
html = render(view)
|
|
assert html =~ "SNMP insight"
|
|
refute html =~ "Gaiia insight"
|
|
end
|
|
end
|
|
|
|
describe "site health grid" do
|
|
test "shows site cards with device counts", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "Test Site"
|
|
assert html =~ ~p"/sites/#{site.id}"
|
|
end
|
|
|
|
test "shows subscriber data on site cards when Gaiia mapped", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
Towerops.Devices.update_device_status(device, :up)
|
|
|
|
{:ok, _} =
|
|
Towerops.Gaiia.upsert_network_site(organization.id, %{
|
|
gaiia_id: "ns-site",
|
|
name: "Tower",
|
|
account_count: 42,
|
|
total_mrr: Decimal.new("2100.00"),
|
|
site_id: site.id
|
|
})
|
|
|
|
{:ok, _view, html} = live(conn, ~p"/dashboard")
|
|
|
|
assert html =~ "42"
|
|
end
|
|
end
|
|
|
|
describe "real-time updates" do
|
|
test "PubSub new alert event updates dashboard", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/dashboard")
|
|
|
|
# Create alert and broadcast
|
|
{:ok, _alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: :device_down,
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device went down"
|
|
})
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
Towerops.PubSub,
|
|
"alerts:org:#{organization.id}:new",
|
|
{:new_alert, device.id, :device_down}
|
|
)
|
|
|
|
html = render(view)
|
|
assert html =~ "Device went down"
|
|
end
|
|
|
|
test "PubSub alert resolved event updates dashboard", %{
|
|
conn: conn,
|
|
organization: organization,
|
|
site: site
|
|
} do
|
|
{:ok, device} = create_device(organization, site)
|
|
|
|
{:ok, alert} =
|
|
Towerops.Alerts.create_alert(%{
|
|
device_id: device.id,
|
|
alert_type: :device_down,
|
|
triggered_at: DateTime.utc_now(),
|
|
message: "Device went down"
|
|
})
|
|
|
|
{:ok, view, _html} = live(conn, ~p"/dashboard")
|
|
|
|
# Resolve the alert
|
|
Towerops.Alerts.resolve_alert(alert)
|
|
|
|
Phoenix.PubSub.broadcast(
|
|
Towerops.PubSub,
|
|
"alerts:org:#{organization.id}:resolved",
|
|
{:alert_resolved, device.id, :device_down}
|
|
)
|
|
|
|
html = render(view)
|
|
refute html =~ "Device went down"
|
|
end
|
|
end
|
|
|
|
defp create_device(organization, site, name \\ "Test Device", ip \\ "192.168.1.1") do
|
|
Towerops.Devices.create_device(%{
|
|
name: name,
|
|
ip_address: ip,
|
|
site_id: site.id,
|
|
organization_id: organization.id
|
|
})
|
|
end
|
|
end
|