New “Active Calls” Dashboard Widget — Feedback Wanted!

TharunV

New Member
Jul 24, 2024
18
1
3
36
Hi everyone,

I’ve just built a brand-new Live Active Calls widget for FusionPBX’s dashboard and I’d love your feedback (and code review!) before we propose it as an official core widget.

——



Key Features



  • Real-time, AJAX-powered refresh (every 5 seconds, no page reload)
  • One row per call UUID (even if the same number calls multiple times)
    • Ringing legs get a blinking yellow bell icon
    • Dialed (outbound before answer) shows a blue up-arrow
    • Connected shows green arrows (down for inbound, up for outbound)
  • Auto-formatting caller number vs. extension—outbound “Dialed” legs show your extension as the caller until answered
  • Single bottom expander bar and matching FusionPBX HUD styling (icon, count bubble, “…” toggle)
  • Graceful error fallback if the event socket can’t be reached





Code


<?php
/*
app/extensions/resources/dashboard/active_calls.php
FusionPBX Dashboard Widget: Live Active Calls
*/

require_once dirname(__DIR__,4) . '/resources/require.php';
require_once 'resources/check_auth.php';

if (!permission_exists('call_active_view')) {
echo 'access denied';
exit;
}
if (session_status() !== PHP_SESSION_ACTIVE) {
session_start();
}

// ———————————————————————————————————————————————————————————
// Pull & choose one leg per call_uuid
function get_active_calls() {
$es = event_socket::create();
if (!$es->is_connected()) return [];

$json = trim($es->api('show channels as json'));
$data = json_decode($json, true);
$rows = $data['rows'] ?? [];

// group legs by call_uuid (fallback uuid)
$groups = [];
foreach ($rows as $r) {
if (!in_array($r['direction'], ['inbound','outbound'])) continue;
$key = $r['call_uuid'] ?: $r['uuid'];
$groups[$key][] = $r;
}

$out = [];
$domain = $_SESSION['domain_name'];
$me = $_SESSION['user']['extension'][0]['user'] ?? '';
$show_all = permission_exists('call_active_all');

foreach ($groups as $legs) {
// domain filter
$keep = false;
foreach ($legs as $l) {
$ctx = $l['context'] ?: $l['presence_id'];
$dom = strpos($ctx,'@')!==false?explode('@',$ctx)[1]:$ctx;
if ($show_all || $dom === $domain) { $keep = true; break; }
}
if (!$keep) continue;

// drop the gateway inbound leg
$legs = array_filter($legs, function($l){
return !( $l['direction']==='inbound'
&& stripos($l['application_data'],'sofia/gateway/')!==false );
});
if (empty($legs)) continue;

// detect EARLY = ringing
$ring = false;
foreach ($legs as $l) {
if ($l['callstate']==='EARLY') { $ring = true; break; }
}

// pick the leg
if ($ring) {
foreach ($legs as $l) {
if ($l['callstate']==='EARLY') { $leg = $l; break; }
}
}
else {
// prefer inbound ACTIVE
$leg = null;
foreach ($legs as $l) {
if ($l['direction']==='inbound' && $l['callstate']==='ACTIVE') {
$leg = $l; break;
}
}
// else outbound ACTIVE
if (!$leg) {
foreach ($legs as $l) {
if ($l['direction']==='outbound' && $l['callstate']==='ACTIVE') {
$leg = $l; break;
}
}
}
// fallback first
if (!$leg) {
$leg = reset($legs);
}
}

// status & icon
if ($ring) {
$status = 'Ringing';
$icon = 'fas fa-bell blink yellow';
}
elseif ($leg['callstate']==='ACTIVE') {
$status = 'Connected';
$icon = ($leg['direction']==='outbound')
? 'fas fa-arrow-up blue'
: 'fas fa-arrow-down green';
}
else {
// must be outbound but not ACTIVE
$status = 'Dialed';
$icon = 'fas fa-arrow-up blue';
}

// caller
$cid = $leg['cid_num'] ?: $leg['initial_cid_num'];
if (!$ring && $leg['direction']==='outbound' && $me) {
$cid = $me;
}

// destination
if ($ring || $leg['direction']==='outbound') {
$dst = $leg['dest'] ?: $leg['initial_dest'];
} else {
if (preg_match_all("/\]sofia\/[^\/]+\/sip:([^@]+)/", $leg['application_data'], $m)) {
$dst = end($m[1]);
}
else {
$dst = $leg['dest'];
}
}

$out[] = compact('icon','cid','dst','status');
}

return $out;
}

// ———————————————————————————————————————————————————————————
// AJAX endpoint
if (!empty($_GET['ajax'])) {
header('Content-Type:application/json');
$calls = get_active_calls();
$count = count($calls);
$rows_html = '';
foreach ($calls as $c) {
$rows_html .= '<tr>'
."<td style='text-align:center;'><i class='{$c['icon']}'></i></td>"
."<td class='hud_text'>{$c['cid']}</td>"
."<td class='hud_text'>{$c['dst']}</td>"
."<td class='hud_text'>{$c['status']}</td>"
.'</tr>';
}
if ($rows_html === '') {
$rows_html = "<tr><td colspan='4' class='hud_text' style='text-align:center;color:#888'>"
."No active calls</td></tr>";
}
echo json_encode(['count'=>$count,'rows'=>$rows_html]);
exit;
}

// ———————————————————————————————————————————————————————————
// toggle for expand/collapse
$toggle = ($dashboard_details_state === 'disabled')
? ''
: " onclick=\"\$('#hud_active_calls_details').slideToggle('fast');"
."toggle_grid_row_end('{$dashboard_name}');refreshActiveCalls();\"";
?>
<div class="hud_box" id="active_calls_widget">

