diff --git a/lib/towerops_web/live/dashboard_live.html.heex b/lib/towerops_web/live/dashboard_live.html.heex index c5f7a809..134f9356 100644 --- a/lib/towerops_web/live/dashboard_live.html.heex +++ b/lib/towerops_web/live/dashboard_live.html.heex @@ -113,18 +113,20 @@ <%= if assigns[:summary] && @device_count > 0 do %> <%!-- ═══════════════════════════════════════════════ - STAT CARDS — max 4 per row, generous sizing + STAT CARDS — compact row ═══════════════════════════════════════════════ --%> -
+
<%!-- Health Score --%>
-

Health Score

+ + Health + 80 -> "bg-green-100 text-green-700 dark:bg-green-900/40 dark:text-green-400" @@ -137,605 +139,497 @@ end ]}> {cond do - @summary.health_score > 80 -> "Healthy" + @summary.health_score > 80 -> "Good" @summary.health_score > 50 -> "Degraded" true -> "Critical" end}

{@summary.health_score}

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

Network Uptime

-

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

+ + 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} of {@device_count} devices up +

+ {@device_up}/{@device_count} up

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

Devices

-

+ + Devices + +

{@device_count}

-
- - - {@device_up} up +
+ + {@device_up} up - - - {@device_down} down - - <%= if @device_unknown > 0 do %> - - - {@device_unknown} + <%= if @device_down > 0 do %> + + {@device_down} down <% end %>
- <%!-- Active Alerts --%> + <%!-- 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" + class="rounded-lg border border-gray-200 bg-white px-4 py-3 transition hover:shadow-sm dark:border-white/10 dark:bg-gray-800/50" > -

Active Alerts

+ + Alerts +

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

-

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

+ {if @total_alert_count == 0, do: "All clear", else: "View details →"}

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

Subscribers

-

- {format_number(@summary.subscribers.total)} -

-

- {format_mrr(@summary.subscribers.total_mrr)}/mo MRR -

-
+ <%!-- Secondary stats (only show if data exists) --%> + <%= if @has_subscribers || (@current_scope.organization.use_sites && @sites_count > 0) do %> +
+ <%= if @has_subscribers do %> +
+ + Subscribers + +

+ {format_number(@summary.subscribers.total)} +

+

+ {format_mrr(@summary.subscribers.total_mrr)}/mo +

+
-
-

MRR at Risk

-

- {format_mrr(@impact_summary.mrr_at_risk)} -

-

0} - class="mt-1 text-sm text-red-600 dark:text-red-400" - > - {format_number(@impact_summary.subscribers_affected)} subscribers affected -

-
- <% end %> + + MRR at Risk + +

+ {format_mrr(@impact_summary.mrr_at_risk)} +

+

0} + class="text-xs text-red-600 dark:text-red-400" + > + {format_number(@impact_summary.subscribers_affected)} affected +

+
+ <% end %> + + <%= if @current_scope.organization.use_sites && @sites_count > 0 do %> + <.link + navigate={~p"/sites"} + class="rounded-lg border border-gray-200 bg-white px-4 py-3 transition hover:shadow-sm dark:border-white/10 dark:bg-gray-800/50" + > + + Sites + +

+ {@sites_count} +

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

Sites

-

- {@sites_count} + + Insights + +

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

+
+ 0} + class="inline-flex items-center rounded-full bg-red-100 px-1.5 py-0.5 text-[10px] 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-1.5 py-0.5 text-[10px] font-medium text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400" + > + {@summary.insights.warning} warning + +
- <% end %> - - <.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} -

-
- 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 - -
- -
+
+ <% end %> <%!-- ═══════════════════════════════════════════════ - ACTIVE INCIDENTS + ACTIVE INCIDENTS (only when there ARE incidents) ═══════════════════════════════════════════════ --%> <%= if @active_incidents != [] do %> -
-
-

