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"""
Singletons
Chat Runners {length(node_runners)}
LiveViews
No active processes
<% end %>Refreshes every 1s or on node events.