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"""
<.link navigate={~p"/"} class="text-sm text-zinc-500 hover:text-zinc-900">← Back

{@host.name}

{sys_line(@slow)} · Uptime {uptime(@fast)} · Last seen {last_seen(@host.last_seen_at)}

status_class(@host.status)}> {@host.status}

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