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
+66 -43
View File
@@ -173,7 +173,9 @@ if (permission_exists('call_active_profile')) {
echo " <th class='hide-small'>" . $text['label-profile'] . "</th>\n";
}
echo " <th>" . $text['label-duration'] . "</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>" . $text['label-cid-number'] . "</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>
const timers = [];
const callsMap = new Map();
const calls_map = new Map();
var showAll = false;
const websockets_domain_name = '<?= $_SESSION['domain_name'] ?>';
@@ -283,6 +285,17 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
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 = truncate_application_data_length > 0;
@@ -323,11 +336,11 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
}
let client = null;
let reconnectAttempts = 0;
let reconnect_attempts = 0;
function connectWebsocket() {
const maxReconnectDelay = 30000; // 30 seconds
const baseReconnectDelay = 1000; // 1 second
function connect_websocket() {
const max_reconnect_delay = 30000; // 30 seconds
const base_reconnect_delay = 1000; // 1 second
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 () => {
try {
console.log('Connected');
reconnectAttempts = 0;
reconnect_attempts = 0;
const status = document.getElementById('calls_active_count');
status.style.backgroundColor = colors.INACTIVE;
} catch (err) {
@@ -358,7 +371,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
await client.request('authentication');
console.log('Authentication sent');
const status = document.getElementById('calls_active_count');
bindEventHandlers(client);
bind_event_handlers(client);
console.log('Sent request for calls in progress');
client.request('active.calls', 'in.progress');
status.style.backgroundColor = colors.CONNECTED;
@@ -375,7 +388,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
console.warn("Websocket Disconnected");
// reconnect to web socket server
reconnectAttempts++;
reconnect_attempts++;
// delay timer to reload page
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
document.getElementById("checkbox_all").addEventListener("change", e => {
// wire up "select all" checkbox only when hangup permission renders it
const checkbox_all = document.getElementById("checkbox_all");
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')): ?>
// Show all listener
@@ -447,7 +463,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
/////////////////////
// Event Functions //
/////////////////////
function bindEventHandlers(client) {
function bind_event_handlers(client) {
client.onEvent("CHANNEL_CALLSTATE", channel_callstate_event);
client.onEvent("CHANNEL_EXECUTE", channel_execute_event);
<?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
if (row === null) {
new_call(call);
row = document.getElementById(uuid) || null;
}
const other_leg_rdnis = call.other_leg_rdnis ?? '';
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 {
if (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);
}
} else {
@@ -577,7 +594,7 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
function playback_start_event(call) {
//console.log(call.event_name, call.unique_id, call);
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 file = call.playback_file_path;
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) {
//console.log(call.event_name, call.unique_id, call);
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;
//update application cell
@@ -603,9 +620,9 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
//update the application cell
function channel_application_event(call) {
//console.log(call.event_name, call.unique_id, call);
const tbody = document.getElementById("calls_active_body");
if (!callsMap.has(call.unique_id)) {
update_call_element(`application_${uuid}`, call.application_name);
if (calls_map.has(call.unique_id)) {
const uuid = call.unique_id;
update_call_element(`application_${uuid}`, call.application_name ?? '');
}
}
<?php endif; ?>
@@ -656,18 +673,19 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
//get the table cell
const span = document.getElementById(`arrow_${uuid}`) ?? null;
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
if (color === span.dataset.color) {
if (normalized_color === (arrow_color_key[span.dataset.color] ?? span.dataset.color)) {
return;
}
span.dataset.icon = icon;
span.dataset.color = color;
span.dataset.color = normalized_color;
//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);
//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
const span = document.getElementById(`arrow_${uuid}`) ?? null;
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) {
@@ -695,15 +714,15 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
}
//nothing to do
if (icon === span.dataset.icon) {
if (normalized_color === span.dataset.icon) {
return;
}
span.dataset.icon = icon;
span.dataset.icon = normalized_color;
span.dataset.color = color;
//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 span_arrow = span.firstChild ?? null;
@@ -717,13 +736,13 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
function new_call(call) {
//console.log(call);
const tbody = document.getElementById("calls_active_body");
if (!callsMap.has(call.unique_id)) {
if (!calls_map.has(call.unique_id)) {
// create the row
const uuid = call.unique_id;
//set the 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 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
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_duration_timer(call.unique_id, call.caller_channel_created_time);
// 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')): ?>
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) {
const tbody = document.getElementById("calls_active_body")
if (callsMap.has(call.unique_id)) {
if (calls_map.has(call.unique_id)) {
//set values
const uuid = call.unique_id;
const row = document.getElementById(uuid);
<?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 if (permission_exists('call_active_all')): ?>
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) {
const row = callsMap.get(call.unique_id);
const row = calls_map.get(call.unique_id);
if (row) {
const uuid = call.unique_id;
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'; ?>) {
row.remove();
}
callsMap.delete(uuid);
calls_map.delete(uuid);
stop_duration_timer_and_update_call_count(uuid);
updateCount();
}
@@ -977,21 +1000,21 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
}
function basename(path) {
return path.replace(/^.*[\\\/]/, '')
return path ? path.replace(/^.*[\\\/]/, '') : '';
}
function updateCount() {
const calls_active_count = document.getElementById('calls_active_count');
let visibleCount = 0;
callsMap.forEach((row) => {
let visible_count = 0;
calls_map.forEach((row) => {
if (row.style.display !== 'none') {
visibleCount++;
visible_count++;
}
});
const totalCount = callsMap.size;
calls_active_count.textContent = `${visibleCount}`;
const total_count = calls_map.size;
calls_active_count.textContent = `${visible_count}`;
}
function start_duration_timer(uuid, start_time) {
@@ -1018,12 +1041,12 @@ echo "<script src='resources/javascript/arrows.js?v=$version'></script>\n";
}
render();
const timerId = setInterval(render, 1000);
const timer_id = setInterval(render, 1000);
timers[uuid] = timerId
timers[uuid] = timer_id
// Return stop function
return () => clearInterval(timerId);
return () => clearInterval(timer_id);
}
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 //
//////////////////////////
connectWebsocket();
connect_websocket();
</script>
<?php require_once "resources/footer.php"; ?>
+2
View File
@@ -108,9 +108,11 @@
$y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_application";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_codec";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
$apps[$x]['permissions'][$y]['groups'][] = "admin";
$y++;
$apps[$x]['permissions'][$y]['name'] = "call_active_secure";
$apps[$x]['permissions'][$y]['groups'][] = "superadmin";
@@ -91,6 +91,8 @@ class active_calls_service extends service implements websocket_service_interfac
'content_type',
];
const PERMISSION_PREFIX = 'call_active_';
//
// Maps the event key to the permission name
//
@@ -102,8 +104,11 @@ class active_calls_service extends service implements websocket_service_interfac
'caller_channel_name' => 'call_active_profile',
'secure' => 'call_active_secure',
'application' => 'call_active_application',
'application_data' => 'call_active_application',
'playback_file_path' => 'call_active_application',
'variable_current_application' => 'call_active_application',
'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 {
// Do not filter domain
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([
new event_filter(self::SWITCH_EVENTS),
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
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([
new event_filter(self::SWITCH_EVENTS),
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
return filter_chain::and_link([
new event_filter(self::SWITCH_EVENTS),
@@ -480,7 +488,7 @@ class active_calls_service extends service implements websocket_service_interfac
// Respond with bad command
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');
@@ -102,13 +102,24 @@ class ws_client {
*/
_dispatchEvent(service, env) {
// if service==='event', topic carries the real event name:
let event = (typeof env === 'string')
? JSON.parse(env)
: env;
let event = null;
try {
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
if (service === 'active.calls') {
const topic = event.event_name;
if (!topic) {
return;
}
let handlers = this._eventHandlers.get(topic) || [];
if (handlers.length === 0) {
@@ -238,6 +238,7 @@ class websocket_service extends service {
$class_name = $subscriber_service->service_class();
// Make sure we can call the 'create_filter_chain_for' method
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 {
// Call the service class method to validate the subscriber
$filter = $class_name::create_filter_chain_for($subscriber);