Dashboard: timezone-aware timestamps, site health as table, remove dead code

This commit is contained in:
Graham McIntire 2026-02-14 16:05:25 -06:00
parent 2cbf6db8de
commit 3049411337
3 changed files with 104 additions and 52 deletions

View file

@ -181,8 +181,21 @@ defmodule ToweropsWeb.TimeHelpers do
end
@doc """
Formats a DateTime into a short date string in UTC.
Formats a DateTime into a short "Mon DD HH:MM" string in the given timezone.
"""
@spec format_short_datetime(DateTime.t() | nil, String.t()) :: String.t()
def format_short_datetime(nil, _timezone), do: ""
def format_short_datetime(datetime, timezone) do
case to_user_timezone(datetime, timezone || "UTC") do
{:ok, shifted_dt, _tz_abbr} ->
Calendar.strftime(shifted_dt, "%b %d %H:%M")
{:error, _} ->
Calendar.strftime(datetime, "%b %d %H:%M")
end
end
@spec format_date(DateTime.t() | nil) :: String.t()
def format_date(datetime), do: format_date(datetime, "UTC")

View file

@ -27,7 +27,9 @@ defmodule ToweropsWeb.DashboardLive do
socket
|> assign(:page_title, organization.name)
|> assign(:device_count, device_count)
|> assign(:insight_source, nil)}
|> assign(:insight_source, nil)
|> assign(:timezone, socket.assigns.current_scope.timezone)
|> assign(:time_format, socket.assigns.current_scope.time_format)}
end
@impl true
@ -259,10 +261,6 @@ defmodule ToweropsWeb.DashboardLive do
end
end
defp site_health_border(:red), do: "border-red-300 bg-red-50/50 dark:border-red-800 dark:bg-red-950/20"
defp site_health_border(:yellow), do: "border-yellow-300 bg-yellow-50/50 dark:border-yellow-800 dark:bg-yellow-950/20"
defp site_health_border(_), do: "border-gray-200 bg-white dark:border-white/10 dark:bg-gray-800/50"
defp site_health_dot(:red), do: "bg-red-500"
defp site_health_dot(:yellow), do: "bg-yellow-500"
defp site_health_dot(_), do: "bg-green-500"
@ -287,4 +285,8 @@ defmodule ToweropsWeb.DashboardLive do
defp format_qoe(nil), do: ""
defp format_qoe(score), do: :erlang.float_to_binary(score / 1, decimals: 1)
defp format_short_datetime(datetime, timezone) do
ToweropsWeb.TimeHelpers.format_short_datetime(datetime, timezone)
end
end

View file

