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, inbound waiting/offers, health, panel |
ext:{extension} | { role: 'agent', extension } | That agent’s status, dialer, call_connected, inbound ringing/answered/ended |
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 sales_dialer_employee.panel_status = active |
pause | { reason, note? } required | Presence paused. Writes sales_dialer_agent_pauses.reason. note required for other (12 words) |
presence | { presence: 'ready' | 'paused', reason?, note? } | Same as ready / pause. reason required when pausing |
ping | — | Reply pong |
redial | { oli_id, ondemand? } | Re-originate that lead on this extension (skip wrap / queue). Skill must match. Attempt call_type = redial |
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 sales_dialer_employee roster → extension is not assigned to this dialer. Ready/pause before join also emits server_error. Agent pause (and presence paused) requires a picker code from public GET /api/pause-reasons. Missing, unknown, or system-only codes emit server_error { "code": "invalid_pause_reason" } and do not pause. other also needs { note } (max 12 words). Pick ondemand before the on-demand OLI dialog. ready needs no reason. 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.
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) |
inbound | Dashboard; desk except waiting | PSTN park, offer, talk, hangup |
health | dashboard | AMI/DB health |
pong | That socket | Reply to ping |
server_error | That socket | Join / presence failures, and redial failures when no ack callback is used |
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, inbound, 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, call_type (outgoing / incoming / redial while live; null when waiting / buffer / paused — this is the attempt column, not engine mode). Predictive adds outbound (other ringing/hold/offer legs, phase ringing, waiting, or connecting) and a floor-wide on_hold count of answered customers waiting for an agent (outbound MOH + mid agent-offer + inbound waiting).
Every dialer event (both modes) also carries outbound_blocked (true when the agent has an overdue missed activity; outbound stops, inbound still rings) and missed_activities: [{ oli_id, due_at }] (overdue only, max 20; empty when not blocked). The block clears when the CRM stores a pending callback or disposition for the OLI.
call_connected
{
"extension": "1007",
"oli_id": "DUMMY-001",
"form_type": "GST",
"priority_id": 6,
"call_type": "outgoing"
}
Once per call when the conversation is actually up. call_type is outgoing / incoming / redial (same as sales_dialer_call_attempts).
redial
After a drop, the CRM or agent page emits redial from an agent socket. The engine skips wrap time and the queue, claims the lead if oli_id exists and form_type matches this agent’s skills (no priority_ids gate), and originates again (AMI). ondemand: true allows redial from pause; on success the hub also sets presence ready. Auto-dial attempts stay outgoing; this attempt (and a decline on that predictive offer) is stored with call_type = redial.
socket.emit('redial', { oli_id: 'OLI124' }, (result) => {
// { ok: true, oli_id: 'OLI124' }
// { ok: false, error: 'busy'|'not_ready'|'unknown_oli'|'not_owned'|'originate_failed'|'maintenance'|'missed_activity', message: '…' }
});
Reopens any active master row, including Received, Rejected, Not Connected, Abandoned, and Disposed. Rejected if the agent is already on another call, SIP is not idle, (unless ondemand) paused, or the floor is in maintenance. Skill mismatch returns not_owned. An outbound-blocked agent gets missed_activity (on-demand too) unless oli_id is one of their own missed OLIs. 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 sales_dialer_employee.display_name, assigned_roles from sales_dialer_employee.skills (JSON), panel_status, buffer_time, decline_cooldown_seconds, is_maintenance, microsip_alert when the panel is up but AMI is unavailable. Socket call_type here is engine mode (Progressive / Predictive), not the attempt column. CRM profile fields such as email/mobile are not on sales_dialer_employee and may be null.
snapshot
updated_at, ami, summary (AMI on_call / dialing / idle / unavailable / total plus on_hold; dialing is desks still ringing a customer — progressive phase dialing, or predictive outbound legs with phase ringing; agent offer after answer is on_hold, not dialing), dialer (active config for this module’s module_id, including decline_cooldown_seconds and is_maintenance; hold_timeout_seconds is not sent), extensions[] (same shape as status, only endpoints present in this module’s sales_dialer_employee roster — not every PJSIP contact).
inbound
PSTN caller. kind is waiting (dashboard only), ringing, answered, offer_failed, or ended. Fields include extension, caller_number, oli_id, client_name, form_type. Waiting callers stay on hold music until a skilled agent is offered. Decline uses decline_cooldown_seconds. Hold is unlimited until hangup or connect.
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 (on-demand) | status, dialer, call_connected, inbound, joined, server_error |
The agent page shows an outbound-blocked banner with the missed OLIs. The dashboard shows Outbound blocked in the Dialer column and an Outgoing blocked panel under Waiting idle. The CRM sales-dialer page joins the same ext:{extension} room and adds a Missed activities panel (from GET /api/missed-activities).
Socket does not push leads. Use POST /api/leads and POST /api/send-callback-request; after-call callbacks and dispositions use POST /api/pending-callbacks / pending-dispositions. 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.