defmodule ToweropsWeb.CheckLive.FormComponent do @moduledoc false use ToweropsWeb, :live_component alias Towerops.Agents alias Towerops.Monitoring @impl true def render(assigns) do ~H"""
<.form for={@form} id="check-form" phx-target={@myself} phx-change="validate" phx-submit="save" >

{if @action == :edit, do: "Edit Service Check", else: "Add Service Check"}

<.input field={@form[:check_type]} type="select" options={[ {"HTTP/HTTPS Check", "http"}, {"TCP Port Check", "tcp"}, {"DNS Check", "dns"}, {"SSL Certificate Check", "ssl"} ]} phx-change="change_type" phx-target={@myself} disabled={@action == :edit} />
<.input field={@form[:name]} type="text" label="Check Name" placeholder="e.g., Web Server Check" />
<%= case @selected_type do %> <% "http" -> %> {render_http_fields(assigns)} <% "tcp" -> %> {render_tcp_fields(assigns)} <% "dns" -> %> {render_dns_fields(assigns)} <% "ssl" -> %> {render_ssl_fields(assigns)} <% _ -> %>
Select a check type to configure
<% end %>
<.input field={@form[:interval_seconds]} type="number" label="Interval (seconds)" />
<.input field={@form[:timeout_ms]} type="number" label="Timeout (ms)" />
<.button type="submit" phx-disable-with={if @action == :edit, do: "Saving...", else: "Creating..."} > {if @action == :edit, do: "Save Changes", else: "Create Check"}
""" end defp render_http_fields(assigns) do ~H"""
<.input field={@form[:url]} type="text" label="URL" placeholder="https://example.com/health" />
<.input field={@form[:method]} type="select" label="Method" options={["GET", "POST", "PUT", "DELETE", "HEAD"]} />
<.input field={@form[:expected_status]} type="number" label="Expected Status" placeholder="200" />
<.input field={@form[:verify_ssl]} type="checkbox" label="Verify SSL Certificate" /> <.input field={@form[:follow_redirects]} type="checkbox" label="Follow Redirects" />
Advanced Options
<.input field={@form[:content_match]} type="text" label="Content Match (regex)" placeholder="Optional regex pattern" />
""" end defp render_tcp_fields(assigns) do ~H"""
<.input field={@form[:host]} type="text" label="Host" placeholder="example.com" />
<.input field={@form[:port]} type="number" label="Port" placeholder="80" />
<.input field={@form[:send_string]} type="text" label="Send String (optional)" placeholder="Text to send after connecting" />
<.input field={@form[:expect_string]} type="text" label="Expect String (optional)" placeholder="Expected response pattern" />
""" end defp render_dns_fields(assigns) do ~H"""
<.input field={@form[:hostname]} type="text" label="Hostname" placeholder="example.com" />
<.input field={@form[:record_type]} type="select" label="Record Type" options={["A", "AAAA", "CNAME", "MX", "TXT", "NS"]} />
<.input field={@form[:dns_server]} type="text" label="DNS Server (optional)" placeholder="8.8.8.8" />
<.input field={@form[:expected_result]} type="text" label="Expected Result (optional)" placeholder="Expected IP or value" />
""" end defp render_ssl_fields(assigns) do ~H"""
<.input field={@form[:host]} type="text" label="Host" placeholder="example.com" />
<.input field={@form[:port]} type="number" label="Port" placeholder="443" />
<.input field={@form[:warning_days]} type="number" label="Warning Days" placeholder="30" />

Alert when certificate expires within this many days

""" end @impl true def mount(socket) do {:ok, socket} end @impl true def update(assigns, socket) do action = assigns[:action] || :new check = assigns[:check] {selected_type, changeset} = if action == :edit && check do # Editing: populate from existing check type = check.check_type config = check.config || %{} form_attrs = config |> config_to_form_fields(type) |> Map.merge(%{ "name" => check.name, "check_type" => type, "interval_seconds" => check.interval_seconds, "timeout_ms" => check.timeout_ms }) {type, Monitoring.change_check(check, form_attrs)} else # Creating: start with defaults {"http", Monitoring.change_check(%Monitoring.Check{})} end {:ok, socket |> assign(assigns) |> assign(:action, action) |> assign(:selected_type, selected_type) |> assign(:form, to_form(changeset)) |> assign_default_config(selected_type)} end @impl true def handle_event("change_type", %{"check" => %{"check_type" => type}}, socket) do socket = socket |> assign(:selected_type, type) |> assign_default_config(type) # Auto-fill host from device IP for TCP and SSL checks socket = if type in ["tcp", "ssl"] && socket.assigns[:device] do device = socket.assigns.device if device.ip_address && device.ip_address != "" do ip_str = to_string(device.ip_address) form = socket.assigns.form params = form.params || %{} params = Map.put(params, "host", ip_str) changeset = Monitoring.change_check(%Monitoring.Check{}, params) assign(socket, :form, to_form(changeset)) else socket end else socket end {:noreply, socket} end @impl true def handle_event("validate", %{"check" => check_params}, socket) do check_params = merge_config_params(check_params, socket.assigns.selected_type) check = if socket.assigns.action == :edit, do: socket.assigns.check, else: %Monitoring.Check{} changeset = check |> Monitoring.change_check(check_params) |> Map.put(:action, :validate) {:noreply, assign(socket, form: to_form(changeset))} end @impl true def handle_event("save", %{"check" => check_params}, socket) do check_params = merge_config_params(check_params, socket.assigns.selected_type) if socket.assigns.action == :edit do save_edit(socket, check_params) else save_new(socket, check_params) end end @impl true def handle_event("close", _params, socket) do notify_parent(:close) {:noreply, socket} end defp save_new(socket, check_params) do device = socket.assigns.device effective_agent_token_id = Agents.get_effective_agent_token(device) check_params = check_params |> Map.put("organization_id", device.organization_id) |> Map.put("device_id", device.id) |> Map.put("source_type", "manual") |> Map.put("agent_token_id", effective_agent_token_id) case Monitoring.create_check(check_params) do {:ok, check} -> _ = Monitoring.schedule_check(check) notify_parent({:check_created, check}) {:noreply, socket} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end end defp save_edit(socket, check_params) do check = socket.assigns.check case Monitoring.update_check(check, check_params) do {:ok, updated_check} -> notify_parent({:check_updated, updated_check}) {:noreply, socket} {:error, %Ecto.Changeset{} = changeset} -> {:noreply, assign(socket, form: to_form(changeset))} end end @doc false def config_to_form_fields(config, "http") do %{ "url" => config["url"] || "", "method" => config["method"] || "GET", "expected_status" => to_string(config["expected_status"] || 200), "verify_ssl" => to_string(config["verify_ssl"] != false), "follow_redirects" => to_string(config["follow_redirects"] != false), "content_match" => config["regex"] || "" } end def config_to_form_fields(config, "tcp") do %{ "host" => config["host"] || "", "port" => to_string(config["port"] || ""), "send_string" => config["send"] || "", "expect_string" => config["expect"] || "" } end def config_to_form_fields(config, "dns") do %{ "hostname" => config["hostname"] || "", "record_type" => config["record_type"] || "A", "dns_server" => config["server"] || "", "expected_result" => config["expected"] || "" } end def config_to_form_fields(config, "ssl") do %{ "host" => config["host"] || "", "port" => to_string(config["port"] || 443), "warning_days" => to_string(config["warning_days"] || 30) } end def config_to_form_fields(_config, _type), do: %{} defp assign_default_config(socket, type) do assign(socket, :default_config, default_config_for_type(socket, type)) end defp default_config_for_type(_socket, "http"), do: default_config_by_type("http", "") defp default_config_for_type(socket, "tcp"), do: default_config_by_type("tcp", device_host_from_socket(socket)) defp default_config_for_type(_socket, "dns"), do: default_config_by_type("dns", "") defp default_config_for_type(socket, "ssl"), do: default_config_by_type("ssl", device_host_from_socket(socket)) defp default_config_for_type(_socket, _type), do: %{} @doc false def default_config_by_type("http", _host) do %{ "url" => "", "method" => "GET", "expected_status" => "200", "verify_ssl" => "true", "follow_redirects" => "true" } end def default_config_by_type("tcp", host), do: %{"host" => host, "port" => ""} def default_config_by_type("dns", _host), do: %{"hostname" => "", "record_type" => "A"} def default_config_by_type("ssl", host), do: %{"host" => host, "port" => "443", "warning_days" => "30"} def default_config_by_type(_type, _host), do: %{} @doc false def device_host(nil), do: "" def device_host(%{ip_address: nil}), do: "" def device_host(%{ip_address: ip}), do: to_string(ip) def device_host(_), do: "" defp device_host_from_socket(socket) do device_host(socket.assigns[:device]) end @doc false def merge_config_params(check_params, type) do check_params |> Map.put("config", config_from_params(check_params, type)) |> Map.put("check_type", type) end def config_from_params(check_params, "http") do maybe_add( %{ "url" => check_params["url"], "method" => check_params["method"] || "GET", "expected_status" => String.to_integer(check_params["expected_status"] || "200"), "verify_ssl" => check_params["verify_ssl"] == "true", "follow_redirects" => check_params["follow_redirects"] == "true" }, "regex", check_params["content_match"] ) end def config_from_params(check_params, "tcp") do %{ "host" => check_params["host"], "port" => String.to_integer(check_params["port"] || "0") } |> maybe_add("send", check_params["send_string"]) |> maybe_add("expect", check_params["expect_string"]) end def config_from_params(check_params, "dns") do %{ "hostname" => check_params["hostname"], "record_type" => check_params["record_type"] || "A" } |> maybe_add("server", check_params["dns_server"]) |> maybe_add("expected", check_params["expected_result"]) end def config_from_params(check_params, "ssl") do %{ "host" => check_params["host"], "port" => String.to_integer(check_params["port"] || "443"), "warning_days" => String.to_integer(check_params["warning_days"] || "30") } end def config_from_params(_check_params, _type), do: %{} defp maybe_add(map, _key, nil), do: map defp maybe_add(map, _key, ""), do: map defp maybe_add(map, key, value), do: Map.put(map, key, value) defp notify_parent(msg), do: send(self(), {__MODULE__, msg}) end