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