towerops/lib/towerops_web/live/capacity_live.html.heex
Graham McIntire 1c222f8ff5
feat: add capacity insight worker, UI, and reporting views
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.
2026-03-07 14:42:40 -06:00

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>