#### Context Replace the custom Elixir observability TCP server with Phoenix while keeping the shipped escript and existing operational API calls working the same way they did before. #### TL;DR *Move the Elixir observability dashboard and API onto Phoenix without breaking the escript or curl workflows.* #### Summary - replace the custom HTTP server with a Phoenix endpoint, router, controller, and shared presenter - add a LiveView operations dashboard with PubSub-driven updates and embed its CSS and JS assets in code - preserve runtime compatibility by accepting form-style POSTs and extending the endpoint coverage around those paths #### Alternatives - keep extending the hand-rolled TCP server, but that keeps custom parsing and routing complexity in the app - serve dashboard assets from priv directories, but the shipped escript cannot rely on those files existing on disk #### Test Plan - [x] `make -C elixir all` - [x] `mix -C elixir test test/symphony_elixir/extensions_test.exs` - [x] `./bin/symphony --i-understand-that-this-will-be-running-without-the-usual-guardrails --port 40123` - [x] `curl -si http://127.0.0.1:40123/dashboard.css` - [x] `curl -si http://127.0.0.1:40123/vendor/phoenix/phoenix.js` - [x] `curl -si -X POST -d '' http://127.0.0.1:40123/api/v1/refresh` - [x] `curl -si -X POST -d '' http://127.0.0.1:40123/api/v1/state` --------- Co-authored-by: Codex <codex@openai.com>
89 lines
2.5 KiB
Elixir
89 lines
2.5 KiB
Elixir
defmodule SymphonyElixir.HttpServer do
|
|
@moduledoc """
|
|
Compatibility facade that starts the Phoenix observability endpoint when enabled.
|
|
"""
|
|
|
|
alias SymphonyElixir.{Config, Orchestrator}
|
|
alias SymphonyElixirWeb.Endpoint
|
|
|
|
@secret_key_bytes 48
|
|
|
|
@spec child_spec(keyword()) :: Supervisor.child_spec()
|
|
def child_spec(opts) do
|
|
%{
|
|
id: __MODULE__,
|
|
start: {__MODULE__, :start_link, [opts]}
|
|
}
|
|
end
|
|
|
|
@spec start_link(keyword()) :: GenServer.on_start() | :ignore
|
|
def start_link(opts \\ []) do
|
|
case Keyword.get(opts, :port, Config.server_port()) do
|
|
port when is_integer(port) and port >= 0 ->
|
|
host = Keyword.get(opts, :host, Config.server_host())
|
|
orchestrator = Keyword.get(opts, :orchestrator, Orchestrator)
|
|
snapshot_timeout_ms = Keyword.get(opts, :snapshot_timeout_ms, 15_000)
|
|
|
|
with {:ok, ip} <- parse_host(host) do
|
|
endpoint_opts = [
|
|
server: true,
|
|
http: [ip: ip, port: port],
|
|
url: [host: normalize_host(host)],
|
|
orchestrator: orchestrator,
|
|
snapshot_timeout_ms: snapshot_timeout_ms,
|
|
secret_key_base: secret_key_base()
|
|
]
|
|
|
|
endpoint_config =
|
|
:symphony_elixir
|
|
|> Application.get_env(Endpoint, [])
|
|
|> Keyword.merge(endpoint_opts)
|
|
|
|
Application.put_env(:symphony_elixir, Endpoint, endpoint_config)
|
|
Endpoint.start_link()
|
|
end
|
|
|
|
_ ->
|
|
:ignore
|
|
end
|
|
end
|
|
|
|
@spec bound_port(term()) :: non_neg_integer() | nil
|
|
def bound_port(_server \\ __MODULE__) do
|
|
case Bandit.PhoenixAdapter.server_info(Endpoint, :http) do
|
|
{:ok, {_ip, port}} when is_integer(port) -> port
|
|
_ -> nil
|
|
end
|
|
rescue
|
|
_error -> nil
|
|
catch
|
|
:exit, _reason -> nil
|
|
end
|
|
|
|
defp parse_host({_, _, _, _} = ip), do: {:ok, ip}
|
|
defp parse_host({_, _, _, _, _, _, _, _} = ip), do: {:ok, ip}
|
|
|
|
defp parse_host(host) when is_binary(host) do
|
|
charhost = String.to_charlist(host)
|
|
|
|
case :inet.parse_address(charhost) do
|
|
{:ok, ip} ->
|
|
{:ok, ip}
|
|
|
|
{:error, _reason} ->
|
|
case :inet.getaddr(charhost, :inet) do
|
|
{:ok, ip} -> {:ok, ip}
|
|
{:error, _reason} -> :inet.getaddr(charhost, :inet6)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp normalize_host(host) when host in ["", nil], do: "127.0.0.1"
|
|
defp normalize_host(host) when is_binary(host), do: host
|
|
defp normalize_host(host), do: to_string(host)
|
|
|
|
defp secret_key_base do
|
|
Base.encode64(:crypto.strong_rand_bytes(@secret_key_bytes), padding: false)
|
|
end
|
|
end
|