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, page_title: host.name) |> 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.name}

{sys_line(@slow)} · uptime {uptime(@fast)} · last seen {last_seen(@host.last_seen_at)}
{@host.status}
Host
load (1/5/15)
{host_load(@fast)}
memory
{host_mem(@fast)}
agent
{@host.agent_version || "—"}
ZFS pools{length(pools(@fast))}
No data.
{pool["name"]} {pool_layout(pool)}
{pool["health"]}
used {format_bytes(pool["allocated_bytes"] || 0)} · free {format_bytes(pool["free_bytes"] || 0)} · total {format_bytes(pool["size_bytes"] || 0)} ({pool["capacity_percent"] || 0}%)
frag {pool["fragmentation_percent"] || 0}% · err {pool["error_count"] || 0} · vdevs {pool["vdev_count"] || 0} (deg {pool["degraded_vdev_count"] || 0}) · {pool_scrub_line(pool)}
{v["name"]} {v["state"]} · r={v["read_errors"]} w={v["write_errors"]} cksum={v["checksum_errors"]}
Snapshots{length(datasets(@medium))}
DatasetCountOldestNewest
{ds["name"]} {ds["snapshot_count"]} {unix_to_date(ds["oldest_snapshot_unix"])} {unix_to_date(ds["newest_snapshot_unix"])}
No data.
Storage{length(storages(@fast))}
NameTypeUsage
{s["name"]} {s["type"]} {storage_usage(s)}
No data.
VMs / LXCs{length(vms(@fast))}
VMIDNameTypeStatus
{vm["vmid"]} {vm["name"]} {vm["type"]} {vm["status"]}
No data.
""" end defp vm_status(%{"status" => "running"}), do: "ok" defp vm_status(%{"status" => "stopped"}), do: "offline" defp vm_status(_), do: "warning" defp pool_badge_style("ONLINE"), do: "color: var(--ok);" defp pool_badge_style(_), do: "color: var(--crit);" defp pool_layout(pool) do case pool["pool_type"] do nil -> "—" "" -> "—" t -> t end end defp capbar_level(cap) when is_number(cap) and cap >= 90, do: "crit" defp capbar_level(cap) when is_number(cap) and cap >= 80, do: "warn" defp capbar_level(_), do: "ok" defp pool_scrub_line(%{"scan_state" => "SCANNING"}), do: "scrub scanning" defp pool_scrub_line(%{"scan_state" => "FINISHED", "last_scrub_end" => end_time}) when is_binary(end_time) and end_time != "", do: "scrub finished #{end_time}" defp pool_scrub_line(%{"last_scrub_end" => end_time}) when is_binary(end_time) and end_time != "", do: "scrub #{end_time}" defp pool_scrub_line(_), do: "scrub never" defp degraded_vdevs(pool) do (pool["vdevs"] || []) |> Enum.filter(fn v -> Map.get(v, "state") not in [nil, "ONLINE"] end) end 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