defmodule Bloccs.Web.Panels.Metrics do @moduledoc """ Panel 3 — live per-node metrics. Renders one row per node from the latest collector `frame` (throughput, p50/p95 latency, completed, error rate) with a state pill. The frame arrives on a 1 Hz PubSub tick handled by `Bloccs.Web.DashboardLive`; idle nodes show "—" until traffic flows. """ use Bloccs.Web, :html alias Bloccs.Web.Format attr :network, :any, required: true attr :base_path, :string, required: true attr :frame, :map, default: %{nodes: %{}, updated_at: nil} def render(assigns) do assigns = assign(assigns, :rows, rows(assigns.network, assigns.frame)) ~H"""

Live metrics

{header_note(@frame)}
Node State Throughput p50 p95 Completed Errors
<.hex_glyph glyph={node.glyph} state={state(m)} /> {node.id} <.status_pill state={state(m)} /> {Format.rate(m && m.throughput)} {Format.latency(m && m.p50)} {Format.latency(m && m.p95)} {Format.count(m && m.completed)} {errors(m)}

Updates live as messages flow through the network (1 Hz). Idle nodes show "—".

""" end defp rows(network, frame) do Enum.map(network.nodes, fn node -> {node, Map.get(frame.nodes, node.id)} end) end defp state(nil), do: :idle defp state(%{state: state}), do: state defp errors(nil), do: "—" defp errors(%{errors: 0}), do: "0" defp errors(%{errors: n, completed: c}), do: "#{n} (#{Format.percent(safe_rate(n, c))})" defp error_class(%{errors: n}) when n > 0, do: "bloccs-num--error" defp error_class(_), do: nil defp safe_rate(_n, 0), do: 0.0 defp safe_rate(n, c), do: n / c defp header_note(%{updated_at: nil}), do: "waiting for traffic" defp header_note(%{nodes: nodes}) when map_size(nodes) == 0, do: "waiting for traffic" defp header_note(_), do: "live" end