From bfe552cb4740c4ac7abb565bf4e7e4232a7cff7e Mon Sep 17 00:00:00 2001 From: Graham McIntie Date: Fri, 13 Feb 2026 19:13:04 -0600 Subject: [PATCH] feat: enhance dashboard with uptime, quick actions, mini activity feed --- lib/towerops_web/live/dashboard_live.ex | 32 ++++- .../live/dashboard_live.html.heex | 125 ++++++++++++++++-- 2 files changed, 147 insertions(+), 10 deletions(-) diff --git a/lib/towerops_web/live/dashboard_live.ex b/lib/towerops_web/live/dashboard_live.ex index 4185035d..8264be5f 100644 --- a/lib/towerops_web/live/dashboard_live.ex +++ b/lib/towerops_web/live/dashboard_live.ex @@ -123,13 +123,27 @@ defmodule ToweropsWeb.DashboardLive do [] end + device_up = Map.get(status_counts, :up, 0) + + uptime_percentage = + if device_count > 0, + do: Float.round(device_up / device_count * 100, 1), + else: 100.0 + + recent_activity = + try do + ActivityFeed.list_org_activity(organization_id, limit: 5) + rescue + _ -> [] + end + socket |> assign(:summary, summary) |> assign(:active_alerts, Enum.take(active_alerts, 20)) |> assign(:total_alert_count, length(active_alerts)) |> assign(:sites_count, sites_count) |> assign(:device_count, device_count) - |> assign(:device_up, Map.get(status_counts, :up, 0)) + |> assign(:device_up, device_up) |> assign(:device_down, Map.get(status_counts, :down, 0)) |> assign(:device_unknown, Map.get(status_counts, :unknown, 0)) |> assign(:has_subscribers, has_subscribers) @@ -137,6 +151,9 @@ defmodule ToweropsWeb.DashboardLive do |> assign(:impact_summary, impact_summary) |> assign(:active_incidents, active_incidents) |> assign(:site_impact_summaries, site_impact_summaries) + |> assign(:uptime_percentage, uptime_percentage) + |> assign(:recent_activity, recent_activity) + |> assign(:last_updated, DateTime.utc_now()) |> load_insights(organization_id) |> load_recent_config_changes(organization_id) end @@ -258,6 +275,19 @@ defmodule ToweropsWeb.DashboardLive do defp mrr_at_risk_positive?(n) when is_number(n), do: n > 0 defp mrr_at_risk_positive?(_), do: false + defp uptime_color(pct) when pct >= 99.0, do: "text-green-600 dark:text-green-400" + defp uptime_color(pct) when pct >= 95.0, do: "text-yellow-600 dark:text-yellow-400" + defp uptime_color(_pct), do: "text-red-600 dark:text-red-400" + + defp uptime_bg(pct) when pct >= 99.0, + do: "bg-green-50 border-green-200 dark:bg-green-900/20 dark:border-green-800" + + defp uptime_bg(pct) when pct >= 95.0, + do: "bg-yellow-50 border-yellow-200 dark:bg-yellow-900/20 dark:border-yellow-800" + + defp uptime_bg(_pct), + do: "bg-red-50 border-red-200 dark:bg-red-900/20 dark:border-red-800" + defp format_qoe(nil), do: "—" defp format_qoe(score), do: :erlang.float_to_binary(score / 1, decimals: 1) end diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex index c4c82be5..7c2c6e73 100644 --- a/lib/towerops_web/live/dashboard_live.html.heex +++ b/lib/towerops_web/live/dashboard_live.html.heex @@ -8,6 +8,24 @@ <:subtitle>Welcome to {@current_scope.organization.name} + <%!-- Last Updated + Refresh --%> + <%= if assigns[:last_updated] do %> +
+ + <.icon name="hero-clock" class="h-3.5 w-3.5" /> + Last updated: {Calendar.strftime(@last_updated, "%b %d, %H:%M:%S UTC")} + + +
+ <% end %> + <%= if @device_count == 0 do %>
<.icon @@ -99,6 +117,20 @@

+ <%!-- Network Uptime --%> +
+

Network Uptime

+

+ {if @uptime_percentage == 100.0, do: "100", else: :erlang.float_to_binary(@uptime_percentage, decimals: 1)}% +

+

+ {@device_up}/{@device_count} devices up +

+
+ <%!-- Active Alerts --%> <.link navigate={~p"/alerts"} @@ -212,13 +244,30 @@ + <%!-- Quick Actions --%> +
+ <.link navigate={~p"/devices/new"} class="btn btn-outline btn-sm gap-1.5"> + <.icon name="hero-plus-circle" class="h-4 w-4" /> Add Device + + <.link navigate={~p"/discovery"} class="btn btn-outline btn-sm gap-1.5"> + <.icon name="hero-magnifying-glass-circle" class="h-4 w-4" /> Run Discovery + + <.link navigate={~p"/alerts"} class="btn btn-outline btn-sm gap-1.5"> + <.icon name="hero-bell-alert" class="h-4 w-4" /> View Alerts + + <.link navigate={~p"/subscribers/trace"} class="btn btn-outline btn-sm gap-1.5"> + <.icon name="hero-signal" class="h-4 w-4" /> Subscriber Trace + +
+ <%!-- Active Incidents (sorted by revenue impact) --%> <%= if @active_incidents != [] do %>
-

+

+ <.icon name="hero-fire" class="h-5 w-5 text-red-500" /> Active Incidents - + {length(@active_incidents)}

@@ -284,7 +333,10 @@ <%!-- Left: Alert Feed (3 cols) --%>
-

Active Alerts

+

+ <.icon name="hero-bell-alert" class="h-5 w-5 text-red-500" /> + Active Alerts +

<.link navigate={~p"/alerts"} class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400" @@ -394,10 +446,18 @@ <%= if Enum.any?(@recent_config_changes) do %>
-

- <.icon name="hero-clock" class="h-4 w-4 text-orange-500" /> - Recent Config Changes -

+
+

+ <.icon name="hero-document-text" class="h-4 w-4 text-orange-500" /> + Recent Config Changes +

+ <.link + navigate={~p"/config-changes"} + class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400" + > + View All → + +
<%= for change <- @recent_config_changes do %> <.link @@ -420,10 +480,54 @@
<% end %> + <%!-- Recent Activity Mini-Feed --%> + <%= if assigns[:recent_activity] && @recent_activity != [] do %> +
+
+
+

+ <.icon name="hero-clock" class="h-4 w-4 text-blue-500" /> + Recent Activity +

+ <.link + navigate={~p"/activity"} + class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400" + > + View All → + +
+
+
+
+ +
+
+

+ {activity.description} +

+

+ {Calendar.strftime(activity.inserted_at, "%b %d %H:%M")} + <%= if Map.get(activity, :actor_name) do %> + by {activity.actor_name} + <% end %> +

+
+
+
+
+
+ <% end %> + <%!-- Right: Insights Feed (2 cols) --%>
-

Insights

+

+ <.icon name="hero-light-bulb" class="h-5 w-5 text-yellow-500" /> + Insights +

<.link navigate={~p"/insights"} class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400" @@ -527,7 +631,10 @@ <%= if @current_scope.organization.use_sites && @site_impact_summaries != [] do %>
-

Site Health

+

+ <.icon name="hero-map-pin" class="h-5 w-5 text-blue-500" /> + Site Health +

<.link navigate={~p"/sites"} class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400"