diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex index db01cc5c..c5f7a809 100644 --- a/lib/towerops_web/live/dashboard_live.html.heex +++ b/lib/towerops_web/live/dashboard_live.html.heex @@ -3,98 +3,101 @@ current_scope={@current_scope} active_page="dashboard" > - <.header> - Command Center - <: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")} - - + <%!-- Header --%> +
+
+

+ {@current_scope.organization.name} +

+

Network overview

- <% end %> + <%= if assigns[:last_updated] do %> +
+ + Updated {Calendar.strftime(@last_updated, "%H:%M:%S UTC")} + + +
+ <% end %> +
<%= if !assigns[:summary] do %> <.skeleton_dashboard /> <% end %> + <%!-- Empty State --%> <%= if assigns[:summary] && @device_count == 0 do %> -
+
<.icon name="hero-light-bulb" - class="mx-auto h-16 w-16 text-blue-500 dark:text-blue-400" + class="mx-auto h-14 w-14 text-blue-500 dark:text-blue-400" />

Let's get started!

-

- Follow these steps to set up your network monitoring: +

+ Set up your network monitoring in a few steps.

-
-
+
+
<%= if @current_scope.organization.use_sites do %>
-
+
1

Create a Site

-

- Sites represent your physical locations (offices, data centers, towers, etc.) +

+ Sites represent your physical locations

-
+
2

Add Devices

-

- Add network devices (routers, switches, servers) to your sites +

+ Add routers, switches, and servers to your sites

<% else %>
-
+
1

Add Devices

-

- Add network devices (routers, switches, servers) to monitor +

+ Add network devices to monitor

<% end %>
-
+
{if @current_scope.organization.use_sites, do: "3", else: "2"}

Monitor & Receive Alerts

-

- Towerops will automatically monitor your devices and alert you of issues +

+ TowerOps automatically monitors your devices and alerts you of issues

-
+
<%= if @current_scope.organization.use_sites do %> <.button navigate={~p"/sites/new"} variant="primary"> <.icon name="hero-plus" class="h-5 w-5" /> Create Your First Site @@ -109,78 +112,119 @@ <% end %> <%= if assigns[:summary] && @device_count > 0 do %> - <%!-- Section A: Health Overview --%> -
+ <%!-- ═══════════════════════════════════════════════ + STAT CARDS — max 4 per row, generous sizing + ═══════════════════════════════════════════════ --%> +
<%!-- Health Score --%>
-

Health Score

-

+

+

Health Score

+ 80 -> + "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400" + + @summary.health_score > 50 -> + "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/40 dark:text-yellow-400" + + true -> + "bg-red-100 text-red-700 dark:bg-red-900/40 dark:text-red-400" + end + ]}> + {cond do + @summary.health_score > 80 -> "Healthy" + @summary.health_score > 50 -> "Degraded" + true -> "Critical" + end} + +
+

{@summary.health_score}

<%!-- Network Uptime --%>

Network Uptime

-

+

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

-

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

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

- <%!-- Active Alerts --%> - <.link - navigate={~p"/alerts"} - class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" - > -

Active Alerts

-

0 && "text-red-600 dark:text-red-500") || - "text-gray-900 dark:text-white" - ]}> - {@total_alert_count} -

- - <%!-- Devices --%> <.link navigate={~p"/devices"} - class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" + class="rounded-xl border border-gray-200 bg-white p-5 transition hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" >

Devices

-

{@device_count}

-
- +

+ {@device_count} +

+
+ - {@device_up} Up + {@device_up} up - + - {@device_down} Down - - - - {@device_unknown} Unknown + {@device_down} down + <%= if @device_unknown > 0 do %> + + + {@device_unknown} + + <% end %>
- <%!-- Subscribers (only when Gaiia data exists) --%> + <%!-- Active Alerts --%> + <.link + navigate={~p"/alerts"} + class="rounded-xl border border-gray-200 bg-white p-5 transition hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" + > +

Active Alerts

+

0, + do: "text-red-600 dark:text-red-400", + else: "text-gray-900 dark:text-white" + ) + ]}> + {@total_alert_count} +

