towerops/lib/towerops_web/live/check_live/form_component.ex
Graham McIntire de986bddf6
Prevent Oban polling/monitoring job stacking per device
When the Oban queue backs up, the 60-second uniqueness window expires
and duplicate jobs stack up per device. Switch to period: :infinity so
only one poll/monitor job exists per device at any time. Add replace
option to supersede stale scheduled jobs with updated scheduled_at.
Remove :executing from unique states so self-scheduling works while
the current job runs. Set max_attempts: 1 since retrying stale polls
is pointless.

Also fix all credo --strict issues across the codebase.
2026-02-16 16:37:48 -06:00

381 lines
11 KiB
Elixir

defmodule ToweropsWeb.CheckLive.FormComponent do
@moduledoc false
use ToweropsWeb, :live_component
alias Towerops.Monitoring
@impl true
def render(assigns) do
~H"""
<div class="relative z-50">
<div class="fixed inset-0 bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-80 transition-opacity">
</div>
<div class="fixed inset-0 z-10 overflow-y-auto">
<div class="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<div class="relative transform overflow-hidden rounded-lg bg-white dark:bg-gray-800 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg">
<.form
for={@form}
id="check-form"
phx-target={@myself}
phx-change="validate"
phx-submit="save"
>
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="flex items-start justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-white">
Add Service Check
</h3>
<button
type="button"
phx-click="close"
phx-target={@myself}
class="text-gray-400 hover:text-gray-500 dark:hover:text-gray-300"
>
<.icon name="hero-x-mark" class="h-6 w-6" />
</button>
</div>
<div class="space-y-4">
<!-- Check Type Selection -->
<div>
<label class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Check Type
</label>
<.input
field={@form[:check_type]}
type="select"
options={[
{"HTTP/HTTPS Check", "http"},
{"TCP Port Check", "tcp"},
{"DNS Check", "dns"}
]}
phx-change="change_type"
phx-target={@myself}
/>
</div>
<!-- Check Name -->
<div>
<.input
field={@form[:name]}
type="text"
label="Check Name"
placeholder="e.g., Web Server Check"
/>
</div>
<!-- Dynamic fields based on check type -->
<%= case @selected_type do %>
<% "http" -> %>
{render_http_fields(assigns)}
<% "tcp" -> %>
{render_tcp_fields(assigns)}
<% "dns" -> %>
{render_dns_fields(assigns)}
<% _ -> %>
<div class="text-sm text-gray-500 dark:text-gray-400">
Select a check type to configure
</div>
<% end %>
<!-- Common Settings -->
<div class="grid grid-cols-2 gap-4">
<div>
<.input
field={@form[:interval_seconds]}
type="number"
label="Interval (seconds)"
/>
</div>
<div>
<.input field={@form[:timeout_ms]} type="number" label="Timeout (ms)" />
</div>
</div>
</div>
</div>
<div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6 gap-2">
<.button type="submit" phx-disable-with="Creating...">
Create Check
</.button>
<.button
type="button"
phx-click="close"
phx-target={@myself}
class="bg-white dark:bg-gray-700"
>
Cancel
</.button>
</div>
</.form>
</div>
</div>
</div>
</div>
"""
end
defp render_http_fields(assigns) do
~H"""
<div class="space-y-4">
<div>
<.input field={@form[:url]} type="text" label="URL" placeholder="https://example.com/health" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<.input
field={@form[:method]}
type="select"
label="Method"
options={["GET", "POST", "PUT", "DELETE", "HEAD"]}
/>
</div>
<div>
<.input
field={@form[:expected_status]}
type="number"
label="Expected Status"
placeholder="200"
/>
</div>
</div>
<div class="flex items-center gap-4">
<label class="flex items-center gap-2">
<.input field={@form[:verify_ssl]} type="checkbox" />
<span class="text-sm text-gray-700 dark:text-gray-300">Verify SSL Certificate</span>
</label>
<label class="flex items-center gap-2">
<.input field={@form[:follow_redirects]} type="checkbox" />
<span class="text-sm text-gray-700 dark:text-gray-300">Follow Redirects</span>
</label>
</div>
<details class="border border-gray-200 dark:border-gray-700 rounded-lg p-3">
<summary class="cursor-pointer text-sm font-medium text-gray-700 dark:text-gray-300">
Advanced Options
</summary>
<div class="mt-3 space-y-3">
<div>
<.input
field={@form[:content_match]}
type="text"
label="Content Match (regex)"
placeholder="Optional regex pattern"
/>
</div>
</div>
</details>
</div>
"""
end
defp render_tcp_fields(assigns) do
~H"""
<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div>
<.input field={@form[:host]} type="text" label="Host" placeholder="example.com" />
</div>
<div>
<.input field={@form[:port]} type="number" label="Port" placeholder="80" />
</div>
</div>
<div>
<.input
field={@form[:send_string]}
type="text"
label="Send String (optional)"
placeholder="Text to send after connecting"
/>
</div>
<div>
<.input
field={@form[:expect_string]}
type="text"
label="Expect String (optional)"
placeholder="Expected response pattern"
/>
</div>
</div>
"""
end
defp render_dns_fields(assigns) do
~H"""
<div class="space-y-4">
<div>
<.input field={@form[:hostname]} type="text" label="Hostname" placeholder="example.com" />
</div>
<div class="grid grid-cols-2 gap-4">
<div>
<.input
field={@form[:record_type]}
type="select"
label="Record Type"
options={["A", "AAAA", "CNAME", "MX", "TXT", "NS"]}
/>
</div>
<div>
<.input
field={@form[:dns_server]}
type="text"
label="DNS Server (optional)"
placeholder="8.8.8.8"
/>
</div>
</div>
<div>
<.input
field={@form[:expected_result]}
type="text"
label="Expected Result (optional)"
placeholder="Expected IP or value"
/>
</div>
</div>
"""
end
@impl true
def mount(socket) do
{:ok, socket}
end
@impl true
def update(assigns, socket) do
changeset = Monitoring.change_check(%Monitoring.Check{})
{:ok,
socket
|> assign(assigns)
|> assign(:selected_type, "http")
|> assign(:form, to_form(changeset))
|> assign_default_config("http")}
end
@impl true
def handle_event("change_type", %{"check" => %{"check_type" => type}}, socket) do
{:noreply, socket |> assign(:selected_type, type) |> assign_default_config(type)}
end
@impl true
def handle_event("validate", %{"check" => check_params}, socket) do
check_params = merge_config_params(check_params, socket.assigns.selected_type)
changeset =
%Monitoring.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)
check_params =
check_params
|> Map.put("organization_id", socket.assigns.device.organization_id)
|> Map.put("device_id", socket.assigns.device.id)
|> Map.put("source_type", "manual")
case Monitoring.create_check(check_params) do
{:ok, check} ->
# Schedule first execution
Monitoring.schedule_check(check)
notify_parent({:check_created, check})
{:noreply, socket}
{:error, %Ecto.Changeset{} = changeset} ->
{:noreply, assign(socket, form: to_form(changeset))}
end
end
@impl true
def handle_event("close", _params, socket) do
notify_parent(:close)
{:noreply, socket}
end
defp assign_default_config(socket, type) do
config =
case type do
"http" ->
%{
"url" => "",
"method" => "GET",
"expected_status" => "200",
"verify_ssl" => "true",
"follow_redirects" => "true"
}
"tcp" ->
%{"host" => "", "port" => ""}
"dns" ->
%{"hostname" => "", "record_type" => "A"}
_ ->
%{}
end
assign(socket, :default_config, config)
end
defp merge_config_params(check_params, type) do
config =
case type do
"http" ->
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"]
)
"tcp" ->
%{
"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"])
"dns" ->
%{
"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
check_params
|> Map.put("config", config)
|> Map.put("check_type", type)
end
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