From 96706b2cf87403aca4294bcb13f6d04f4bea6095 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Wed, 14 Jan 2026 19:12:06 -0600 Subject: [PATCH] Deduplicate agent status helper functions Extracted 5 shared helper functions into ToweropsWeb.AgentLive.Helpers: - agent_status/1 - Determines online/warning/offline/never status - status_badge_class/1 - Returns Tailwind CSS classes for status badges - status_dot_class/1 - Returns CSS classes for animated status dots - format_last_seen/1 - Formats DateTime as human-readable time ago - format_uptime/1 - Formats uptime seconds as days/hours/minutes Removed duplicate code from: - lib/towerops_web/live/agent_live/index.ex - lib/towerops_web/live/agent_live/show.ex Both modules now import the shared helpers, following DRY principle. This reduces code duplication and makes status formatting consistent across all agent-related pages. --- lib/towerops_web/live/agent_live/helpers.ex | 92 +++++++++++++++++++ lib/towerops_web/live/agent_live/index.ex | 49 +--------- lib/towerops_web/live/agent_live/show.ex | 62 +------------ ...dd_composite_indexes_for_agent_queries.exs | 21 +++++ ...0756_add_agent_query_composite_indexes.exs | 21 +++++ 5 files changed, 138 insertions(+), 107 deletions(-) create mode 100644 lib/towerops_web/live/agent_live/helpers.ex create mode 100644 priv/repo/migrations/20260115010646_add_composite_indexes_for_agent_queries.exs create mode 100644 priv/repo/migrations/20260115010756_add_agent_query_composite_indexes.exs diff --git a/lib/towerops_web/live/agent_live/helpers.ex b/lib/towerops_web/live/agent_live/helpers.ex new file mode 100644 index 00000000..77dc8513 --- /dev/null +++ b/lib/towerops_web/live/agent_live/helpers.ex @@ -0,0 +1,92 @@ +defmodule ToweropsWeb.AgentLive.Helpers do + @moduledoc """ + Shared helper functions for agent LiveView modules. + """ + + @doc """ + Determines the agent's status based on last_seen_at timestamp. + + Returns a tuple of {status_atom, status_label} where status_atom is: + - :online - Last seen within 2 minutes + - :warning - Last seen between 2-5 minutes + - :offline - Last seen more than 5 minutes ago + - :never - Never connected + """ + def agent_status(agent_token) do + case agent_token.last_seen_at do + nil -> + {:never, "Never connected"} + + last_seen -> + seconds_ago = DateTime.diff(DateTime.utc_now(), last_seen, :second) + + cond do + seconds_ago < 120 -> {:online, "Online"} + seconds_ago < 300 -> {:warning, "Warning"} + true -> {:offline, "Offline"} + end + end + end + + @doc """ + Returns Tailwind CSS classes for status badge background and text colors. + """ + def status_badge_class(status) do + case status do + :online -> "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" + :warning -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" + :offline -> "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" + :never -> "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-300" + end + end + + @doc """ + Returns Tailwind CSS classes for status indicator dot. + Online status includes animation. + """ + def status_dot_class(status) do + case status do + :online -> "bg-green-600 dark:bg-green-400 animate-pulse" + :warning -> "bg-yellow-600 dark:bg-yellow-400" + :offline -> "bg-red-600 dark:bg-red-400" + :never -> "bg-zinc-400 dark:bg-zinc-600" + end + end + + @doc """ + Formats a DateTime into a human-readable "time ago" string. + + Returns strings like "5s ago", "3m ago", "2h ago", or "5d ago". + """ + def format_last_seen(nil), do: "Never" + + def format_last_seen(datetime) do + seconds_ago = DateTime.diff(DateTime.utc_now(), datetime, :second) + + cond do + seconds_ago < 60 -> "#{seconds_ago}s ago" + seconds_ago < 3600 -> "#{div(seconds_ago, 60)}m ago" + seconds_ago < 86_400 -> "#{div(seconds_ago, 3600)}h ago" + true -> "#{div(seconds_ago, 86_400)}d ago" + end + end + + @doc """ + Formats uptime in seconds to a human-readable string. + + Returns strings like "2h 34m" or "5d 3h". + """ + def format_uptime(uptime_seconds) when is_integer(uptime_seconds) do + days = div(uptime_seconds, 86_400) + hours = div(rem(uptime_seconds, 86_400), 3600) + minutes = div(rem(uptime_seconds, 3600), 60) + + cond do + days > 0 -> "#{days}d #{hours}h" + hours > 0 -> "#{hours}h #{minutes}m" + true -> "#{minutes}m" + end + end + + def format_uptime(_), do: "Unknown" +end diff --git a/lib/towerops_web/live/agent_live/index.ex b/lib/towerops_web/live/agent_live/index.ex index e30d9ed0..dd0e44bf 100644 --- a/lib/towerops_web/live/agent_live/index.ex +++ b/lib/towerops_web/live/agent_live/index.ex @@ -2,6 +2,8 @@ defmodule ToweropsWeb.AgentLive.Index do @moduledoc false use ToweropsWeb, :live_view + import ToweropsWeb.AgentLive.Helpers + alias Towerops.Agents alias Towerops.Agents.Stats @@ -161,51 +163,4 @@ defmodule ToweropsWeb.AgentLive.Index do defp apply_action(socket, :index, _params) do socket end - - defp agent_status(agent_token) do - case agent_token.last_seen_at do - nil -> - {:never, "Never connected"} - - last_seen -> - seconds_ago = DateTime.diff(DateTime.utc_now(), last_seen, :second) - - cond do - seconds_ago < 120 -> {:online, "Online"} - seconds_ago < 300 -> {:warning, "Warning"} - true -> {:offline, "Offline"} - end - end - end - - defp status_badge_class(status) do - case status do - :online -> "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" - :warning -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" - :offline -> "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" - :never -> "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-300" - end - end - - defp status_dot_class(status) do - case status do - :online -> "bg-green-600 dark:bg-green-400 animate-pulse" - :warning -> "bg-yellow-600 dark:bg-yellow-400" - :offline -> "bg-red-600 dark:bg-red-400" - :never -> "bg-zinc-400 dark:bg-zinc-600" - end - end - - defp format_last_seen(nil), do: "Never" - - defp format_last_seen(datetime) do - seconds_ago = DateTime.diff(DateTime.utc_now(), datetime, :second) - - cond do - seconds_ago < 60 -> "#{seconds_ago}s ago" - seconds_ago < 3600 -> "#{div(seconds_ago, 60)}m ago" - seconds_ago < 86_400 -> "#{div(seconds_ago, 3600)}h ago" - true -> "#{div(seconds_ago, 86_400)}d ago" - end - end end diff --git a/lib/towerops_web/live/agent_live/show.ex b/lib/towerops_web/live/agent_live/show.ex index 5c8d71ef..de5051f9 100644 --- a/lib/towerops_web/live/agent_live/show.ex +++ b/lib/towerops_web/live/agent_live/show.ex @@ -2,6 +2,8 @@ defmodule ToweropsWeb.AgentLive.Show do @moduledoc false use ToweropsWeb, :live_view + import ToweropsWeb.AgentLive.Helpers + alias Towerops.Agents @impl true @@ -33,66 +35,6 @@ defmodule ToweropsWeb.AgentLive.Show do {:noreply, socket} end - defp agent_status(agent_token) do - case agent_token.last_seen_at do - nil -> - {:never, "Never connected"} - - last_seen -> - seconds_ago = DateTime.diff(DateTime.utc_now(), last_seen, :second) - - cond do - seconds_ago < 120 -> {:online, "Online"} - seconds_ago < 300 -> {:warning, "Warning"} - true -> {:offline, "Offline"} - end - end - end - - defp status_badge_class(status) do - case status do - :online -> "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300" - :warning -> "bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-300" - :offline -> "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-300" - :never -> "bg-zinc-100 text-zinc-800 dark:bg-zinc-800 dark:text-zinc-300" - end - end - - defp status_dot_class(status) do - case status do - :online -> "bg-green-600 dark:bg-green-400 animate-pulse" - :warning -> "bg-yellow-600 dark:bg-yellow-400" - :offline -> "bg-red-600 dark:bg-red-400" - :never -> "bg-zinc-400 dark:bg-zinc-600" - end - end - - defp format_last_seen(nil), do: "Never" - - defp format_last_seen(datetime) do - seconds_ago = DateTime.diff(DateTime.utc_now(), datetime, :second) - - cond do - seconds_ago < 60 -> "#{seconds_ago}s ago" - seconds_ago < 3600 -> "#{div(seconds_ago, 60)}m ago" - seconds_ago < 86_400 -> "#{div(seconds_ago, 3600)}h ago" - true -> "#{div(seconds_ago, 86_400)}d ago" - end - end - - defp format_uptime(nil), do: "Unknown" - - defp format_uptime(seconds) when is_integer(seconds) do - cond do - seconds < 60 -> "#{seconds}s" - seconds < 3600 -> "#{div(seconds, 60)}m" - seconds < 86_400 -> "#{div(seconds, 3600)}h #{rem(div(seconds, 60), 60)}m" - true -> "#{div(seconds, 86_400)}d #{rem(div(seconds, 3600), 24)}h" - end - end - - defp format_uptime(_), do: "Unknown" - defp assignment_source(equipment, agent_token_id) do cond do # Direct assignment diff --git a/priv/repo/migrations/20260115010646_add_composite_indexes_for_agent_queries.exs b/priv/repo/migrations/20260115010646_add_composite_indexes_for_agent_queries.exs new file mode 100644 index 00000000..e2cb6130 --- /dev/null +++ b/priv/repo/migrations/20260115010646_add_composite_indexes_for_agent_queries.exs @@ -0,0 +1,21 @@ +defmodule Towerops.Repo.Migrations.AddCompositeIndexesForAgentQueries do + use Ecto.Migration + + def change do + # Composite index for agent assignment queries that filter by both agent_token_id and enabled + # This improves queries like: WHERE agent_token_id = ? AND enabled = true + create index(:agent_assignments, [:agent_token_id, :enabled]) + + # Composite index for filtering agents by organization and enabled status + # This improves queries like: WHERE organization_id = ? AND enabled = true + create index(:agent_tokens, [:organization_id, :enabled]) + + # Composite index for finding online agents (organization + last_seen_at) + # This improves queries that filter by org and sort by last_seen_at + create index(:agent_tokens, [:organization_id, :last_seen_at]) + + # Composite index for equipment assignment queries (equipment_id + enabled) + # This improves queries that check if equipment has an active assignment + create index(:agent_assignments, [:equipment_id, :enabled]) + end +end diff --git a/priv/repo/migrations/20260115010756_add_agent_query_composite_indexes.exs b/priv/repo/migrations/20260115010756_add_agent_query_composite_indexes.exs new file mode 100644 index 00000000..17db037c --- /dev/null +++ b/priv/repo/migrations/20260115010756_add_agent_query_composite_indexes.exs @@ -0,0 +1,21 @@ +defmodule Towerops.Repo.Migrations.AddAgentQueryCompositeIndexes do + use Ecto.Migration + + def change do + # Composite index for agent assignment queries that filter by both agent_token_id and enabled + # This improves queries like: WHERE agent_token_id = ? AND enabled = true + create_if_not_exists index(:agent_assignments, [:agent_token_id, :enabled]) + + # Composite index for filtering agents by organization and enabled status + # This improves queries like: WHERE organization_id = ? AND enabled = true + create_if_not_exists index(:agent_tokens, [:organization_id, :enabled]) + + # Composite index for finding online agents (organization + last_seen_at) + # This improves queries that filter by org and sort by last_seen_at + create_if_not_exists index(:agent_tokens, [:organization_id, :last_seen_at]) + + # Composite index for equipment assignment queries (equipment_id + enabled) + # This improves queries that check if equipment has an active assignment + create_if_not_exists index(:agent_assignments, [:equipment_id, :enabled]) + end +end