feat: add real-time status emoji updates in page titles
Dynamically update page title emoji when device/alert status changes: - Add PubSub broadcasts when alerts are created/resolved - Create StatusTitleComponent LiveComponent to subscribe to alert changes - Add StatusTitle JavaScript hook to update document.title in real-time - Subscribe to organization:alerts channel on LiveView mount - Push emoji update events when alert status changes This provides immediate visual feedback in browser tabs when critical/warning/healthy status changes without requiring page refresh.
This commit is contained in:
parent
a2ce5817a2
commit
c86ca57864
5 changed files with 93 additions and 4 deletions
|
|
@ -1178,10 +1178,21 @@ const GlobalSearch = {
|
|||
}
|
||||
}
|
||||
|
||||
// Status Title - Updates page title with status emoji when alerts change
|
||||
const StatusTitle = {
|
||||
mounted(this: any) {
|
||||
this.handleEvent("update_status_emoji", ({ emoji }: { emoji: string }) => {
|
||||
const currentTitle = document.title
|
||||
const titleWithoutEmoji = currentTitle.replace(/^[🔴🟡🟢]\s/, "")
|
||||
document.title = emoji ? `${emoji} ${titleWithoutEmoji}` : titleWithoutEmoji
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const liveSocket = new LiveSocket("/live", Socket, {
|
||||
longPollFallbackMs: 2500,
|
||||
params: { _csrf_token: csrfToken, timezone: userTimezone },
|
||||
hooks: { ...colocatedHooks, SensorChart, CopyToClipboard, ScrollToTop, AutoDismissFlash, BetaBannerDismiss, NetworkMap, SitesMap, LeafletMap, DeviceListReorder, MikrotikPortSync, GlobalSearch, GlobalSearchTrigger, DynamicFavicon },
|
||||
hooks: { ...colocatedHooks, SensorChart, CopyToClipboard, ScrollToTop, AutoDismissFlash, BetaBannerDismiss, NetworkMap, SitesMap, LeafletMap, DeviceListReorder, MikrotikPortSync, GlobalSearch, GlobalSearchTrigger, DynamicFavicon, StatusTitle },
|
||||
})
|
||||
|
||||
// Show progress bar on live navigation and form submits
|
||||
|
|
|
|||
|
|
@ -10,6 +10,16 @@ defmodule Towerops.Alerts do
|
|||
alias Towerops.Repo
|
||||
alias Towerops.Workers.AlertNotificationWorker
|
||||
|
||||
defp broadcast_alert_change(%Alert{organization_id: org_id}) when not is_nil(org_id) do
|
||||
Phoenix.PubSub.broadcast(
|
||||
Towerops.PubSub,
|
||||
"organization:#{org_id}:alerts",
|
||||
{:alert_changed, org_id}
|
||||
)
|
||||
end
|
||||
|
||||
defp broadcast_alert_change(_alert), do: :ok
|
||||
|
||||
@doc """
|
||||
Creates an alert for device status change.
|
||||
|
||||
|
|
@ -27,6 +37,7 @@ defmodule Towerops.Alerts do
|
|||
|
||||
with {:ok, alert} <- Repo.insert(Alert.changeset(%Alert{}, attrs)) do
|
||||
maybe_compute_gaiia_impact(alert)
|
||||
broadcast_alert_change(alert)
|
||||
{:ok, alert}
|
||||
end
|
||||
end
|
||||
|
|
@ -295,6 +306,7 @@ defmodule Towerops.Alerts do
|
|||
case result do
|
||||
{:ok, updated_alert} ->
|
||||
AlertNotificationWorker.enqueue_resolve(updated_alert.id)
|
||||
broadcast_alert_change(updated_alert)
|
||||
{:ok, updated_alert}
|
||||
|
||||
error ->
|
||||
|
|
@ -306,9 +318,19 @@ defmodule Towerops.Alerts do
|
|||
Resolves an alert without notifying PagerDuty (used when PagerDuty is the source).
|
||||
"""
|
||||
def resolve_alert_silent(alert) do
|
||||
alert
|
||||
|> Alert.changeset(%{resolved_at: DateTime.truncate(DateTime.utc_now(), :second)})
|
||||
|> Repo.update()
|
||||
result =
|
||||
alert
|
||||
|> Alert.changeset(%{resolved_at: DateTime.truncate(DateTime.utc_now(), :second)})
|
||||
|> Repo.update()
|
||||
|
||||
case result do
|
||||
{:ok, updated_alert} ->
|
||||
broadcast_alert_change(updated_alert)
|
||||
{:ok, updated_alert}
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
|
|
|
|||
|
|
@ -699,6 +699,11 @@ defmodule ToweropsWeb.Layouts do
|
|||
id="global-search"
|
||||
organization_id={@current_organization.id}
|
||||
/>
|
||||
<.live_component
|
||||
module={ToweropsWeb.Live.Components.StatusTitleComponent}
|
||||
id="status-title"
|
||||
organization_id={@current_organization.id}
|
||||
/>
|
||||
<% end %>
|
||||
"""
|
||||
end
|
||||
|
|
|
|||
41
lib/towerops_web/live/components/status_title_component.ex
Normal file
41
lib/towerops_web/live/components/status_title_component.ex
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
defmodule ToweropsWeb.Live.Components.StatusTitleComponent do
|
||||
@moduledoc """
|
||||
LiveComponent that subscribes to organization alert changes and updates the page title emoji.
|
||||
"""
|
||||
use ToweropsWeb, :live_component
|
||||
|
||||
alias ToweropsWeb.Helpers.StatusHelpers
|
||||
|
||||
def mount(socket) do
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
def update(%{organization_id: org_id}, socket) do
|
||||
if connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "organization:#{org_id}:alerts")
|
||||
end
|
||||
|
||||
{:ok, assign(socket, :organization_id, org_id)}
|
||||
end
|
||||
|
||||
def handle_info({:alert_changed, _org_id}, socket) do
|
||||
# Get the organization from the parent socket
|
||||
org_id = socket.assigns.organization_id
|
||||
organization = Towerops.Organizations.get_organization!(org_id)
|
||||
|
||||
if organization do
|
||||
emoji = StatusHelpers.status_emoji(organization)
|
||||
# Push event to JavaScript hook to update the browser title
|
||||
{:noreply, push_event(socket, "update_status_emoji", %{emoji: emoji})}
|
||||
else
|
||||
{:noreply, socket}
|
||||
end
|
||||
end
|
||||
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<div id="status-title-watcher" phx-hook="StatusTitle" phx-target={@myself} style="display: none;">
|
||||
</div>
|
||||
"""
|
||||
end
|
||||
end
|
||||
|
|
@ -542,6 +542,16 @@ defmodule ToweropsWeb.UserAuth do
|
|||
{:cont, socket}
|
||||
end
|
||||
|
||||
def on_mount(:subscribe_to_status_updates, _params, _session, socket) do
|
||||
organization = socket.assigns[:current_scope] && socket.assigns.current_scope.organization
|
||||
|
||||
if organization && LiveView.connected?(socket) do
|
||||
Phoenix.PubSub.subscribe(Towerops.PubSub, "organization:#{organization.id}:alerts")
|
||||
end
|
||||
|
||||
{:cont, socket}
|
||||
end
|
||||
|
||||
def on_mount(:load_default_organization, _params, session, socket) do
|
||||
user = socket.assigns.current_scope && socket.assigns.current_scope.user
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue