312 lines
15 KiB
Text
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>
|