Enhanced dashboard with comprehensive metrics display

- Add GPU metrics display for NVIDIA and AMD cards with utilization, temperature, and memory
- Add Network interfaces display with RX/TX bytes and error tracking
- Add Temperature monitoring with CPU and sensor data
- Add Top processes display with CPU/memory usage
- Implement color-coded temperature indicators
- Add data formatting helpers for bytes, MB/GB, and percentages
- Conditional rendering for available metric modules
- Enhanced grid layout for optimal metrics viewing

Dashboard now displays all metric modules from the configuration system:
CPU load, memory usage, disk usage, GPU status, network I/O, temperatures, and top processes.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ryan 2025-08-05 21:06:50 -07:00
parent b1cd085f6b
commit 616e3f3765

View File

@ -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
</div>
<% 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 %>
</div>
@ -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"""
<div class="rounded-lg border border-zinc-200 bg-white p-6 shadow-sm">
<div class="flex items-center justify-between">
<!-- Host Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center space-x-3">
<div class="rounded-full bg-green-100 p-2">
<.icon name="hero-server" class="h-5 w-5 text-green-600" />
@ -89,17 +105,121 @@ defmodule DashboardWeb.HostsLive do
</p>
</div>
</div>
<div class="flex items-center space-x-2">
<button
phx-click="toggle_raw"
phx-value-hostname={@hostname}
class="text-xs px-2 py-1 rounded border border-zinc-300 hover:bg-zinc-50"
>
<%= if @show_raw, do: "Hide Raw", else: "Show Raw" %>
</button>
<div class="rounded-full bg-green-100 px-3 py-1">
<span class="text-xs font-medium text-green-800">Online</span>
</div>
</div>
</div>
<%= if @show_raw do %>
<!-- Raw Data View -->
<div class="mt-4">
<h4 class="text-sm font-medium text-zinc-700 mb-2">Raw Data:</h4>
<pre class="text-xs bg-zinc-50 p-3 rounded border overflow-x-auto">
<%= Jason.encode!(@data, pretty: true) %>
</pre>
</div>
<% else %>
<!-- Graphical Dashboard View -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<!-- CPU Load Averages -->
<.metric_card
title="CPU Load Average"
icon="hero-cpu-chip"
data={@data["cpu"]}
type={:load_average}
/>
<!-- Memory Usage -->
<.metric_card
title="Memory Usage"
icon="hero-circle-stack"
data={@data["memory"]}
type={:memory}
/>
<!-- Disk Usage -->
<.metric_card
title="Disk Usage"
icon="hero-hard-drive"
data={@data["disk"]}
type={:disk}
/>
<!-- GPU Metrics -->
<%= if @data["gpu"] do %>
<.metric_card
title="GPU Status"
icon="hero-tv"
data={@data["gpu"]}
type={:gpu}
/>
<% end %>
<!-- Network Interfaces -->
<%= if @data["network"] && length(@data["network"]) > 0 do %>
<.metric_card
title="Network Interfaces"
icon="hero-signal"
data={@data["network"]}
type={:network}
/>
<% end %>
<!-- Temperature Sensors -->
<%= if @data["temperature"] do %>
<.metric_card
title="Temperature"
icon="hero-fire"
data={@data["temperature"]}
type={:temperature}
/>
<% end %>
</div>
<!-- Additional Metrics Row -->
<%= if @data["processes"] do %>
<div class="mt-6">
<.metric_card
title="Top Processes"
icon="hero-list-bullet"
data={@data["processes"]}
type={:processes}
/>
</div>
<% end %>
<!-- System Info -->
<div class="mt-6 p-4 bg-zinc-50 rounded-lg">
<h4 class="text-sm font-medium text-zinc-700 mb-2">System Information</h4>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-zinc-600">Uptime:</span>
<span class="ml-1 font-medium"><%= format_uptime(@data["system"]["uptime_seconds"]) %></span>
</div>
<div>
<span class="text-zinc-600">Erlang:</span>
<span class="ml-1 font-medium"><%= @data["system"]["erlang_version"] %></span>
</div>
<div>
<span class="text-zinc-600">OTP:</span>
<span class="ml-1 font-medium"><%= @data["system"]["otp_release"] %></span>
</div>
<div>
<span class="text-zinc-600">Schedulers:</span>
<span class="ml-1 font-medium"><%= @data["system"]["schedulers"] %></span>
</div>
</div>
</div>
<% end %>
</div>
"""
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"""
<div class="bg-white border border-zinc-200 rounded-lg p-4">
<div class="flex items-center space-x-2 mb-3">
<.icon name={@icon} class="h-5 w-5 text-zinc-600" />
<h4 class="font-medium text-zinc-900"><%= @title %></h4>
</div>
<%= 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} />
<% _ -> %>
<p class="text-sm text-zinc-500">No data available</p>
<% end %>
</div>
"""
end
defp load_average_display(assigns) do
~H"""
<%= if @data do %>
<div class="space-y-2">
<div class="flex justify-between items-center">
<span class="text-sm text-zinc-600">1 min</span>
<span class="font-mono text-sm"><%= format_float(@data["avg1"]) %></span>
</div>
<.progress_bar value={@data["avg1"]} max={4.0} color={load_color(@data["avg1"])} />
<div class="flex justify-between items-center">
<span class="text-sm text-zinc-600">5 min</span>
<span class="font-mono text-sm"><%= format_float(@data["avg5"]) %></span>
</div>
<.progress_bar value={@data["avg5"]} max={4.0} color={load_color(@data["avg5"])} />
<div class="flex justify-between items-center">
<span class="text-sm text-zinc-600">15 min</span>
<span class="font-mono text-sm"><%= format_float(@data["avg15"]) %></span>
</div>
<.progress_bar value={@data["avg15"]} max={4.0} color={load_color(@data["avg15"])} />
</div>
<% else %>
<p class="text-sm text-zinc-500">No load data available</p>
<% end %>
"""
end
defp memory_display(assigns) do
~H"""
<%= if @data && @data["total_kb"] do %>
<div class="space-y-2">
<div class="flex justify-between items-center">
<span class="text-sm text-zinc-600">Used</span>
<span class="font-mono text-sm"><%= @data["used_percent"] %>%</span>
</div>
<.progress_bar value={@data["used_percent"]} max={100} color={memory_color(@data["used_percent"])} />
<div class="text-xs text-zinc-500 space-y-1">
<div class="flex justify-between">
<span>Total:</span>
<span><%= format_kb(@data["total_kb"]) %></span>
</div>
<div class="flex justify-between">
<span>Used:</span>
<span><%= format_kb(@data["used_kb"]) %></span>
</div>
<div class="flex justify-between">
<span>Available:</span>
<span><%= format_kb(@data["available_kb"]) %></span>
</div>
</div>
</div>
<% else %>
<p class="text-sm text-zinc-500">No memory data available</p>
<% end %>
"""
end
defp disk_display(assigns) do
~H"""
<%= if @data && @data["disks"] do %>
<div class="space-y-3">
<%= for disk <- @data["disks"] do %>
<div class="space-y-1">
<div class="flex justify-between items-center">
<span class="text-xs text-zinc-600 truncate"><%= disk["mounted_on"] %></span>
<span class="font-mono text-xs"><%= disk["use_percent"] %>%</span>
</div>
<.progress_bar value={disk["use_percent"]} max={100} color={disk_color(disk["use_percent"])} />
<div class="flex justify-between text-xs text-zinc-500">
<span><%= disk["used"] %> used</span>
<span><%= disk["available"] %> free</span>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-sm text-zinc-500">No disk data available</p>
<% 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"""
<div class="w-full bg-zinc-200 rounded-full h-2">
<div
class={"h-2 rounded-full transition-all duration-300 #{@color}"}
style={"width: #{@percentage}%"}
>
</div>
</div>
"""
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 %>
<div class="space-y-3">
<!-- NVIDIA GPUs -->
<%= if @data["nvidia"] && length(@data["nvidia"]) > 0 do %>
<div class="text-xs text-zinc-600 font-medium mb-2">NVIDIA</div>
<%= for gpu <- @data["nvidia"] do %>
<div class="space-y-1">
<div class="flex justify-between items-center">
<span class="text-xs text-zinc-600 truncate"><%= gpu["name"] %></span>
<span class="font-mono text-xs"><%= gpu["utilization_percent"] %>%</span>
</div>
<.progress_bar value={gpu["utilization_percent"]} max={100} color={gpu_color(gpu["utilization_percent"])} />
<div class="flex justify-between text-xs text-zinc-500">
<span><%= gpu["temperature_c"] %>°C</span>
<span><%= format_mb(gpu["memory_used_mb"]) %>/<%= format_mb(gpu["memory_total_mb"]) %></span>
</div>
</div>
<% end %>
<% end %>
<!-- AMD GPUs -->
<%= if @data["amd"] && length(@data["amd"]) > 0 do %>
<div class="text-xs text-zinc-600 font-medium mb-2">AMD</div>
<%= for gpu <- @data["amd"] do %>
<div class="space-y-1">
<div class="flex justify-between items-center">
<span class="text-xs text-zinc-600 truncate"><%= gpu["name"] %></span>
<span class="font-mono text-xs"><%= gpu["utilization_percent"] || "N/A" %>%</span>
</div>
<.progress_bar value={gpu["utilization_percent"] || 0} max={100} color={gpu_color(gpu["utilization_percent"])} />
<div class="text-xs text-zinc-500">
<span><%= format_float(gpu["temperature_c"]) %>°C</span>
</div>
</div>
<% end %>
<% end %>
<%= if (length(@data["nvidia"] || []) + length(@data["amd"] || [])) == 0 do %>
<p class="text-sm text-zinc-500">No GPUs detected</p>
<% end %>
</div>
<% else %>
<p class="text-sm text-zinc-500">No GPU data available</p>
<% end %>
"""
end
# Network Display Component
defp network_display(assigns) do
~H"""
<%= if @data && length(@data) > 0 do %>
<div class="space-y-3">
<%= for interface <- Enum.take(@data, 3) do %>
<div class="space-y-1">
<div class="flex justify-between items-center">
<span class="text-xs text-zinc-600 font-medium"><%= interface["interface"] %></span>
</div>
<div class="grid grid-cols-2 gap-2 text-xs">
<div class="text-zinc-500">
<span class="text-green-600"></span> <%= format_bytes(interface["rx_bytes"]) %>
</div>
<div class="text-zinc-500">
<span class="text-blue-600"></span> <%= format_bytes(interface["tx_bytes"]) %>
</div>
</div>
<%= if (interface["rx_errors"] + interface["tx_errors"]) > 0 do %>
<div class="text-xs text-red-500">
Errors: RX <%= interface["rx_errors"] %>, TX <%= interface["tx_errors"] %>
</div>
<% end %>
</div>
<% end %>
</div>
<% else %>
<p class="text-sm text-zinc-500">No network interfaces</p>
<% end %>
"""
end
# Temperature Display Component
defp temperature_display(assigns) do
~H"""
<%= if @data do %>
<div class="space-y-3">
<!-- CPU Temperature -->
<%= if @data["cpu"] do %>
<div class="space-y-1">
<div class="flex justify-between items-center">
<span class="text-xs text-zinc-600">CPU</span>
<span class={"font-mono text-xs text-#{temp_color(@data["cpu"])}"}><%= format_float(@data["cpu"]) %>°C</span>
</div>
</div>
<% end %>
<!-- Sensor Data -->
<%= if @data["sensors"] && map_size(@data["sensors"]) > 0 do %>
<%= for {chip_name, temps} <- Enum.take(@data["sensors"], 3) do %>
<div class="text-xs text-zinc-600 font-medium"><%= chip_name %></div>
<%= for {sensor, temp} <- Enum.take(temps, 2) do %>
<div class="flex justify-between items-center">
<span class="text-xs text-zinc-500"><%= sensor %></span>
<span class={"font-mono text-xs text-#{temp_color(temp)}"}><%= format_float(temp) %>°C</span>
</div>
<% end %>
<% end %>
<% end %>
<%= if !@data["cpu"] && (!@data["sensors"] || map_size(@data["sensors"]) == 0) do %>
<p class="text-sm text-zinc-500">No temperature sensors</p>
<% end %>
</div>
<% else %>
<p class="text-sm text-zinc-500">No temperature data</p>
<% end %>
"""
end
# Processes Display Component
defp processes_display(assigns) do
~H"""
<%= if @data && length(@data) > 0 do %>
<div class="space-y-2">
<%= for process <- Enum.take(@data, 8) do %>
<div class="flex justify-between items-center text-xs">
<div class="flex-1 min-w-0">
<div class="truncate font-mono text-zinc-700"><%= process["command"] %></div>
<div class="text-zinc-500"><%= process["user"] %> (PID <%= process["pid"] %>)</div>
</div>
<div class="text-right ml-2">
<div class="font-mono text-zinc-700"><%= format_float(process["cpu_percent"]) %>%</div>
<div class="text-zinc-500"><%= format_float(process["memory_percent"]) %>%</div>
</div>
</div>
<% end %>
</div>
<% else %>
<p class="text-sm text-zinc-500">No process data</p>
<% 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