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.
381 lines
11 KiB
Elixir
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
|