+

+ <%= if @total_alert_count == 0 do %> + All clear + <% else %> + View details → + <% end %> +

+ +
+ + <%!-- Secondary stats row (subscribers, MRR, sites, insights) --%> +
<%= if @has_subscribers do %> -
+

Subscribers

-

+

{format_number(@summary.subscribers.total)}

@@ -188,17 +232,16 @@

- <%!-- MRR at Risk --%>

MRR at Risk

<% end %> - <%!-- Sites (if enabled) --%> <%= if @current_scope.organization.use_sites do %> <.link navigate={~p"/sites"} - class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm transition-shadow hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" + class="rounded-xl border border-gray-200 bg-white p-5 transition hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" >

Sites

-

{@sites_count}

+

+ {@sites_count} +

<% end %> - <%!-- Insights --%> -
+ <.link + navigate={~p"/insights"} + class="rounded-xl border border-gray-200 bg-white p-5 transition hover:shadow-md dark:border-white/10 dark:bg-gray-800/50" + >

Insights

-
-

- {@summary.insights.critical + @summary.insights.warning + @summary.insights.info} -

+

+ {@summary.insights.critical + @summary.insights.warning + @summary.insights.info} +

+
+ 0} + class="inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-700 dark:bg-red-900/30 dark:text-red-400" + > + {@summary.insights.critical} critical + + 0} + class="inline-flex items-center rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-medium text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" + > + {@summary.insights.warning} warning +
-
- <%= if @summary.insights.critical > 0 do %> - - {@summary.insights.critical} critical - - <% end %> - <%= if @summary.insights.warning > 0 do %> - - {@summary.insights.warning} warning - - <% end %> - <%= if @summary.insights.info > 0 do %> - - {@summary.insights.info} info - - <% end %> -
-
-
- - <%!-- 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"/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"/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) --%> + <%!-- ═══════════════════════════════════════════════ + ACTIVE INCIDENTS + ═══════════════════════════════════════════════ --%> <%= if @active_incidents != [] do %> -
-
+
+

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

+ + {length(@active_incidents)} +
-
-
+
+
-

+

<.link navigate={~p"/devices/#{incident.device_id}"} class="hover:text-blue-600 hover:underline dark:hover:text-blue-400" @@ -307,25 +337,25 @@

-
- - <.icon name="hero-clock" class="h-3 w-3" /> +
+ + <.icon name="hero-clock" class="h-3.5 w-3.5" /> Down {format_duration(incident.duration_seconds)} <%= if incident.subscribers_affected > 0 do %> - - <.icon name="hero-users" class="h-3 w-3" /> + + <.icon name="hero-users" class="h-3.5 w-3.5" /> {format_number(incident.subscribers_affected)} affected <% end %> <%= if mrr_at_risk_positive?(incident.mrr_at_risk) do %> - - <.icon name="hero-currency-dollar" class="h-3 w-3" /> + + <.icon name="hero-currency-dollar" class="h-3.5 w-3.5" /> {format_mrr(incident.mrr_at_risk)}/mo at risk <% end %> <%= if incident.qoe_score do %> - + QoE {format_qoe(incident.qoe_score)} <% end %> @@ -337,147 +367,149 @@
<% end %> - <%!-- Section B+C: Two-column layout --%> -
- <%!-- Left: Alert Feed (3 cols) --%> -
-
-

- <.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" - > - View All → - -
- - <%= if @active_alerts == [] do %> -
- <.icon name="hero-check-circle" class="mx-auto h-10 w-10 text-green-500" /> -

No active alerts

-
- <% else %> -
-
+
+ <%!-- LEFT COLUMN: Alerts + Activity --%> +
+ <%!-- 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-400 dark:hover:text-blue-300" > -
-
-
- - <.icon name="hero-exclamation-triangle" class="h-3.5 w-3.5" /> Device Down - - <%= if alert.acknowledged_at do %> - + View all → + +
+ + <%= if @active_alerts == [] do %> +
+ <.icon name="hero-check-circle" class="mx-auto h-10 w-10 text-green-500" /> +

+ No active alerts — all systems operational +

