defmodule ServerWeb.HostDetailLive do
use ServerWeb, :live_view
alias Server.{Metrics, Repo, Schema.Host}
@impl true
def mount(%{"name" => name}, _session, socket) do
case Repo.get_by(Host, name: name) do
nil ->
{:ok,
socket
|> put_flash(:error, "Host not found")
|> push_navigate(to: ~p"/")}
%Host{} = host ->
if connected?(socket),
do: Phoenix.PubSub.subscribe(Server.PubSub, "metrics:#{host.id}")
{:ok, socket |> assign(:host, host) |> load_samples()}
end
end
@impl true
def handle_info({:metric_inserted, _host_id, _interval}, socket) do
{:noreply, load_samples(socket)}
end
defp load_samples(socket) do
host_id = socket.assigns.host.id
assign(socket,
fast: Metrics.latest_sample(host_id, "fast"),
medium: Metrics.latest_sample(host_id, "medium"),
slow: Metrics.latest_sample(host_id, "slow")
)
end
@impl true
def render(assigns) do
~H"""
Host metrics
<.metric_row label="Load (1/5/15)" value={host_load(@fast)} />
<.metric_row label="Memory" value={host_mem(@fast)} />
ZFS pools
No data.
{pool["name"]}
{pool["health"]}
Capacity {pool["capacity_percent"]}% · Fragmentation {pool["fragmentation_percent"] || 0}% · Errors {pool[
"error_count"
] || 0} · vdevs {pool["vdev_count"] || 0} (degraded {pool["degraded_vdev_count"] || 0}) · Last scrub {pool[
"last_scrub_end"
] || "never"}
Snapshots
| Dataset |
Count |
Oldest |
Newest |
| {ds["name"]} |
{ds["snapshot_count"]} |
{unix_to_date(ds["oldest_snapshot_unix"])} |
{unix_to_date(ds["newest_snapshot_unix"])} |
No data.
Storage
| Name |
Type |
Usage |
| {s["name"]} |
{s["type"]} |
{storage_usage(s)} |
No data.
VMs / LXCs
| VMID |
Name |
Type |
Status |
| {vm["vmid"]} |
{vm["name"]} |
{vm["type"]} |
{vm["status"]} |
No data.
"""
end
attr :label, :string, required: true
attr :value, :string, required: true
def metric_row(assigns) do
~H"""
{@label}
{@value}
"""
end
defp status_class("online"), do: "text-green-600"
defp status_class("offline"), do: "text-zinc-500"
defp status_class(_), do: "text-zinc-500"
defp pool_class("ONLINE"), do: "text-green-600 font-mono"
defp pool_class(_), do: "text-red-600 font-mono"
defp sys_line(nil), do: "—"
defp sys_line(%{payload: p}) do
get_in(p, ["system_info", "pve_version"]) || "—"
end
defp uptime(nil), do: "—"
defp uptime(%{payload: p}) do
case get_in(p, ["host", "uptime_seconds"]) do
nil -> "—"
s when is_integer(s) -> "#{div(s, 86_400)}d"
_ -> "—"
end
end
defp last_seen(nil), do: "never"
defp last_seen(%DateTime{} = dt) do
secs = DateTime.diff(DateTime.utc_now(), dt, :second)
cond do
secs < 60 -> "#{secs}s ago"
secs < 3600 -> "#{div(secs, 60)}m ago"
true -> "#{div(secs, 3600)}h ago"
end
end
defp host_load(nil), do: "—"
defp host_load(%{payload: p}) do
l1 = get_in(p, ["host", "load1"]) || "—"
l5 = get_in(p, ["host", "load5"]) || "—"
l15 = get_in(p, ["host", "load15"]) || "—"
"#{l1} / #{l5} / #{l15}"
end
defp host_mem(nil), do: "—"
defp host_mem(%{payload: p}) do
used = get_in(p, ["host", "mem_used_bytes"])
total = get_in(p, ["host", "mem_total_bytes"])
case {used, total} do
{u, t} when is_integer(u) and is_integer(t) and t > 0 ->
"#{Float.round(u / t * 100, 1)}% (#{format_bytes(u)} / #{format_bytes(t)})"
_ ->
"—"
end
end
defp pools(nil), do: []
defp pools(%{payload: p}), do: get_in(p, ["zfs_pools", "pools"]) || []
defp datasets(nil), do: []
defp datasets(%{payload: p}), do: get_in(p, ["zfs_datasets", "datasets"]) || []
defp storages(nil), do: []
defp storages(%{payload: p}), do: get_in(p, ["storage", "storages"]) || []
defp vms(nil), do: []
defp vms(%{payload: p}), do: get_in(p, ["vms_runtime", "vms"]) || []
defp storage_usage(%{"used_bytes" => u, "total_bytes" => t})
when is_integer(u) and is_integer(t) and t > 0 do
"#{Float.round(u / t * 100, 1)}% (#{format_bytes(u)} / #{format_bytes(t)})"
end
defp storage_usage(_), do: "—"
defp unix_to_date(nil), do: "—"
defp unix_to_date(unix) when is_integer(unix) do
case DateTime.from_unix(unix) do
{:ok, dt} -> Calendar.strftime(dt, "%Y-%m-%d")
_ -> "—"
end
end
defp format_bytes(n) when is_integer(n) do
units = ["B", "KB", "MB", "GB", "TB"]
{val, unit} =
Enum.reduce_while(units, {n * 1.0, "B"}, fn u, {v, _} ->
if v < 1024, do: {:halt, {v, u}}, else: {:cont, {v / 1024, u}}
end)
"#{Float.round(val, 1)} #{unit}"
end
end