DEV Community

Jason Shouldice
Jason Shouldice

Posted on • Originally published at vicistack.com

Building a VICIdial Wallboard That Supervisors Actually Use

VICIdial's default real-time report is the screen every call center supervisor stares at all day. It tells you who's talking, who's paused, who's waiting. At 10 agents, it works. At 25+, it falls apart — no at-a-glance KPI summary, no visual grouping by status, a page refresh that flickers and resets scroll position, and zero historical context.

Here's how to build something better without replacing VICIdial.

What the Default Report Gets Wrong

Four specific problems that matter at scale:

No aggregate summary. You can't glance at the screen and know your average talk time, calls per hour, or how many agents are productive right now. You have to mentally tally individual rows while the data refreshes underneath you.

Flat layout. Every agent occupies one row regardless of status. INCALL agents (making money) are mixed with PAUSED agents (costing money). There's no color coding, no priority grouping, no visual separation between "healthy" and "needs attention."

Full-page refresh. The meta-refresh tag reloads the entire page at a fixed interval. Flash. Scroll position reset. On a wallboard TV, it looks unprofessional and distracts everyone.

No trending. It's purely instantaneous. You can't see whether an agent's calls per hour are declining through their shift or whether wait time has been climbing for 30 minutes.

URL Parameters Most Admins Never Discover

Before building a custom dashboard, try the built-in parameters:

/vicidial/realtime_report.php?DB=0&group=SALESCAMP&RR=4&Session=your_session
Enter fullscreen mode Exit fullscreen mode
Parameter Purpose Example
group Filter to a specific campaign group=SALESCAMP
RR Refresh interval in seconds RR=4
show_parks Show parked calls show_parks=1
NOLOGin Use without agent login NOLOGin=Y

These help, but they don't fix the fundamental layout and aggregation problems.

The 5 Metrics Worth Wallboard Space

Not every metric deserves a spot on the TV. Focus on numbers that drive behavior:

1. Calls Per Hour by agent and campaign average. The single most actionable outbound metric. Agents self-correct when they see how they compare to the group average.

SELECT user, COUNT(*) AS calls,
    ROUND(COUNT(*) / (TIMESTAMPDIFF(MINUTE, MIN(event_time), NOW()) / 60), 1) AS cph
FROM vicidial_agent_log
WHERE event_time >= CURDATE()
  AND campaign_id = 'SALESCAMP' AND talk_sec > 0
GROUP BY user ORDER BY cph DESC;
Enter fullscreen mode Exit fullscreen mode

2. Talk/Pause ratio. An agent with 3 hours talk and 45 minutes pause is performing very differently from one with 1 hour talk and 2.5 hours pause. Show the ratio, not just the raw numbers.

SELECT vla.user, vu.full_name,
    val_sum.talk_seconds, val_sum.pause_seconds,
    ROUND(val_sum.talk_seconds / NULLIF(val_sum.pause_seconds, 0), 2) AS talk_pause_ratio
FROM vicidial_live_agents vla
JOIN vicidial_users vu ON vla.user = vu.user
LEFT JOIN (
    SELECT user, SUM(talk_sec) AS talk_seconds, SUM(pause_sec) AS pause_seconds
    FROM vicidial_agent_log WHERE event_time >= CURDATE()
    GROUP BY user
) val_sum ON vla.user = val_sum.user
WHERE vla.campaign_id = 'SALESCAMP'
ORDER BY talk_pause_ratio DESC;
Enter fullscreen mode Exit fullscreen mode

3. Active pause codes. Don't just show that agents are paused — show why. The pause_code field in vicidial_live_agents tells you whether someone is on a legitimate lunch or has been in BREAK for 45 minutes.

SELECT va.user, vu.full_name, va.pause_code,
    UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(va.last_state_change) AS seconds_paused
FROM vicidial_live_agents va
JOIN vicidial_users vu ON va.user = vu.user
WHERE va.status = 'PAUSED'
ORDER BY seconds_paused DESC;
Enter fullscreen mode Exit fullscreen mode

4. Wait time. High wait time means agents are idle waiting for calls — a list exhaustion problem, a dial level problem, or a trunk capacity problem. Show it prominently so supervisors investigate before it gets worse.

5. Drop count. For outbound, show drops in the last 30 minutes. This is a compliance number that should always be visible.

SELECT campaign_id, COUNT(*) AS drops_30min
FROM vicidial_log
WHERE call_date >= DATE_SUB(NOW(), INTERVAL 30 MINUTE)
  AND status = 'DROP'
GROUP BY campaign_id;
Enter fullscreen mode Exit fullscreen mode

Replace the Flicker With AJAX

The biggest usability win is replacing the full-page refresh with AJAX polling. Create a PHP endpoint that returns agent data as JSON, then fetch it every 4 seconds and update the DOM without a page reload.

Backend: JSON Data Endpoint

<?php
// wallboard_data.php — place in /var/www/html/vicidial/
header('Content-Type: application/json');
require_once("dbconnect_mysqli.php");

$campaign = mysqli_real_escape_string($link, $_GET['campaign'] ?? 'SALESCAMP');

