diff --git a/dashboard/lib/dashboard_web/live/hosts_live.ex b/dashboard/lib/dashboard_web/live/hosts_live.ex index 7e26a56..f5a629a 100644 --- a/dashboard/lib/dashboard_web/live/hosts_live.ex +++ b/dashboard/lib/dashboard_web/live/hosts_live.ex @@ -21,6 +21,7 @@ defmodule DashboardWeb.HostsLive do socket = socket |> assign(:hosts, hosts) + |> assign(:show_raw_data, %{}) # Track which hosts show raw data |> assign(:page_title, "Systant Hosts") {:ok, socket} @@ -34,6 +35,13 @@ defmodule DashboardWeb.HostsLive do {:noreply, assign(socket, :hosts, updated_hosts)} end + @impl true + def handle_event("toggle_raw", %{"hostname" => hostname}, socket) do + current_state = Map.get(socket.assigns.show_raw_data, hostname, false) + updated_raw_data = Map.put(socket.assigns.show_raw_data, hostname, !current_state) + {:noreply, assign(socket, :show_raw_data, updated_raw_data)} + end + @impl true def render(assigns) do @@ -62,7 +70,11 @@ defmodule DashboardWeb.HostsLive do <% else %> <%= for {hostname, host_data} <- @hosts do %> - <.host_card hostname={hostname} data={host_data} /> + <.host_card + hostname={hostname} + data={host_data} + show_raw={Map.get(@show_raw_data, hostname, false)} + /> <% end %> <% end %> @@ -73,11 +85,15 @@ defmodule DashboardWeb.HostsLive do attr :hostname, :string, required: true attr :data, :map, required: true + attr :show_raw, :boolean, default: false defp host_card(assigns) do + assigns = assign(assigns, :show_raw, assigns[:show_raw] || false) + ~H"""
-
+ +
<.icon name="hero-server" class="h-5 w-5 text-green-600" /> @@ -89,17 +105,121 @@ defmodule DashboardWeb.HostsLive do

-
- Online +
+ +
+ Online +
- -
-

Raw Data:

-
-          <%= Jason.encode!(@data, pretty: true) %>
-        
-
+ + <%= if @show_raw do %> + +
+

Raw Data:

+
+            <%= Jason.encode!(@data, pretty: true) %>
+          
+
+ <% else %> + +
+ + <.metric_card + title="CPU Load Average" + icon="hero-cpu-chip" + data={@data["cpu"]} + type={:load_average} + /> + + + <.metric_card + title="Memory Usage" + icon="hero-circle-stack" + data={@data["memory"]} + type={:memory} + /> + + + <.metric_card + title="Disk Usage" + icon="hero-hard-drive" + data={@data["disk"]} + type={:disk} + /> + + + <%= if @data["gpu"] do %> + <.metric_card + title="GPU Status" + icon="hero-tv" + data={@data["gpu"]} + type={:gpu} + /> + <% end %> + + + <%= if @data["network"] && length(@data["network"]) > 0 do %> + <.metric_card + title="Network Interfaces" + icon="hero-signal" + data={@data["network"]} + type={:network} + /> + <% end %> + + + <%= if @data["temperature"] do %> + <.metric_card + title="Temperature" + icon="hero-fire" + data={@data["temperature"]} + type={:temperature} + /> + <% end %> +
+ + + <%= if @data["processes"] do %> +
+ <.metric_card + title="Top Processes" + icon="hero-list-bullet" + data={@data["processes"]} + type={:processes} + /> +
+ <% end %> + + +
+

System Information

+
+
+ Uptime: + <%= format_uptime(@data["system"]["uptime_seconds"]) %> +
+
+ Erlang: + <%= @data["system"]["erlang_version"] %> +
+
+ OTP: + <%= @data["system"]["otp_release"] %> +
+
+ Schedulers: + <%= @data["system"]["schedulers"] %> +
+
+
+ <% end %>
""" end @@ -109,4 +229,377 @@ defmodule DashboardWeb.HostsLive do end defp format_datetime(_), do: "Unknown" + + defp format_uptime(nil), do: "Unknown" + defp format_uptime(seconds) when is_integer(seconds) do + days = div(seconds, 86400) + hours = div(rem(seconds, 86400), 3600) + minutes = div(rem(seconds, 3600), 60) + + cond do + days > 0 -> "#{days}d #{hours}h #{minutes}m" + hours > 0 -> "#{hours}h #{minutes}m" + true -> "#{minutes}m" + end + end + defp format_uptime(_), do: "Unknown" + + attr :title, :string, required: true + attr :icon, :string, required: true + attr :data, :map, required: true + attr :type, :atom, required: true + + defp metric_card(assigns) do + ~H""" +
+
+ <.icon name={@icon} class="h-5 w-5 text-zinc-600" /> +