+
+ <% else %> +
+
+
+
+
+ + <.icon name="hero-exclamation-triangle" class="h-3.5 w-3.5" /> Down + + Acknowledged +
+

+ <.link + navigate={~p"/devices/#{alert.device.id}"} + class="hover:text-blue-600 hover:underline dark:hover:text-blue-400" + > + {alert.device.name} + +

+

{alert.message}

+ + <%!-- Gaiia impact --%> + <%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %> +
+ + <.icon name="hero-users" class="h-3 w-3" /> + {alert.gaiia_impact["total_subscribers"]} affected + + <%= if alert.gaiia_impact["total_mrr"] && alert.gaiia_impact["total_mrr"] != "0" do %> + + <.icon name="hero-currency-dollar" class="h-3 w-3" /> + ${alert.gaiia_impact["total_mrr"]}/mo + + <% end %> +
+ <% end %> + +
+ <.timestamp datetime={alert.triggered_at} /> + + at + <.link + navigate={~p"/sites/#{alert.device.site.id}"} + class="text-blue-600 hover:text-blue-700 dark:text-blue-400" + > + {alert.device.site.name} + + +
+
+ +
+ <%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %> + <% end %>
-

- <.link - navigate={~p"/devices/#{alert.device.id}"} - class="hover:text-blue-600 hover:underline dark:hover:text-blue-400" - > - {alert.device.name} - -

-

{alert.message}

- - <%!-- Gaiia impact badges --%> - <%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %> -
- - <.icon name="hero-users" class="h-3 w-3" /> - {alert.gaiia_impact["total_subscribers"]} affected - - <%= if alert.gaiia_impact["total_mrr"] && alert.gaiia_impact["total_mrr"] != "0" do %> - - <.icon name="hero-currency-dollar" class="h-3 w-3" /> - ${alert.gaiia_impact["total_mrr"]}/mo - - <% end %> -
- <% end %> - -
- <.timestamp datetime={alert.triggered_at} /> - - at - <.link - navigate={~p"/sites/#{alert.device.site.id}"} - class="text-blue-600 hover:text-blue-700 dark:text-blue-400" - > - {alert.device.site.name} - - -
-
- -
- <%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %> - - <% end %>
-
- <%= if @total_alert_count > 20 do %> -
- + {@total_alert_count - 20} more alerts. - <.link - navigate={~p"/alerts"} - class="font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400" - > - View All - -
+ <%= if @total_alert_count > 20 do %> +
+ <.link + navigate={~p"/alerts"} + class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400" + > + + {@total_alert_count - 20} more alerts → + +
+ <% end %> <% end %> - <% end %> -
+
- <%!-- Recent Config Changes --%> - <%= if Enum.any?(@recent_config_changes) do %> -
-
-
-

- <.icon name="hero-document-text" class="h-4 w-4 text-orange-500" /> + <%!-- Recent Config Changes --%> + <%= if Enum.any?(@recent_config_changes) do %> +
+
+

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

+

<.link navigate={~p"/insights"} - class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-500 dark:hover:text-blue-400" + class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > - View All → + View all →
-
+
<%= for change <- @recent_config_changes do %> <.link navigate={~p"/devices/#{change.device_id}/config-timeline"} - class="flex items-center gap-3 p-2 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors" + class="flex items-center gap-4 px-5 py-3.5 hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors first:rounded-t-xl last:rounded-b-xl" > -
+

{if change.device, do: change.device.name, else: "Unknown Device"}

-

+

