From ae9b393137693294910d1ceff6c5034a2eac1dac Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 9 Feb 2026 13:39:59 -0600 Subject: [PATCH] Fix agent page jumping by using streams for real-time updates - Convert agent_tokens from assign to stream to prevent full page re-renders - Update handle_info callbacks to only update the specific agent that changed - Only update stats when agent status changes, not device counts - Prevents IP address from temporarily disappearing during updates --- lib/towerops_web/live/agent_live/index.ex | 39 +++++++++++++++---- .../live/agent_live/index.html.heex | 32 +++++++-------- 2 files changed, 47 insertions(+), 24 deletions(-) diff --git a/lib/towerops_web/live/agent_live/index.ex b/lib/towerops_web/live/agent_live/index.ex index 85fb2f3b..6e1d75e4 100644 --- a/lib/towerops_web/live/agent_live/index.ex +++ b/lib/towerops_web/live/agent_live/index.ex @@ -55,7 +55,7 @@ defmodule ToweropsWeb.AgentLive.Index do |> assign(:page_title, "Remote Agents") |> assign(:timezone, socket.assigns.current_scope.timezone) |> assign(:is_superuser, is_superuser) - |> assign(:agent_tokens, agent_tokens) + |> stream(:agent_tokens, agent_tokens) |> assign(:cloud_pollers, cloud_pollers) |> assign(:global_default_cloud_poller_id, global_default_cloud_poller_id) |> assign(:selected_global_default, global_default_cloud_poller_id || "") @@ -355,18 +355,18 @@ defmodule ToweropsWeb.AgentLive.Index do # Handle real-time agent status updates @impl true - def handle_info({:agent_connected, _agent_token_id, _organization_id}, socket) do - {:noreply, reload_agent_data(socket)} + def handle_info({:agent_connected, agent_token_id, _organization_id}, socket) do + {:noreply, update_single_agent(socket, agent_token_id)} end @impl true - def handle_info({:agent_disconnected, _agent_token_id, _organization_id}, socket) do - {:noreply, reload_agent_data(socket)} + def handle_info({:agent_disconnected, agent_token_id, _organization_id}, socket) do + {:noreply, update_single_agent(socket, agent_token_id)} end @impl true - def handle_info({:agent_heartbeat, _agent_token_id, _organization_id}, socket) do - {:noreply, reload_agent_data(socket)} + def handle_info({:agent_heartbeat, agent_token_id, _organization_id}, socket) do + {:noreply, update_single_agent(socket, agent_token_id)} end @impl true @@ -388,7 +388,7 @@ defmodule ToweropsWeb.AgentLive.Index do offline_agents = Stats.get_offline_agents(organization.id) socket - |> assign(:agent_tokens, agent_tokens) + |> stream(:agent_tokens, agent_tokens, reset: true) |> assign(:cloud_pollers, cloud_pollers) |> assign(:device_counts, equipment_counts) |> assign(:cloud_poller_counts, cloud_poller_counts) @@ -396,4 +396,27 @@ defmodule ToweropsWeb.AgentLive.Index do |> assign(:assignment_breakdown, assignment_breakdown) |> assign(:offline_agents, offline_agents) end + + defp update_single_agent(socket, agent_token_id) do + agent_token = Agents.get_agent_token!(agent_token_id) + + socket + |> stream_insert(:agent_tokens, agent_token) + |> maybe_update_stats() + rescue + Ecto.NoResultsError -> + # Agent was deleted, just update stats + maybe_update_stats(socket) + end + + defp maybe_update_stats(socket) do + organization = socket.assigns.current_scope.organization + + agent_health_stats = Stats.get_organization_agent_health(organization.id) + offline_agents = Stats.get_offline_agents(organization.id) + + socket + |> assign(:agent_health_stats, agent_health_stats) + |> assign(:offline_agents, offline_agents) + end end diff --git a/lib/towerops_web/live/agent_live/index.html.heex b/lib/towerops_web/live/agent_live/index.html.heex index 09c6eb56..9b17aa7f 100644 --- a/lib/towerops_web/live/agent_live/index.html.heex +++ b/lib/towerops_web/live/agent_live/index.html.heex @@ -52,7 +52,7 @@ - <%= if @agent_tokens == [] do %> + <%= if Enum.empty?(@streams.agent_tokens) do %>
<.icon name="hero-server" class="mx-auto h-12 w-12 text-gray-400 dark:text-gray-500" />

No agents

@@ -67,8 +67,8 @@
<% else %>
- <.table id="agents-table" rows={@agent_tokens}> - <:col :let={agent} label="Name"> + <.table id="agents-table" rows={@streams.agent_tokens}> + <:col :let={{_id, agent}} label="Name">
<.link navigate={~p"/agents/#{agent.id}"} @@ -89,7 +89,7 @@
- <:col :let={agent} label="Status"> + <:col :let={{_id, agent}} label="Status"> <% {status, label} = agent_status(agent) %> @@ -97,7 +97,7 @@ - <:col :let={agent} label="Device"> + <:col :let={{_id, agent}} label="Device"> <% counts = Map.get(@device_counts, agent.id, %{direct: 0, total: 0}) %>
{counts.total} total @@ -110,7 +110,7 @@
- <:col :let={agent} label="Last Seen"> + <:col :let={{_id, agent}} label="Last Seen">
<.timestamp datetime={agent.last_seen_at} timezone={@timezone} />
@@ -126,7 +126,7 @@ <% end %> - <:col :let={agent} label="Metadata"> + <:col :let={{_id, agent}} label="Metadata"> <%= if agent.metadata["hostname"] do %>
<.icon name="hero-computer-desktop" class="h-4 w-4 inline" /> @@ -140,13 +140,13 @@ <% end %> - <:col :let={agent} label="Created"> + <:col :let={{_id, agent}} label="Created">
{ToweropsWeb.TimeHelpers.format_date(agent.inserted_at, @timezone)}
- <:action :let={agent}> + <:action :let={{_id, agent}}> <%= if agent.enabled do %>