Rewrite site detail page to use all show.ex helpers

- Add summary stat cards (subscribers, MRR, device count, alerts)
- Add QoE summary with capacity bar (qoe_color, qoe_bg, capacity_bar_color)
- Device health grid with status dots and response times (status_dot_class, format_response_time)
- Active alerts panel with urgency icons (insight_urgency_class, insight_urgency_icon, time_ago)
- Config changes panel with size labels (change_size_label, change_size_color, time_ago)
- Preseem insights panel with urgency styling
- Keep existing latency chart
- Use DaisyUI card/badge components throughout
This commit is contained in:
Graham McIntire 2026-02-13 18:54:59 -06:00
parent 9c266e57ff
commit ba97281beb

View file

@ -22,167 +22,319 @@
</:actions>
</.header>
<%!-- Metrics Bar --%>
<div class="mt-6 flex flex-wrap items-center gap-3">
<span class="inline-flex items-center gap-1.5 rounded-md bg-gray-100 px-2.5 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-300">
<.icon name="hero-server" class="h-3.5 w-3.5" />
{@site_summary.device_count} {if @site_summary.device_count == 1,
do: "device",
else: "devices"}
</span>
<%= if @site_summary.devices_down > 0 do %>
<span class="inline-flex items-center gap-1.5 rounded-md bg-red-100 px-2.5 py-1 text-xs font-medium text-red-800 dark:bg-red-900/30 dark:text-red-400">
<span class="h-2 w-2 rounded-full bg-red-500"></span>
{@site_summary.devices_down} down
</span>
<% end %>
<%= if @site_summary.alert_count > 0 do %>
<span class="inline-flex items-center gap-1.5 rounded-md bg-red-50 px-2.5 py-1 text-xs font-medium text-red-700 ring-1 ring-inset ring-red-600/20 dark:bg-red-900/20 dark:text-red-400 dark:ring-red-400/20">
<.icon name="hero-bell-alert" class="h-3.5 w-3.5" />
{@site_summary.alert_count} {if @site_summary.alert_count == 1,
do: "alert",
else: "alerts"}
</span>
<% end %>
<%= if @site_summary.subscribers do %>
<span class="inline-flex items-center gap-1.5 rounded-md bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-800 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/20">
<.icon name="hero-users" class="h-3.5 w-3.5" />
{@site_summary.subscribers} subscribers
</span>
<%= if @site_summary.mrr do %>
<span class="inline-flex items-center gap-1.5 rounded-md bg-amber-50 px-2.5 py-1 text-xs font-medium text-amber-800 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-400/10 dark:text-amber-400 dark:ring-amber-400/20">
<.icon name="hero-currency-dollar" class="h-3.5 w-3.5" />
{format_mrr(@site_summary.mrr)}/mo MRR
</span>
<% end %>
<% end %>
<%!-- Summary Stat Cards --%>
<div class="mt-6 grid grid-cols-2 gap-4 sm:grid-cols-4">
<%!-- Subscribers --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<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" />
Subscribers
</div>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{format_number(@site_summary.subscribers || 0)}
</p>
</div>
</div>
<%!-- MRR --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<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" />
Monthly Revenue
</div>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{format_mrr(@site_summary.mrr)}
</p>
</div>
</div>
<%!-- Device Count --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<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" />
Devices
</div>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{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} down)
</span>
<% end %>
</p>
</div>
</div>
<%!-- Active Alerts --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<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" />
Active Alerts
</div>
<p class="mt-1 text-2xl font-bold text-gray-900 dark:text-white">
{length(@active_alerts)}
</p>
</div>
</div>
</div>
<div class="mt-8 grid gap-6 md:grid-cols-2">
<div>
<h3 class="text-lg font-semibold mb-4 text-gray-900 dark:text-white">Site Details</h3>
<dl class="space-y-4">
<%= 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-1">
<.link
navigate={~p"/sites/#{@site.parent_site.id}"}
class="text-blue-600 hover:text-blue-700 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-1 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-1 text-gray-900 dark:text-white">{@site.description}</dd>
</div>
<% end %>
</dl>
<%= if @site.child_sites != [] do %>
<div class="mt-6">
<h4 class="text-sm font-semibold mb-3 text-gray-900 dark:text-white">Child Sites</h4>
<ul class="space-y-2">
<li :for={child <- @site.child_sites}>
<.link
navigate={~p"/sites/#{child.id}"}
class="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
>
{child.name}
</.link>
</li>
</ul>
<%!-- 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">Preseem QoE Score</h3>
<p class={["mt-1 text-3xl font-bold", qoe_color(@qoe_summary[:score])]}>
{if @qoe_summary[:score], do: :erlang.float_to_binary(@qoe_summary[:score], decimals: 1), else: "—"}
</p>
</div>
<% end %>
<%= 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 %>
<div>
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">Device</h3>
<.button
navigate={~p"/devices/new?site_id=#{@site.id}"}
variant="primary"
>
<.icon name="hero-plus" class="h-4 w-4" /> Add Device
</.button>
<div class="mt-8 grid gap-6 lg:grid-cols-2">
<%!-- Left Column --%>
<div class="space-y-6">
<%!-- Site Details --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<h3 class="card-title text-base">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="link link-primary text-sm"
>
{@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-base-300 pt-3">
<h4 class="text-sm font-semibold text-gray-900 dark:text-white">Child Sites</h4>
<ul class="mt-2 space-y-1">
<li :for={child <- @site.child_sites}>
<.link navigate={~p"/sites/#{child.id}"} class="link link-primary text-sm">
{child.name}
</.link>
</li>
</ul>
</div>
<% end %>
</div>
</div>
<%= if @device == [] do %>
<div class="rounded-lg border-2 border-dashed border-gray-300 bg-gray-50 p-8 text-center dark:border-white/20 dark:bg-gray-800/50">
<.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">
Add your first device
</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
Start monitoring by adding network devices to this site.
</p>
<p class="mt-1 text-sm text-gray-600 dark:text-gray-400">
You can add routers, switches, servers, or any device with an IP address.
</p>
<div class="mt-6">
<%!-- Active Alerts Panel --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<h3 class="card-title text-base">
<.icon name="hero-bell-alert" class="h-5 w-5" />
Active Alerts
<%= if @active_alerts != [] do %>
<span class="badge badge-error badge-sm">{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-base-300 p-3"
>
<.icon
name={insight_urgency_icon(alert.severity)}
class={["h-5 w-5 mt-0.5 shrink-0", insight_urgency_class(alert.severity)]}
/>
<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 --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<h3 class="card-title text-base">
<.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-base-300 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 badge-sm", 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">
<%!-- Device Health Grid --%>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<div class="flex items-center justify-between">
<h3 class="card-title text-base">
<.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-sm"
>
<.icon name="hero-plus" class="h-5 w-5" /> Add Device
<.icon name="hero-plus" class="h-4 w-4" /> Add
</.button>
</div>
</div>
<% else %>
<div class="space-y-3">
<.link
:for={eq <- @device}
navigate={~p"/devices/#{eq.id}"}
class="flex items-center justify-between rounded-lg border border-gray-200 bg-white p-4 dark:border-white/10 dark:bg-gray-800/50 hover:border-gray-300 dark:hover:border-white/20 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors cursor-pointer"
>
<div>
<p class="font-medium text-gray-900 dark:text-white">{eq.name}</p>
<p class="text-sm text-gray-600 dark:text-gray-400">{eq.ip_address}</p>
<%= if @device == [] do %>
<div class="mt-2 rounded-lg border-2 border-dashed border-base-300 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">
Add your first device
</h4>
<p class="mt-2 text-sm text-gray-600 dark:text-gray-400">
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>
<span class={[
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
eq.status == :up &&
"bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200",
eq.status == :down && "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200",
eq.status == :unknown &&
"bg-gray-100 text-gray-800 dark:bg-gray-800 dark:text-gray-200"
]}>
{eq.status |> to_string() |> String.upcase()}
</span>
</.link>
<% else %>
<div class="mt-2 grid gap-2 sm:grid-cols-2">
<.link
:for={eq <- @device}
navigate={~p"/devices/#{eq.id}"}
class="flex items-center gap-3 rounded-lg border border-base-300 p-3 hover:bg-base-200 transition-colors cursor-pointer"
>
<span class={["h-3 w-3 shrink-0 rounded-full", status_dot_class(eq.status)]} />
<div class="min-w-0 flex-1">
<p class="text-sm font-medium text-gray-900 dark:text-white truncate">{eq.name}</p>
<p class="text-xs text-gray-500 dark:text-gray-400">{eq.ip_address}</p>
</div>
<span class="text-xs font-mono text-gray-500 dark:text-gray-400">
{format_response_time(@response_times[eq.id])}
</span>
</.link>
</div>
<% end %>
</div>
</div>
<%!-- Insights Panel --%>
<%= if @insights != [] do %>
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<h3 class="card-title text-base">
<.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-base-300 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 -->
<%!-- Latency Chart --%>
<%= if @latency_chart_data do %>
<div class="mt-8">
<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">
<h3 class="text-sm font-semibold text-gray-900 dark:text-white">
Site Latency - Last 24 Hours
<div class="mt-6">
<div class="card bg-base-100 border border-base-300 shadow-sm">
<div class="card-body p-4">
<h3 class="card-title text-base">
<.icon name="hero-chart-bar" class="h-5 w-5" />
Site Latency — Last 24 Hours
</h3>
</div>
<div class="p-4">
<div
id="site-latency-chart"
phx-hook="SensorChart"
data-chart={@latency_chart_data}
data-unit="ms"
data-auto-scale="true"
style="height: 400px;"
>
<canvas></canvas>
<div class="mt-2">
<div
id="site-latency-chart"
phx-hook="SensorChart"
data-chart={@latency_chart_data}
data-unit="ms"
data-auto-scale="true"
style="height: 400px;"
>
<canvas></canvas>
</div>
</div>
</div>
</div>