{Calendar.strftime(change.changed_at, "%b %d %H:%M")} · {change.change_size} lines · {Enum.join( Enum.take(change.sections_changed, 2), ", " @@ -488,37 +520,35 @@ <% end %>

-
- <% end %> + <% 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 -

+ <%!-- Recent Activity --%> + <%= if assigns[:recent_activity] && @recent_activity != [] do %> +
+
+

+ <.icon name="hero-clock" class="h-5 w-5 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" + class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > - View All → + View all →
-
+
-
+
-

+

{activity.summary}

-

+

{Calendar.strftime(activity.timestamp, "%b %d %H:%M")} <%= if Map.get(activity, :actor_name) do %> by {activity.actor_name} @@ -528,31 +558,31 @@

-
- <% end %> + <% end %> +
- <%!-- Right: Insights Feed (2 cols) --%> -
-
+ <%!-- RIGHT COLUMN: 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" + class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > - View All → + View all →
<%!-- Source filter pills --%> -
+
<.link patch={~p"/dashboard"} class={[ - "rounded-md px-2.5 py-1 text-xs font-medium", + "rounded-full px-3 py-1 text-xs font-medium transition", if(is_nil(@insight_source), - do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white", + do: "bg-gray-900 text-white dark:bg-white dark:text-gray-900", else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10" ) ]} @@ -563,9 +593,9 @@ :for={source <- ~w(preseem snmp gaiia system)} patch={~p"/dashboard?insight_source=#{source}"} class={[ - "rounded-md px-2.5 py-1 text-xs font-medium", + "rounded-full px-3 py-1 text-xs font-medium transition", if(@insight_source == source, - do: "bg-gray-200 text-gray-800 dark:bg-white/20 dark:text-white", + do: "bg-gray-900 text-white dark:bg-white dark:text-gray-900", else: "text-gray-500 hover:bg-gray-100 dark:text-gray-400 dark:hover:bg-white/10" ) ]} @@ -575,37 +605,37 @@
<%= if @insights == [] do %> -
+
<.icon name="hero-light-bulb" - class="mx-auto h-10 w-10 text-gray-400 dark:text-gray-500" + class="mx-auto h-10 w-10 text-gray-300 dark:text-gray-600" /> -

No active insights

+

No active insights

<% else %>
-
+
-
+
{insight.urgency} {insight.source}
-

+

<.link navigate={~p"/insights?source=#{insight.source}"} class="hover:text-blue-600 hover:underline dark:hover:text-blue-400" @@ -614,7 +644,7 @@

<%= if insight.description do %> -

+

{insight.description}

<% end %> @@ -624,7 +654,7 @@ id={"dismiss-insight-#{insight.id}"} phx-click="dismiss_insight" phx-value-id={insight.id} - class="flex-shrink-0 rounded-md p-1 text-gray-400 hover:bg-gray-100 hover:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-300" + class="flex-shrink-0 rounded-lg p-1.5 text-gray-300 hover:bg-gray-100 hover:text-gray-500 dark:text-gray-600 dark:hover:bg-white/10 dark:hover:text-gray-400" title="Dismiss" > <.icon name="hero-x-mark" class="h-4 w-4" /> @@ -636,30 +666,32 @@
- <%!-- Section D: Site Health Grid --%> + <%!-- ═══════════════════════════════════════════════ + SITE HEALTH GRID + ═══════════════════════════════════════════════ --%> <%= if @current_scope.organization.use_sites && @site_impact_summaries != [] do %> -
-
+
+

<.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" + class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" > - View All Sites → + View all →
-
+
<.link :for={site <- @site_impact_summaries} navigate={~p"/sites/#{site.site_id}"} class={[ - "rounded-lg border p-4 shadow-sm transition-shadow hover:shadow-md", + "rounded-xl border p-5 transition hover:shadow-md", site_health_border(site.site_health) ]} > -
+

{site.name}

-
- +
+ {site.device_count} devices <%= if site.devices_down > 0 do %> - + {site.devices_down} down @@ -684,7 +716,7 @@ <% end %>
<%= if site.subscribers do %> -
+
<.icon name="hero-users" class="h-3.5 w-3.5" /> {site.subscribers} subscribers @@ -695,7 +727,7 @@
<% end %> <%= if site.subscribers_affected > 0 do %> -
+
<.icon name="hero-exclamation-triangle" class="h-3.5 w-3.5" /> {format_number(site.subscribers_affected)} affected @@ -711,5 +743,18 @@
<% end %> + + <%!-- 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"/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"/trace"} class="btn btn-outline btn-sm gap-1.5"> + <.icon name="hero-signal" class="h-4 w-4" /> Subscriber Trace + +
<% end %>