feat(ui): detailed per-pool block with type, capacity bar, scrub state

This commit is contained in:
Carsten 2026-04-22 17:55:07 +02:00
parent 612091ff1e
commit f05c20ed0b
2 changed files with 75 additions and 14 deletions

View file

@ -66,21 +66,35 @@ defmodule ServerWeb.HostDetailLive do
<header><span>ZFS pools</span><span class="mono">{length(pools(@fast))}</span></header> <header><span>ZFS pools</span><span class="mono">{length(pools(@fast))}</span></header>
<div class="body tight"> <div class="body tight">
<div :if={pools(@fast) == []} class="empty">No data.</div> <div :if={pools(@fast) == []} class="empty">No data.</div>
<div :for={pool <- pools(@fast)} class="pool-row" style="padding: 0.6rem 0.9rem;"> <div :for={pool <- pools(@fast)} class="pool-block">
<div> <div class="head">
<span class="mono" style="color: var(--fg-bright); font-weight: 600;"> <div>
{pool["name"]} <span class="mono" style="color: var(--fg-bright); font-weight: 600;">{pool["name"]}</span>
</span> <span class="layout">{pool_layout(pool)}</span>
<span class="badge" style={pool_badge_style(pool["health"])}>{pool["health"]}</span>
<div class="details">
cap {pool["capacity_percent"]}% ·
frag {pool["fragmentation_percent"] || 0}% ·
err {pool["error_count"] || 0} ·
vdevs {pool["vdev_count"] || 0} (deg {pool["degraded_vdev_count"] || 0})
</div> </div>
<span class="badge" style={pool_badge_style(pool["health"])}>{pool["health"]}</span>
</div> </div>
<div class="mono muted" style="font-size: 0.75rem; align-self: center; text-align: right;">
scrub<br/>{pool["last_scrub_end"] || "never"} <div class="capbar" data-level={capbar_level(pool["capacity_percent"])}>
<span style={"width: #{pool["capacity_percent"] || 0}%"}></span>
</div>
<div class="sizes">
used {format_bytes(pool["allocated_bytes"] || 0)} ·
free {format_bytes(pool["free_bytes"] || 0)} ·
total {format_bytes(pool["size_bytes"] || 0)}
<span class="muted">({pool["capacity_percent"] || 0}%)</span>
</div>
<div class="details">
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)}
</div>
<div :for={v <- degraded_vdevs(pool)} class="callout err" style="margin-top: 0.4rem;">
{v["name"]} {v["state"]} · r={v["read_errors"]} w={v["write_errors"]} cksum={v["checksum_errors"]}
</div> </div>
</div> </div>
</div> </div>
@ -155,6 +169,34 @@ defmodule ServerWeb.HostDetailLive do
defp pool_badge_style("ONLINE"), do: "color: var(--ok);" defp pool_badge_style("ONLINE"), do: "color: var(--ok);"
defp pool_badge_style(_), do: "color: var(--crit);" 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 #{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(nil), do: ""
defp sys_line(%{payload: p}) do defp sys_line(%{payload: p}) do
get_in(p, ["system_info", "pve_version"]) || "" get_in(p, ["system_info", "pve_version"]) || ""

View file

@ -17,9 +17,22 @@ defmodule ServerWeb.HostDetailLiveTest do
%{ %{
"name" => "rpool", "name" => "rpool",
"health" => "ONLINE", "health" => "ONLINE",
"pool_type" => "mirror",
"size_bytes" => 500_000_000_000,
"allocated_bytes" => 200_000_000_000,
"free_bytes" => 300_000_000_000,
"capacity_percent" => 40, "capacity_percent" => 40,
"fragmentation_percent" => 17,
"error_count" => 0, "error_count" => 0,
"last_scrub_end" => "Sat Apr 19 02:00:00 2026" "vdev_count" => 1,
"degraded_vdev_count" => 0,
"scan_function" => "scrub",
"scan_state" => "FINISHED",
"last_scrub_end" => "Sat Apr 19 02:00:00 2026",
"vdevs" => [
%{"name" => "mirror-0", "type" => "mirror", "state" => "ONLINE",
"read_errors" => 0, "write_errors" => 0, "checksum_errors" => 0}
]
} }
] ]
}, },
@ -75,6 +88,12 @@ defmodule ServerWeb.HostDetailLiveTest do
assert html =~ "nginx" assert html =~ "nginx"
assert html =~ "rpool/data" assert html =~ "rpool/data"
assert html =~ "local" assert html =~ "local"
assert html =~ "mirror"
assert html =~ "465.7 GB" # size_bytes formatted
assert html =~ "186.3 GB" # allocated_bytes formatted
assert html =~ "279.4 GB" # free_bytes formatted
assert html =~ "capbar"
assert html =~ "scrub"
end end
test "404 for unknown host", %{conn: conn} do test "404 for unknown host", %{conn: conn} do