586 lines
25 KiB
Text
586 lines
25 KiB
Text
<Layouts.authenticated
|
|
flash={@flash}
|
|
current_scope={@current_scope}
|
|
active_page="sites"
|
|
unresolved_alert_count={assigns[:unresolved_alert_count] || 0}
|
|
>
|
|
<.breadcrumb items={[
|
|
%{label: "Dashboard", navigate: ~p"/dashboard"},
|
|
%{label: "Sites", navigate: ~p"/sites"},
|
|
%{label: @site.name}
|
|
]} />
|
|
<!-- Prominent site header -->
|
|
<div class="mb-6">
|
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{@site.name}</h1>
|
|
<p :if={@site.location} class="mt-1 text-sm text-gray-600 dark:text-gray-400">
|
|
<.icon name="hero-map-pin" class="h-4 w-4 inline" /> {@site.location}
|
|
<%= if @site.latitude && @site.longitude do %>
|
|
<span class="ml-2 font-mono text-xs text-gray-400">
|
|
({Float.round(@site.latitude, 4)}, {Float.round(@site.longitude, 4)})
|
|
</span>
|
|
<% end %>
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Leaflet map if coordinates exist -->
|
|
<%= if @site.latitude && @site.longitude do %>
|
|
<div
|
|
id="site-map"
|
|
phx-hook="LeafletMap"
|
|
phx-update="ignore"
|
|
data-lat={@site.latitude}
|
|
data-lng={@site.longitude}
|
|
data-zoom="14"
|
|
data-marker-title={@site.name}
|
|
class="h-48 w-full rounded-lg border border-gray-200 dark:border-white/10 mb-6 z-0"
|
|
>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%= if @device != [] do %>
|
|
<div class="mt-2">
|
|
<.button
|
|
type="button"
|
|
phx-click="force_rediscover_all"
|
|
class="btn text-sm !py-1 !px-3"
|
|
data-confirm={
|
|
t("This will trigger SNMP discovery for all devices at this site. Continue?")
|
|
}
|
|
>
|
|
<.icon name="hero-magnifying-glass" class="h-4 w-4" /> {t("Force Rediscover All")}
|
|
</.button>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- Quick Actions --%>
|
|
<div class="mt-4 flex flex-wrap gap-2">
|
|
<.button
|
|
navigate={~p"/devices/new?site_id=#{@site.id}"}
|
|
variant="primary"
|
|
class="btn text-sm !py-1 !px-3"
|
|
>
|
|
<.icon name="hero-plus" class="h-4 w-4" /> {t("Add Device")}
|
|
</.button>
|
|
<%= if @site.latitude && @site.longitude do %>
|
|
<.button
|
|
navigate={~p"/sites-map?lat=#{@site.latitude}&lng=#{@site.longitude}&zoom=15"}
|
|
class="btn text-sm !py-1 !px-3"
|
|
>
|
|
<.icon name="hero-map" class="h-4 w-4" /> {t("View on Map")}
|
|
</.button>
|
|
<% end %>
|
|
<.button navigate={~p"/sites/#{@site.id}/edit"} class="btn text-sm !py-1 !px-3">
|
|
<.icon name="hero-pencil" class="h-4 w-4" /> {t("Edit Site")}
|
|
</.button>
|
|
</div>
|
|
|
|
<%!-- Summary Stat Cards --%>
|
|
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
|
|
<%!-- Subscribers --%>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
|
|
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
<.icon name="hero-users" class="h-4 w-4" /> {t("Subscribers")}
|
|
</div>
|
|
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white font-mono">
|
|
{format_number(@site_summary.subscribers || 0)}
|
|
</p>
|
|
</div>
|
|
<%!-- Financial data: role-gated --%>
|
|
<%= if @can_view_financials do %>
|
|
<%!-- MRR --%>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
|
|
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
<.icon name="hero-currency-dollar" class="h-4 w-4" /> {t("Monthly Revenue")}
|
|
</div>
|
|
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
|
|
{format_mrr(@site_summary.mrr)}
|
|
</p>
|
|
</div>
|
|
<% end %>
|
|
<%!-- Device Count --%>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm">
|
|
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
<.icon name="hero-server" class="h-4 w-4" /> {t("Devices")}
|
|
</div>
|
|
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white font-mono">
|
|
{format_number(@site_summary.device_count)}
|
|
<%= if @site_summary.devices_down > 0 do %>
|
|
<span class="text-sm font-medium text-red-600 dark:text-red-400">
|
|
({@site_summary.devices_down} {t("down")})
|
|
</span>
|
|
<% end %>
|
|
</p>
|
|
</div>
|
|
<%!-- Active Alerts --%>
|
|
<div class={[
|
|
"rounded-lg border bg-white dark:bg-gray-800/50 p-4 transition-shadow hover:shadow-sm",
|
|
if(length(@active_alerts) > 0,
|
|
do: "border-red-200 dark:border-red-900/50",
|
|
else: "border-gray-200 dark:border-white/10"
|
|
)
|
|
]}>
|
|
<div class="flex items-center gap-2 text-sm font-medium text-gray-500 dark:text-gray-400">
|
|
<.icon name="hero-bell-alert" class="h-4 w-4" /> {t("Active Alerts")}
|
|
</div>
|
|
<p class={[
|
|
"mt-1 text-2xl font-bold font-mono",
|
|
if(length(@active_alerts) > 0,
|
|
do: "text-red-600 dark:text-red-400",
|
|
else: "text-gray-900 dark:text-white"
|
|
)
|
|
]}>
|
|
{length(@active_alerts)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Backhaul Capacity Card --%>
|
|
<%= if @capacity_summary.interfaces != [] do %>
|
|
<div class="mt-6 rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50 p-4">
|
|
<div class="flex items-center justify-between mb-3">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
<.icon name="hero-signal" class="h-4 w-4 inline mr-1" />{t("Backhaul Capacity")}
|
|
</h3>
|
|
<span class="text-xs text-gray-500 dark:text-gray-400">
|
|
{format_capacity(@capacity_summary.total_capacity_bps)} {t("total")}
|
|
</span>
|
|
</div>
|
|
<div class="flex items-center gap-3 mb-3">
|
|
<div class="flex-1 bg-gray-200 dark:bg-gray-700 rounded-full h-3 overflow-hidden">
|
|
<div
|
|
class={[
|
|
"h-full rounded-full transition-all",
|
|
utilization_bar_color(@capacity_summary.utilization_pct)
|
|
]}
|
|
style={"width: #{min(@capacity_summary.utilization_pct, 100)}%"}
|
|
>
|
|
</div>
|
|
</div>
|
|
<span class={[
|
|
"text-sm font-bold font-mono whitespace-nowrap",
|
|
utilization_text_color(@capacity_summary.utilization_pct)
|
|
]}>
|
|
{Float.round(@capacity_summary.utilization_pct, 1)}%
|
|
</span>
|
|
</div>
|
|
<div class="grid grid-cols-3 gap-2 text-xs">
|
|
<div>
|
|
<span class="text-gray-500 dark:text-gray-400">{t("Throughput")}</span>
|
|
<p class="font-mono font-medium text-gray-900 dark:text-white">
|
|
{format_capacity(@capacity_summary.total_throughput_bps)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-500 dark:text-gray-400">{t("Headroom")}</span>
|
|
<p class="font-mono font-medium text-gray-900 dark:text-white">
|
|
{format_capacity(
|
|
max(
|
|
0,
|
|
@capacity_summary.total_capacity_bps - @capacity_summary.total_throughput_bps
|
|
)
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<span class="text-gray-500 dark:text-gray-400">{t("Interfaces")}</span>
|
|
<p class="font-mono font-medium text-gray-900 dark:text-white">
|
|
{length(@capacity_summary.interfaces)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- QoE Summary Card --%>
|
|
<%= if @qoe_summary do %>
|
|
<div class={["mt-6 rounded-lg border p-4", qoe_bg(@qoe_summary[:score])]}>
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h3 class="text-sm font-semibold text-gray-700 dark:text-gray-300">
|
|
{t("Preseem QoE Score")}
|
|
</h3>
|
|
<p class={["mt-1 text-3xl font-bold", qoe_color(@qoe_summary[:score])]}>
|
|
{if @qoe_summary[:score], do: Float.round(@qoe_summary[:score] / 1, 1), else: "—"}
|
|
</p>
|
|
</div>
|
|
<%= if @qoe_summary[:capacity_score] do %>
|
|
<div class="text-right">
|
|
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Capacity</p>
|
|
<div class="mt-1 w-32">
|
|
<div class="h-2 w-full rounded-full bg-gray-200 dark:bg-gray-700">
|
|
<div
|
|
class={["h-2 rounded-full", capacity_bar_color(@qoe_summary[:capacity_score])]}
|
|
style={"width: #{min(@qoe_summary[:capacity_score], 100)}%"}
|
|
/>
|
|
</div>
|
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
|
{@qoe_summary[:capacity_score]}%
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
|
|
<%!-- Subscriber Impact Breakdown by Device --%>
|
|
<%= if @site_impact && @site_impact.subscriber_count > 0 do %>
|
|
<div class="mt-6 rounded-lg border border-indigo-200 bg-indigo-50 dark:border-indigo-800/50 dark:bg-indigo-950/30">
|
|
<div class="px-4 py-3 border-b border-indigo-200 dark:border-indigo-800/50">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-indigo-900 dark:text-indigo-200 flex items-center gap-1.5">
|
|
<.icon name="hero-users" class="h-4 w-4" /> {t("Subscriber Impact")}
|
|
</h3>
|
|
<div class="flex items-center gap-4 text-sm font-medium text-indigo-900 dark:text-indigo-200">
|
|
<span>{format_number(@site_impact.subscriber_count)} subscribers</span>
|
|
<%= if @can_view_financials && @site_impact.mrr do %>
|
|
<span>{format_mrr(@site_impact.mrr)}/mo</span>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<%= if @site_impact.devices != [] do %>
|
|
<div class="overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-indigo-200 dark:divide-indigo-800/50">
|
|
<thead>
|
|
<tr class="text-xs text-indigo-700 dark:text-indigo-300">
|
|
<th class="px-4 py-2 text-left font-medium">{t("Device")}</th>
|
|
<th class="px-4 py-2 text-right font-medium">{t("Subscribers")}</th>
|
|
<%= if @can_view_financials do %>
|
|
<th class="px-4 py-2 text-right font-medium">{t("MRR")}</th>
|
|
<% end %>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-indigo-100 dark:divide-indigo-900/30">
|
|
<%= for dev <- @site_impact.devices do %>
|
|
<tr class="text-sm">
|
|
<td class="px-4 py-2 text-indigo-900 dark:text-indigo-200 font-medium">
|
|
{dev.name || dev.device_id}
|
|
</td>
|
|
<td class="px-4 py-2 text-right text-indigo-800 dark:text-indigo-300 font-mono">
|
|
{dev.count}
|
|
</td>
|
|
<%= if @can_view_financials do %>
|
|
<td class="px-4 py-2 text-right text-indigo-800 dark:text-indigo-300 font-mono">
|
|
{format_mrr(dev.mrr)}
|
|
</td>
|
|
<% end %>
|
|
</tr>
|
|
<% end %>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
<% end %>
|
|
|
|
<div class="mt-8 grid gap-6 lg:grid-cols-2">
|
|
<%!-- Left Column --%>
|
|
<div class="space-y-6 min-w-0">
|
|
<%!-- Site Details --%>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
|
<div class="p-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
{t("Site Details")}
|
|
</h3>
|
|
<dl class="mt-2 space-y-3">
|
|
<%= if @site.parent_site do %>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Parent Site</dt>
|
|
<dd class="mt-0.5">
|
|
<.link
|
|
navigate={~p"/sites/#{@site.parent_site.id}"}
|
|
class="text-sm text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
{@site.parent_site.name}
|
|
</.link>
|
|
</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if @site.location do %>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Location</dt>
|
|
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{@site.location}</dd>
|
|
</div>
|
|
<% end %>
|
|
<%= if @site.description do %>
|
|
<div>
|
|
<dt class="text-sm font-medium text-gray-600 dark:text-gray-400">Description</dt>
|
|
<dd class="mt-0.5 text-sm text-gray-900 dark:text-white">{@site.description}</dd>
|
|
</div>
|
|
<% end %>
|
|
</dl>
|
|
<%= if @site.child_sites != [] do %>
|
|
<div class="mt-4 border-t border-gray-300 dark:border-gray-600 pt-3">
|
|
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">
|
|
{t("Child Sites")}
|
|
</h4>
|
|
<ul class="mt-2 space-y-1">
|
|
<li :for={child <- @site.child_sites}>
|
|
<.link
|
|
navigate={~p"/sites/#{child.id}"}
|
|
class="text-sm text-blue-600 hover:text-blue-700 hover:underline dark:text-blue-400 dark:hover:text-blue-300"
|
|
>
|
|
{child.name}
|
|
</.link>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Active Alerts Panel --%>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
|
<div class="p-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
<.icon name="hero-bell-alert" class="h-5 w-5" /> Active Alerts
|
|
<%= if @active_alerts != [] do %>
|
|
<span class="ml-2 inline-flex items-center rounded-full bg-red-100 px-2 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
|
|
{length(@active_alerts)}
|
|
</span>
|
|
<% end %>
|
|
</h3>
|
|
<%= if @active_alerts == [] do %>
|
|
<div class="mt-2 text-center py-6 text-gray-500 dark:text-gray-400">
|
|
<.icon name="hero-check-circle" class="mx-auto h-8 w-8 text-green-500" />
|
|
<p class="mt-2 text-sm">No active alerts</p>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-2 space-y-2">
|
|
<div
|
|
:for={alert <- @active_alerts}
|
|
class="flex items-start gap-3 rounded-lg border border-gray-200 dark:border-white/10 p-3"
|
|
>
|
|
<.icon
|
|
name={insight_urgency_icon(alert_severity(alert.alert_type))}
|
|
class={[
|
|
"h-5 w-5 mt-0.5 shrink-0",
|
|
insight_urgency_class(alert_severity(alert.alert_type))
|
|
]}
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white">{alert.message}</p>
|
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
|
{time_ago(alert.triggered_at)}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Recent Config Changes Panel — hidden, feature temporarily disabled --%>
|
|
<div class="hidden rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
|
<div class="p-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
<.icon name="hero-document-text" class="h-5 w-5" /> Recent Config Changes
|
|
</h3>
|
|
<%= if @config_changes == [] do %>
|
|
<div class="mt-2 text-center py-6 text-gray-500 dark:text-gray-400">
|
|
<p class="text-sm">No config changes in the last 30 days</p>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-2 space-y-2">
|
|
<div
|
|
:for={change <- @config_changes}
|
|
class="flex items-center justify-between rounded-lg border border-gray-200 dark:border-white/10 p-3"
|
|
>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
{change.device_name || "Unknown device"}
|
|
</p>
|
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400">
|
|
{time_ago(change.detected_at)}
|
|
</p>
|
|
</div>
|
|
<span class={[
|
|
"badge bg-gray-200 text-gray-700 dark:bg-gray-800 dark:text-gray-300",
|
|
change_size_color(change.diff_size || 0)
|
|
]}>
|
|
{change_size_label(change.diff_size || 0)}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Right Column --%>
|
|
<div class="space-y-6 min-w-0">
|
|
<%!-- Device Health Grid --%>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
|
<div class="p-4">
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
<.icon name="hero-server" class="h-5 w-5" /> Device Health
|
|
</h3>
|
|
<.button
|
|
navigate={~p"/devices/new?site_id=#{@site.id}"}
|
|
variant="primary"
|
|
class="btn text-sm !py-1 !px-3"
|
|
>
|
|
<.icon name="hero-plus" class="h-4 w-4" /> Add
|
|
</.button>
|
|
</div>
|
|
<%= if @device == [] do %>
|
|
<div class="mt-2 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600 p-8 text-center">
|
|
<.icon
|
|
name="hero-server"
|
|
class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500"
|
|
/>
|
|
<h4 class="mt-4 text-base font-semibold text-gray-900 dark:text-white">
|
|
{t("Add your first device")}
|
|
</h4>
|
|
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
|
|
{t("Start monitoring by adding network devices to this site.")}
|
|
</p>
|
|
<div class="mt-4">
|
|
<.button navigate={~p"/devices/new?site_id=#{@site.id}"} variant="primary">
|
|
<.icon name="hero-plus" class="h-5 w-5" /> Add Device
|
|
</.button>
|
|
</div>
|
|
</div>
|
|
<% else %>
|
|
<div class="mt-2 overflow-x-auto">
|
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
|
|
<thead>
|
|
<tr class="text-xs text-gray-500 dark:text-gray-400">
|
|
<th class="px-3 py-2 text-left font-medium">{t("Device")}</th>
|
|
<th class="hidden sm:table-cell px-3 py-2 text-left font-medium">
|
|
{t("IP Address")}
|
|
</th>
|
|
<th class="hidden sm:table-cell px-3 py-2 text-right font-medium">
|
|
{t("Subs")}
|
|
</th>
|
|
<%= if @can_view_financials do %>
|
|
<th class="hidden md:table-cell px-3 py-2 text-right font-medium">
|
|
{t("MRR")}
|
|
</th>
|
|
<% end %>
|
|
<th class="px-3 py-2 text-right font-medium">{t("Latency")}</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="divide-y divide-gray-100 dark:divide-white/5">
|
|
<tr
|
|
:for={eq <- @device}
|
|
class="hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
|
|
phx-click={JS.navigate(~p"/devices/#{eq.id}")}
|
|
>
|
|
<td class="px-3 py-2">
|
|
<div class="flex items-center gap-2">
|
|
<span class={[
|
|
"h-2 w-2 shrink-0 rounded-full",
|
|
status_dot_class(eq.status)
|
|
]} />
|
|
<span class="text-sm font-medium text-gray-900 dark:text-white truncate">
|
|
{eq.name}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td class="hidden sm:table-cell px-3 py-2 font-mono text-xs text-gray-500 dark:text-gray-400">
|
|
{eq.ip_address}
|
|
</td>
|
|
<td class="hidden sm:table-cell px-3 py-2 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
|
<%= if di = @device_impact[eq.id] do %>
|
|
{if di.count > 0, do: di.count, else: "—"}
|
|
<% else %>
|
|
—
|
|
<% end %>
|
|
</td>
|
|
<%= if @can_view_financials do %>
|
|
<td class="hidden md:table-cell px-3 py-2 text-right font-mono text-xs text-gray-700 dark:text-gray-300">
|
|
<%= if di = @device_impact[eq.id] do %>
|
|
{if Decimal.gt?(di.mrr, 0), do: format_mrr(di.mrr), else: "—"}
|
|
<% else %>
|
|
—
|
|
<% end %>
|
|
</td>
|
|
<% end %>
|
|
<td class={[
|
|
"px-3 py-2 text-right font-mono text-xs",
|
|
case @response_times[eq.id] do
|
|
nil -> "text-gray-400"
|
|
ms when ms < 50 -> "text-green-600 dark:text-green-400"
|
|
ms when ms < 200 -> "text-yellow-600 dark:text-yellow-400"
|
|
_ -> "text-red-600 dark:text-red-400"
|
|
end
|
|
]}>
|
|
{format_response_time(@response_times[eq.id])}
|
|
</td>
|
|
</tr>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Insights Panel --%>
|
|
<%= if @insights != [] do %>
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
|
<div class="p-4">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
<.icon name="hero-light-bulb" class="h-5 w-5" /> Preseem Insights
|
|
</h3>
|
|
<div class="mt-2 space-y-2">
|
|
<div
|
|
:for={insight <- @insights}
|
|
class="flex items-start gap-3 rounded-lg border border-gray-200 dark:border-white/10 p-3"
|
|
>
|
|
<.icon
|
|
name={insight_urgency_icon(insight.urgency)}
|
|
class={["h-5 w-5 mt-0.5 shrink-0", insight_urgency_class(insight.urgency)]}
|
|
/>
|
|
<div class="min-w-0 flex-1">
|
|
<p class="text-sm font-medium text-gray-900 dark:text-white">{insight.title}</p>
|
|
<%= if insight.description do %>
|
|
<p class="mt-0.5 text-xs text-gray-500 dark:text-gray-400 line-clamp-2">
|
|
{insight.description}
|
|
</p>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
|
|
<%!-- Latency Chart --%>
|
|
<%= if @latency_chart_data do %>
|
|
<div class="mt-6">
|
|
<div class="rounded-lg border border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50">
|
|
<div class="px-4 py-3 border-b border-gray-200 dark:border-white/10">
|
|
<h3 class="text-sm font-semibold text-gray-900 dark:text-white flex items-center gap-1.5">
|
|
<.icon name="hero-chart-bar" class="h-4 w-4" /> {t("Site Latency — Last 24 Hours")}
|
|
</h3>
|
|
</div>
|
|
<div class="p-4">
|
|
<div
|
|
id="site-latency-chart"
|
|
phx-hook="SensorChart"
|
|
phx-update="ignore"
|
|
data-chart={@latency_chart_data}
|
|
data-unit="ms"
|
|
data-auto-scale="true"
|
|
class="relative"
|
|
style="height: 400px;"
|
|
>
|
|
<div class="absolute inset-0 flex items-center justify-center" data-loading>
|
|
<div class="flex flex-col items-center gap-2">
|
|
<div class="h-8 w-8 animate-spin rounded-full border-2 border-gray-200 dark:border-gray-700 border-t-blue-500">
|
|
</div>
|
|
<span class="text-xs text-gray-400">{t("Loading chart...")}</span>
|
|
</div>
|
|
</div>
|
|
<canvas></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<% end %>
|
|
</Layouts.authenticated>
|