- <.icon name="hero-fire" class="h-5 w-5 text-red-500" /> Active Incidents +
+
+

+ Active Incidents

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

- <.link - navigate={~p"/devices/#{incident.device_id}"} - class="hover:text-blue-600 hover:underline dark:hover:text-blue-400" - > - {incident.device_name} - -

- - at - <.link - navigate={~p"/sites/#{incident.site_id}"} - class="text-blue-600 hover:text-blue-700 dark:text-blue-400" - > - {incident.site_name} - - -
-
- - <.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.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.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 %> -
+
+
+ <.link + navigate={~p"/devices/#{incident.device_id}"} + class="text-sm font-semibold text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400" + > + {incident.device_name} + + + at {incident.site_name} +
+
+ + {format_duration(incident.duration_seconds)} + + <%= if incident.subscribers_affected > 0 do %> + + {format_number(incident.subscribers_affected)} subs + + <% end %> + <%= if mrr_at_risk_positive?(incident.mrr_at_risk) do %> + + {format_mrr(incident.mrr_at_risk)}/mo + + <% end %> +
<% end %> <%!-- ═══════════════════════════════════════════════ - MAIN CONTENT — 2 column (60/40 split) + MAIN CONTENT — full width stacked layout ═══════════════════════════════════════════════ --%> -
- <%!-- 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" - > - 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 %> -
-
+ <%= if @active_alerts != [] do %> +
+
+

+ Active Alerts +

+ <.link + navigate={~p"/alerts"} + class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400" + > + View all {@total_alert_count} → + +
+
+ + + + + + + + + + + + -
-
-
- - <.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 %> -
-
- - - - <%= 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 %> + + + + + + + +
+ Device + + Message + + Impact + + When +
+ <.link + navigate={~p"/devices/#{alert.device.id}"} + class="text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400" + > + {alert.device.name} + +

+ {alert.device.site.name} +

+
+ {alert.message} + + <%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %> + + {alert.gaiia_impact["total_subscribers"]} subs + + <% end %> + + <.timestamp datetime={alert.triggered_at} /> + + <%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %> + + <% else %> + Ack'd + <% end %> +
- - <%!-- 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-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" - > - View all → - -
-
- <%= for change <- @recent_config_changes do %> - <.link - navigate={~p"/devices/#{change.device_id}/config-timeline"} - 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), - ", " - )} -

-
- - <% end %> -
-
- <% end %> - - <%!-- 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-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" - > - View all → - -
-
-
-
- -
-
-

- {activity.summary} -

-

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

-
-
-
-
- <% end %>
+ <% end %> - <%!-- RIGHT COLUMN: Insights --%> -
-
-

- <.icon name="hero-light-bulb" class="h-5 w-5 text-yellow-500" /> Insights + <%!-- Insights (compact grouped view) --%> + <%= if @insights != [] do %> +
+
+
+

+ Insights +

