defmodule Bloccs.Web.Panels.Metrics do @moduledoc """ Panel 3 — live per-node metrics. One row per node from the latest collector `frame`: a throughput sparkline + rate, a visual p50/p95 latency bar, completed count, and error rate, with a state pill. A totals row sums the network. The frame arrives on a 1 Hz PubSub tick handled by `Bloccs.Web.DashboardLive`; idle nodes show "—" until traffic flows. """ use Bloccs.Web, :html import Bloccs.Web.Components.Chart 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 rows = rows(assigns.network, assigns.frame) assigns = assigns |> assign(:rows, rows) |> assign(:max_p95, max_of(rows, :p95)) |> assign(:totals, totals(rows)) ~H"""

Live metrics

{header_note(@frame)}
Node State Throughput Latency (p50 · p95) Completed Errors
<.hex_glyph glyph={node.glyph} state={state(m)} /> {node.id} <.status_pill state={state(m)} /> <.sparkline values={series(m)} /> {Format.rate(m && m.throughput)} <%= if m && m.p95 do %>
{Format.latency(m.p50)} · {Format.latency(m.p95)}
<% else %> <% end %>
{Format.count(m && m.completed)} {errors(m)}
Total {Format.rate(@totals.throughput)} {Format.count(@totals.completed)} 0 && "bloccs-num--error"]}> {@totals.errors}

Updates live as messages flow through the network (1 Hz). Sparklines cover the last 10s.

""" end defp rows(network, frame) do Enum.map(network.nodes, fn node -> {node, Map.get(frame.nodes, node.id)} end) end defp series(nil), do: [] defp series(m), do: Map.get(m, :series, []) defp max_of(rows, key) do rows |> Enum.map(fn {_n, m} -> (m && Map.get(m, key)) || 0 end) |> Enum.max(fn -> 0 end) |> max(1) |> Kernel.*(1.0) end defp totals(rows) do Enum.reduce(rows, %{throughput: 0.0, completed: 0, errors: 0}, fn {_n, m}, acc -> if m do %{ throughput: acc.throughput + m.throughput, completed: acc.completed + m.completed, errors: acc.errors + m.errors } else acc end end) end defp pct(_v, max) when max in [0, 0.0], do: 0 defp pct(v, max), do: min(100, round(v / max * 100)) 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