- Add get_impact_summary/1: aggregates current outage subscriber/MRR impact - Add list_active_incidents/1: down devices enriched with Gaiia/Preseem data, sorted by MRR - Add get_site_impact_summaries/1: per-site health with QoE, affected subs, MRR at risk - Dashboard top stats: MRR at Risk and Subscribers Affected cards - Active Incidents section: color-coded by severity, shows duration/subs/MRR/QoE - Site Health Grid: green/yellow/red health dots based on uptime and QoE scores
272 lines
8.7 KiB
Elixir
272 lines
8.7 KiB
Elixir
defmodule Towerops.Dashboard do
|
|
@moduledoc """
|
|
Aggregates data from multiple contexts for the command center dashboard.
|
|
"""
|
|
|
|
alias Towerops.Alerts
|
|
alias Towerops.Devices
|
|
alias Towerops.Gaiia
|
|
alias Towerops.Gaiia.ImpactAnalysis
|
|
alias Towerops.Preseem
|
|
alias Towerops.Preseem.Insights
|
|
alias Towerops.Sites
|
|
|
|
@doc "Returns a complete dashboard summary for an organization."
|
|
def get_dashboard_summary(organization_id) do
|
|
status_counts = Devices.get_device_status_counts(organization_id)
|
|
total_devices = status_counts |> Map.values() |> Enum.sum()
|
|
active_alerts = Alerts.list_organization_active_alerts(organization_id)
|
|
subscriber_totals = Gaiia.get_org_subscriber_totals(organization_id)
|
|
insight_counts = Insights.count_active_by_urgency(organization_id)
|
|
|
|
%{
|
|
health_score: compute_health_score(organization_id),
|
|
devices: %{
|
|
total: total_devices,
|
|
up: Map.get(status_counts, :up, 0),
|
|
down: Map.get(status_counts, :down, 0),
|
|
unknown: Map.get(status_counts, :unknown, 0)
|
|
},
|
|
alerts: %{
|
|
active_count: length(active_alerts)
|
|
},
|
|
subscribers: %{
|
|
total: subscriber_totals.total_subscribers,
|
|
total_mrr: subscriber_totals.total_mrr
|
|
},
|
|
insights: insight_counts
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Compute a weighted 0-100 health score for an organization.
|
|
|
|
Weights:
|
|
- Device uptime % (40%)
|
|
- Response time health (20%) - currently based on device status
|
|
- Critical alert penalty (20%)
|
|
- Preseem QoE average (10%, skipped if unavailable)
|
|
- Agent online % (10%, skipped if no agents)
|
|
|
|
Unavailable components have their weight redistributed proportionally.
|
|
"""
|
|
def compute_health_score(organization_id) do
|
|
status_counts = Devices.get_device_status_counts(organization_id)
|
|
total = status_counts |> Map.values() |> Enum.sum()
|
|
|
|
if total == 0 do
|
|
100
|
|
else
|
|
up = Map.get(status_counts, :up, 0)
|
|
down = Map.get(status_counts, :down, 0)
|
|
active_alerts = Alerts.list_organization_active_alerts(organization_id)
|
|
|
|
# Device uptime percentage (0.0-1.0)
|
|
uptime_ratio = up / total
|
|
|
|
# Response time health - approximate from up/unknown ratio
|
|
responsive_ratio = (total - down) / total
|
|
|
|
# Alert penalty - each alert reduces score, capped at full penalty
|
|
alert_penalty = min(length(active_alerts) * 0.1, 1.0)
|
|
alert_score = 1.0 - alert_penalty
|
|
|
|
# Weighted score (redistribute Preseem/Agent weights to core metrics)
|
|
# Base weights: uptime 40%, response 20%, alerts 20%, preseem 10%, agents 10%
|
|
# Without preseem/agents: uptime 50%, response 25%, alerts 25%
|
|
score =
|
|
uptime_ratio * 50.0 +
|
|
responsive_ratio * 25.0 +
|
|
alert_score * 25.0
|
|
|
|
round(score)
|
|
end
|
|
end
|
|
|
|
@doc "Returns subscriber summary for the org from Gaiia data."
|
|
def get_org_subscriber_summary(organization_id) do
|
|
totals = Gaiia.get_org_subscriber_totals(organization_id)
|
|
|
|
%{
|
|
total: totals.total_subscribers,
|
|
total_mrr: totals.total_mrr
|
|
}
|
|
end
|
|
|
|
@doc "Returns per-site summaries with device counts, alerts, and subscriber data."
|
|
def list_site_summaries(organization_id) do
|
|
sites = Sites.list_organization_sites(organization_id)
|
|
|
|
Enum.map(sites, fn site ->
|
|
device_count = Devices.count_site_devices(site.id)
|
|
down_count = Devices.count_site_devices_down(site.id)
|
|
gaiia_summary = Gaiia.get_site_subscriber_summary(site.id)
|
|
|
|
%{
|
|
site_id: site.id,
|
|
name: site.name,
|
|
device_count: device_count,
|
|
devices_down: down_count,
|
|
subscribers: if(gaiia_summary, do: gaiia_summary.account_count),
|
|
mrr: if(gaiia_summary, do: gaiia_summary.total_mrr)
|
|
}
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Returns aggregate impact summary for current outages.
|
|
|
|
Shows total subscribers affected by down devices, total MRR at risk,
|
|
and number of sites with outages.
|
|
"""
|
|
def get_impact_summary(organization_id) do
|
|
down_devices = Devices.list_organization_devices(organization_id, %{"status" => "down"})
|
|
|
|
# Compute impact for each down device
|
|
impacts =
|
|
Enum.map(down_devices, fn device ->
|
|
ImpactAnalysis.analyze_device_impact(organization_id, device.id)
|
|
end)
|
|
|
|
# Aggregate — count unique sites with outages
|
|
sites_with_outages =
|
|
down_devices
|
|
|> Enum.map(& &1.site_id)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Enum.uniq()
|
|
|> length()
|
|
|
|
total_subscribers = impacts |> Enum.map(& &1.total_subscribers) |> Enum.sum()
|
|
|
|
total_mrr =
|
|
impacts
|
|
|> Enum.map(& &1.total_mrr)
|
|
|> Enum.reduce(Decimal.new("0"), &Decimal.add/2)
|
|
|
|
%{
|
|
subscribers_affected: total_subscribers,
|
|
mrr_at_risk: total_mrr,
|
|
sites_with_outages: sites_with_outages,
|
|
down_device_count: length(down_devices)
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Returns down devices enriched with Gaiia subscriber impact, Preseem QoE data,
|
|
site info, and outage duration. Sorted by MRR impact descending.
|
|
"""
|
|
def list_active_incidents(organization_id) do
|
|
down_devices = Devices.list_organization_devices(organization_id, %{"status" => "down"})
|
|
now = DateTime.utc_now()
|
|
|
|
incidents =
|
|
Enum.map(down_devices, fn device ->
|
|
impact = ImpactAnalysis.analyze_device_impact(organization_id, device.id)
|
|
preseem_ap = Preseem.get_access_point_for_device(device.id)
|
|
|
|
duration_seconds =
|
|
if device.last_status_change_at do
|
|
DateTime.diff(now, device.last_status_change_at, :second)
|
|
else
|
|
nil
|
|
end
|
|
|
|
%{
|
|
device_id: device.id,
|
|
device_name: device.name,
|
|
site_id: device.site_id,
|
|
site_name: if(device.site, do: device.site.name),
|
|
duration_seconds: duration_seconds,
|
|
subscribers_affected: impact.total_subscribers,
|
|
mrr_at_risk: impact.total_mrr,
|
|
qoe_score: if(preseem_ap, do: preseem_ap.qoe_score),
|
|
triggered_at: device.last_status_change_at
|
|
}
|
|
end)
|
|
|
|
Enum.sort_by(incidents, & &1.mrr_at_risk, {:desc, Decimal})
|
|
end
|
|
|
|
@doc """
|
|
Enhanced site summaries with impact data from currently-down devices,
|
|
MRR at risk, average QoE score, and site health score.
|
|
"""
|
|
def get_site_impact_summaries(organization_id) do
|
|
sites = Sites.list_organization_sites(organization_id)
|
|
|
|
Enum.map(sites, fn site ->
|
|
device_count = Devices.count_site_devices(site.id)
|
|
down_count = Devices.count_site_devices_down(site.id)
|
|
gaiia_summary = Gaiia.get_site_subscriber_summary(site.id)
|
|
|
|
# Get impact from down devices at this site
|
|
down_devices = Devices.list_organization_devices(organization_id, %{"status" => "down", "site_id" => site.id})
|
|
|
|
{subscribers_affected, mrr_at_risk} =
|
|
if down_count > 0 do
|
|
impacts =
|
|
Enum.map(down_devices, fn device ->
|
|
ImpactAnalysis.analyze_device_impact(organization_id, device.id)
|
|
end)
|
|
|
|
subs = impacts |> Enum.map(& &1.total_subscribers) |> Enum.sum()
|
|
mrr = impacts |> Enum.map(& &1.total_mrr) |> Enum.reduce(Decimal.new("0"), &Decimal.add/2)
|
|
{subs, mrr}
|
|
else
|
|
{0, Decimal.new("0")}
|
|
end
|
|
|
|
# Get average QoE from Preseem for devices at this site
|
|
site_devices = Devices.list_site_devices(site.id)
|
|
|
|
qoe_scores =
|
|
site_devices
|
|
|> Enum.map(fn d -> Preseem.get_access_point_for_device(d.id) end)
|
|
|> Enum.reject(&is_nil/1)
|
|
|> Enum.map(& &1.qoe_score)
|
|
|> Enum.reject(&is_nil/1)
|
|
|
|
avg_qoe =
|
|
if qoe_scores != [] do
|
|
Enum.sum(qoe_scores) / length(qoe_scores)
|
|
end
|
|
|
|
# Site health: green (all up), yellow (some issues), red (devices down)
|
|
site_health =
|
|
cond do
|
|
down_count > 0 -> :red
|
|
avg_qoe && avg_qoe < 7.0 -> :yellow
|
|
true -> :green
|
|
end
|
|
|
|
%{
|
|
site_id: site.id,
|
|
name: site.name,
|
|
device_count: device_count,
|
|
devices_down: down_count,
|
|
subscribers: if(gaiia_summary, do: gaiia_summary.account_count),
|
|
mrr: if(gaiia_summary, do: gaiia_summary.total_mrr),
|
|
subscribers_affected: subscribers_affected,
|
|
mrr_at_risk: mrr_at_risk,
|
|
avg_qoe: avg_qoe,
|
|
site_health: site_health
|
|
}
|
|
end)
|
|
end
|
|
|
|
@doc "Returns a summary for a single site."
|
|
def get_site_summary(site_id) do
|
|
device_count = Devices.count_site_devices(site_id)
|
|
down_count = Devices.count_site_devices_down(site_id)
|
|
alert_count = Alerts.count_site_active_alerts(site_id)
|
|
gaiia_summary = Gaiia.get_site_subscriber_summary(site_id)
|
|
|
|
%{
|
|
device_count: device_count,
|
|
devices_down: down_count,
|
|
alert_count: alert_count,
|
|
subscribers: if(gaiia_summary, do: gaiia_summary.account_count),
|
|
mrr: if(gaiia_summary, do: gaiia_summary.total_mrr)
|
|
}
|
|
end
|
|
end
|