Built-in PagerDuty-equivalent system for on-call scheduling and alert escalation. Users can now manage schedules, rotation layers, overrides, and escalation policies directly in the app alongside PagerDuty. - On-call schedules with rotation layers (daily/weekly/custom), member management, and temporary overrides - Escalation policies with ordered rules, timeout-based escalation, and user/schedule targets - Automatic escalation via Oban worker with configurable repeat count - Email notifications via Swoosh for on-call alerts - Resolver computes who's on-call from layer stacking and overrides - AlertNotificationWorker integration: starts escalation alongside PagerDuty, acknowledges/resolves incidents on alert state changes - Device and organization schemas support escalation_policy_id - Escalation policy picker on device form - Schedules nav item with tabbed index (schedules + escalation policies) - Full CRUD UI for schedules, layers, members, overrides, rules, targets - 62 LiveView tests, 56 context/schema/resolver/escalation tests - 26 E2E Playwright tests for smoke and critical path coverage
83 lines
2.4 KiB
Elixir
83 lines
2.4 KiB
Elixir
defmodule ToweropsWeb.EscalationPolicyLive.Form do
|
|
@moduledoc false
|
|
use ToweropsWeb, :live_view
|
|
|
|
alias Towerops.OnCall
|
|
alias Towerops.OnCall.EscalationPolicy
|
|
|
|
@impl true
|
|
def mount(_params, _session, socket) do
|
|
{:ok, assign(socket, :active_page, "schedules")}
|
|
end
|
|
|
|
@impl true
|
|
def handle_params(params, _url, socket) do
|
|
case socket.assigns.live_action do
|
|
:new ->
|
|
policy = %EscalationPolicy{}
|
|
changeset = EscalationPolicy.changeset(policy, %{})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:page_title, t("New Escalation Policy"))
|
|
|> assign(:policy, policy)
|
|
|> assign(:form, to_form(changeset))}
|
|
|
|
:edit ->
|
|
policy = OnCall.get_escalation_policy!(params["id"])
|
|
changeset = EscalationPolicy.changeset(policy, %{})
|
|
|
|
{:noreply,
|
|
socket
|
|
|> assign(:page_title, t("Edit Escalation Policy"))
|
|
|> assign(:policy, policy)
|
|
|> assign(:form, to_form(changeset))}
|
|
end
|
|
end
|
|
|
|
@impl true
|
|
def handle_event("validate", %{"escalation_policy" => params}, socket) do
|
|
changeset =
|
|
socket.assigns.policy
|
|
|> EscalationPolicy.changeset(params)
|
|
|> Map.put(:action, :validate)
|
|
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
|
|
def handle_event("save", %{"escalation_policy" => params}, socket) do
|
|
org_id = socket.assigns.current_scope.organization.id
|
|
params = Map.put(params, "organization_id", org_id)
|
|
|
|
case socket.assigns.live_action do
|
|
:new -> save_policy(socket, :create, params)
|
|
:edit -> save_policy(socket, :update, params)
|
|
end
|
|
end
|
|
|
|
defp save_policy(socket, :create, params) do
|
|
case OnCall.create_escalation_policy(params) do
|
|
{:ok, policy} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t("Escalation policy created"))
|
|
|> push_navigate(to: ~p"/schedules/escalation-policies/#{policy.id}")}
|
|
|
|
{:error, changeset} ->
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
end
|
|
|
|
defp save_policy(socket, :update, params) do
|
|
case OnCall.update_escalation_policy(socket.assigns.policy, params) do
|
|
{:ok, policy} ->
|
|
{:noreply,
|
|
socket
|
|
|> put_flash(:info, t("Escalation policy updated"))
|
|
|> push_navigate(to: ~p"/schedules/escalation-policies/#{policy.id}")}
|
|
|
|
{:error, changeset} ->
|
|
{:noreply, assign(socket, :form, to_form(changeset))}
|
|
end
|
|
end
|
|
end
|