defmodule DalaDev.Server.ObserverLive do @moduledoc """ Main Observer dashboard with navigation to specialized views. Provides an overview of all nodes and quick access to detailed views for system info, processes, ETS tables, applications, etc. """ use Phoenix.LiveView, layout: {DalaDev.Server.Layouts, :app} alias DalaDev.Observer @refresh_interval 5_000 def mount(_params, _session, socket) do if connected?(socket) do :timer.send_interval(@refresh_interval, self(), :refresh) end socket = socket |> assign(:node, Node.self()) |> assign(:available_nodes, [Node.self() | Node.list()]) |> assign(:summary, nil) |> assign(:error, nil) |> assign(:loading, false) {:ok, fetch_summary(socket)} end def handle_params(%{"node" => node_str}, _uri, socket) do try do node = String.to_existing_atom(":#{node_str}") {:noreply, assign(socket, :node, node) |> fetch_summary()} rescue _ -> {:noreply, assign(socket, :error, "Invalid node name: #{node_str}")} end end def handle_params(_params, _uri, socket) do {:noreply, socket} end def handle_info(:refresh, socket) do {:noreply, fetch_summary(socket)} end def handle_event("refresh", _params, socket) do {:noreply, fetch_summary(socket)} end def handle_event("select_node", %{"node" => node_str}, socket) do try do node = String.to_existing_atom(node_str) {:noreply, assign(socket, :node, node) |> fetch_summary()} rescue _ -> {:noreply, assign(socket, :error, "Invalid node: #{node_str}")} end end def render(assigns) do ~H"""

Observer: <%= @node %>

<%= length(@available_nodes) %> node(s) available
<%= if @error do %>

<%= @error %>

<% end %> <%= if @loading do %>
Loading...
<% end %> <%= if @summary do %>
Processes
<%= @summary.process_count || 0 %>
ETS Tables
<%= @summary.ets_tables_count || 0 %>
Memory (Total)
<%= format_bytes(@summary.memory_total) %>
Uptime
<%= format_uptime(@summary.uptime_ms) %>
<% end %>

System Info

Memory, uptime, version, and system statistics

Processes

Process list with sorting, filtering, and details

ETS Tables

ETS tables browser with memory and ownership info

Applications

Running applications with versions

Modules

Loaded modules and memory usage

Ports

Port information and I/O statistics

System Load

Scheduler usage and I/O statistics

Tracing

Process tracing and message flow analysis

""" end defp fetch_summary(socket) do node = socket.assigns[:node] socket = socket |> assign(:loading, true) |> assign(:available_nodes, [Node.self() | Node.list()]) case Observer.observe(node) do {:ok, data} -> system = data[:system] || %{} memory = system[:memory] || %{} summary = %{ process_count: system[:process_count] || 0, ets_tables_count: system[:ets_tables_count] || 0, memory_total: memory[:total] || 0, uptime_ms: system[:uptime_ms] || 0 } assign(socket, :summary, summary) |> assign(:error, nil) |> assign(:loading, false) {:error, reason} -> assign(socket, :error, "Failed to observe #{inspect(node)}: #{reason}") |> assign(:loading, false) end end defp format_bytes(nil), do: "0 B" defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" defp format_bytes(bytes) when bytes < 1024 * 1024, do: "#{Float.round(bytes / 1024, 1)} KB" defp format_bytes(bytes) when bytes < 1024 * 1024 * 1024, do: "#{Float.round(bytes / (1024 * 1024), 1)} MB" defp format_bytes(bytes), do: "#{Float.round(bytes / (1024 * 1024 * 1024), 1)} GB" defp format_uptime(nil), do: "N/A" defp format_uptime(ms) do seconds = div(ms, 1000) minutes = div(seconds, 60) hours = div(minutes, 60) days = div(hours, 24) cond do days > 0 -> "#{days}d #{rem(hours, 24)}h" hours > 0 -> "#{hours}h #{rem(minutes, 60)}m" minutes > 0 -> "#{minutes}m #{rem(seconds, 60)}s" true -> "#{seconds}s" end end end