<%= @title %>

+
+ + <%= case @type do %> + <% :load_average -> %> + <.load_average_display data={@data} /> + <% :memory -> %> + <.memory_display data={@data} /> + <% :disk -> %> + <.disk_display data={@data} /> + <% :gpu -> %> + <.gpu_display data={@data} /> + <% :network -> %> + <.network_display data={@data} /> + <% :temperature -> %> + <.temperature_display data={@data} /> + <% :processes -> %> + <.processes_display data={@data} /> + <% _ -> %> +

No data available

+ <% end %> +
+ """ + end + + defp load_average_display(assigns) do + ~H""" + <%= if @data do %> +
+
+ 1 min + <%= format_float(@data["avg1"]) %> +
+ <.progress_bar value={@data["avg1"]} max={4.0} color={load_color(@data["avg1"])} /> + +
+ 5 min + <%= format_float(@data["avg5"]) %> +
+ <.progress_bar value={@data["avg5"]} max={4.0} color={load_color(@data["avg5"])} /> + +
+ 15 min + <%= format_float(@data["avg15"]) %> +
+ <.progress_bar value={@data["avg15"]} max={4.0} color={load_color(@data["avg15"])} /> +
+ <% else %> +

No load data available

+ <% end %> + """ + end + + defp memory_display(assigns) do + ~H""" + <%= if @data && @data["total_kb"] do %> +
+
+ Used + <%= @data["used_percent"] %>% +
+ <.progress_bar value={@data["used_percent"]} max={100} color={memory_color(@data["used_percent"])} /> + +
+
+ Total: + <%= format_kb(@data["total_kb"]) %> +
+
+ Used: + <%= format_kb(@data["used_kb"]) %> +
+
+ Available: + <%= format_kb(@data["available_kb"]) %> +
+
+
+ <% else %> +

No memory data available

+ <% end %> + """ + end + + defp disk_display(assigns) do + ~H""" + <%= if @data && @data["disks"] do %> +
+ <%= for disk <- @data["disks"] do %> +
+
+ <%= disk["mounted_on"] %> + <%= disk["use_percent"] %>% +
+ <.progress_bar value={disk["use_percent"]} max={100} color={disk_color(disk["use_percent"])} /> +
+ <%= disk["used"] %> used + <%= disk["available"] %> free +
+
+ <% end %> +
+ <% else %> +

No disk data available

+ <% end %> + """ + end + + attr :value, :any, required: true + attr :max, :any, required: true + attr :color, :string, default: "bg-blue-500" + + defp progress_bar(assigns) do + assigns = assign(assigns, :percentage, min(assigns.value / assigns.max * 100, 100)) + + ~H""" +
+
+
+
+ """ + end + + # Helper functions for formatting and colors + defp format_float(nil), do: "N/A" + defp format_float(value) when is_float(value), do: :erlang.float_to_binary(value, decimals: 2) + defp format_float(value), do: to_string(value) + + defp format_kb(nil), do: "N/A" + defp format_kb(kb) when is_integer(kb) do + cond do + kb >= 1_048_576 -> "#{Float.round(kb / 1_048_576, 1)} GB" + kb >= 1_024 -> "#{Float.round(kb / 1_024, 1)} MB" + true -> "#{kb} KB" + end + end + + defp load_color(load) when is_float(load) do + cond do + load >= 2.0 -> "bg-red-500" + load >= 1.0 -> "bg-yellow-500" + true -> "bg-green-500" + end + end + defp load_color(_), do: "bg-zinc-400" + + defp memory_color(percent) when is_float(percent) do + cond do + percent >= 90 -> "bg-red-500" + percent >= 75 -> "bg-yellow-500" + true -> "bg-blue-500" + end + end + defp memory_color(_), do: "bg-zinc-400" + + defp disk_color(percent) when is_integer(percent) do + cond do + percent >= 90 -> "bg-red-500" + percent >= 80 -> "bg-yellow-500" + true -> "bg-green-500" + end + end + defp disk_color(_), do: "bg-zinc-400" + + # GPU Display Component + defp gpu_display(assigns) do + ~H""" + <%= if @data do %> +
+ + <%= if @data["nvidia"] && length(@data["nvidia"]) > 0 do %> +
NVIDIA
+ <%= for gpu <- @data["nvidia"] do %> +
+
+ <%= gpu["name"] %> + <%= gpu["utilization_percent"] %>% +
+ <.progress_bar value={gpu["utilization_percent"]} max={100} color={gpu_color(gpu["utilization_percent"])} /> +
+ <%= gpu["temperature_c"] %>°C + <%= format_mb(gpu["memory_used_mb"]) %>/<%= format_mb(gpu["memory_total_mb"]) %> +
+
+ <% end %> + <% end %> + + + <%= if @data["amd"] && length(@data["amd"]) > 0 do %> +
AMD
+ <%= for gpu <- @data["amd"] do %> +
+
+ <%= gpu["name"] %> + <%= gpu["utilization_percent"] || "N/A" %>% +
+ <.progress_bar value={gpu["utilization_percent"] || 0} max={100} color={gpu_color(gpu["utilization_percent"])} /> +
+ <%= format_float(gpu["temperature_c"]) %>°C +
+
+ <% end %> + <% end %> + + <%= if (length(@data["nvidia"] || []) + length(@data["amd"] || [])) == 0 do %> +