<!-- Collapsed HUD -->
<div class="hud_content"<?php echo $toggle;?>>
<span class="hud_title">
<?php echo $text['label-active_calls'] ?? 'Active Calls';?>
</span>
<div style="position:relative;display:inline-block;margin:0.5rem 0;">
<span class="hud_stat">
<i class="fas <?php echo htmlspecialchars($dashboard_icon ?: 'fa-phone');?>"></i>
</span>
<span id="active_calls_count" style="
position:absolute;top:22px;left:24px;
background:<?php echo $settings->get('theme','dashboard_number_background_color')?:'#ea4c46';?>;
color:<?php echo $settings->get('theme','dashboard_number_text_color')?:'#fff';?>;
font-size:12px;font-weight:bold;padding:2px 6px;border-radius:10px;
">0</span>
</div>
</div>

<?php if ($dashboard_details_state !== 'disabled'): ?>
<div class="hud_details hud_box" id="hud_active_calls_details" style="display:none;padding:10px;">
<table class="tr_hover" width="100%" cellpadding="0" cellspacing="0">
<tr>
<th class="hud_heading">&nbsp;</th>
<th class="hud_heading">Caller</th>
<th class="hud_heading">Destination</th>
<th class="hud_heading">Status</th>
</tr>
<tbody id="active_calls_rows">
<tr><td colspan="4" class="hud_text" style="text-align:center;color:#888">
Loading…
</td></tr>
</tbody>
</table>
</div>
<?php endif; ?>

<!-- single bottom expander bar -->
<span class="hud_expander"<?php echo $toggle;?>>
<span class="fas fa-ellipsis-h"></span>
</span>

</div>

<style>
.blink { animation: blinker 1s linear infinite; }
@keyframes blinker {50%{opacity:0}}
.yellow { color:#f1c40f }
.green { color:#2ecc71 }
.blue { color:#417ed3 }
#active_calls_widget .tr_hover th,
#active_calls_widget .tr_hover td {
padding:4px 8px;
}
#active_calls_widget .tr_hover tr:nth-child(even) {
background:#f9f9f9;
}
</style>

<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script>
jQuery(function($){
const URL = '<?php echo PROJECT_PATH;?>'
+ '/app/extensions/resources/dashboard/active_calls.php?ajax=1';

function refreshActiveCalls(){
$.getJSON(URL)
.done(data=>{
$('#active_calls_count').text(data.count);
$('#active_calls_rows').html(data.rows);
})
.fail(()=>{
$('#active_calls_count').text('0');
$('#active_calls_rows').html(
"<tr><td colspan='4' class='hud_text' style='text-align:center;color:#888'>"
+"Error loading</td></tr>"
);
});
}

// initial + every 5s
refreshActiveCalls();
setInterval(refreshActiveCalls, 5000);

// also on expand/collapse
$('#active_calls_widget').on('click','.hud_content,.hud_expander',
refreshActiveCalls
);
});
</script>

——


❓ Feedback & Suggestions



  1. Performance: Is a 5s poll OK, or would you suggest a longer/shorter interval?
  2. UI Tweaks: Icon choices, colors, blinking behavior—what feels best?
  3. Edge Cases: Any callstate scenarios I missed (transfers, park, internal transfers)?
  4. Security: Anything I should sanitize differently or permission-check more strictly?


If this widget looks solid, would the core FusionPBX team consider merging it as an official dashboard widget? I’m happy to refine per your suggestions.
 

Attachments

  • Screenshot 2025-04-23 at 1.13.20 PM.png
    Screenshot 2025-04-23 at 1.13.20 PM.png
    118 KB · Views: 10
  • Screenshot 2025-04-23 at 1.11.58 PM.png
    Screenshot 2025-04-23 at 1.11.58 PM.png
    110.8 KB · Views: 11
  • Like
Reactions: voipBull
Hey! Building this is on my list of low priority projects and you beat me to it! Great idea and work.

I really like the ringing icon.

As you know the active calls page refresh is 2 seconds. In my experience I believe 2 seconds is a good interval. Easily changed.

I am considering trying the same directional arrows that exist on the CDR page.

I will review and test your code. :cool:
 
Hey! Building this is on my list of low priority projects and you beat me to it! Great idea and work.

I really like the ringing icon.

As you know the active calls page refresh is 2 seconds. In my experience I believe 2 seconds is a good interval. Easily changed.

I am considering trying the same directional arrows that exist on the CDR page.

I will review and test your code. :cool:
Feel free to share any tweaks or improvements you’ve made. If you’ve implemented any useful changes, post them here so others can benefit too!”
 
It is a great widget. I suggest you use ESL for this instead of constantly polling the server,, which takes a toll onFreeswitchh. You can wait for Freeswitch to notify you about the new calls and status changes. This is push vs pull.
 
It is a great widget. I suggest you use ESL for this instead of constantly polling the server,, which takes a toll onFreeswitchh. You can wait for Freeswitch to notify you about the new calls and status changes. This is push vs pull.
does any of fusionpbx default apps uses this method?
 
FusionPBX was not originally designed with workflow optimization in mind, and this limitation extends to most of the available open-source modules. While I suspect that some member applications may leverage it, this is more speculative than confirmed—especially since those modules are encrypted and their source code is not accessible for review.

In FS PBX, I’ve begun implementing more efficient approaches within certain modules. For example, with emergency call notifications, rather than polling the system every few seconds to check if someone dialed 911, I subscribe to events and let FreeSWITCH notify me when such a call occurs. Since emergency calls are infrequent, constant polling would unnecessarily consume system resources.

On high-traffic systems, reducing background tasks that continuously run is critical to ensuring that resources remain available for core call handling functions.