Fix active calls browser console error (#7833)

Javascript error caused Active Calls preventing the calls from being removed from the list
This commit is contained in:
frytimo
2026-04-08 11:30:10 -03:00
committed by GitHub
parent ebf1599f5f
commit 1d9fd0a8a5
5 changed files with 106 additions and 61 deletions
+69 -46
View File
@@ -173,7 +173,9 @@ if (permission_exists('call_active_profile')) {
echo " <th class='hide-small'>" . $text['label-profile'] . "</th>\n"; echo " <th class='hide-small'>" . $text['label-profile'] . "</th>\n";
} }
echo " <th>" . $text['label-duration'] . "</th>\n"; echo " <th>" . $text['label-duration'] . "</th>\n";
echo " <th id='th_domain' style='width: 185px; display: none;'>" . $text['label-domain'] . "</th>\n"; if (permission_exists('call_active_all')) {
echo " <th id='th_domain' style='width: 185px; display: none;'>" . $text['label-domain'] . "</th>\n";
}
echo " <th class='hide-small'>" . $text['label-cid-name'] . "</th>\n"; echo " <th class='hide-small'>" . $text['label-cid-name'] . "</th>\n";
echo " <th>" . $text['label-cid-number'] . "</th>\n"; echo " <th>" . $text['label-cid-number'] . "</th>\n";
echo " <th>" . $text['label-destination'] . "</th>\n"; echo " <th>" . $text['label-destination'] . "</th>\n";
@@ -242,7 +244,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
?> ?>
<script> <script>
const timers = []; const timers = [];
const callsMap = new Map(); const calls_map = new Map();
var showAll = false; var showAll = false;
const websockets_domain_name = '<?= $_SESSION['domain_name'] ?>'; const websockets_domain_name = '<?= $_SESSION['domain_name'] ?>';
@@ -283,6 +285,17 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
INACTIVE: 'black' INACTIVE: 'black'
} }
const arrow_color_key = {
[colors.RINGING]: 'blue',
[colors.CONNECTED]: 'green',
[colors.HANGUP]: 'red',
[colors.INACTIVE]: 'black',
blue: 'blue',
green: 'green',
red: 'red',
black: 'black'
};
const truncate_application_data_length = <?php echo $settings->get('active_calls', 'truncate_application_data_length', 80); ?>; const truncate_application_data_length = <?php echo $settings->get('active_calls', 'truncate_application_data_length', 80); ?>;
const truncate_application_data = truncate_application_data_length > 0; const truncate_application_data = truncate_application_data_length > 0;
@@ -323,11 +336,11 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} }
let client = null; let client = null;
let reconnectAttempts = 0; let reconnect_attempts = 0;
function connectWebsocket() { function connect_websocket() {
const maxReconnectDelay = 30000; // 30 seconds const max_reconnect_delay = 30000; // 30 seconds
const baseReconnectDelay = 1000; // 1 second const base_reconnect_delay = 1000; // 1 second
client = new ws_client(`wss://${window.location.hostname}/websockets/`, authToken); client = new ws_client(`wss://${window.location.hostname}/websockets/`, authToken);
@@ -339,7 +352,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
client.ws.addEventListener("open", async () => { client.ws.addEventListener("open", async () => {
try { try {
console.log('Connected'); console.log('Connected');
reconnectAttempts = 0; reconnect_attempts = 0;
const status = document.getElementById('calls_active_count'); const status = document.getElementById('calls_active_count');
status.style.backgroundColor = colors.INACTIVE; status.style.backgroundColor = colors.INACTIVE;
} catch (err) { } catch (err) {
@@ -358,7 +371,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
await client.request('authentication'); await client.request('authentication');
console.log('Authentication sent'); console.log('Authentication sent');
const status = document.getElementById('calls_active_count'); const status = document.getElementById('calls_active_count');
bindEventHandlers(client); bind_event_handlers(client);
console.log('Sent request for calls in progress'); console.log('Sent request for calls in progress');
client.request('active.calls', 'in.progress'); client.request('active.calls', 'in.progress');
status.style.backgroundColor = colors.CONNECTED; status.style.backgroundColor = colors.CONNECTED;
@@ -375,7 +388,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
console.warn("Websocket Disconnected"); console.warn("Websocket Disconnected");
// reconnect to web socket server // reconnect to web socket server
reconnectAttempts++; reconnect_attempts++;
// delay timer to reload page // delay timer to reload page
const auto_reload_seconds = <?php echo $settings->get('active_calls', 'auto_reload_seconds', 0); ?>; const auto_reload_seconds = <?php echo $settings->get('active_calls', 'auto_reload_seconds', 0); ?>;
@@ -387,10 +400,13 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} }
}) })
// wire up select all checkbox // wire up "select all" checkbox only when hangup permission renders it
document.getElementById("checkbox_all").addEventListener("change", e => { const checkbox_all = document.getElementById("checkbox_all");
document.querySelectorAll("#calls_active_body input[type=checkbox]").forEach(cb => cb.checked = e.target.checked); if (checkbox_all) {
}); checkbox_all.addEventListener("change", e => {
document.querySelectorAll("#calls_active_body input[type=checkbox]").forEach(cb => cb.checked = e.target.checked);
});
}
<?php if (permission_exists('call_active_all')): ?> <?php if (permission_exists('call_active_all')): ?>
// Show all listener // Show all listener
@@ -447,7 +463,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
///////////////////// /////////////////////
// Event Functions // // Event Functions //
///////////////////// /////////////////////
function bindEventHandlers(client) { function bind_event_handlers(client) {
client.onEvent("CHANNEL_CALLSTATE", channel_callstate_event); client.onEvent("CHANNEL_CALLSTATE", channel_callstate_event);
client.onEvent("CHANNEL_EXECUTE", channel_execute_event); client.onEvent("CHANNEL_EXECUTE", channel_execute_event);
<?php if (permission_exists('call_active_application')): ?> <?php if (permission_exists('call_active_application')): ?>
@@ -470,6 +486,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
//create a row for the call //create a row for the call
if (row === null) { if (row === null) {
new_call(call); new_call(call);
row = document.getElementById(uuid) || null;
} }
const other_leg_rdnis = call.other_leg_rdnis ?? ''; const other_leg_rdnis = call.other_leg_rdnis ?? '';
const other_leg_unique_id = call.other_leg_unique_id ?? ''; const other_leg_unique_id = call.other_leg_unique_id ?? '';
@@ -488,7 +505,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} else { } else {
if (other_leg_unique_id !== '') { if (other_leg_unique_id !== '') {
const matched_call = document.getElementById(other_leg_unique_id); const matched_call = document.getElementById(other_leg_unique_id);
if (matched_call.dataset.forced_direction) { if (matched_call && matched_call.dataset.forced_direction) {
replace_arrow_icon(uuid, matched_call.dataset.forced_direction); replace_arrow_icon(uuid, matched_call.dataset.forced_direction);
} }
} else { } else {
@@ -577,7 +594,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
function playback_start_event(call) { function playback_start_event(call) {
//console.log(call.event_name, call.unique_id, call); //console.log(call.event_name, call.unique_id, call);
const tbody = document.getElementById("calls_active_body") const tbody = document.getElementById("calls_active_body")
if (callsMap.has(call.unique_id)) { if (calls_map.has(call.unique_id)) {
const uuid = call.unique_id; const uuid = call.unique_id;
const file = call.playback_file_path; const file = call.playback_file_path;
const file_basename = basename(file); const file_basename = basename(file);
@@ -592,7 +609,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
function playback_stop_event(call) { function playback_stop_event(call) {
//console.log(call.event_name, call.unique_id, call); //console.log(call.event_name, call.unique_id, call);
const tbody = document.getElementById("calls_active_body") const tbody = document.getElementById("calls_active_body")
if (callsMap.has(call.unique_id)) { if (calls_map.has(call.unique_id)) {
const uuid = call.unique_id; const uuid = call.unique_id;
//update application cell //update application cell
@@ -603,9 +620,9 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
//update the application cell //update the application cell
function channel_application_event(call) { function channel_application_event(call) {
//console.log(call.event_name, call.unique_id, call); //console.log(call.event_name, call.unique_id, call);
const tbody = document.getElementById("calls_active_body"); if (calls_map.has(call.unique_id)) {
if (!callsMap.has(call.unique_id)) { const uuid = call.unique_id;
update_call_element(`application_${uuid}`, call.application_name); update_call_element(`application_${uuid}`, call.application_name ?? '');
} }
} }
<?php endif; ?> <?php endif; ?>
@@ -656,18 +673,19 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
//get the table cell //get the table cell
const span = document.getElementById(`arrow_${uuid}`) ?? null; const span = document.getElementById(`arrow_${uuid}`) ?? null;
if (!span) { return; } if (!span) { return; }
const icon = span.dataset.icon ?? 'local'; const icon = (span.dataset.icon && arrows[span.dataset.icon]) ? span.dataset.icon : 'local';
const normalized_color = arrow_color_key[color] ?? 'blue';
//nothing to do //nothing to do
if (color === span.dataset.color) { if (normalized_color === (arrow_color_key[span.dataset.color] ?? span.dataset.color)) {
return; return;
} }
span.dataset.icon = icon; span.dataset.icon = icon;
span.dataset.color = color; span.dataset.color = normalized_color;
//copy the cached arrow //copy the cached arrow
const cached_arrow = arrows[icon][color]; const cached_arrow = arrows[icon]?.[normalized_color] ?? arrows.local.blue;
const arrow = cached_arrow.cloneNode(true); const arrow = cached_arrow.cloneNode(true);
//check for exiting arrow and add or replace //check for exiting arrow and add or replace
@@ -687,7 +705,8 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
//get the table cell //get the table cell
const span = document.getElementById(`arrow_${uuid}`) ?? null; const span = document.getElementById(`arrow_${uuid}`) ?? null;
if (!span) { return; } if (!span) { return; }
const color = span.dataset.color ?? colors.RINGING; const color = arrow_color_key[span.dataset.color] ?? 'blue';
const normalized_color = arrows[icon] ? icon : 'local';
if (span.dataset.icon === null) { if (span.dataset.icon === null) {
@@ -695,15 +714,15 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} }
//nothing to do //nothing to do
if (icon === span.dataset.icon) { if (normalized_color === span.dataset.icon) {
return; return;
} }
span.dataset.icon = icon; span.dataset.icon = normalized_color;
span.dataset.color = color; span.dataset.color = color;
//copy the cached arrow //copy the cached arrow
const cached_arrow = arrows[icon][color]; const cached_arrow = arrows[normalized_color]?.[color] ?? arrows.local.blue;
const arrow = cached_arrow.cloneNode(true); const arrow = cached_arrow.cloneNode(true);
const span_arrow = span.firstChild ?? null; const span_arrow = span.firstChild ?? null;
@@ -717,13 +736,13 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
function new_call(call) { function new_call(call) {
//console.log(call); //console.log(call);
const tbody = document.getElementById("calls_active_body"); const tbody = document.getElementById("calls_active_body");
if (!callsMap.has(call.unique_id)) { if (!calls_map.has(call.unique_id)) {
// create the row // create the row
const uuid = call.unique_id; const uuid = call.unique_id;
//set the profile //set the profile
<?php if (permission_exists('call_active_profile')): ?> <?php if (permission_exists('call_active_profile')): ?>
const profile = call?.caller_channel_name.split('/')[1] ?? ''; const profile = call?.caller_channel_name?.split('/')[1] ?? '';
<?php endif; ?> <?php endif; ?>
<?php if (permission_exists('call_active_codec')): ?> <?php if (permission_exists('call_active_codec')): ?>
@@ -800,13 +819,17 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
// Hide/show domain column // Hide/show domain column
const domain = document.getElementById('th_domain'); const domain = document.getElementById('th_domain');
document.getElementById(`caller_context_${call.unique_id}`).style.display = domain.style.display; const caller_context = document.getElementById(`caller_context_${call.unique_id}`);
//console.debug('CONTEXT: ', domain, callerContext);
if (caller_context) {
caller_context.style.display = domain ? domain.style.display : 'none';
}
// start the timer // start the timer
start_duration_timer(call.unique_id, call.caller_channel_created_time); start_duration_timer(call.unique_id, call.caller_channel_created_time);
// add the uuid to the map // add the uuid to the map
callsMap.set(call.unique_id, row); calls_map.set(call.unique_id, row);
<?php /* add hangup button */ if (permission_exists('call_active_hangup')): ?> <?php /* add hangup button */ if (permission_exists('call_active_hangup')): ?>
const hangup = document.getElementById('btn_hangup').cloneNode(true); const hangup = document.getElementById('btn_hangup').cloneNode(true);
@@ -851,13 +874,13 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
function update_call(call) { function update_call(call) {
const tbody = document.getElementById("calls_active_body") const tbody = document.getElementById("calls_active_body")
if (callsMap.has(call.unique_id)) { if (calls_map.has(call.unique_id)) {
//set values //set values
const uuid = call.unique_id; const uuid = call.unique_id;
const row = document.getElementById(uuid); const row = document.getElementById(uuid);
<?php if (permission_exists('call_active_profile')): ?> <?php if (permission_exists('call_active_profile')): ?>
const caller_channel_name = call?.caller_channel_name.split('/')[1] ?? ''; const caller_channel_name = call?.caller_channel_name?.split('/')[1] ?? '';
<?php endif; ?> <?php endif; ?>
<?php if (permission_exists('call_active_all')): ?> <?php if (permission_exists('call_active_all')): ?>
const caller_context = call.caller_context ?? ''; const caller_context = call.caller_context ?? '';
@@ -907,7 +930,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} }
function hangup_call(call) { function hangup_call(call) {
const row = callsMap.get(call.unique_id); const row = calls_map.get(call.unique_id);
if (row) { if (row) {
const uuid = call.unique_id; const uuid = call.unique_id;
remove_button_by_id(`span_hangup_${uuid}`); remove_button_by_id(`span_hangup_${uuid}`);
@@ -921,7 +944,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
if (<?php /* DEBUGGING OPTION */ echo $settings->get('active_calls','remove_completed_calls', true) ? 'true': 'false'; ?>) { if (<?php /* DEBUGGING OPTION */ echo $settings->get('active_calls','remove_completed_calls', true) ? 'true': 'false'; ?>) {
row.remove(); row.remove();
} }
callsMap.delete(uuid); calls_map.delete(uuid);
stop_duration_timer_and_update_call_count(uuid); stop_duration_timer_and_update_call_count(uuid);
updateCount(); updateCount();
} }
@@ -977,21 +1000,21 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} }
function basename(path) { function basename(path) {
return path.replace(/^.*[\\\/]/, '') return path ? path.replace(/^.*[\\\/]/, '') : '';
} }
function updateCount() { function updateCount() {
const calls_active_count = document.getElementById('calls_active_count'); const calls_active_count = document.getElementById('calls_active_count');
let visibleCount = 0; let visible_count = 0;
callsMap.forEach((row) => { calls_map.forEach((row) => {
if (row.style.display !== 'none') { if (row.style.display !== 'none') {
visibleCount++; visible_count++;
} }
}); });
const totalCount = callsMap.size; const total_count = calls_map.size;
calls_active_count.textContent = `${visibleCount}`; calls_active_count.textContent = `${visible_count}`;
} }
function start_duration_timer(uuid, start_time) { function start_duration_timer(uuid, start_time) {
@@ -1018,12 +1041,12 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
} }
render(); render();
const timerId = setInterval(render, 1000); const timer_id = setInterval(render, 1000);
timers[uuid] = timerId timers[uuid] = timer_id
// Return stop function // Return stop function
return () => clearInterval(timerId); return () => clearInterval(timer_id);
} }
function stop_duration_timer_and_update_call_count(uuid) { function stop_duration_timer_and_update_call_count(uuid) {
@@ -1040,7 +1063,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
////////////////////////// //////////////////////////
// Start the connection // // Start the connection //
////////////////////////// //////////////////////////
connectWebsocket(); connect_websocket();
</script> </script>
<?php require_once "resources/footer.php"; ?> <?php require_once "resources/footer.php"; ?>
+2
View File
@@ -108,9 +108,11 @@
$y++; $y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_application"; $apps[$x]['permissions'][$y]['name'] = "call_active_application";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin"; $apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$y++; $y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_codec"; $apps[$x]['permissions'][$y]['name'] = "call_active_codec";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin"; $apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$y++; $y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_secure"; $apps[$x]['permissions'][$y]['name'] = "call_active_secure";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin"; $apps[$x]['permissions'][$y]['groups'][] = "superadmin";
@@ -91,21 +91,26 @@ class active_calls_service extends service implements websocket_service_interfac
'content_type', 'content_type',
]; ];
const PERMISSION_PREFIX = 'call_active_';
// //
// Maps the event key to the permission name // Maps the event key to the permission name
// //
const PERMISSION_MAP = [ const PERMISSION_MAP = [
'channel_read_codec_name' => 'call_active_codec', 'channel_read_codec_name' => 'call_active_codec',
'channel_read_codec_rate' => 'call_active_codec', 'channel_read_codec_rate' => 'call_active_codec',
'channel_write_codec_name' => 'call_active_codec', 'channel_write_codec_name' => 'call_active_codec',
'channel_write_codec_rate' => 'call_active_codec', 'channel_write_codec_rate' => 'call_active_codec',
'caller_channel_name' => 'call_active_profile', 'caller_channel_name' => 'call_active_profile',
'secure' => 'call_active_secure', 'secure' => 'call_active_secure',
'application' => 'call_active_application', 'application' => 'call_active_application',
'playback_file_path' => 'call_active_application', 'application_data' => 'call_active_application',
'variable_current_application'=> 'call_active_application', 'playback_file_path' => 'call_active_application',
'channel_presence_id' => 'call_active_view', 'variable_current_application' => 'call_active_application',
'caller_context' => 'call_active_domain', 'call_direction' => 'call_active_direction',
'variable_call_direction' => 'call_active_direction',
'channel_presence_id' => 'call_active_view',
'caller_context' => 'call_active_domain',
]; ];
/** /**
@@ -181,6 +186,7 @@ class active_calls_service extends service implements websocket_service_interfac
public static function create_filter_chain_for(subscriber $subscriber): filter { public static function create_filter_chain_for(subscriber $subscriber): filter {
// Do not filter domain // Do not filter domain
if ($subscriber->has_permission('call_active_all') || $subscriber->is_service()) { if ($subscriber->has_permission('call_active_all') || $subscriber->is_service()) {
self::log("Subscriber $subscriber->id has permission to view all active calls", LOG_DEBUG);
return filter_chain::and_link([ return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS), new event_filter(self::SWITCH_EVENTS),
new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()), new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()),
@@ -190,6 +196,7 @@ class active_calls_service extends service implements websocket_service_interfac
// Filter on single domain name // Filter on single domain name
if ($subscriber->has_permission('call_active_domain')) { if ($subscriber->has_permission('call_active_domain')) {
self::log("Subscriber $subscriber->id has permission to view active calls for their domain", LOG_DEBUG);
return filter_chain::and_link([ return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS), new event_filter(self::SWITCH_EVENTS),
new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()), new permission_filter(self::PERMISSION_MAP, $subscriber->get_permissions()),
@@ -198,6 +205,7 @@ class active_calls_service extends service implements websocket_service_interfac
]); ]);
} }
self::log("Subscriber $subscriber->id does not have permission to view all active calls or active calls for their domain. Filtering on extensions.", LOG_DEBUG);
// Filter on extensions // Filter on extensions
return filter_chain::and_link([ return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS), new event_filter(self::SWITCH_EVENTS),
@@ -480,7 +488,7 @@ class active_calls_service extends service implements websocket_service_interfac
// Respond with bad command // Respond with bad command
if (empty($uuid)) { if (empty($uuid)) {
websocket_client::send(websocket_message::request_is_bad($request_id, SERVICE_NAME, 'hangup')); websocket_client::send($this->ws_client->socket(), websocket_message::request_is_bad($request_id, SERVICE_NAME, 'hangup'));
} }
$host = self::$switch_host ?? parent::$config->get('switch.event_socket.host', '127.0.0.1'); $host = self::$switch_host ?? parent::$config->get('switch.event_socket.host', '127.0.0.1');
@@ -102,13 +102,24 @@ class ws_client {
*/ */
_dispatchEvent(service, env) { _dispatchEvent(service, env) {
// if service==='event', topic carries the real event name: // if service==='event', topic carries the real event name:
let event = (typeof env === 'string') let event = null;
? JSON.parse(env) try {
: env; event = (typeof env === 'string') ? JSON.parse(env) : env;
} catch (err) {
console.warn('Ignoring invalid event payload:', err);
return;
}
if (event === null || typeof event !== 'object') {
return;
}
// dispatch event handlers // dispatch event handlers
if (service === 'active.calls') { if (service === 'active.calls') {
const topic = event.event_name; const topic = event.event_name;
if (!topic) {
return;
}
let handlers = this._eventHandlers.get(topic) || []; let handlers = this._eventHandlers.get(topic) || [];
if (handlers.length === 0) { if (handlers.length === 0) {
@@ -238,6 +238,7 @@ class websocket_service extends service {
$class_name = $subscriber_service->service_class(); $class_name = $subscriber_service->service_class();
// Make sure we can call the 'create_filter_chain_for' method // Make sure we can call the 'create_filter_chain_for' method
if (is_a($class_name, 'websocket_service_interface', true)) { if (is_a($class_name, 'websocket_service_interface', true)) {
$this->debug('Creating filter chain for subscriber ' . $subscriber->id . ' for service ' . $subscriber_service->service_name() . ' using class ' . $class_name);
try { try {
// Call the service class method to validate the subscriber // Call the service class method to validate the subscriber
$filter = $class_name::create_filter_chain_for($subscriber); $filter = $class_name::create_filter_chain_for($subscriber);