defmodule VikWeb.DashboardLive do @moduledoc false use VikWeb, :live_view alias Vik.Repo alias Vik.Store alias Vik.Shard alias Vik.PubSub @memory_usage_sections [ {:atom, "Atoms", "#22C55ED9"}, {:binary, "Binary", "#3B82F6D9"}, {:code, "Code", "#A855F7D9"}, {:ets, "ETS", "#EAB308D9"}, {:process, "Processes", "#F97316D9"}, {:other, "Other", "#374151D9"} ] import Ecto.Query on_mount {VikWeb.SystemHandler, :realtime} @impl true def mount(_params, _session, socket) do PubSub.subscribe("vik:dashboard") {:ok, assign(socket, shards: load_shards())} end @impl true def handle_info({:status, _}, socket) do {:noreply, assign(socket, shards: load_shards())} end @impl true def handle_info({:new, _}, socket) do {:noreply, assign(socket, shards: load_shards())} end @impl true def handle_info(_, socket) do {:noreply, socket} end defp load_shards do Shard |> order_by([s], desc: s.updated_at) |> Repo.all() |> subscribe_shards() |> group_shards() end defp subscribe_shards(shards) do for %Shard{} = shard <- shards do PubSub.subscribe(shard.slug) {shard, Store.status(shard)} end end # Excuse me for the mess below. Claude wrote it and I honestly cannot # be bothered to clean this blood bath up. Viewer discretion advised. defp group_shards(shards) do {flat, grouped} = Enum.split_with(shards, fn {shard, _} -> not String.contains?(shard.slug, "/") end) groups = Enum.group_by(grouped, fn {shard, _} -> shard.slug |> String.split("/", parts: 2) |> hd() end) {remaining_flat, enhance_pls} = Enum.split_with(flat, fn {shard, _} -> shard.slug not in Map.keys(groups) end) enhanced = Enum.group_by(enhance_pls, fn {s, _} -> s.slug end) groups |> Map.merge(enhanced, fn _, grouped, extra -> extra ++ grouped end) |> Map.new(fn {group, items} -> {{:group, group}, items} end) |> Map.put(:flat, remaining_flat) end @impl true def render(assigns) do ~H"""
<.system_info versions={@versions} usage={@usage} info={@info} debug={assigns[:debug]} />
<.shards_listing shards={@shards} /> <.system_limits usage={@usage} limits={@limits} /> <.memory_usage usage={@usage} />
""" end attr :versions, :map, required: true attr :usage, :map, required: true attr :info, :map, required: true attr :debug, :boolean, default: false defp system_info(assigns) do ~H"""
<.card title="Erlang" value={@versions.erlang} class="erl" /> <.card title="Elixir" value={@versions.elixir} class="ex" /> <.card title="Phoenix" value={@versions.phoenix} class="phx" /> <.card title="Uptime" value={format_uptime(@usage.uptime)} /> <.card title="Network in" value={@usage.io |> elem(0) |> format_bytes()} /> <.card title="Network out" value={@usage.io |> elem(1) |> format_bytes()} /> <.card title="Memory" value={format_bytes(@usage.memory.total)} />

{extract_flags(@info.banner)} [{@info.architecture}]

""" end attr :shards, :map, required: true defp shards_listing(assigns) do ~H"""

Shards

<.link class="button" navigate={~p"/new"}>New shard

<%= case List.first(group_shards) do {%{slug: ^group}, _} -> List.first(group_shards) |> elem(0) |> Map.get(:title) _ -> String.capitalize(group) end %> ({length(group_shards)})

""" end attr :usage, :map, required: true attr :limits, :map, required: true defp system_limits(assigns) do ~H"""

System limits

<.limit_usage :for={type <- [:atoms, :ports, :processes]} title={Phoenix.Naming.humanize(type)} current={@usage[type]} limit={@limits[type]} />
""" end attr :usage, :map, required: true defp memory_usage(assigns) do ~H"""

Memory

{name} {format_bytes(value)}

""" end attr :title, :string, required: true attr :value, :string, required: true attr :rest, :global defp card(assigns) do ~H"""

{@title}

{@value}

""" end attr :title, :integer, required: true attr :current, :integer, required: true attr :limit, :integer, required: true defp limit_usage(assigns) do ~H"""

{@title} ({@current} / {@limit}) {format_percentage(@current, @limit)}%

{format_percentage(@current, @limit)}% """ end defp calculate_memory_usage(memory_usage) do for {key, name, color} <- @memory_usage_sections, value = memory_usage[key] do {name, value, color} end end defp format_percentage(value, total) do Float.round(value / total * 100, 1) end def extract_flags(banner) do ["Erlang/OTP", _version, flags] = banner |> to_string() |> String.split(" ", parts: 3) flags end def format_uptime(uptime) do {d, {h, m, _s}} = :calendar.seconds_to_daystime(div(uptime, 1000)) cond do d > 0 -> "#{d}d#{h}h#{m}min" h > 0 -> "#{h}h#{m}min" true -> "#{m}min" end end def format_bytes(bytes) when is_integer(bytes) do cond do bytes >= memory_unit(:TB) -> format_bytes(bytes, :TB) bytes >= memory_unit(:GB) -> format_bytes(bytes, :GB) bytes >= memory_unit(:MB) -> format_bytes(bytes, :MB) bytes >= memory_unit(:KB) -> format_bytes(bytes, :KB) true -> format_bytes(bytes, :B) end end defp format_bytes(bytes, :B) when is_integer(bytes), do: "#{bytes} B" defp format_bytes(bytes, unit) when is_integer(bytes) do value = bytes / memory_unit(unit) "#{:erlang.float_to_binary(value, decimals: 1)}#{unit}" end defp memory_unit(:TB), do: 1024 * 1024 * 1024 * 1024 defp memory_unit(:GB), do: 1024 * 1024 * 1024 defp memory_unit(:MB), do: 1024 * 1024 defp memory_unit(:KB), do: 1024 def dot_color(:stale), do: "#fbbf24" def dot_color(:up), do: "#4ade80" def dot_color(:down), do: "#f87171" end