From 579d7fc6e8763e03057d07bce78e034b42e3f3e0 Mon Sep 17 00:00:00 2001 From: Carsten Date: Wed, 22 Apr 2026 08:48:14 +0200 Subject: [PATCH] feat(server): public GET /health endpoint for uptime monitors Returns 200 with {status: ok, version, db: ok} when SQLite is reachable, 503 when the DB probe fails. Unauthenticated so external monitors can poll without credentials. --- .../controllers/health_controller.ex | 35 +++++++++++++++++++ server/lib/server_web/router.ex | 6 ++++ .../controllers/health_controller_test.exs | 11 ++++++ 3 files changed, 52 insertions(+) create mode 100644 server/lib/server_web/controllers/health_controller.ex create mode 100644 server/test/server_web/controllers/health_controller_test.exs diff --git a/server/lib/server_web/controllers/health_controller.ex b/server/lib/server_web/controllers/health_controller.ex new file mode 100644 index 0000000..45f55e4 --- /dev/null +++ b/server/lib/server_web/controllers/health_controller.ex @@ -0,0 +1,35 @@ +defmodule ServerWeb.HealthController do + use ServerWeb, :controller + + @moduledoc """ + Public health check for uptime monitors. + Returns 200 when the process is up AND SQLite is reachable, 503 otherwise. + """ + + def show(conn, _params) do + case probe_db() do + :ok -> + json(conn, %{ + status: "ok", + version: Application.spec(:server, :vsn) |> to_string(), + db: "ok" + }) + + {:error, reason} -> + conn + |> put_status(:service_unavailable) + |> json(%{status: "degraded", db: "error", reason: inspect(reason)}) + end + end + + defp probe_db do + try do + case Ecto.Adapters.SQL.query(Server.Repo, "SELECT 1", []) do + {:ok, _} -> :ok + {:error, e} -> {:error, e} + end + rescue + e -> {:error, e} + end + end +end diff --git a/server/lib/server_web/router.ex b/server/lib/server_web/router.ex index bffd6f2..64f6b61 100644 --- a/server/lib/server_web/router.ex +++ b/server/lib/server_web/router.ex @@ -45,6 +45,12 @@ defmodule ServerWeb.Router do get "/hosts/:name", HostController, :show end + scope "/", ServerWeb do + pipe_through :api + + get "/health", HealthController, :show + end + if Application.compile_env(:server, :dev_routes) do import Phoenix.LiveDashboard.Router diff --git a/server/test/server_web/controllers/health_controller_test.exs b/server/test/server_web/controllers/health_controller_test.exs new file mode 100644 index 0000000..191cbbc --- /dev/null +++ b/server/test/server_web/controllers/health_controller_test.exs @@ -0,0 +1,11 @@ +defmodule ServerWeb.HealthControllerTest do + use ServerWeb.ConnCase, async: true + + test "GET /health returns 200 with status=ok", %{conn: conn} do + conn = get(conn, ~p"/health") + body = json_response(conn, 200) + assert body["status"] == "ok" + assert body["db"] == "ok" + assert is_binary(body["version"]) + end +end