$result = mysqli_query($link, "SELECT va.user, vu.full_name, va.status,
    va.pause_code AS sub_status, va.calls_today,
    UNIX_TIMESTAMP(NOW()) - UNIX_TIMESTAMP(va.last_state_change) AS seconds_in_state
FROM vicidial_live_agents va
JOIN vicidial_users vu ON va.user = vu.user
WHERE va.campaign_id = '$campaign'
ORDER BY va.status, vu.full_name");

$agents = [];
while ($row = mysqli_fetch_assoc($result)) $agents[] = $row;

$summary_result = mysqli_query($link, "SELECT
    COUNT(*) AS total_agents,
    SUM(CASE WHEN status='INCALL' THEN 1 ELSE 0 END) AS agents_incall,
    SUM(CASE WHEN status='READY' THEN 1 ELSE 0 END) AS agents_ready,
    SUM(CASE WHEN status='PAUSED' THEN 1 ELSE 0 END) AS agents_paused,
    ROUND(AVG(calls_today), 1) AS avg_calls
FROM vicidial_live_agents WHERE campaign_id = '$campaign'");
$summary = mysqli_fetch_assoc($summary_result);

echo json_encode([
    'timestamp' => time(),
    'campaign' => $campaign,
    'summary' => $summary,
    'agents' => $agents
]);
?>
Enter fullscreen mode Exit fullscreen mode

Frontend: AJAX Polling With Color-Coded Cards

The key is rendering agents as cards grouped by status, with smooth transitions:

const CAMPAIGN = 'SALESCAMP';
const REFRESH_MS = 4000;

async function fetchData() {
    try {
        const resp = await fetch(`wallboard_data.php?campaign=${CAMPAIGN}`);
        const data = await resp.json();
        renderSummary(data.summary);
        renderAgents(data.agents);
    } catch (e) {
        console.error('Fetch failed:', e);
    }
}

function renderAgents(agents) {
    const html = agents.map(a => `
        <div class="agent-card ${a.status}">
            <strong>${a.full_name}</strong><br>
            ${a.status}${a.sub_status ? ' (' + a.sub_status + ')' : ''}<br>
            In state: ${formatTime(parseInt(a.seconds_in_state))}<br>
            Calls: ${a.calls_today}
        </div>
    `).join('');
    document.getElementById('agents').innerHTML = html;
}

fetchData();
setInterval(fetchData, REFRESH_MS);
Enter fullscreen mode Exit fullscreen mode

Use CSS: green background for INCALL, blue for READY, red for PAUSED. Set the card border color to match. Large text visible from across the room. No flicker, no scroll reset, smooth transitions when agents change state.

Set REFRESH_MS to 3000-5000ms. Faster than 3 seconds adds database load for minimal benefit. Slower than 5 seconds makes the display feel stale.

Advanced Queries for Live Dashboards

Campaign Performance Summary

SELECT vla.campaign_id,
    COUNT(DISTINCT vla.user) AS logged_in_agents,
    SUM(CASE WHEN vla.status = 'INCALL' THEN 1 ELSE 0 END) AS on_call,
    (SELECT COUNT(*) FROM vicidial_log vl
     WHERE vl.campaign_id = vla.campaign_id AND vl.call_date >= CURDATE()) AS calls_today,
    (SELECT COUNT(*) FROM vicidial_log vl
     WHERE vl.campaign_id = vla.campaign_id
       AND vl.call_date >= DATE_SUB(NOW(), INTERVAL 1 HOUR)) AS calls_last_hour
FROM vicidial_live_agents vla
GROUP BY vla.campaign_id;
Enter fullscreen mode Exit fullscreen mode

Hopper Health

SELECT campaign_id, COUNT(*) AS leads_in_hopper
FROM vicidial_hopper
GROUP BY campaign_id;
Enter fullscreen mode Exit fullscreen mode

A hopper count below 2x your agent count is a warning. Below 1x and the dialer is starving. Color-code the threshold on your wallboard.

Grafana for Historical Trending

For operations that want to see trends over time, Grafana with a MySQL datasource is the way to go. Install on a separate box. Create a read-only MySQL user:

CREATE USER 'grafana_ro'@'10.0.1.%' IDENTIFIED BY 'strong_password';
GRANT SELECT ON asterisk.vicidial_live_agents TO 'grafana_ro'@'10.0.1.%';
GRANT SELECT ON asterisk.vicidial_agent_log TO 'grafana_ro'@'10.0.1.%';
GRANT SELECT ON asterisk.vicidial_log TO 'grafana_ro'@'10.0.1.%';
GRANT SELECT ON asterisk.vicidial_hopper TO 'grafana_ro'@'10.0.1.%';
FLUSH PRIVILEGES;
Enter fullscreen mode Exit fullscreen mode

Most Valuable Panels

Calls Per Hour Trend:

SELECT $__timeGroup(call_date, '1h') AS time, COUNT(*) AS calls
FROM vicidial_log
WHERE $__timeFilter(call_date) AND campaign_id = 'SALESCAMP'
GROUP BY time ORDER BY time;
Enter fullscreen mode Exit fullscreen mode

Drop Rate Gauge: Set thresholds at green below 2%, yellow 2-3%, red above 3% (the FTC safe harbor limit).

Grafana Alerts: Configure notifications for drop rate exceeding 2.5%, hopper below threshold, wait time over 30 seconds, more than 50% of agents paused simultaneously.

Physical Wallboard Layout

For a 25+ agent room with multiple screens:

  • Screen 1 — Agent status grid (the AJAX card layout). Color coded, large text, visible from across the room.
  • Screen 2 — Leaderboard: top 10 agents by conversions and CPH.
  • Screen 3 — Grafana: CPH trend, drop rate gauge, hopper health.

Use a Raspberry Pi per display running Chromium in kiosk mode:

chromium-browser --kiosk --disable-restore-session-state \
    "http://10.0.1.10/vicidial/custom_wallboard.php?campaign=SALESCAMP"
Enter fullscreen mode Exit fullscreen mode

The complete guide with full HTML/CSS/JS for the AJAX wallboard, all Grafana queries, and alerting YAML is at ViciStack's real-time dashboard guide.


Originally published at https://vicistack.com/blog/vicidial-realtime-agent-dashboard/

Top comments (0)