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

312 lines
15 KiB
Text

<Layouts.authenticated
flash={@flash}
current_scope={@current_scope}
active_page="rf-links"
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
>
<.breadcrumb items={[
%{label: "Dashboard", navigate: ~p"/dashboard"},
%{label: "RF Links"}
]} />
<div class="mb-6">
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("RF Link Health")}</h1>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
{t("Wireless link quality triage — weakest links first")}
</p>
</div>
<%!-- Summary Stat Cards --%>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
<button
phx-click="filter"
phx-value-filter="all"
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-blue-300 dark:hover:border-blue-700 transition-colors #{stat_card_class(@filter, :all)}"}
>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Total Links")}
</div>
<div class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{@summary.total}
</div>
</button>
<button
phx-click="filter"
phx-value-filter="healthy"
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-green-300 dark:hover:border-green-700 transition-colors #{stat_card_class(@filter, :healthy)}"}
>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Healthy")}
</div>
<div class="mt-1 text-2xl font-bold text-green-600 dark:text-green-400">
{@summary.healthy}
</div>
</button>
<button
phx-click="filter"
phx-value-filter="degraded"
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-yellow-300 dark:hover:border-yellow-700 transition-colors #{stat_card_class(@filter, :degraded)}"}
>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Degraded")}
</div>
<div class="mt-1 text-2xl font-bold text-yellow-600 dark:text-yellow-400">
{@summary.degraded}
</div>
</button>
<button
phx-click="filter"
phx-value-filter="critical"
class={"bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4 text-left cursor-pointer hover:border-red-300 dark:hover:border-red-700 transition-colors #{stat_card_class(@filter, :critical)}"}
>
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
{t("Critical")}
</div>
<div class="mt-1 text-2xl font-bold text-red-600 dark:text-red-400">
{@summary.critical}
</div>
</button>
</div>
<%!-- Weakest Links Table --%>
<%= if @links != [] do %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
<h2 class="text-sm font-semibold text-gray-900 dark:text-white">
{t("RF Links")}
<span class="text-gray-400 font-normal ml-1">
({length(@links)})
</span>
</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/75">
<tr class="text-xs text-gray-600 dark:text-gray-400">
<th class="px-4 py-2 text-left font-medium">{t("Client")}</th>
<th class="px-4 py-2 text-left font-medium">{t("AP / Device")}</th>
<th class="px-4 py-2 text-left font-medium">{t("Signal")}</th>
<th class="px-4 py-2 text-left font-medium">{t("SNR")}</th>
<th class="px-4 py-2 text-left font-medium">{t("TX / RX")}</th>
<th class="px-4 py-2 text-left font-medium">{t("Health")}</th>
<th class="px-4 py-2 text-left font-medium">{t("Last Seen")}</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
<%= for link <- @links do %>
<%!-- Main row --%>
<tr
phx-click="toggle_expand"
phx-value-id={link.id}
class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
>
<td class="px-4 py-3">
<div class="font-medium text-gray-900 dark:text-white">
{link_name(link)}
</div>
<div class="text-xs text-gray-500 dark:text-gray-400">
{link.mac_address}
</div>
</td>
<td class="px-4 py-3">
<%= if link.device do %>
<.link
navigate={~p"/devices/#{link.device_id}"}
class="text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
>
{device_name(link)}
</.link>
<% else %>
<span class="text-gray-400">-</span>
<% end %>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-16 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full #{signal_bar_color(link.signal_strength)}"}
style={"width: #{signal_bar_width(link.signal_strength)}%"}
>
</div>
</div>
<span class={"text-xs font-medium #{health_color(link.health)}"}>
{format_signal(link.signal_strength)}
</span>
</div>
</td>
<td class="px-4 py-3">
<div class="flex items-center gap-2">
<div class="w-12 h-2 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full #{snr_bar_color(link.snr)}"}
style={"width: #{snr_bar_width(link.snr)}%"}
>
</div>
</div>
<span class="text-xs text-gray-600 dark:text-gray-400">
{format_snr(link.snr)}
</span>
</div>
</td>
<td class="px-4 py-3 text-xs text-gray-600 dark:text-gray-400">
<span>{format_rate(link.tx_rate)}</span>
<span class="text-gray-300 dark:text-gray-600 mx-1">/</span>
<span>{format_rate(link.rx_rate)}</span>
</td>
<td class="px-4 py-3">
<span class={"inline-flex items-center px-2 py-0.5 rounded text-xs font-medium #{health_bg(link.health)}"}>
{health_label(link.health)}
</span>
</td>
<td class="px-4 py-3 text-xs text-gray-500 dark:text-gray-400">
<%= if link.last_seen_at do %>
<time datetime={DateTime.to_iso8601(link.last_seen_at)}>
{Calendar.strftime(link.last_seen_at, "%H:%M")}
</time>
<% else %>
-
<% end %>
</td>
</tr>
<%!-- Expandable detail row --%>
<%= if @expanded_id == link.id do %>
<tr class="bg-gray-50/50 dark:bg-gray-800/30">
<td colspan="7" class="px-4 py-4">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<%!-- Signal Strength Detail --%>
<div>
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Signal Strength")}
</h4>
<div class="flex items-center gap-3">
<div class="flex-1 h-3 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full transition-all #{signal_bar_color(link.signal_strength)}"}
style={"width: #{signal_bar_width(link.signal_strength)}%"}
>
</div>
</div>
<span class={"text-sm font-bold #{health_color(link.health)}"}>
{format_signal(link.signal_strength)}
</span>
</div>
<div class="mt-2 flex justify-between text-[10px] text-gray-400">
<span>-100 dBm</span>
<span class="text-yellow-500">-75</span>
<span class="text-green-500">-65</span>
<span>-30 dBm</span>
</div>
</div>
<%!-- SNR Detail --%>
<div>
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Signal-to-Noise Ratio")}
</h4>
<div class="flex items-center gap-3">
<div class="flex-1 h-3 rounded-full bg-gray-200 dark:bg-gray-700 overflow-hidden">
<div
class={"h-full rounded-full transition-all #{snr_bar_color(link.snr)}"}
style={"width: #{snr_bar_width(link.snr)}%"}
>
</div>
</div>
<span class="text-sm font-bold text-gray-900 dark:text-white">
{format_snr(link.snr)}
</span>
</div>
<div class="mt-2 flex justify-between text-[10px] text-gray-400">
<span>0 dB</span>
<span class="text-yellow-500">15</span>
<span class="text-green-500">25</span>
<span>40 dB</span>
</div>
</div>
<%!-- Additional Metrics --%>
<div>
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Link Details")}
</h4>
<dl class="space-y-1 text-sm">
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">{t("Distance")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_distance(link.distance)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">{t("TX Rate")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_rate(link.tx_rate)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">{t("RX Rate")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_rate(link.rx_rate)}
</dd>
</div>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">{t("Uptime")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{format_uptime(link.uptime_seconds)}
</dd>
</div>
<%= if link.capacity_utilization do %>
<div class="flex justify-between">
<dt class="text-gray-500 dark:text-gray-400">{t("Capacity")}</dt>
<dd class="text-gray-900 dark:text-white font-medium">
{link.capacity_utilization}%
</dd>
</div>
<% end %>
</dl>
</div>
</div>
<%!-- 24h Sparkline --%>
<%= if sparkline = @sparkline_data[link.id] do %>
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-white/10">
<h4 class="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-2">
{t("Signal Strength — Last 24 Hours")}
</h4>
<svg
viewBox={"0 0 #{sparkline.width} #{sparkline.height}"}
class="w-full h-10"
preserveAspectRatio="none"
>
<polyline
points={sparkline.points}
fill="none"
stroke="currentColor"
stroke-width="1.5"
class="text-blue-500"
/>
</svg>
</div>
<% end %>
</td>
</tr>
<% end %>
<% end %>
</tbody>
</table>
</div>
</div>
<% else %>
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-12 text-center">
<.icon name="hero-signal" class="w-12 h-12 mx-auto text-gray-300 dark:text-gray-600" />
<h3 class="mt-4 text-sm font-semibold text-gray-900 dark:text-white">
{t("No RF links found")}
</h3>
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
{t("RF links will appear here when wireless clients are discovered via SNMP polling.")}
</p>
</div>
<% end %>
</Layouts.authenticated>