From 7d65dd1be43a4dd4b23ddd99618fdc523c0471d5 Mon Sep 17 00:00:00 2001 From: Mikael Hugo Date: Fri, 15 May 2026 07:07:51 +0200 Subject: [PATCH] Add emergency page and HostBill ticket creation - New /emergency page at my.centralcloud.com/emergency - Customers can submit emergency tickets that page on-call engineer - HostBill API client now supports addTicket call - Emergency form creates ticket via HostBill then notifies ops backend Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- _build/dev/lib/jose/.mix/compile.erlang | Bin 11369 -> 11369 bytes .../lib/centralcloud_core/hostbill.ex | 13 + .../centralcloud_my/live/emergency_live.ex | 237 ++++++++++++++++++ .../lib/centralcloud_my/router.ex | 1 + 4 files changed, 251 insertions(+) create mode 100644 apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex diff --git a/_build/dev/lib/jose/.mix/compile.erlang b/_build/dev/lib/jose/.mix/compile.erlang index 9d8b8dbd1c410bf8889a88518762e98f0f4c9839..9b9eebaa6586db38ddd2daef0489fdcf7b3f6712 100644 GIT binary patch delta 714 zcmX|9OK1~e5N4C?Zkoq#{yerxHjg&7iD?C^D1!B*+a=XXVnRs?R@<7S+Gvc#L&1vT z#Rmx1?;wK3n|Kl-VDZv|P(;)o>I1Sb7FJi&ME?2 z9$`sGh8M7Al5x~E#_d9po56vA1~aEnZ2hg6B&<`|dIw4vAq&5DPHoY zQ1<(vF*M7-`T(? zUtT=Y-+%ZRj;C_S`2y(k&LC^M2>CVCJ6i&`*gIMzAb{}m=Yr-VfI>?Hnq0g{!%b6ld z7da +
+

🚨 Emergency Support

+

+ This will immediately alert our on-call engineer. Only use for outages or security incidents. +

+
+ + <%= if @submitted do %> +
+

✓ Emergency reported

+

Our on-call engineer has been paged. Ticket reference: <%= @ticket_ref %>

+

+ You will receive updates via email and push notification. +

+
+ <% else %> +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + <%= if @error do %> +
+ <%= @error %> +
+ <% end %> +
+ <% end %> + + """ + end + + def mount(_params, _session, socket) do + services = fetch_services(socket.assigns[:current_user]) + + {:ok, + assign(socket, + subject: "", + body: "", + service_id: "", + services: services, + valid: false, + submitted: false, + ticket_ref: nil, + error: nil + )} + end + + def handle_event("validate", params, socket) do + subject = params["subject"] || "" + body = params["body"] || "" + confirm = params["confirm"] == "true" + + valid = + String.trim(subject) != "" and + String.trim(body) != "" and + confirm + + {:noreply, + assign(socket, + subject: subject, + body: body, + service_id: params["service_id"] || "", + valid: valid, + error: nil + )} + end + + def handle_event("submit", params, socket) do + user = socket.assigns[:current_user] + subject = String.trim(params["subject"] || "") + body = String.trim(params["body"] || "") + service_id = params["service_id"] || "" + + if subject == "" or body == "" do + {:noreply, assign(socket, error: "Subject and description are required.")} + else + client_id = user[:hostbill_client_id] || user["hostbill_client_id"] + + # Create emergency ticket in HostBill + ticket_result = + HostBill.call("addTicket", %{ + clientid: client_id, + subject: "[EMERGENCY] #{subject}", + message: body, + departmentid: 1, + priority: "High" + }) + + case ticket_result do + {:ok, %{"ticketid" => ticket_id}} -> + # Notify ops backend via webhook + notify_ops_emergency(ticket_id, subject, body, client_id, service_id) + + {:noreply, + assign(socket, + submitted: true, + ticket_ref: ticket_id, + error: nil + )} + + {:ok, _} -> + {:noreply, assign(socket, error: "Ticket created but response format unexpected.")} + + {:error, reason} -> + Logger.error("[EmergencyLive] Failed to create HostBill ticket: #{inspect(reason)}") + {:noreply, assign(socket, error: "Failed to create ticket. Please try again or call directly.")} + end + end + end + + defp fetch_services(nil), do: [] + + defp fetch_services(user) do + client_id = user[:hostbill_client_id] || user["hostbill_client_id"] + + if client_id do + case HostBill.get_client_services(client_id) do + {:ok, services} when is_list(services) -> + Enum.map(services, fn s -> + %{id: s["id"] || s["service_id"] || "", name: s["name"] || s["domain"] || "Service"} + end) + |> Enum.reject(&(&1.id == "")) + + _ -> + [] + end + else + [] + end + end + + defp notify_ops_emergency(ticket_id, subject, body, client_id, service_id) do + ops_url = System.get_env("OPS_URL", "https://ops.centralcloud.com") + + payload = %{ + event: "emergency_ticket", + details: %{ + ticket_id: ticket_id, + subject: subject, + body: body, + client_id: client_id, + service_id: service_id, + source: "my.centralcloud.com" + } + } + + # Fire webhook to ops backend + Task.start(fn -> + secret = System.get_env("HOSTBILL_WEBHOOK_SECRET", "") + raw_body = Jason.encode!(payload) + signature = :crypto.mac(:hmac, :sha256, secret, raw_body) |> Base.encode16(case: :lower) + + Req.post("#{ops_url}/api/hostbill/events", + body: raw_body, + headers: [ + {"content-type", "application/json"}, + {"x-centralcloud-signature", signature} + ], + retry: false + ) + end) + end +end diff --git a/apps/centralcloud_my/lib/centralcloud_my/router.ex b/apps/centralcloud_my/lib/centralcloud_my/router.ex index 9967dd0..f61b652 100644 --- a/apps/centralcloud_my/lib/centralcloud_my/router.ex +++ b/apps/centralcloud_my/lib/centralcloud_my/router.ex @@ -47,5 +47,6 @@ defmodule CentralcloudMy.Router do live "/billing", BillingLive, :index live "/security", SecurityLive, :index live "/support", SupportLive, :index + live "/emergency", EmergencyLive, :index end end