From 3123743c1c987e9de18e8e5b05092da34ede72a5 Mon Sep 17 00:00:00 2001 From: Carsten Date: Tue, 21 Apr 2026 22:50:33 +0200 Subject: [PATCH] feat(server): hosts list/delete/rotate helpers + pubsub on metric insert --- server/lib/server/hosts.ex | 23 ++++++++++++++++++++++ server/lib/server/metrics.ex | 18 +++++++++++++++-- server/test/server/hosts_test.exs | 30 +++++++++++++++++++++++++++++ server/test/server/metrics_test.exs | 8 ++++++++ 4 files changed, 77 insertions(+), 2 deletions(-) diff --git a/server/lib/server/hosts.ex b/server/lib/server/hosts.ex index b08135f..d25d28f 100644 --- a/server/lib/server/hosts.ex +++ b/server/lib/server/hosts.ex @@ -63,4 +63,27 @@ defmodule Server.Hosts do defp generate_token do :crypto.strong_rand_bytes(32) |> Base.url_encode64(padding: false) end + + @spec list_all() :: [Host.t()] + def list_all do + import Ecto.Query + Repo.all(from h in Host, order_by: [asc: h.name]) + end + + @spec delete_host(Host.t()) :: {:ok, Host.t()} | {:error, Ecto.Changeset.t()} + def delete_host(%Host{} = host), do: Repo.delete(host) + + @spec rotate_token(Host.t()) :: {:ok, {Host.t(), String.t()}} | {:error, Ecto.Changeset.t()} + def rotate_token(%Host{} = host) do + token = generate_token() + hash = Bcrypt.hash_pwd_salt(token) + + host + |> Ecto.Changeset.change(token_hash: hash) + |> Repo.update() + |> case do + {:ok, updated} -> {:ok, {updated, token}} + {:error, cs} -> {:error, cs} + end + end end diff --git a/server/lib/server/metrics.ex b/server/lib/server/metrics.ex index 928fb1b..4597d34 100644 --- a/server/lib/server/metrics.ex +++ b/server/lib/server/metrics.ex @@ -18,11 +18,25 @@ defmodule Server.Metrics do }) with %Ecto.Changeset{valid?: true} = cs <- changeset, - true <- host_exists?(host_id) || {:host_missing, cs} do - Repo.insert(cs) + true <- host_exists?(host_id) || {:host_missing, cs}, + {:ok, metric} <- Repo.insert(cs) do + Phoenix.PubSub.broadcast( + Server.PubSub, + "metrics", + {:metric_inserted, host_id, interval_type} + ) + + Phoenix.PubSub.broadcast( + Server.PubSub, + "metrics:#{host_id}", + {:metric_inserted, host_id, interval_type} + ) + + {:ok, metric} else %Ecto.Changeset{} = cs -> {:error, cs} {:host_missing, cs} -> {:error, Ecto.Changeset.add_error(cs, :host, "does not exist")} + {:error, %Ecto.Changeset{} = cs} -> {:error, cs} end end diff --git a/server/test/server/hosts_test.exs b/server/test/server/hosts_test.exs index 7c542aa..bf78bfc 100644 --- a/server/test/server/hosts_test.exs +++ b/server/test/server/hosts_test.exs @@ -52,4 +52,34 @@ defmodule Server.HostsTest do assert offline.status == "offline" end end + + describe "list_all/0" do + test "returns every host ordered by name" do + {:ok, {_, _}} = Hosts.create_host("pve-02") + {:ok, {_, _}} = Hosts.create_host("pve-01") + names = Hosts.list_all() |> Enum.map(& &1.name) + assert names == ["pve-01", "pve-02"] + end + end + + describe "delete_host/1" do + test "deletes the host row" do + {:ok, {host, _}} = Hosts.create_host("pve-01") + assert {:ok, _} = Hosts.delete_host(host) + assert Server.Repo.get(Server.Schema.Host, host.id) == nil + end + end + + describe "rotate_token/1" do + test "replaces token_hash and returns new plaintext token" do + {:ok, {host, old_token}} = Hosts.create_host("pve-01") + assert {:ok, {updated, new_token}} = Hosts.rotate_token(host) + assert updated.id == host.id + refute updated.token_hash == host.token_hash + assert is_binary(new_token) + refute new_token == old_token + assert {:error, :invalid_token} = Hosts.authenticate("pve-01", old_token) + assert {:ok, _} = Hosts.authenticate("pve-01", new_token) + end + end end diff --git a/server/test/server/metrics_test.exs b/server/test/server/metrics_test.exs index de6de14..c2b76b2 100644 --- a/server/test/server/metrics_test.exs +++ b/server/test/server/metrics_test.exs @@ -32,6 +32,14 @@ defmodule Server.MetricsTest do assert {:error, cs} = Metrics.record_sample(999_999, "fast", ts, %{}) assert %{host: ["does not exist"]} = errors_on(cs) end + + test "broadcasts {:metric_inserted, host_id, interval} on success", %{host: host} do + Phoenix.PubSub.subscribe(Server.PubSub, "metrics") + ts = DateTime.utc_now() + {:ok, _} = Metrics.record_sample(host.id, "fast", ts, %{"v" => 1}) + assert_receive {:metric_inserted, host_id, "fast"}, 500 + assert host_id == host.id + end end describe "latest_sample/2" do