+
+ <.link + patch={~p"/dashboard"} + class={[ + "rounded-full px-2 py-0.5 text-[10px] font-medium transition", + if(is_nil(@insight_source), + 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" + ) + ]} + > + All + + <.link + :for={source <- ~w(preseem snmp gaiia system)} + patch={~p"/dashboard?insight_source=#{source}"} + class={[ + "rounded-full px-2 py-0.5 text-[10px] font-medium transition", + if(@insight_source == source, + 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" + ) + ]} + > + {source} + +
+
+ <.link + navigate={~p"/insights"} + class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400" + > + View all → + +
+
+
+
+
+ + {insight.urgency} + + + {insight.source} + +
+

+ <.link + navigate={~p"/insights?source=#{insight.source}"} + class="hover:text-blue-600 dark:hover:text-blue-400" + > + {insight.title} + +

+

+ {insight.description} +

+
+ +
+
+
+ <% end %> + + <%!-- Recent Config Changes --%> + <%= if Enum.any?(@recent_config_changes) do %> +
+
+

+ Config Changes (7d)

<.link navigate={~p"/insights"} - class="text-sm font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300" + class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400" > View all →
- - <%!-- Source filter pills --%> -
- <.link - patch={~p"/dashboard"} - class={[ - "rounded-full px-3 py-1 text-xs font-medium transition", - if(is_nil(@insight_source), - 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" - ) - ]} - > - All - - <.link - :for={source <- ~w(preseem snmp gaiia system)} - patch={~p"/dashboard?insight_source=#{source}"} - class={[ - "rounded-full px-3 py-1 text-xs font-medium transition", - if(@insight_source == source, - 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" - ) - ]} - > - {source} - -
- - <%= if @insights == [] do %> -
- <.icon - name="hero-light-bulb" - class="mx-auto h-10 w-10 text-gray-300 dark:text-gray-600" - /> -

No active insights

-
- <% else %> -
-
+ <%= for change <- @recent_config_changes do %> + <.link + navigate={~p"/devices/#{change.device_id}/config-timeline"} + class="flex items-center gap-3 px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-800/50 dark:hover:bg-white/5 transition-colors" > -
-
-
- - {insight.urgency} - - - {insight.source} - -
-

- <.link - navigate={~p"/insights?source=#{insight.source}"} - class="hover:text-blue-600 hover:underline dark:hover:text-blue-400" - > - {insight.title} - -

- <%= if insight.description do %> -

- {insight.description} -

- <% end %> -
- + +
+ + {if change.device, do: change.device.name, else: "Unknown"} + + + {change.change_size} lines · {Enum.join( + Enum.take(change.sections_changed, 2), + ", " + )} +
-
-
- <% end %> + + {Calendar.strftime(change.changed_at, "%b %d %H:%M")} + + + <% end %> +
-

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

+ Site Health

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

{site.name}

-
-
- - {site.device_count} devices + + {site.name} +
+
+ {site.device_count} devices <%= if site.devices_down > 0 do %> - - - {site.devices_down} down - + {site.devices_down} down <% end %> <%= if site.avg_qoe do %> QoE {format_qoe(site.avg_qoe)} <% end %> + <%= if site.subscribers do %> + {site.subscribers} subs + <% end %>
- <%= if site.subscribers do %> -
- - <.icon name="hero-users" class="h-3.5 w-3.5" /> - {site.subscribers} subscribers - - <%= if site.mrr do %> - {format_mrr(site.mrr)}/mo - <% end %> -
- <% 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 - +
+ {format_number(site.subscribers_affected)} affected <%= if mrr_at_risk_positive?(site.mrr_at_risk) do %> - - {format_mrr(site.mrr_at_risk)}/mo at risk - + · {format_mrr(site.mrr_at_risk)}/mo at risk <% end %>
<% end %> @@ -744,16 +638,51 @@
<% end %> + <%!-- Recent Activity (compact, only show meaningful entries) --%> + <%= if assigns[:recent_activity] && @recent_activity != [] do %> +
+
+

+ Recent Activity +

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

+ {activity.summary} +

+ + {Calendar.strftime(activity.timestamp, "%b %d %H:%M")} + +
+
+
+ <% 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"/devices/new"} class="btn btn-outline btn-sm btn-xs gap-1"> + <.icon name="hero-plus-circle" class="h-3.5 w-3.5" /> 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"/alerts"} class="btn btn-outline btn-sm btn-xs gap-1"> + <.icon name="hero-bell-alert" class="h-3.5 w-3.5" /> 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 + <.link navigate={~p"/trace"} class="btn btn-outline btn-sm btn-xs gap-1"> + <.icon name="hero-signal" class="h-3.5 w-3.5" /> Trace + + <.link navigate={~p"/network-map"} class="btn btn-outline btn-sm btn-xs gap-1"> + <.icon name="hero-globe-alt" class="h-3.5 w-3.5" /> Network Map
<% end %>