defmodule ElixirAiWeb.AdminLive do use ElixirAiWeb, :live_view require Logger @refresh_ms 1_000 def mount(_params, _session, socket) do if connected?(socket) do :net_kernel.monitor_nodes(true) :pg.join(ElixirAi.LiveViewPG, {:liveview, __MODULE__}, self()) schedule_refresh() end {:ok, assign(socket, cluster_info: gather_info())} end def handle_info({:nodeup, _node}, socket) do {:noreply, assign(socket, cluster_info: gather_info())} end def handle_info({:nodedown, _node}, socket) do {:noreply, assign(socket, cluster_info: gather_info())} end def handle_info(:refresh, socket) do schedule_refresh() {:noreply, assign(socket, cluster_info: gather_info())} end defp schedule_refresh, do: Process.send_after(self(), :refresh, @refresh_ms) defp gather_info do import ElixirAi.PubsubTopics all_nodes = [Node.self() | Node.list()] configured = ElixirAi.ClusterSingleton.configured_singletons() node_statuses = Enum.map(all_nodes, fn node -> status = if node == Node.self() do try do ElixirAi.ClusterSingleton.status() catch _, _ -> :unreachable end else case :rpc.call(node, ElixirAi.ClusterSingleton, :status, [], 3_000) do {:badrpc, _} -> :unreachable result -> result end end {node, status} end) singleton_locations = Enum.map(configured, fn module -> location = case Horde.Registry.lookup(ElixirAi.ChatRegistry, module) do [{pid, _}] -> node(pid) _ -> nil end {module, location} end) # All ChatRunner entries in the distributed registry, keyed by conversation name. # Each entry is a {name, node, pid, supervisor_node} tuple. chat_runners = Horde.DynamicSupervisor.which_children(ElixirAi.ChatRunnerSupervisor) |> Enum.flat_map(fn {_, pid, _, _} when is_pid(pid) -> case Horde.Registry.select(ElixirAi.ChatRegistry, [ {{:"$1", pid, :"$2"}, [], [{{:"$1", pid, :"$2"}}]} ]) do [{name, ^pid, _}] when is_binary(name) -> [{name, node(pid), pid}] _ -> [] end _ -> [] end) |> Enum.sort_by(&elem(&1, 0)) # :pg is cluster-wide — one local call returns members from all nodes. # Processes are automatically removed from their group when they die. liveviews = :pg.which_groups(ElixirAi.LiveViewPG) |> Enum.flat_map(fn {:liveview, view} -> :pg.get_members(ElixirAi.LiveViewPG, {:liveview, view}) |> Enum.map(fn pid -> {view, node(pid)} end) _ -> [] end) %{ nodes: node_statuses, configured_singletons: configured, singleton_locations: singleton_locations, chat_runners: chat_runners, liveviews: liveviews } end def render(assigns) do ~H"""

Cluster Admin

<%= for {node, status} <- @cluster_info.nodes do %> <% node_singletons = Enum.filter(@cluster_info.singleton_locations, fn {_, loc} -> loc == node end) %> <% node_runners = Enum.filter(@cluster_info.chat_runners, fn {_, rnode, _} -> rnode == node end) %> <% node_liveviews = @cluster_info.liveviews |> Enum.filter(fn {_, n} -> n == node end) |> Enum.group_by(fn {view, _} -> view end) %>
{node} <%= if node == Node.self() do %> self <% end %>
<.status_badge status={status} />
<%= if node_singletons != [] do %>

Singletons

<%= for {module, _} <- node_singletons do %>
{inspect(module)}
<% end %>
<% end %> <%= if node_runners != [] do %>

Chat Runners {length(node_runners)}

<%= for {name, _, _} <- node_runners do %>
{name}
<% end %>
<% end %> <%= if node_liveviews != %{} do %>

LiveViews

<%= for {view, instances} <- node_liveviews do %>
{short_module(view)} ×{length(instances)}
<% end %>
<% end %> <%= if node_singletons == [] and node_runners == [] and node_liveviews == %{} do %>

No active processes

<% end %>
<% end %>
<% unlocated = Enum.filter(@cluster_info.singleton_locations, fn {_, loc} -> is_nil(loc) end) %> <%= if unlocated != [] do %>

Singletons Not Running

<%= for {module, _} <- unlocated do %> {inspect(module)} <% end %>
<% end %>

Refreshes every 1s or on node events.

""" end defp short_module(module) when is_atom(module) do module |> Atom.to_string() |> String.replace_prefix("Elixir.", "") |> String.split(".") |> List.last() end defp status_badge(assigns) do ~H""" <%= case @status do %> <% :started -> %> started <% :pending -> %> pending <% :unreachable -> %> unreachable <% other -> %> {inspect(other)} <% end %> """ end end