No GPUs detected

+ <% end %> +
+ <% else %> +

No GPU data available

+ <% end %> + """ + end + + # Network Display Component + defp network_display(assigns) do + ~H""" + <%= if @data && length(@data) > 0 do %> +
+ <%= for interface <- Enum.take(@data, 3) do %> +
+
+ <%= interface["interface"] %> +
+
+
+ <%= format_bytes(interface["rx_bytes"]) %> +
+
+ <%= format_bytes(interface["tx_bytes"]) %> +
+
+ <%= if (interface["rx_errors"] + interface["tx_errors"]) > 0 do %> +
+ Errors: RX <%= interface["rx_errors"] %>, TX <%= interface["tx_errors"] %> +
+ <% end %> +
+ <% end %> +
+ <% else %> +

No network interfaces

+ <% end %> + """ + end + + # Temperature Display Component + defp temperature_display(assigns) do + ~H""" + <%= if @data do %> +
+ + <%= if @data["cpu"] do %> +
+
+ CPU + <%= format_float(@data["cpu"]) %>°C +
+
+ <% end %> + + + <%= if @data["sensors"] && map_size(@data["sensors"]) > 0 do %> + <%= for {chip_name, temps} <- Enum.take(@data["sensors"], 3) do %> +
<%= chip_name %>
+ <%= for {sensor, temp} <- Enum.take(temps, 2) do %> +
+ <%= sensor %> + <%= format_float(temp) %>°C +
+ <% end %> + <% end %> + <% end %> + + <%= if !@data["cpu"] && (!@data["sensors"] || map_size(@data["sensors"]) == 0) do %> +

No temperature sensors

+ <% end %> +
+ <% else %> +

No temperature data

+ <% end %> + """ + end + + # Processes Display Component + defp processes_display(assigns) do + ~H""" + <%= if @data && length(@data) > 0 do %> +
+ <%= for process <- Enum.take(@data, 8) do %> +
+
+
<%= process["command"] %>
+
<%= process["user"] %> (PID <%= process["pid"] %>)
+
+
+
<%= format_float(process["cpu_percent"]) %>%
+
<%= format_float(process["memory_percent"]) %>%
+
+
+ <% end %> +
+ <% else %> +

No process data

+ <% end %> + """ + end + + # Additional helper functions + defp format_mb(nil), do: "N/A" + defp format_mb(mb) when is_integer(mb) do + cond do + mb >= 1024 -> "#{Float.round(mb / 1024, 1)} GB" + true -> "#{mb} MB" + end + end + + defp format_bytes(bytes) when is_integer(bytes) do + cond do + bytes >= 1_073_741_824 -> "#{Float.round(bytes / 1_073_741_824, 1)} GB" + bytes >= 1_048_576 -> "#{Float.round(bytes / 1_048_576, 1)} MB" + bytes >= 1_024 -> "#{Float.round(bytes / 1_024, 1)} KB" + true -> "#{bytes} B" + end + end + defp format_bytes(_), do: "N/A" + + defp gpu_color(util) when is_integer(util) do + cond do + util >= 80 -> "bg-red-500" + util >= 50 -> "bg-yellow-500" + true -> "bg-green-500" + end + end + defp gpu_color(_), do: "bg-zinc-400" + + defp temp_color(temp) when is_number(temp) do + cond do + temp >= 80 -> "red-600" + temp >= 70 -> "yellow-600" + temp >= 60 -> "yellow-500" + true -> "green-600" + end + end + defp temp_color(_), do: "zinc-500" end \ No newline at end of file