towerops/lib/towerops/dashboard.ex
Graham McIntire 20f5f48372
feat: command center dashboard with unified insights and contextual enrichment
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
2026-02-13 14:52:49 -06:00

129 lines
4.2 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.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 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