towerops/lib/towerops_web/live/dashboard_live.html.heex

726 lines
32 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="dashboard"
>
<%!-- 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>
<%= if assigns[:last_updated] do %>
<div class="flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
<span>
Updated {ToweropsWeb.TimeHelpers.format_datetime(@last_updated, @timezone, @time_format)}
</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-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-14 w-14 text-blue-500 dark:text-blue-400"
/>
<h3 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
{t("Let's get started!")}
</h3>
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400 max-w-md mx-auto">
{t("Set up your network monitoring in a few steps.")}
</p>
<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 text-sm dark:bg-blue-900 dark:text-blue-300">
1
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">{t("Create a Site")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("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-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">{t("Add Devices")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("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 text-sm dark:bg-blue-900 dark:text-blue-300">
1
</div>
<div>
<h4 class="font-medium text-gray-900 dark:text-white">{t("Add Devices")}</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("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-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">
{t("Monitor & Receive Alerts")}
</h4>
<p class="mt-0.5 text-sm text-gray-500 dark:text-gray-400">
{t("TowerOps automatically monitors your devices and alerts you of issues")}
</p>
</div>
</div>
</div>
</div>
<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
</.button>
<% else %>
<.button navigate={~p"/devices/new"} variant="primary">
<.icon name="hero-plus" class="h-5 w-5" /> Add Your First Device
</.button>
<% end %>
</div>
</div>
<% end %>
<%= if assigns[:summary] && @device_count > 0 do %>
<%!-- ═══════════════════════════════════════════════
STAT CARDS — compact row
═══════════════════════════════════════════════ --%>
<div class="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<%!-- Health Score --%>
<div class={[
"rounded-lg border px-4 py-3",
health_score_bg(@summary.health_score)
]}>
<div class="flex items-center justify-between">
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Health")}
</span>
<span class={[
"inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] font-semibold",
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 -> "Good"
@summary.health_score > 50 -> "Degraded"
true -> "Critical"
end}
</span>
</div>
<p class={[
"mt-1 text-3xl font-bold tracking-tight",
health_score_color(@summary.health_score)
]}>
{@summary.health_score}
</p>
</div>
<%!-- Uptime --%>
<div class={["rounded-lg border px-4 py-3", uptime_bg(@uptime_percentage)]}>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Uptime")}
</span>
<p class={["mt-1 text-3xl font-bold tracking-tight", uptime_color(@uptime_percentage)]}>
{if @uptime_percentage == 100.0,
do: "100",
else: :erlang.float_to_binary(@uptime_percentage, decimals: 1)}<span class="text-lg">%</span>
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{@device_up}/{@device_count} up
</p>
</div>
<%!-- Devices --%>
<.link
navigate={~p"/devices"}
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"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Devices")}
</span>
<p class="mt-1 text-3xl font-bold tracking-tight text-gray-900 dark:text-white">
{@device_count}
</p>
<div class="flex items-center gap-3 text-xs">
<span class="flex items-center gap-1 text-green-600 dark:text-green-400">
<span class="h-1.5 w-1.5 rounded-full bg-green-500"></span> {@device_up} up
</span>
<%= if @device_down > 0 do %>
<span class="flex items-center gap-1 text-red-600 dark:text-red-400">
<span class="h-1.5 w-1.5 rounded-full bg-red-500"></span> {@device_down} down
</span>
<% end %>
</div>
</.link>
<%!-- Alerts --%>
<.link
navigate={~p"/alerts"}
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"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Alerts")}
</span>
<p class={[
"mt-1 text-3xl font-bold tracking-tight",
if(@total_alert_count > 0,
do: "text-red-600 dark:text-red-400",
else: "text-green-600 dark:text-green-400"
)
]}>
{if @total_alert_count == 0, do: "✓", else: @total_alert_count}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{if @total_alert_count == 0, do: "All clear", else: "View details →"}
</p>
</.link>
</div>
<%!-- Secondary stats (only show if data exists) --%>
<%= if @has_subscribers || (@current_scope.organization.use_sites && @sites_count > 0) do %>
<div class="mt-3 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
<%= if @has_subscribers do %>
<div class="rounded-lg border border-gray-200 bg-white px-4 py-3 dark:border-white/10 dark:bg-gray-800/50">
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Subscribers")}
</span>
<p class="mt-1 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{format_number(@summary.subscribers.total)}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400">
{format_mrr(@summary.subscribers.total_mrr)}/mo
</p>
</div>
<div class={[
"rounded-lg border px-4 py-3",
if(mrr_at_risk_positive?(@impact_summary.mrr_at_risk),
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"
)
]}>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("MRR at Risk")}
</span>
<p class={[
"mt-1 text-2xl 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"
)
]}>
{format_mrr(@impact_summary.mrr_at_risk)}
</p>
<p
:if={@impact_summary.subscribers_affected > 0}
class="text-xs text-red-600 dark:text-red-400"
>
{format_number(@impact_summary.subscribers_affected)} affected
</p>
</div>
<% 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"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Sites")}
</span>
<p class="mt-1 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{@sites_count}
</p>
</.link>
<% end %>
<.link
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"
>
<span class="text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Insights")}
</span>
<p class="mt-1 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
{@summary.insights.critical + @summary.insights.warning + @summary.insights.info}
</p>
<div class="flex items-center gap-1.5">
<span
:if={@summary.insights.critical > 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
</span>
<span
:if={@summary.insights.warning > 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
</span>
</div>
</.link>
</div>
<% end %>
<%!-- ═══════════════════════════════════════════════
{t("ACTIVE INCIDENTS (only when there ARE incidents)")}
═══════════════════════════════════════════════ --%>
<%= if @active_incidents != [] do %>
<div class="mt-8">
<div class="flex items-center gap-2 mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-red-600 dark:text-red-400">
{t("Active Incidents")}
</h2>
<span class="inline-flex items-center rounded-full bg-red-100 px-2 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-2">
<div
:for={incident <- Enum.take(@active_incidents, 8)}
class={[
"flex items-center justify-between gap-4 rounded-lg border px-4 py-3",
incident_severity_classes(incident.mrr_at_risk)
]}
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2 flex-wrap">
<.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}
</.link>
<span :if={incident.site_name} class="text-xs text-gray-500 dark:text-gray-400">
at {incident.site_name}
</span>
</div>
</div>
<div class="flex items-center gap-2 flex-shrink-0">
<span class="text-xs font-medium text-red-700 dark:text-red-400">
{format_duration(incident.duration_seconds)}
</span>
<%= if incident.subscribers_affected > 0 do %>
<span class="text-xs text-amber-700 dark:text-amber-400">
{format_number(incident.subscribers_affected)} subs
</span>
<% end %>
<%= if mrr_at_risk_positive?(incident.mrr_at_risk) do %>
<span class="text-xs text-amber-700 dark:text-amber-400">
{format_mrr(incident.mrr_at_risk)}/mo
</span>
<% end %>
</div>
</div>
</div>
</div>
<% end %>
<%!-- ═══════════════════════════════════════════════
{t("MAIN CONTENT — full width stacked layout")}
═══════════════════════════════════════════════ --%>
<%!-- Active Alerts (only show section when there ARE alerts) --%>
<%= if @active_alerts != [] do %>
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Active Alerts")}
</h2>
<.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} →
</.link>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Device")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Message")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Impact")}
</th>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("When")}
</th>
<th class="px-4 py-2"></th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white dark:divide-white/5 dark:bg-gray-800/50">
<tr
:for={alert <- @active_alerts}
id={"alert-#{alert.id}"}
class="hover:bg-gray-50 dark:hover:bg-white/5"
>
<td class="px-4 py-2.5">
<.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}
</.link>
<p :if={alert.device.site} class="text-xs text-gray-500 dark:text-gray-400">
{alert.device.site.name}
</p>
</td>
<td class="px-4 py-2.5 text-sm text-gray-600 dark:text-gray-400 max-w-xs truncate">
{alert.message}
</td>
<td class="px-4 py-2.5">
<%= if alert.gaiia_impact && alert.gaiia_impact["total_subscribers"] && alert.gaiia_impact["total_subscribers"] > 0 do %>
<span class="text-xs text-amber-700 dark:text-amber-400">
{alert.gaiia_impact["total_subscribers"]} subs
</span>
<% end %>
</td>
<td class="px-4 py-2.5 whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
<.timestamp datetime={alert.triggered_at} timezone={@timezone} />
</td>
<td class="px-4 py-2.5 text-right">
<%= 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 px-2 py-1 text-xs font-medium text-gray-600 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 dark:text-gray-300 dark:ring-gray-600 dark:hover:bg-gray-700"
>
{t("Ack")}
</button>
<% else %>
<span class="text-xs text-blue-600 dark:text-blue-400">Ack'd</span>
<% end %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<% end %>
<%!-- Insights (compact grouped view) --%>
<%= if @insights != [] do %>
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<div class="flex items-center gap-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Insights")}
</h2>
<div class="flex items-center gap-1">
<.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"
)
]}
>
{t("All")}
</.link>
<.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>
</div>
</div>
<.link
navigate={~p"/insights"}
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
View all →
</.link>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
<div
:for={insight <- Enum.take(@insights, 4)}
id={"insight-#{insight.id}"}
class="flex items-start justify-between gap-3 px-4 py-3 bg-white dark:bg-gray-800/50 hover:bg-gray-50 dark:hover:bg-white/5"
>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5">
<span class={[
"inline-flex rounded-full px-1.5 py-0.5 text-[10px] font-medium",
urgency_classes(insight.urgency)
]}>
{insight.urgency}
</span>
<span class={[
"inline-flex rounded-full px-1.5 py-0.5 text-[10px] font-medium",
source_classes(insight.source)
]}>
{insight.source}
</span>
</div>
<p class="mt-1 text-sm font-medium text-gray-900 dark:text-white">
<.link
navigate={~p"/insights?source=#{insight.source}"}
class="hover:text-blue-600 dark:hover:text-blue-400"
>
{insight.title}
</.link>
</p>
<p
:if={insight.description}
class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-1"
>
{insight.description}
</p>
</div>
<button
type="button"
phx-click="dismiss_insight"
phx-value-id={insight.id}
class="flex-shrink-0 rounded p-1 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-3.5 w-3.5" />
</button>
</div>
</div>
</div>
<% end %>
<%!-- Recent Config Changes --%>
<%= if Enum.any?(@recent_config_changes) do %>
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
Config Changes <span class="font-normal text-gray-400 dark:text-gray-500">(7d)</span>
</h2>
<.link
navigate={~p"/insights"}
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
View all →
</.link>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10 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 px-4 py-2.5 bg-white hover:bg-gray-50 dark:bg-gray-800/50 dark:hover:bg-white/5 transition-colors"
>
<span class={"w-2 h-2 rounded-full flex-shrink-0 #{config_change_impact_color(change)}"} />
<div class="min-w-0 flex-1">
<span class="text-sm font-medium text-gray-900 dark:text-white">
{if change.device, do: change.device.name, else: "Unknown"}
</span>
<span class="ml-2 text-xs text-gray-500 dark:text-gray-400">
{change.change_size} lines · {Enum.join(
Enum.take(change.sections_changed, 2),
", "
)}
</span>
</div>
<span class="flex-shrink-0 text-xs text-gray-400 dark:text-gray-500">
{format_short_datetime(change.changed_at, @timezone)}
</span>
</.link>
<% end %>
</div>
</div>
<% end %>
<%!-- 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-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Site Health")}
</h2>
<.link
navigate={~p"/sites"}
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
View all →
</.link>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Site")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Devices")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Down")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("QoE")}
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Subscribers")}
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white dark:divide-white/5 dark:bg-gray-800/50">
<tr
:for={site <- @site_impact_summaries}
class="hover:bg-gray-50 dark:hover:bg-white/5"
>
<td class="px-4 py-2.5">
<.link
navigate={~p"/sites/#{site.site_id}"}
class="flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
>
<span class={[
"h-2 w-2 rounded-full flex-shrink-0",
site_health_dot(site.site_health)
]}>
</span>
{site.name}
</.link>
</td>
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
{site.device_count}
</td>
<td class="px-4 py-2.5 text-right text-sm">
<%= if site.devices_down > 0 do %>
<span class="text-red-600 dark:text-red-400">{site.devices_down}</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">0</span>
<% end %>
</td>
<td class="px-4 py-2.5 text-right text-sm">
<%= if site.avg_qoe do %>
<span class={
cond do
site.avg_qoe < 2.0 -> "text-red-600 dark:text-red-400"
site.avg_qoe < 4.0 -> "text-yellow-600 dark:text-yellow-400"
true -> "text-green-600 dark:text-green-400"
end
}>
{format_qoe(site.avg_qoe)}
</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">—</span>
<% end %>
</td>
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
<%= if site.subscribers do %>
{site.subscribers}
<%= if site.subscribers_affected > 0 do %>
<span class="text-red-600 dark:text-red-400">
({format_number(site.subscribers_affected)} affected)
</span>
<% end %>
<% else %>
<span class="text-gray-400 dark:text-gray-500">—</span>
<% end %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<% end %>
<%!-- Recent Activity (compact, only show meaningful entries) --%>
<%= if assigns[:recent_activity] && @recent_activity != [] do %>
<div class="mt-8">
<div class="flex items-center justify-between mb-3">
<h2 class="text-sm font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
{t("Recent Activity")}
</h2>
<.link
navigate={~p"/activity"}
class="text-xs font-medium text-blue-600 hover:text-blue-700 dark:text-blue-400"
>
View all →
</.link>
</div>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10 divide-y divide-gray-100 dark:divide-white/5">
<div
:for={activity <- @recent_activity}
class="flex items-center gap-3 px-4 py-2.5 bg-white dark:bg-gray-800/50"
>
<span class="h-1.5 w-1.5 rounded-full bg-blue-400 dark:bg-blue-500 flex-shrink-0">
</span>
<p class="text-sm text-gray-700 dark:text-gray-300 min-w-0 flex-1 truncate">
{activity.summary}
</p>
<span class="flex-shrink-0 text-xs text-gray-400 dark:text-gray-500">
{format_short_datetime(activity.timestamp, @timezone)}
</span>
</div>
</div>
</div>
<% end %>
<%!-- Quick Actions --%>
<div class="mt-8 flex flex-wrap items-center gap-2 border-t border-gray-200 pt-6 dark:border-white/10">
<.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>
<.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>
<.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>
<.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
</.link>
</div>
<% end %>
</Layouts.authenticated>