@ -14,7 +14,7 @@
<%= if assigns[:last_updated] do %>
<div class="flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
<span>
Updated {Calendar.strftime(@last_updated, "%H:%M:%S UTC")}
Updated {ToweropsWeb.TimeHelpers.format_datetime(@last_updated, @timezone, @time_format)}
</span>
<button
type="button"
@ -419,7 +419,7 @@
<% end %>
</td>
<td class="px-4 py-2.5 whitespace-nowrap text-xs text-gray-500 dark:text-gray-400">
<.timestamp datetime={alert.triggered_at} />
<.timestamp datetime={alert.triggered_at} timezone={@timezone} />
</td>
<td class="px-4 py-2.5 text-right">
<%= if is_nil(alert.acknowledged_at) && is_nil(alert.resolved_at) do %>
@ -570,7 +570,7 @@
</span>
</div>
<span class="flex-shrink-0 text-xs text-gray-400 dark:text-gray-500">
{Calendar.strftime(change.changed_at, "%b %d %H:%M")}
{format_short_datetime(change.changed_at, @timezone)}
</span>
</.link>
<% end %>
@ -592,48 +592,85 @@
View all →
</.link>
</div>
<div class="grid gap-3 sm:grid-cols-2 lg:grid-cols-3">
<.link
:for={site <- @site_impact_summaries}
navigate={~p"/sites/#{site.site_id}"}
class={[
"rounded-lg border px-4 py-3 transition hover:shadow-sm",
site_health_border(site.site_health)
]}
>
<div class="flex items-center gap-2">
<span class={[
"h-2 w-2 rounded-full flex-shrink-0",
site_health_dot(site.site_health)
]}>
</span>
<span class="text-sm font-semibold text-gray-900 dark:text-white truncate">
{site.name}
</span>
</div>
<div class="mt-1.5 flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
<span>{site.device_count} devices</span>
<%= if site.devices_down > 0 do %>
<span class="text-red-600 dark:text-red-400">{site.devices_down} down</span>
<% end %>
<%= if site.avg_qoe do %>
<span class="text-purple-600 dark:text-purple-400">
QoE {format_qoe(site.avg_qoe)}
</span>
<% end %>
<%= if site.subscribers do %>
<span>{site.subscribers} subs</span>
<% end %>
</div>
<%= if site.subscribers_affected > 0 do %>
<div class="mt-1 text-xs text-red-600 dark:text-red-400">
{format_number(site.subscribers_affected)} affected
<%= if mrr_at_risk_positive?(site.mrr_at_risk) do %>
· {format_mrr(site.mrr_at_risk)}/mo at risk
<% end %>
</div>
<% end %>
</.link>
<div class="overflow-hidden rounded-lg border border-gray-200 dark:border-white/10">
<table class="min-w-full divide-y divide-gray-200 dark:divide-white/10">
<thead class="bg-gray-50 dark:bg-gray-800/80">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Site
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Devices
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Down
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
QoE
</th>
<th class="px-4 py-2 text-right text-xs font-medium uppercase tracking-wide text-gray-500 dark:text-gray-400">
Subscribers
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-100 bg-white dark:divide-white/5 dark:bg-gray-800/50">
<tr
:for={site <- @site_impact_summaries}
class="hover:bg-gray-50 dark:hover:bg-white/5"
>
<td class="px-4 py-2.5">
<.link
navigate={~p"/sites/#{site.site_id}"}
class="flex items-center gap-2 text-sm font-medium text-gray-900 hover:text-blue-600 dark:text-white dark:hover:text-blue-400"
>
<span class={[
"h-2 w-2 rounded-full flex-shrink-0",
site_health_dot(site.site_health)
]}>
</span>
{site.name}
</.link>
</td>
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
{site.device_count}
</td>
<td class="px-4 py-2.5 text-right text-sm">
<%= if site.devices_down > 0 do %>
<span class="text-red-600 dark:text-red-400">{site.devices_down}</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">0</span>
<% end %>
</td>
<td class="px-4 py-2.5 text-right text-sm">
<%= if site.avg_qoe do %>
<span class={
cond do
site.avg_qoe < 2.0 -> "text-red-600 dark:text-red-400"
site.avg_qoe < 4.0 -> "text-yellow-600 dark:text-yellow-400"
true -> "text-green-600 dark:text-green-400"
end
}>
{format_qoe(site.avg_qoe)}
</span>
<% else %>
<span class="text-gray-400 dark:text-gray-500">—</span>
<% end %>
</td>
<td class="px-4 py-2.5 text-right text-sm text-gray-600 dark:text-gray-400">
<%= if site.subscribers do %>
{site.subscribers}
<%= if site.subscribers_affected > 0 do %>
<span class="text-red-600 dark:text-red-400">
({format_number(site.subscribers_affected)} affected)
</span>
<% end %>
<% else %>
<span class="text-gray-400 dark:text-gray-500">—</span>
<% end %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<% end %>
@ -663,7 +700,7 @@
{activity.summary}
</p>
<span class="flex-shrink-0 text-xs text-gray-400 dark:text-gray-500">
{Calendar.strftime(activity.timestamp, "%b %d %H:%M")}
{format_short_datetime(activity.timestamp, @timezone)}
</span>
</div>
</div>