- 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>
605 lines
20 KiB
Elixir
605 lines
20 KiB
Elixir
defmodule DashboardWeb.HostsLive do
|
|
@moduledoc """
|
|
LiveView for real-time systant host monitoring.
|
|
"""
|
|
use DashboardWeb, :live_view
|
|
|
|
alias Phoenix.PubSub
|
|
|
|
@pubsub_topic "systant:hosts"
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
if connected?(socket) do
|
|
# Subscribe to host updates from MQTT
|
|
PubSub.subscribe(Dashboard.PubSub, @pubsub_topic)
|
|
end
|
|
|
|
# Start with empty hosts - will be populated by MQTT
|
|
hosts = %{}
|
|
|
|
socket =
|
|
socket
|
|
|> assign(:hosts, hosts)
|
|
|> assign(:show_raw_data, %{}) # Track which hosts show raw data
|
|
|> assign(:page_title, "Systant Hosts")
|
|
|
|
{:ok, socket}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:host_update, hostname, host_data}, socket) do
|
|
require Logger
|
|
Logger.info("LiveView received host update for #{hostname}: #{inspect(host_data)}")
|
|
updated_hosts = Map.put(socket.assigns.hosts, hostname, host_data)
|
|
{: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
|
|
~H"""
|
|
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
|
|
<div class="mx-auto max-w-xl lg:mx-0 lg:max-w-3xl">
|
|
<h1 class="text-brand mt-10 flex items-center text-sm font-semibold leading-6">
|
|
<.icon name="hero-computer-desktop" class="h-4 w-4" />
|
|
Systant Host Monitor
|
|
</h1>
|
|
<p class="text-[2rem] mt-4 font-semibold leading-10 tracking-tighter text-zinc-900">
|
|
Real-time system monitoring across all hosts
|
|
</p>
|
|
<p class="mt-4 text-base leading-7 text-zinc-600">
|
|
Live MQTT-powered dashboard showing statistics from all your systant-enabled hosts.
|
|
</p>
|
|
|
|
<div class="mt-10 grid gap-6">
|
|
<%= if Enum.empty?(@hosts) do %>
|
|
<div class="rounded-lg border border-zinc-200 p-8 text-center">
|
|
<.icon name="hero-signal-slash" class="mx-auto h-12 w-12 text-zinc-400" />
|
|
<h3 class="mt-4 text-lg font-semibold text-zinc-900">No hosts detected</h3>
|
|
<p class="mt-2 text-sm text-zinc-600">
|
|
Waiting for systant hosts to publish data via MQTT...
|
|
</p>
|
|
</div>
|
|
<% else %>
|
|
<%= for {hostname, host_data} <- @hosts do %>
|
|
<.host_card
|
|
hostname={hostname}
|
|
data={host_data}
|
|
show_raw={Map.get(@show_raw_data, hostname, false)}
|
|
/>
|
|
<% end %>
|
|
<% end %>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
"""
|
|
end
|
|
|
|
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">
|
|
<!-- 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" />
|
|
</div>
|
|
<div>
|
|
<h3 class="text-lg font-semibold text-zinc-900"><%= @hostname %></h3>
|
|
<p class="text-sm text-zinc-600">
|
|
Last seen: <%= format_datetime(@data["last_seen"]) %>
|
|
</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
|
|
|
|
defp format_datetime(%DateTime{} = datetime) do
|
|
Calendar.strftime(datetime, "%Y-%m-%d %H:%M:%S UTC")
|
|
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 |