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.

WS_SECRET handshake Rooms: dashboard / ext:{n} src/ws/events.js

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

RoomJoin asReceives
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.

EmitPayloadServer 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? } requiredPresence 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.

ListenRoomWhen
joinedThat socketAfter a successful join
snapshotdashboardDashboard join, and full AMI snapshot
patchdashboardAMI incremental device-state changes
statusext:{n}AMI + panel/presence for that extension
paneldashboardAgent join, leave, ready, pause
presencedashboardLast tab for an extension left (reason: 'left')
dialerBothEngine phase: claim, buffer, connect, hangup
call_connectedBothBoth parties talking (once per call)
inboundDashboard; desk except waitingPSTN park, offer, talk, hangup
healthdashboardAMI/DB health
pongThat socketReply to ping
server_errorThat socketJoin / 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

PageEmitsListens
Dashboard /join dashboardsnapshot, patch, dialer, panel, presence, health
Agent /agent.html?ext=1007join 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.