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.
This commit is contained in:
Graham McIntire 2026-01-14 19:12:06 -06:00
parent 21cdb600a0
commit 96706b2cf8
No known key found for this signature in database
5 changed files with 138 additions and 107 deletions

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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