Dashboard makeover: more breathing room, cleaner layout
- Stat cards: 4-column grid with larger text (text-4xl) and rounded-xl - Split stats into primary (health/uptime/devices/alerts) and secondary rows - Main content: 2/3 + 1/3 split instead of cramped 3/5 + 2/5 - Increased gaps throughout (gap-5, gap-10, mt-10 between sections) - Cards use rounded-xl, p-5 for more padding - Config changes + activity in proper list layout with dividers - Quick actions moved to bottom with separator - Filter pills use rounded-full with stronger active state - Removed 'Command Center' header, cleaner org name + subtitle - Better empty states with more whitespace
This commit is contained in:
parent
7b72ed3164
commit
a91e5f9be5
1 changed files with 353 additions and 308 deletions
|
|
@ -3,98 +3,101 @@
|
|||
current_scope={@current_scope}
|
||||
active_page="dashboard"
|
||||
>
|
||||
<.header>
|
||||
Command Center
|
||||
<: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>
|
||||
<%!-- Header --%>
|
||||
<div class="flex items-end justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">
|
||||
{@current_scope.organization.name}
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">Network overview</p>
|
||||
</div>
|
||||
<% end %>
|
||||
<%= if assigns[:last_updated] do %>
|
||||
<div class="flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
|
||||
<span>
|
||||
Updated {Calendar.strftime(@last_updated, "%H:%M:%S UTC")}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="refresh_dashboard"
|
||||
class="inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-gray-500 hover:bg-gray-100 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/10 dark:hover:text-gray-200"
|
||||
>
|
||||
<.icon name="hero-arrow-path" class="h-3.5 w-3.5" /> Refresh
|
||||
</button>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%= if !assigns[:summary] do %>
|
||||
<.skeleton_dashboard />
|
||||
<% end %>
|
||||
|
||||
<%!-- Empty State --%>
|
||||
<%= if assigns[:summary] && @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">
|
||||
<div class="mt-12 rounded-xl border-2 border-dashed border-gray-300 bg-gray-50/50 p-16 text-center dark:border-white/20 dark:bg-gray-800/30">
|
||||
<.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"
|
||||
/>
|
||||
<h3 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
|
||||
Let's get started!
|
||||
</h3>
|
||||
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
||||
Follow these steps to set up your network monitoring:
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto">
|
||||
Set up your network monitoring in a few steps.
|
||||
</p>
|
||||
<div class="mt-8 max-w-2xl mx-auto">
|
||||
<div class="space-y-4 text-left">
|
||||
<div class="mt-10 max-w-lg mx-auto">
|
||||
<div class="space-y-5 text-left">
|
||||
<%= if @current_scope.organization.use_sites do %>
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-600 font-semibold dark:bg-blue-900 dark:text-blue-300">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm dark:bg-blue-900 dark:text-blue-300">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">Create a Site</h4>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Sites represent your physical locations (offices, data centers, towers, etc.)
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
Sites represent your physical locations
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 text-gray-600 font-semibold dark:bg-gray-700 dark:text-gray-400">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-500 font-semibold text-sm dark:bg-gray-700 dark:text-gray-400">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">Add Devices</h4>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Add network devices (routers, switches, servers) to your sites
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
Add routers, switches, and servers to your sites
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-600 font-semibold dark:bg-blue-900 dark:text-blue-300">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 text-blue-600 font-semibold text-sm dark:bg-blue-900 dark:text-blue-300">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">Add Devices</h4>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Add network devices (routers, switches, servers) to monitor
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
Add network devices to monitor
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="flex gap-4 items-start">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-200 text-gray-600 font-semibold dark:bg-gray-700 dark:text-gray-400">
|
||||
<div class="flex-shrink-0 flex items-center justify-center w-8 h-8 rounded-full bg-gray-100 text-gray-500 font-semibold text-sm dark:bg-gray-700 dark:text-gray-400">
|
||||
{if @current_scope.organization.use_sites, do: "3", else: "2"}
|
||||
</div>
|
||||
<div>
|
||||
<h4 class="font-medium text-gray-900 dark:text-white">
|
||||
Monitor & Receive Alerts
|
||||
</h4>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
||||
Towerops will automatically monitor your devices and alert you of issues
|
||||
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
|
||||
TowerOps automatically monitors your devices and alerts you of issues
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8">
|
||||
<div class="mt-10">
|
||||
<%= 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 --%>
|
||||
<div class="mt-8 grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-6">
|
||||
<%!-- ═══════════════════════════════════════════════
|
||||
STAT CARDS — max 4 per row, generous sizing
|
||||
═══════════════════════════════════════════════ --%>
|
||||
<div class="mt-8 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<%!-- Health Score --%>
|
||||
<div class={[
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
"rounded-xl border p-5",
|
||||
health_score_bg(@summary.health_score)
|
||||
]}>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Health Score</h3>
|
||||
<p class={["mt-1 text-3xl font-bold", health_score_color(@summary.health_score)]}>
|
||||
<div class="flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Health Score</h3>
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
cond do
|
||||
@summary.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}
|
||||
</span>
|
||||
</div>
|
||||
<p class={[
|
||||
"mt-3 text-4xl font-bold tracking-tight",
|
||||
health_score_color(@summary.health_score)
|
||||
]}>
|
||||
{@summary.health_score}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- Network Uptime --%>
|
||||
<div class={[
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
"rounded-xl border p-5",
|
||||
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)]}>
|
||||
<p class={["mt-3 text-4xl font-bold tracking-tight", uptime_color(@uptime_percentage)]}>
|
||||
{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)}<span class="text-2xl">%</span>
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
{@device_up}/{@device_count} devices up
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
{@device_up} of {@device_count} devices up
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- 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"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Alerts</h3>
|
||||
<p class={[
|
||||
"mt-1 text-3xl font-bold",
|
||||
(@total_alert_count > 0 && "text-red-600 dark:text-red-500") ||
|
||||
"text-gray-900 dark:text-white"
|
||||
]}>
|
||||
{@total_alert_count}
|
||||
</p>
|
||||
</.link>
|
||||
|
||||
<%!-- 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"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Devices</h3>
|
||||
<p class="mt-1 text-3xl font-bold text-gray-900 dark:text-white">{@device_count}</p>
|
||||
<div class="mt-2 flex items-center gap-3 text-xs">
|
||||
<span class="flex items-center gap-1 text-green-600 dark:text-green-400">
|
||||
<p class="mt-3 text-4xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{@device_count}
|
||||
</p>
|
||||
<div class="mt-3 flex items-center gap-4 text-sm">
|
||||
<span class="flex items-center gap-1.5 text-green-600 dark:text-green-400">
|
||||
<span class="h-2 w-2 rounded-full bg-green-500"></span>
|
||||
{@device_up} Up
|
||||
{@device_up} up
|
||||
</span>
|
||||
<span class="flex items-center gap-1 text-red-600 dark:text-red-400">
|
||||
<span class="flex items-center gap-1.5 text-red-600 dark:text-red-400">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{@device_down} Down
|
||||
</span>
|
||||
<span class="flex items-center gap-1 text-gray-500 dark:text-gray-400">
|
||||
<span class="h-2 w-2 rounded-full bg-gray-400"></span>
|
||||
{@device_unknown} Unknown
|
||||
{@device_down} down
|
||||
</span>
|
||||
<%= if @device_unknown > 0 do %>
|
||||
<span class="flex items-center gap-1.5 text-gray-400">
|
||||
<span class="h-2 w-2 rounded-full bg-gray-400"></span>
|
||||
{@device_unknown}
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</.link>
|
||||
|
||||
<%!-- 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"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Active Alerts</h3>
|
||||
<p class={[
|
||||
"mt-3 text-4xl font-bold tracking-tight",
|
||||
if(@total_alert_count > 0,
|
||||
do: "text-red-600 dark:text-red-400",
|
||||
else: "text-gray-900 dark:text-white"
|
||||
)
|
||||
]}>
|
||||
{@total_alert_count}
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<%= if @total_alert_count == 0 do %>
|
||||
All clear
|
||||
<% else %>
|
||||
View details →
|
||||
<% end %>
|
||||
</p>
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<%!-- Secondary stats row (subscribers, MRR, sites, insights) --%>
|
||||
<div class="mt-5 grid gap-5 sm:grid-cols-2 lg:grid-cols-4">
|
||||
<%= if @has_subscribers do %>
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-5 dark:border-white/10 dark:bg-gray-800/50">
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Subscribers</h3>
|
||||
<p class="mt-1 text-3xl font-bold text-gray-900 dark:text-white">
|
||||
<p class="mt-3 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{format_number(@summary.subscribers.total)}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
|
|
@ -188,17 +232,16 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<%!-- MRR at Risk --%>
|
||||
<div class={[
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
"rounded-xl border p-5",
|
||||
if(mrr_at_risk_positive?(@impact_summary.mrr_at_risk),
|
||||
do: "border-red-300 bg-red-50 dark:border-red-800 dark:bg-red-950/30",
|
||||
do: "border-red-200 bg-red-50 dark:border-red-800/50 dark:bg-red-950/30",
|
||||
else: "border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50"
|
||||
)
|
||||
]}>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">MRR at Risk</h3>
|
||||
<p class={[
|
||||
"mt-1 text-3xl font-bold",
|
||||
"mt-3 text-3xl font-bold tracking-tight",
|
||||
if(mrr_at_risk_positive?(@impact_summary.mrr_at_risk),
|
||||
do: "text-red-600 dark:text-red-400",
|
||||
else: "text-gray-900 dark:text-white"
|
||||
|
|
@ -215,81 +258,68 @@
|
|||
</div>
|
||||
<% 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"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Sites</h3>
|
||||
<p class="mt-1 text-3xl font-bold text-gray-900 dark:text-white">{@sites_count}</p>
|
||||
<p class="mt-3 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{@sites_count}
|
||||
</p>
|
||||
</.link>
|
||||
<% end %>
|
||||
|
||||
<%!-- Insights --%>
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-800/50">
|
||||
<.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"
|
||||
>
|
||||
<h3 class="text-sm font-medium text-gray-500 dark:text-gray-400">Insights</h3>
|
||||
<div class="mt-1 flex items-baseline gap-2">
|
||||
<p class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
{@summary.insights.critical + @summary.insights.warning + @summary.insights.info}
|
||||
</p>
|
||||
<p class="mt-3 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
{@summary.insights.critical + @summary.insights.warning + @summary.insights.info}
|
||||
</p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span
|
||||
:if={@summary.insights.critical > 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
|
||||
</span>
|
||||
<span
|
||||
:if={@summary.insights.warning > 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
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-2 text-xs">
|
||||
<%= if @summary.insights.critical > 0 do %>
|
||||
<span class="inline-flex items-center rounded-full bg-red-100 px-1.5 py-0.5 text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||
{@summary.insights.critical} critical
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if @summary.insights.warning > 0 do %>
|
||||
<span class="inline-flex items-center rounded-full bg-yellow-100 px-1.5 py-0.5 text-yellow-800 dark:bg-yellow-900/30 dark:text-yellow-400">
|
||||
{@summary.insights.warning} warning
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if @summary.insights.info > 0 do %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-1.5 py-0.5 text-blue-800 dark:bg-blue-900/30 dark:text-blue-400">
|
||||
{@summary.insights.info} info
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
</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"/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"/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) --%>
|
||||
<%!-- ═══════════════════════════════════════════════
|
||||
ACTIVE INCIDENTS
|
||||
═══════════════════════════════════════════════ --%>
|
||||
<%= if @active_incidents != [] do %>
|
||||
<div class="mt-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="mt-10">
|
||||
<div class="flex items-center gap-3 mb-5">
|
||||
<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-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>
|
||||
<span class="inline-flex items-center rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/30 dark:text-red-400">
|
||||
{length(@active_incidents)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
:for={incident <- Enum.take(@active_incidents, 10)}
|
||||
class={[
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
"rounded-xl border p-5",
|
||||
incident_severity_classes(incident.mrr_at_risk)
|
||||
]}
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">
|
||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">
|
||||
<.link
|
||||
navigate={~p"/devices/#{incident.device_id}"}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
|
|
@ -307,25 +337,25 @@
|
|||
</.link>
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3 flex-wrap">
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||
<.icon name="hero-clock" class="h-3 w-3" />
|
||||
<div class="mt-3 flex items-center gap-3 flex-wrap">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-lg bg-red-100 px-2.5 py-1 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
||||
<.icon name="hero-clock" class="h-3.5 w-3.5" />
|
||||
Down {format_duration(incident.duration_seconds)}
|
||||
</span>
|
||||
<%= if incident.subscribers_affected > 0 do %>
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/20">
|
||||
<.icon name="hero-users" class="h-3 w-3" />
|
||||
<span class="inline-flex items-center gap-1.5 rounded-lg bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
<.icon name="hero-users" class="h-3.5 w-3.5" />
|
||||
{format_number(incident.subscribers_affected)} affected
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if mrr_at_risk_positive?(incident.mrr_at_risk) do %>
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/20">
|
||||
<.icon name="hero-currency-dollar" class="h-3 w-3" />
|
||||
<span class="inline-flex items-center gap-1.5 rounded-lg bg-amber-100 px-2.5 py-1 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
<.icon name="hero-currency-dollar" class="h-3.5 w-3.5" />
|
||||
{format_mrr(incident.mrr_at_risk)}/mo at risk
|
||||
</span>
|
||||
<% end %>
|
||||
<%= if incident.qoe_score do %>
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-purple-50 px-2 py-0.5 text-xs font-medium text-purple-800 ring-1 ring-inset ring-purple-600/20 dark:bg-purple-400/10 dark:text-purple-400 dark:ring-purple-400/20">
|
||||
<span class="inline-flex items-center gap-1.5 rounded-lg bg-purple-100 px-2.5 py-1 text-xs font-medium text-purple-800 dark:bg-purple-900/30 dark:text-purple-400">
|
||||
QoE {format_qoe(incident.qoe_score)}
|
||||
</span>
|
||||
<% end %>
|
||||
|
|
@ -337,147 +367,149 @@
|
|||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Section B+C: Two-column layout --%>
|
||||
<div class="mt-8 grid gap-8 lg:grid-cols-5">
|
||||
<%!-- 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 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"
|
||||
>
|
||||
View All →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<%= if @active_alerts == [] do %>
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-8 text-center dark:border-white/10 dark:bg-gray-800/50">
|
||||
<.icon name="hero-check-circle" class="mx-auto h-10 w-10 text-green-500" />
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">No active alerts</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
:for={alert <- @active_alerts}
|
||||
id={"alert-#{alert.id}"}
|
||||
class={[
|
||||
"rounded-lg border p-4 shadow-sm",
|
||||
"border-l-4 border-red-500 bg-red-50 dark:bg-red-950/30 dark:border-red-700"
|
||||
]}
|
||||
<%!-- ═══════════════════════════════════════════════
|
||||
MAIN CONTENT — 2 column (60/40 split)
|
||||
═══════════════════════════════════════════════ --%>
|
||||
<div class="mt-10 grid gap-10 lg:grid-cols-3">
|
||||
<%!-- LEFT COLUMN: Alerts + Activity --%>
|
||||
<div class="lg:col-span-2 space-y-10">
|
||||
<%!-- Active Alerts --%>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<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-400 dark:hover:text-blue-300"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">
|
||||
<.icon name="hero-exclamation-triangle" class="h-3.5 w-3.5" /> Device Down
|
||||
</span>
|
||||
<%= if alert.acknowledged_at do %>
|
||||
<span class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-800 dark:bg-blue-900 dark:text-blue-200">
|
||||
View all →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<%= if @active_alerts == [] do %>
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-10 text-center dark:border-white/10 dark:bg-gray-800/50">
|
||||
<.icon name="hero-check-circle" class="mx-auto h-10 w-10 text-green-500" />
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">
|
||||
No active alerts — all systems operational
|
||||
</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
:for={alert <- @active_alerts}
|
||||
id={"alert-#{alert.id}"}
|
||||
class="rounded-xl border-l-4 border-red-500 bg-white p-5 shadow-sm ring-1 ring-gray-200 dark:bg-gray-800/50 dark:ring-white/10"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-2.5 flex-wrap">
|
||||
<span class="inline-flex items-center gap-1 rounded-full bg-red-100 px-2.5 py-0.5 text-xs font-semibold text-red-700 dark:bg-red-900/40 dark:text-red-300">
|
||||
<.icon name="hero-exclamation-triangle" class="h-3.5 w-3.5" /> Down
|
||||
</span>
|
||||
<span
|
||||
:if={alert.acknowledged_at}
|
||||
class="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700 dark:bg-blue-900/40 dark:text-blue-300"
|
||||
>
|
||||
Acknowledged
|
||||
</span>
|
||||
</div>
|
||||
<h3 class="mt-2 text-base font-semibold text-gray-900 dark:text-white">
|
||||
<.link
|
||||
navigate={~p"/devices/#{alert.device.id}"}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
>
|
||||
{alert.device.name}
|
||||
</.link>
|
||||
</h3>
|
||||
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">{alert.message}</p>
|
||||
|
||||
<%!-- Gaiia impact --%>
|
||||
<%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %>
|
||||
<div class="mt-3 flex items-center gap-2.5">
|
||||
<span class="inline-flex items-center gap-1 rounded-lg bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
<.icon name="hero-users" class="h-3 w-3" />
|
||||
{alert.gaiia_impact["total_subscribers"]} affected
|
||||
</span>
|
||||
<%= if alert.gaiia_impact["total_mrr"] && alert.gaiia_impact["total_mrr"] != "0" do %>
|
||||
<span class="inline-flex items-center gap-1 rounded-lg bg-amber-100 px-2 py-0.5 text-xs font-medium text-amber-800 dark:bg-amber-900/30 dark:text-amber-400">
|
||||
<.icon name="hero-currency-dollar" class="h-3 w-3" />
|
||||
${alert.gaiia_impact["total_mrr"]}/mo
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-3 text-xs text-gray-400 dark:text-gray-500">
|
||||
<.timestamp datetime={alert.triggered_at} />
|
||||
<span :if={alert.device.site} class="ml-2">
|
||||
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}
|
||||
</.link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
<%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="acknowledge_alert"
|
||||
phx-value-id={alert.id}
|
||||
class="rounded-lg bg-white px-3 py-1.5 text-xs font-medium text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-700 dark:text-gray-200 dark:ring-gray-600 dark:hover:bg-gray-600"
|
||||
>
|
||||
Acknowledge
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
<h3 class="mt-1.5 font-semibold text-gray-900 dark:text-white">
|
||||
<.link
|
||||
navigate={~p"/devices/#{alert.device.id}"}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
>
|
||||
{alert.device.name}
|
||||
</.link>
|
||||
</h3>
|
||||
<p class="mt-0.5 text-sm text-gray-700 dark:text-gray-300">{alert.message}</p>
|
||||
|
||||
<%!-- Gaiia impact badges --%>
|
||||
<%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/20">
|
||||
<.icon name="hero-users" class="h-3 w-3" />
|
||||
{alert.gaiia_impact["total_subscribers"]} affected
|
||||
</span>
|
||||
<%= if alert.gaiia_impact["total_mrr"] && alert.gaiia_impact["total_mrr"] != "0" do %>
|
||||
<span class="inline-flex items-center gap-1 rounded-md bg-amber-50 px-2 py-0.5 text-xs font-medium text-amber-800 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/20">
|
||||
<.icon name="hero-currency-dollar" class="h-3 w-3" />
|
||||
${alert.gaiia_impact["total_mrr"]}/mo
|
||||
</span>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<.timestamp datetime={alert.triggered_at} />
|
||||
<span :if={alert.device.site} class="ml-2">
|
||||
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}
|
||||
</.link>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-shrink-0">
|
||||
<%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
|
||||
<button
|
||||
type="button"
|
||||
phx-click="acknowledge_alert"
|
||||
phx-value-id={alert.id}
|
||||
class="rounded-md bg-white px-2.5 py-1.5 text-xs font-medium text-gray-700 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-700"
|
||||
>
|
||||
Acknowledge
|
||||
</button>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%= if @total_alert_count > 20 do %>
|
||||
<div class="mt-3 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
+ {@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
|
||||
</.link>
|
||||
</div>
|
||||
<%= if @total_alert_count > 20 do %>
|
||||
<div class="mt-4 text-center">
|
||||
<.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 →
|
||||
</.link>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Recent Config Changes --%>
|
||||
<%= 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">
|
||||
<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 --%>
|
||||
<%= if Enum.any?(@recent_config_changes) do %>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<.icon name="hero-document-text" class="h-5 w-5 text-orange-500" />
|
||||
Recent Config Changes
|
||||
</h3>
|
||||
</h2>
|
||||
<.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 →
|
||||
</.link>
|
||||
</div>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-2">
|
||||
<div class="rounded-xl border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 divide-y divide-gray-100 dark:divide-white/5">
|
||||
<%= 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"
|
||||
>
|
||||
<span class={"w-2.5 h-2.5 rounded-full flex-shrink-0 #{config_change_impact_color(change)}"} />
|
||||
<div class="min-w-0">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
||||
{if change.device, do: change.device.name, else: "Unknown Device"}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400">
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
{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 %>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% 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>
|
||||
<%!-- Recent Activity --%>
|
||||
<%= if assigns[:recent_activity] && @recent_activity != [] do %>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<h2 class="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
|
||||
<.icon name="hero-clock" class="h-5 w-5 text-blue-500" /> Recent Activity
|
||||
</h2>
|
||||
<.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 →
|
||||
</.link>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="rounded-xl border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 divide-y divide-gray-100 dark:divide-white/5">
|
||||
<div
|
||||
:for={activity <- @recent_activity}
|
||||
class="flex items-start gap-3 text-sm"
|
||||
class="flex items-start gap-4 px-5 py-3.5"
|
||||
>
|
||||
<div class="flex-shrink-0 mt-1">
|
||||
<div class="flex-shrink-0 mt-1.5">
|
||||
<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">
|
||||
<p class="text-sm text-gray-900 dark:text-white">
|
||||
{activity.summary}
|
||||
</p>
|
||||
<p class="text-xs text-gray-500 dark:text-gray-400 mt-0.5">
|
||||
<p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
|
||||
{Calendar.strftime(activity.timestamp, "%b %d %H:%M")}
|
||||
<%= if Map.get(activity, :actor_name) do %>
|
||||
<span class="ml-1">by {activity.actor_name}</span>
|
||||
|
|
@ -528,31 +558,31 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<%!-- Right: Insights Feed (2 cols) --%>
|
||||
<div class="lg:col-span-2">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<%!-- RIGHT COLUMN: Insights --%>
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<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"
|
||||
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 →
|
||||
</.link>
|
||||
</div>
|
||||
|
||||
<%!-- Source filter pills --%>
|
||||
<div class="mb-4 flex flex-wrap items-center gap-2">
|
||||
<div class="mb-5 flex flex-wrap items-center gap-1.5">
|
||||
<.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 @@
|
|||
</div>
|
||||
|
||||
<%= if @insights == [] do %>
|
||||
<div class="rounded-lg border border-gray-200 bg-white p-8 text-center dark:border-white/10 dark:bg-gray-800/50">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-10 text-center dark:border-white/10 dark:bg-gray-800/50">
|
||||
<.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"
|
||||
/>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">No active insights</p>
|
||||
<p class="mt-3 text-sm text-gray-500 dark:text-gray-400">No active insights</p>
|
||||
</div>
|
||||
<% else %>
|
||||
<div class="space-y-3">
|
||||
<div
|
||||
:for={insight <- @insights}
|
||||
id={"insight-#{insight.id}"}
|
||||
class="rounded-lg border border-gray-200 bg-white p-3 dark:border-white/10 dark:bg-gray-800/50"
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-gray-800/50"
|
||||
>
|
||||
<div class="flex items-start justify-between gap-2">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5 flex-wrap">
|
||||
<div class="flex items-center gap-2 flex-wrap">
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium",
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
urgency_classes(insight.urgency)
|
||||
]}>
|
||||
{insight.urgency}
|
||||
</span>
|
||||
<span class={[
|
||||
"inline-flex items-center rounded-full px-1.5 py-0.5 text-xs font-medium",
|
||||
"inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
|
||||
source_classes(insight.source)
|
||||
]}>
|
||||
{insight.source}
|
||||
</span>
|
||||
</div>
|
||||
<h4 class="mt-1 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<h4 class="mt-2 text-sm font-semibold text-gray-900 dark:text-white">
|
||||
<.link
|
||||
navigate={~p"/insights?source=#{insight.source}"}
|
||||
class="hover:text-blue-600 hover:underline dark:hover:text-blue-400"
|
||||
|
|
@ -614,7 +644,7 @@
|
|||
</.link>
|
||||
</h4>
|
||||
<%= if insight.description do %>
|
||||
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
||||
{insight.description}
|
||||
</p>
|
||||
<% 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 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<%!-- Section D: Site Health Grid --%>
|
||||
<%!-- ═══════════════════════════════════════════════
|
||||
SITE HEALTH GRID
|
||||
═══════════════════════════════════════════════ --%>
|
||||
<%= if @current_scope.organization.use_sites && @site_impact_summaries != [] do %>
|
||||
<div class="mt-8">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<div class="mt-10">
|
||||
<div class="flex items-center justify-between mb-5">
|
||||
<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"
|
||||
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>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<div class="grid gap-5 sm:grid-cols-2 lg:grid-cols-3">
|
||||
<.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)
|
||||
]}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<span class={[
|
||||
"h-2.5 w-2.5 rounded-full flex-shrink-0",
|
||||
site_health_dot(site.site_health)
|
||||
|
|
@ -667,12 +699,12 @@
|
|||
</span>
|
||||
<h3 class="font-semibold text-gray-900 dark:text-white">{site.name}</h3>
|
||||
</div>
|
||||
<div class="mt-2 flex items-center gap-3 text-sm">
|
||||
<span class="text-gray-600 dark:text-gray-400">
|
||||
<div class="mt-3 flex items-center gap-4 text-sm">
|
||||
<span class="text-gray-500 dark:text-gray-400">
|
||||
{site.device_count} devices
|
||||
</span>
|
||||
<%= if site.devices_down > 0 do %>
|
||||
<span class="flex items-center gap-1 text-red-600 dark:text-red-400">
|
||||
<span class="flex items-center gap-1.5 text-red-600 dark:text-red-400">
|
||||
<span class="h-2 w-2 rounded-full bg-red-500"></span>
|
||||
{site.devices_down} down
|
||||
</span>
|
||||
|
|
@ -684,7 +716,7 @@
|
|||
<% end %>
|
||||
</div>
|
||||
<%= if site.subscribers do %>
|
||||
<div class="mt-2 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
|
||||
<div class="mt-2 flex items-center gap-4 text-xs text-gray-400 dark:text-gray-500">
|
||||
<span class="flex items-center gap-1">
|
||||
<.icon name="hero-users" class="h-3.5 w-3.5" />
|
||||
{site.subscribers} subscribers
|
||||
|
|
@ -695,7 +727,7 @@
|
|||
</div>
|
||||
<% end %>
|
||||
<%= if site.subscribers_affected > 0 do %>
|
||||
<div class="mt-2 flex items-center gap-3 text-xs">
|
||||
<div class="mt-2 flex items-center gap-4 text-xs">
|
||||
<span class="flex items-center gap-1 text-red-600 dark:text-red-400">
|
||||
<.icon name="hero-exclamation-triangle" class="h-3.5 w-3.5" />
|
||||
{format_number(site.subscribers_affected)} affected
|
||||
|
|
@ -711,5 +743,18 @@
|
|||
</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<%!-- Quick Actions --%>
|
||||
<div class="mt-10 flex flex-wrap items-center gap-3 border-t border-gray-200 pt-8 dark:border-white/10">
|
||||
<.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"/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"/trace"} class="btn btn-outline btn-sm gap-1.5">
|
||||
<.icon name="hero-signal" class="h-4 w-4" /> Subscriber Trace
|
||||
</.link>
|
||||
</div>
|
||||
<% end %>
|
||||
</Layouts.authenticated>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue