Add CapacityInsightWorker (every 15 min) that generates critical/warning insights when backhaul utilization exceeds 90%/75% and auto-resolves when it drops below 70%. Add capacity and utilization columns to the device ports tab with set/clear capacity controls. Add organization-level /capacity page with summary cards and per-site capacity table. Add capacity summary card to site show page. Add Capacity link to nav menu.
137 lines
6.1 KiB
Text
137 lines
6.1 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="capacity"
|
|
>
|
|
<.breadcrumb items={[
|
|
%{label: "Dashboard", navigate: ~p"/dashboard"},
|
|
%{label: "Capacity"}
|
|
]} />
|
|
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{t("Network Capacity")}</h1>
|
|
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
{t("Backhaul capacity utilization across all sites")}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Summary Cards -->
|
|
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
{t("Total Capacity")}
|
|
</div>
|
|
<div class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
|
{format_capacity(@total_capacity)}
|
|
</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
{t("Current Throughput")}
|
|
</div>
|
|
<div class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
|
{format_capacity(@total_throughput)}
|
|
</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
{t("Sites at Risk")}
|
|
</div>
|
|
<div class="mt-1 text-2xl font-bold text-yellow-600 dark:text-yellow-400">
|
|
{@sites_at_risk}
|
|
</div>
|
|
</div>
|
|
<div class="bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10 p-4">
|
|
<div class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
|
{t("Sites with Headroom")}
|
|
</div>
|
|
<div class="mt-1 text-2xl font-bold text-green-600 dark:text-green-400">
|
|
{@sites_with_headroom}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Site Capacity Table -->
|
|
<%= if @summaries != [] 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("Site Capacity")}
|
|
</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("Site")}</th>
|
|
<th class="px-4 py-2 text-left font-medium">{t("Capacity")}</th>
|
|
<th class="px-4 py-2 text-left font-medium">{t("Throughput")}</th>
|
|
<th class="px-4 py-2 text-left font-medium">{t("Utilization")}</th>
|
|
<th class="px-4 py-2 text-left font-medium">{t("Status")}</th>
|
|
<th class="px-4 py-2 text-left font-medium">{t("Interfaces")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-200 dark:divide-white/10">
|
|
<%= for summary <- Enum.sort_by(@summaries, & &1.utilization_pct, :desc) do %>
|
|
<tr class="text-sm hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors">
|
|
<td class="px-4 py-3">
|
|
<.link
|
|
navigate={~p"/sites/#{summary.site_id}"}
|
|
class="font-medium text-gray-900 dark:text-white hover:text-blue-600 dark:hover:text-blue-400 hover:underline"
|
|
>
|
|
{summary.site_name}
|
|
</.link>
|
|
</td>
|
|
<td class="px-4 py-3 font-mono text-xs text-gray-900 dark:text-white">
|
|
{format_capacity(summary.total_capacity_bps)}
|
|
</td>
|
|
<td class="px-4 py-3 font-mono text-xs text-gray-900 dark:text-white">
|
|
{format_capacity(summary.total_throughput_bps)}
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<div class="flex items-center gap-2 min-w-[120px]">
|
|
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-2 overflow-hidden">
|
|
<div
|
|
class={[
|
|
"h-full rounded-full transition-all",
|
|
utilization_color(summary.utilization_pct)
|
|
]}
|
|
style={"width: #{min(summary.utilization_pct, 100)}%"}
|
|
>
|
|
</div>
|
|
</div>
|
|
<span class={[
|
|
"font-mono text-xs whitespace-nowrap",
|
|
utilization_text_color(summary.utilization_pct)
|
|
]}>
|
|
{Float.round(summary.utilization_pct, 1)}%
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="px-4 py-3">
|
|
<% {label, classes} = status_badge(summary.utilization_pct) %>
|
|
<span class={["px-2 py-0.5 rounded-full text-xs font-medium", classes]}>
|
|
{label}
|
|
</span>
|
|
</td>
|
|
<td class="px-4 py-3 text-xs text-gray-600 dark:text-gray-400">
|
|
{length(summary.interfaces)}
|
|
</td>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="text-center py-12 bg-white dark:bg-gray-800/50 rounded-lg border border-gray-200 dark:border-white/10">
|
|
<.icon name="hero-signal" class="h-12 w-12 mx-auto text-gray-300 dark:text-gray-600" />
|
|
<h3 class="mt-4 text-sm font-medium text-gray-900 dark:text-white">
|
|
{t("No capacity data")}
|
|
</h3>
|
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
|
{t("Configure capacity on device interfaces to see utilization data here.")}
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
</Layouts.authenticated>
|