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.Integrations 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 = Integrations.get_billing_summary(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 billing integrations." def get_org_subscriber_summary(organization_id) do totals = Integrations.get_billing_summary(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) 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} = calculate_down_impact(organization_id, down_devices, down_count) # 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 defp calculate_down_impact(_organization_id, _down_devices, 0), do: {0, Decimal.new("0")} defp calculate_down_impact(organization_id, down_devices, _down_count) 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} end end