Socket.IO
What the client emits. What the server emits. Who listens.
Agent panel and dashboard share one Socket.IO server on /socket.io/. This is the wire protocol only. Calling is in the Progressive and Predictive guides. Lead ingest is the HTTP API.
Connect
const socket = io('https://YOUR_HOST', {
transports: ['websocket'],
auth: { token: 'YOUR_WS_SECRET' }
});
socket.on('connect', () => {
socket.emit('join', { role: 'agent', extension: '1007' });
});
WS_SECRET is required. Send it as handshake.auth.token, or query token / key. Wrong key → connect_error unauthorized. After connect, emit join or you are in no room.
Rooms
| Room | Join as | Receives |
|---|---|---|
dashboard | { role: 'dashboard' } | Snapshot, patch, every dialer event, health, panel |
ext:{extension} | { role: 'agent', extension } | That agent’s status, dialer, call_connected |
One socket is one role. Join again leaves the previous room. Several tabs on the same extension share the room. Disconnect waits 3 seconds before the agent is treated as gone (refresh-safe).
Client emits — server listens
The browser emits. Hub in src/ws/hub.js listens.
| Emit | Payload | Server does |
|---|---|---|
join | { role: 'dashboard' } or { role: 'agent', extension: '1007' } | Join room, then emit joined plus initial data |
leave | — | Leave room. Agent: decrement panel count |
ready | — | Presence ready. Writes dialer_employee.panel_status = active |
pause | — | Presence paused. Writes dialer_employee.panel_status = pause |
presence | { presence: 'ready' | 'paused' } | Same as ready / pause |
ping | — | Reply pong |
redial | { oli_id } | Re-originate that lead on this extension (skip wrap / queue) |
Socket.IO disconnect is treated as leave. Missing extension on agent join → server_error extension is required to join as agent. Extension not in this module’s dialer_employee roster → extension is not assigned to this dialer. Ready/pause before join also emits server_error. redial before join, or without oli_id, returns { ok: false } (and server_error if you did not pass an ack callback). A successful agent join reloads that employee row and syncs the AMI roster.
Daily day reset at PAUSE_DAY_RESET_AT (default 09:30 IST / DB_TIMEZONE) closes open pauses that started before today (resume_reason = day_reset) and sets panel_status = yet_to_login. The dialer ignores those desks until the agent goes Active. Same-morning pauses stay open. Kill switch: PAUSE_DAY_RESET_ENABLED=0. The dashboard On pause list excludes yet_to_login; Work / Reason show yet to login instead of a pause clock.
Server emits — client listens
The hub emits. The browser listens.
| Listen | Room | When |
|---|---|---|
joined | That socket | After a successful join |
snapshot | dashboard | Dashboard join, and full AMI snapshot |
patch | dashboard | AMI incremental device-state changes |
status | ext:{n} | AMI + panel/presence for that extension |
panel | dashboard | Agent join, leave, ready, pause |
presence | dashboard | Last tab for an extension left (reason: 'left') |
dialer | Both | Engine phase: claim, buffer, connect, hangup |
call_connected | Both | Both parties talking (once per call) |
health | dashboard | AMI/DB health |
pong | That socket | Reply to ping |
server_error | That socket | Join / presence failures |
Payloads
joined
{ "role": "agent", "extension": "1007", "rooms": ["ext:1007"] }
Dashboard join is { "role": "dashboard", "rooms": ["dashboard"] }.
dialer
Always has extension and phase: idle, paused, buffer, waiting, dialing, on_call. Also buffer_ms / buffer_ends_at (wrap after a connected call only), connect_ms, on_call_at, talk_ms, client_name, oli_id, form_type. Predictive adds outbound (other ringing/hold legs, phase ringing or waiting) and a floor-wide on_hold count of answered customers on MusicOnHold.
call_connected
{
"extension": "1007",
"oli_id": "DUMMY-001",
"form_type": "GST",
"priority_id": 6
}
Once per call when the conversation is actually up.
redial
After a drop, the CRM or the agent page emits redial from an agent socket. The engine skips wrap time and the queue, pins the lead to this agent, and originates again (progressive call.php, predictive customer-first to this extension). The agent panel Answered calls list uses student_id and sends ondemand: true while paused.
socket.emit('redial', { oli_id: 'OLI124' }, (result) => {
// { ok: true, oli_id: 'OLI124' }
// { ok: false, error: 'busy'|'not_ready'|'unknown_oli'|'originate_failed', message: '…' }
});
Works on Received, Not Connected, Abandoned, and Disposed rows. Rejected if the agent is already on another call, paused, or SIP is not idle. Then listen for dialer phase: dialing and call_connected.
status / panel
One extension: AMI category, panel active/inactive, presence ready/paused, pause_reason (open pause code while paused), pause_reason_note when the reason is Other, paused_at (epoch ms when the current pause started; dashboard Work column counts from this), name from fic_dialer_employee.display_name, assigned_roles from fic_dialer_employee.skills (JSON), panel_status (active / pause / yet_to_login), buffer_time, hold_timeout_seconds, microsip_alert when the panel is up but AMI is unavailable. CRM profile fields such as email/mobile are not on fic_dialer_employee and may be null. After day reset, panel_status is yet_to_login and pause reason/clock are null until the agent goes Active.
snapshot
updated_at, ami, summary (AMI on_call / idle / unavailable / total plus predictive on_hold), dialer (active config for this module’s module_id, including hold_timeout_seconds), extensions[] (same shape as status, only endpoints present in this module’s dialer_employee roster — not every PJSIP contact).
Built-in pages
| Page | Emits | Listens |
|---|---|---|
Dashboard / | join dashboard | snapshot, patch, dialer, panel, presence, health |
Agent /agent.html?ext=1007 | join agent, ready / pause, redial | status, dialer, call_connected, joined, server_error |
Socket does not push leads. Use POST /api/leads and PUT /api/escalated-tickets. Normal outbound is still engine-driven; redial is the client asking the engine to originate one specific lead now. Hub onAgentState is an in-process callback into the dialer, not a Socket.IO event.