New “Active Calls” Dashboard Widget — Feedback Wanted!

TharunV

New Member
Jul 24, 2024
17
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: 8
  • Screenshot 2025-04-23 at 1.11.58 PM.png
    Screenshot 2025-04-23 at 1.11.58 PM.png
    110.8 KB · Views: 9
  • 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.