diff --git a/_build/dev/lib/jose/.mix/compile.erlang b/_build/dev/lib/jose/.mix/compile.erlang
index 9d8b8db..9b9eeba 100644
Binary files a/_build/dev/lib/jose/.mix/compile.erlang and b/_build/dev/lib/jose/.mix/compile.erlang differ
diff --git a/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex b/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex
index 22ee133..69ffd9e 100644
--- a/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex
+++ b/apps/centralcloud_core/lib/centralcloud_core/hostbill.ex
@@ -26,6 +26,19 @@ defmodule CentralcloudCore.HostBill do
call("getInvoices", %{clientid: client_id})
end
+ @doc "Create a ticket via HostBill API."
+ @spec add_ticket(integer(), String.t(), String.t(), integer(), String.t()) ::
+ {:ok, map()} | {:error, term()}
+ def add_ticket(client_id, subject, message, department_id \\ 1, priority \\ "High") do
+ call("addTicket", %{
+ clientid: client_id,
+ subject: subject,
+ message: message,
+ departmentid: department_id,
+ priority: priority
+ })
+ end
+
# ---------------------------------------------------------------------------
# Private
diff --git a/apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex b/apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex
new file mode 100644
index 0000000..ea073b7
--- /dev/null
+++ b/apps/centralcloud_my/lib/centralcloud_my/live/emergency_live.ex
@@ -0,0 +1,237 @@
+defmodule CentralcloudMy.EmergencyLive do
+ @moduledoc """
+ Customer emergency page at /emergency.
+
+ Allows authenticated customers to raise an emergency ticket that immediately
+ creates an incident in ops and pages the on-call engineer.
+ """
+
+ use Phoenix.LiveView
+
+ alias CentralcloudCore.HostBill
+
+ require Logger
+
+ def render(assigns) do
+ ~H"""
+
+
+
🚨 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 %>
+
+ <% 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