feat(super-admin): Add push diagnostics tool (#14105)
We're getting many customer reports saying "I'm not getting notifications." We can't always identify the root cause since there are multiple points of failure. Added a **Push Diagnostics** tool in Super Admin to help us investigate mobile/web push issues. Here's how it works: - Look up a user by email/ID → see all their registered subscriptions with device info (iOS/Android, brand, model), token freshness, and last-updated time - Send a customizable test push and read the raw FCM/web-push/relay response to see if the customer is receiving push notifications—if not, it will show proper errors. - Delete broken subscriptions so the mobile app re-registers on next launch <img width="3816" height="1974" alt="CleanShot 2026-04-20 at 12 56 56@2x" src="https://github.com/user-attachments/assets/08ecab6f-7ec3-44b3-a114-5e6eb8cf0879" /> Fixes https://linear.app/chatwoot/issue/CW-6892/push-diagnostics-tool --------- Co-authored-by: Muhsin <12408980+muhsin-k@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Muhsin
Claude Opus 4.7
parent
6928f14a76
commit
6a9c44476e
@@ -0,0 +1,68 @@
|
||||
class SuperAdmin::PushDiagnosticsController < SuperAdmin::ApplicationController
|
||||
def show
|
||||
@query = params[:user_query].to_s.strip
|
||||
@user = resolve_user(@query)
|
||||
@subscriptions = @user ? @user.notification_subscriptions.order(:id) : []
|
||||
@results = []
|
||||
end
|
||||
|
||||
def create
|
||||
@user = User.find_by(id: params[:user_id])
|
||||
return redirect_to super_admin_push_diagnostics_path, alert: I18n.t('super_admin.push_diagnostics.user_not_found') if @user.nil?
|
||||
|
||||
ids = parsed_subscription_ids
|
||||
if ids.empty?
|
||||
return redirect_to super_admin_push_diagnostics_path(user_query: @user.id),
|
||||
alert: I18n.t('super_admin.push_diagnostics.no_subscriptions_to_test')
|
||||
end
|
||||
|
||||
run_test_and_render(ids)
|
||||
end
|
||||
|
||||
def destroy_subscriptions
|
||||
user = User.find_by(id: params[:user_id])
|
||||
return redirect_to super_admin_push_diagnostics_path, alert: I18n.t('super_admin.push_diagnostics.user_not_found') if user.nil?
|
||||
|
||||
ids = parsed_subscription_ids
|
||||
if ids.empty?
|
||||
return redirect_to super_admin_push_diagnostics_path(user_query: user.id),
|
||||
alert: I18n.t('super_admin.push_diagnostics.no_subscriptions_to_delete')
|
||||
end
|
||||
|
||||
deleted_count = user.notification_subscriptions.where(id: ids).destroy_all.size
|
||||
log_super_admin_action("deleted #{deleted_count} subscriptions for user #{user.id}: #{ids}")
|
||||
redirect_to super_admin_push_diagnostics_path(user_query: user.id),
|
||||
notice: I18n.t('super_admin.push_diagnostics.subscriptions_deleted', count: deleted_count)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def run_test_and_render(ids)
|
||||
@query = @user.id.to_s
|
||||
@subscriptions = @user.notification_subscriptions.order(:id)
|
||||
@results = Notification::PushTestService.new(
|
||||
user: @user, subscription_ids: ids,
|
||||
title: params[:push_title], body: params[:push_body]
|
||||
).perform
|
||||
|
||||
log_super_admin_action("test sent for user #{@user.id} subscriptions #{ids}")
|
||||
render :show
|
||||
end
|
||||
|
||||
def log_super_admin_action(message)
|
||||
Rails.logger.info(
|
||||
"[SuperAdmin] push diagnostics #{message} " \
|
||||
"(actor_id=#{current_super_admin&.id}, actor_email=#{current_super_admin&.email})"
|
||||
)
|
||||
end
|
||||
|
||||
def resolve_user(query)
|
||||
return if query.blank?
|
||||
|
||||
query.match?(/\A\d+\z/) ? User.find_by(id: query) : User.from_email(query)
|
||||
end
|
||||
|
||||
def parsed_subscription_ids
|
||||
Array(params[:subscription_ids]).reject(&:blank?).map(&:to_i)
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,160 @@
|
||||
class Notification::PushTestService
|
||||
pattr_initialize [:user!, :subscription_ids!, :title, :body]
|
||||
|
||||
DEFAULT_TITLE = '%<installation_name>s notification test'.freeze
|
||||
DEFAULT_BODY = 'This is a test from our team to check notification delivery on your device. No action needed.'.freeze
|
||||
|
||||
def self.default_title
|
||||
format(DEFAULT_TITLE, installation_name: GlobalConfigService.load('INSTALLATION_NAME', 'Chatwoot'))
|
||||
end
|
||||
|
||||
def self.default_body
|
||||
DEFAULT_BODY
|
||||
end
|
||||
|
||||
def perform
|
||||
selected_subscriptions.map { |subscription| test_send(subscription) }
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resolved_title
|
||||
title.presence || self.class.default_title
|
||||
end
|
||||
|
||||
def resolved_body
|
||||
body.presence || self.class.default_body
|
||||
end
|
||||
|
||||
def selected_subscriptions
|
||||
user.notification_subscriptions.where(id: subscription_ids).order(:id)
|
||||
end
|
||||
|
||||
def test_send(subscription)
|
||||
if subscription.browser_push?
|
||||
test_browser_push(subscription)
|
||||
elsif subscription.fcm?
|
||||
test_fcm(subscription)
|
||||
else
|
||||
result(subscription, subscription.subscription_type.to_s, :skipped, 'Unknown subscription type')
|
||||
end
|
||||
end
|
||||
|
||||
def test_browser_push(subscription)
|
||||
return result(subscription, 'browser_push', :skipped, 'VAPID keys not configured') unless VapidService.public_key
|
||||
|
||||
WebPush.payload_send(**browser_push_payload(subscription))
|
||||
result(subscription, 'browser_push', :success, 'Web push accepted by endpoint')
|
||||
rescue StandardError => e
|
||||
result(subscription, 'browser_push', :failure, "#{e.class.name}: #{e.message}")
|
||||
end
|
||||
|
||||
def test_fcm(subscription)
|
||||
if firebase_credentials_present?
|
||||
test_fcm_direct(subscription)
|
||||
elsif chatwoot_hub_enabled?
|
||||
test_fcm_via_hub(subscription)
|
||||
else
|
||||
result(subscription, 'fcm', :skipped, 'No Firebase credentials and push relay disabled')
|
||||
end
|
||||
end
|
||||
|
||||
def test_fcm_direct(subscription)
|
||||
fcm_service = Notification::FcmService.new(
|
||||
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil),
|
||||
GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
|
||||
)
|
||||
response = fcm_service.fcm_client.send_v1(fcm_options(subscription))
|
||||
status_code = response[:status_code].to_i
|
||||
status = status_code.between?(200, 299) ? :success : :failure
|
||||
result(subscription, 'fcm', status, "HTTP #{status_code} — #{response[:body]}")
|
||||
rescue StandardError => e
|
||||
result(subscription, 'fcm', :failure, "#{e.class.name}: #{e.message}")
|
||||
end
|
||||
|
||||
def test_fcm_via_hub(subscription)
|
||||
response = ChatwootHub.send_push_with_response(fcm_options(subscription))
|
||||
result(subscription, 'fcm_via_hub', :success, "HTTP #{response.code} — #{response.body}")
|
||||
rescue RestClient::ExceptionWithResponse => e
|
||||
result(subscription, 'fcm_via_hub', :failure, "HTTP #{e.response&.code} — #{e.response&.body}")
|
||||
rescue StandardError => e
|
||||
result(subscription, 'fcm_via_hub', :failure, "#{e.class.name}: #{e.message}")
|
||||
end
|
||||
|
||||
def firebase_credentials_present?
|
||||
GlobalConfigService.load('FIREBASE_PROJECT_ID', nil) && GlobalConfigService.load('FIREBASE_CREDENTIALS', nil)
|
||||
end
|
||||
|
||||
def chatwoot_hub_enabled?
|
||||
ActiveModel::Type::Boolean.new.cast(ENV.fetch('ENABLE_PUSH_RELAY_SERVER', true))
|
||||
end
|
||||
|
||||
def browser_push_payload(subscription)
|
||||
{
|
||||
message: JSON.generate(
|
||||
title: resolved_title,
|
||||
tag: "super_admin_test_#{Time.zone.now.to_i}",
|
||||
url: ENV.fetch('FRONTEND_URL', 'https://app.chatwoot.com')
|
||||
),
|
||||
endpoint: subscription.subscription_attributes['endpoint'],
|
||||
p256dh: subscription.subscription_attributes['p256dh'],
|
||||
auth: subscription.subscription_attributes['auth'],
|
||||
vapid: {
|
||||
subject: ENV.fetch('FRONTEND_URL', 'https://app.chatwoot.com'),
|
||||
public_key: VapidService.public_key,
|
||||
private_key: VapidService.private_key
|
||||
},
|
||||
ssl_timeout: 5,
|
||||
open_timeout: 5,
|
||||
read_timeout: 5
|
||||
}
|
||||
end
|
||||
|
||||
def fcm_options(subscription)
|
||||
{
|
||||
'token': subscription.subscription_attributes['push_token'],
|
||||
'data': { payload: { data: { notification: { type: 'test' } } }.to_json },
|
||||
'notification': { title: resolved_title, body: resolved_body },
|
||||
'android': { priority: 'high' },
|
||||
'apns': { payload: { aps: { sound: 'default', category: Time.zone.now.to_i.to_s } } },
|
||||
'fcm_options': { analytics_label: 'SuperAdminTest' }
|
||||
}
|
||||
end
|
||||
|
||||
def result(subscription, type, status, message)
|
||||
attrs = subscription.subscription_attributes || {}
|
||||
{
|
||||
id: subscription.id,
|
||||
type: type.to_s,
|
||||
device: device_label(subscription, attrs),
|
||||
token_tail: token_tail(subscription, attrs),
|
||||
status: status,
|
||||
message: message
|
||||
}
|
||||
end
|
||||
|
||||
def device_label(subscription, attrs)
|
||||
if subscription.browser_push?
|
||||
endpoint_host(attrs['endpoint'].to_s)
|
||||
else
|
||||
attrs['device_id'].present? ? "…#{attrs['device_id'].to_s.last(6)}" : '—'
|
||||
end
|
||||
end
|
||||
|
||||
def endpoint_host(endpoint)
|
||||
return '—' if endpoint.blank?
|
||||
|
||||
URI.parse(endpoint).host.presence || endpoint
|
||||
rescue URI::InvalidURIError
|
||||
endpoint
|
||||
end
|
||||
|
||||
def token_tail(subscription, attrs)
|
||||
if subscription.browser_push?
|
||||
endpoint = attrs['endpoint'].to_s
|
||||
endpoint.present? ? "…#{endpoint.last(6)}" : '—'
|
||||
else
|
||||
attrs['push_token'].present? ? "…#{attrs['push_token'].to_s.last(6)}" : '—'
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -32,7 +32,7 @@ as defined by the routes in the `admin/` namespace
|
||||
<ul class="my-4">
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-grid-line', url: super_admin_root_url, label: 'Dashboard' } %>
|
||||
<% Administrate::Namespace.new(namespace).resources.each do |resource| %>
|
||||
<% next if ["account_users", "access_tokens", "installation_configs", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "settings"].include? resource.resource %>
|
||||
<% next if ["account_users", "access_tokens", "installation_configs", "dashboard", "devise/sessions", "app_configs", "instance_statuses", "settings", "push_diagnostics"].include? resource.resource %>
|
||||
<%= render partial: "nav_item", locals: {
|
||||
icon: sidebar_icons[resource.resource.to_sym],
|
||||
url: resource_index_route(resource),
|
||||
@@ -48,6 +48,7 @@ as defined by the routes in the `admin/` namespace
|
||||
<ul class="my-4">
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-mist-fill', url: sidekiq_web_url, label: 'Sidekiq Dashboard' } %>
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-health-book-line', url: super_admin_instance_status_url, label: 'Instance Health' } %>
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-mail-send-fill', url: super_admin_push_diagnostics_url, label: 'Push Diagnostics' } %>
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-dashboard-line', url: '/', label: 'Agent Dashboard' } %>
|
||||
<%= render partial: "nav_item", locals: { icon: 'icon-logout-circle-r-line', url: super_admin_logout_url, label: 'Logout' } %>
|
||||
</ul>
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<% content_for(:title) do %>Push Diagnostics<% end %>
|
||||
|
||||
<header class="main-content__header" role="banner">
|
||||
<h1 class="main-content__page-title" id="page-title">Push Diagnostics</h1>
|
||||
</header>
|
||||
|
||||
<section class="main-content__body">
|
||||
<p class="text-sm text-slate-600 mb-6">
|
||||
Send a test push notification to a specific user's registered devices to diagnose delivery issues.
|
||||
Results show the raw FCM / Web Push / relay response so you can see exactly what failed.
|
||||
</p>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-base font-semibold mb-2">1. Look up user</h2>
|
||||
<%= form_with url: super_admin_push_diagnostics_path, method: :get, local: true, class: 'flex gap-2 items-center' do |f| %>
|
||||
<%= f.text_field :user_query,
|
||||
value: @query,
|
||||
placeholder: 'user@example.com or numeric user ID',
|
||||
class: 'border border-slate-100 p-1.5 rounded-md w-80' %>
|
||||
<%= f.submit 'Look up', class: 'border border-slate-200 bg-slate-50 px-3 py-1.5 rounded-md cursor-pointer' %>
|
||||
<% end %>
|
||||
<% if @query.present? && @user.nil? %>
|
||||
<p class="text-sm text-red-600 mt-2">No user found for "<%= @query %>".</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @user %>
|
||||
<div class="mb-6 border border-slate-100 rounded-md p-4 bg-slate-25">
|
||||
<p class="text-sm">
|
||||
<strong><%= @user.name %></strong>
|
||||
· <%= @user.email %>
|
||||
· ID <%= @user.id %>
|
||||
</p>
|
||||
<% if @user.accounts.any? %>
|
||||
<p class="text-xs text-slate-500 mt-1">
|
||||
Accounts:
|
||||
<% @user.accounts.each_with_index do |account, index| %>
|
||||
<%= ', ' if index.positive? %>
|
||||
<%= account.name %> (ID <%= account.id %>)
|
||||
<% end %>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<div class="mb-8">
|
||||
<h2 class="text-base font-semibold mb-2">2. Select subscriptions (<%= @subscriptions.count %>)</h2>
|
||||
<p class="text-xs text-amber-700 mb-3">
|
||||
⚠️ This sends a real push to every selected device. Use only when diagnosing a reported issue.
|
||||
</p>
|
||||
|
||||
<% if @subscriptions.empty? %>
|
||||
<p class="text-sm text-slate-600">This user has no push subscriptions registered.</p>
|
||||
<% else %>
|
||||
<%= form_with url: super_admin_push_diagnostics_path, method: :post, local: true do |f| %>
|
||||
<%= f.hidden_field :user_id, value: @user.id %>
|
||||
<div class="mb-4 max-w-2xl">
|
||||
<div class="mb-3">
|
||||
<%= label_tag :push_title, 'Title', class: 'block text-xs font-medium text-slate-600 mb-1' %>
|
||||
<%= text_field_tag :push_title,
|
||||
params[:push_title].presence || Notification::PushTestService.default_title,
|
||||
class: 'border border-slate-100 p-1.5 rounded-md w-full text-sm' %>
|
||||
</div>
|
||||
<div>
|
||||
<%= label_tag :push_body, 'Body', class: 'block text-xs font-medium text-slate-600 mb-1' %>
|
||||
<%= text_area_tag :push_body,
|
||||
params[:push_body].presence || Notification::PushTestService.default_body,
|
||||
rows: 2,
|
||||
class: 'border border-slate-100 p-1.5 rounded-md w-full text-sm' %>
|
||||
</div>
|
||||
<p class="text-xs text-slate-400 mt-1">Customers will see this text as a real push notification on their device.</p>
|
||||
</div>
|
||||
<table class="w-full text-sm border border-slate-100 rounded-md">
|
||||
<thead class="text-left text-xs text-slate-500 bg-slate-25">
|
||||
<tr>
|
||||
<th class="px-3 py-2 align-middle first:!pl-4 last:!pr-4"><input type="checkbox" id="toggle-all"></th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">ID</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Type</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Device / endpoint</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Push token</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Device details</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Created</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Last updated</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @subscriptions.each do |sub| %>
|
||||
<% attrs = (sub.subscription_attributes || {}).stringify_keys %>
|
||||
<% if sub.browser_push? %>
|
||||
<% endpoint = attrs['endpoint'].to_s %>
|
||||
<% host = (begin; URI.parse(endpoint).host; rescue URI::InvalidURIError; nil; end) %>
|
||||
<% device_display = host.presence || endpoint.presence || '—' %>
|
||||
<% token_display = endpoint.present? ? "…#{endpoint.last(6)}" : '—' %>
|
||||
<% else %>
|
||||
<% device_display = attrs['device_id'].present? ? "…#{attrs['device_id'].to_s.last(6)}" : '—' %>
|
||||
<% token_display = attrs['push_token'].present? ? "…#{attrs['push_token'].to_s.last(6)}" : '—' %>
|
||||
<% end %>
|
||||
<% extra_attrs = attrs.except('endpoint', 'p256dh', 'auth', 'push_token', 'device_id') %>
|
||||
<tr class="border-t border-slate-100 align-middle">
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4">
|
||||
<%= check_box_tag 'subscription_ids[]', sub.id, false, class: 'subscription-checkbox' %>
|
||||
</td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4"><%= sub.id %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4"><%= sub.subscription_type %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 font-mono text-xs break-all"><%= device_display %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 font-mono text-xs break-all"><%= token_display %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 text-xs align-top">
|
||||
<% if extra_attrs.present? %>
|
||||
<% extra_attrs.each do |k, v| %>
|
||||
<div><span class="text-slate-500"><%= k %>:</span> <span class="font-mono"><%= v.to_s.truncate(40) %></span></div>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<span class="text-slate-400">—</span>
|
||||
<% end %>
|
||||
</td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 whitespace-nowrap"><%= sub.created_at.strftime('%Y-%m-%d %H:%M') %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 whitespace-nowrap">
|
||||
<%= sub.updated_at.strftime('%Y-%m-%d %H:%M') %>
|
||||
<span class="text-xs text-slate-400">(<%= time_ago_in_words(sub.updated_at) %> ago)</span>
|
||||
</td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="mt-4 flex gap-2">
|
||||
<%= f.submit 'Send Test Push to Selected',
|
||||
class: 'border border-slate-200 bg-slate-50 px-3 py-1.5 rounded-md cursor-pointer font-medium' %>
|
||||
<%= submit_tag 'Delete Selected Subscriptions',
|
||||
formaction: destroy_subscriptions_super_admin_push_diagnostics_path,
|
||||
formmethod: 'post',
|
||||
data: { confirm: "Delete the selected subscription(s)? The user won't receive pushes on those devices until their app re-registers." },
|
||||
class: 'border border-red-200 bg-red-50 text-red-700 px-3 py-1.5 rounded-md cursor-pointer font-medium' %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
|
||||
<% if @results.present? %>
|
||||
<div class="mb-8">
|
||||
<h2 class="text-base font-semibold mb-2">3. Results</h2>
|
||||
<table class="w-full text-sm border border-slate-100 rounded-md">
|
||||
<thead class="text-left text-xs text-slate-500 bg-slate-25">
|
||||
<tr>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Sub ID</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Type</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Device</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Push token</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Status</th>
|
||||
<th class="px-3 py-2 first:!pl-4 last:!pr-4">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<% @results.each do |r| %>
|
||||
<tr class="border-t border-slate-100 align-top">
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4"><%= r[:id] %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4"><%= r[:type] %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 font-mono text-xs break-all"><%= r[:device] %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 font-mono text-xs break-all"><%= r[:token_tail] %></td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4">
|
||||
<%
|
||||
color = {
|
||||
success: 'bg-green-100 text-green-800',
|
||||
failure: 'bg-red-100 text-red-800',
|
||||
skipped: 'bg-slate-100 text-slate-600'
|
||||
}[r[:status]]
|
||||
%>
|
||||
<span class="px-2 py-0.5 rounded-full text-xs <%= color %>"><%= r[:status] %></span>
|
||||
</td>
|
||||
<td class="px-3 py-2 first:!pl-4 last:!pr-4 font-mono text-xs break-all whitespace-pre-wrap"><%= r[:message] %></td>
|
||||
</tr>
|
||||
<% end %>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</section>
|
||||
|
||||
<% content_for :javascript do %>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const toggle = document.getElementById('toggle-all');
|
||||
if (!toggle) return;
|
||||
toggle.addEventListener('change', (event) => {
|
||||
document.querySelectorAll('.subscription-checkbox').forEach((cb) => {
|
||||
cb.checked = event.target.checked;
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<% end %>
|
||||
@@ -496,3 +496,9 @@ en:
|
||||
subject: 'Finish setting up %{custom_domain}'
|
||||
ssl_status:
|
||||
custom_domain_not_configured: 'Custom domain is not configured'
|
||||
super_admin:
|
||||
push_diagnostics:
|
||||
user_not_found: 'User not found.'
|
||||
no_subscriptions_to_test: 'Select at least one subscription to test.'
|
||||
no_subscriptions_to_delete: 'Select at least one subscription to delete.'
|
||||
subscriptions_deleted: "Deleted %{count} subscription(s). The user's device(s) will re-register on next app launch."
|
||||
|
||||
@@ -625,6 +625,9 @@ Rails.application.routes.draw do
|
||||
root to: 'dashboard#index'
|
||||
|
||||
resource :app_config, only: [:show, :create]
|
||||
resource :push_diagnostics, only: [:show, :create] do
|
||||
post :destroy_subscriptions, on: :collection
|
||||
end
|
||||
|
||||
# order of resources affect the order of sidebar navigation in super admin
|
||||
resources :accounts, only: [:index, :new, :create, :show, :edit, :update, :destroy] do
|
||||
|
||||
+6
-2
@@ -106,14 +106,18 @@ class ChatwootHub
|
||||
end
|
||||
|
||||
def self.send_push(fcm_options)
|
||||
info = { fcm_options: fcm_options }
|
||||
RestClient.post(push_notification_url, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||
send_push_with_response(fcm_options)
|
||||
rescue *ExceptionList::REST_CLIENT_EXCEPTIONS => e
|
||||
Rails.logger.error "Exception: #{e.message}"
|
||||
rescue StandardError => e
|
||||
ChatwootExceptionTracker.new(e).capture_exception
|
||||
end
|
||||
|
||||
def self.send_push_with_response(fcm_options)
|
||||
info = { fcm_options: fcm_options }
|
||||
RestClient.post(push_notification_url, info.merge(instance_config).to_json, { content_type: :json, accept: :json })
|
||||
end
|
||||
|
||||
def self.emit_event(event_name, event_data)
|
||||
return if ENV['DISABLE_TELEMETRY']
|
||||
|
||||
|
||||
Reference in New Issue
Block a user