feat: enhance dashboard with uptime, quick actions, mini activity feed

This commit is contained in:
Graham McIntire 2026-02-13 19:13:04 -06:00
parent 88b5b55a01
commit bfe552cb47
2 changed files with 147 additions and 10 deletions

View file

@ -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

View file

@ -8,6 +8,24 @@
<:subtitle>Welcome to {@current_scope.organization.name}</:subtitle>
</.header>
<%!-- Last Updated + Refresh --%>
<%= if assigns[:last_updated] do %>
<div class="mt-4 flex items-center justify-end gap-3 text-xs text-gray-500 dark:text-gray-400">
<span class="flex items-center gap-1">
<.icon name="hero-clock" class="h-3.5 w-3.5" />
Last updated: {Calendar.strftime(@last_updated, "%b %d, %H:%M:%S UTC")}
</span>
<button
type="button"
phx-click="refresh_dashboard"
class="btn btn-ghost btn-xs gap-1"
>
<.icon name="hero-arrow-path" class="h-3.5 w-3.5" />
Refresh
</button>
</div>
<% end %>
<%= if @device_count == 0 do %>
<div class="mt-8 rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-12 text-center dark:border-white/20 dark:bg-gray-800/50">
<.icon
@ -99,6 +117,20 @@
</p>
</div>
<%!-- Network Uptime --%>
<div class={[
"rounded-lg border p-4 shadow-sm",
uptime_bg(@uptime_percentage)
]}>
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Network Uptime</h3>
<p class={["mt-1 text-3xl font-bold", uptime_color(@uptime_percentage)]}>
{if @uptime_percentage == 100.0, do: "100", else: :erlang.float_to_binary(@uptime_percentage, decimals: 1)}%
</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
{@device_up}/{@device_count} devices up
</p>
</div>
<%!-- Active Alerts --%>
<.link
navigate={~p"/alerts"}
@ -212,13 +244,30 @@
</div>
</div>
<%!-- Quick Actions --%>
<div class="mt-6 flex flex-wrap items-center gap-3">
<.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>
<.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>
<.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>
<.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
</.link>
</div>
<%!-- Active Incidents (sorted by revenue impact) --%>
<%= if @active_incidents != [] do %>
<div class="mt-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<.icon name="hero-fire" class="h-5 w-5 text-red-500" />
Active Incidents
<span class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
<span class="ml-1 inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
{length(@active_incidents)}
</span>
</h2>
@ -284,7 +333,10 @@
<%!-- Left: Alert Feed (3 cols) --%>
<div class="lg:col-span-3">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Active Alerts</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<.icon name="hero-bell-alert" class="h-5 w-5 text-red-500" />
Active Alerts
</h2>
<.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 %>
<div class="lg:col-span-5">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5 mb-3">
<.icon name="hero-clock" class="h-4 w-4 text-orange-500" />
Recent Config Changes
</h3>
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-document-text" class="h-4 w-4 text-orange-500" />
Recent Config Changes
</h3>
<.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 →
</.link>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
<%= for change <- @recent_config_changes do %>
<.link
@ -420,10 +480,54 @@
</div>
<% end %>
<%!-- Recent Activity Mini-Feed --%>
<%= if assigns[:recent_activity] && @recent_activity != [] do %>
<div class="lg:col-span-3">
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
<div class="flex items-center justify-between mb-3">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
<.icon name="hero-clock" class="h-4 w-4 text-blue-500" />
Recent Activity
</h3>
<.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 →
</.link>
</div>
<div class="space-y-3">
<div
:for={activity <- @recent_activity}
class="flex items-start gap-3 text-sm"
>
<div class="flex-shrink-0 mt-1">
<span class="block h-2 w-2 rounded-full bg-blue-400 dark:bg-blue-500"></span>
</div>
<div class="min-w-0 flex-1">
<p class="text-gray-900 dark:text-white text-sm">
{activity.description}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
{Calendar.strftime(activity.inserted_at, "%b %d %H:%M")}
<%= if Map.get(activity, :actor_name) do %>
<span class="ml-1">by {activity.actor_name}</span>
<% end %>
</p>
</div>
</div>
</div>
</div>
</div>
<% end %>
<%!-- Right: Insights Feed (2 cols) --%>
<div class="lg:col-span-2">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Insights</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<.icon name="hero-light-bulb" class="h-5 w-5 text-yellow-500" />
Insights
</h2>
<.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 %>
<div class="mt-8">
<div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-semibold text-gray-900 dark:text-white">Site Health</h2>
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<.icon name="hero-map-pin" class="h-5 w-5 text-blue-500" />
Site Health
</h2>
<.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"