During a load test, FreeSWITCH had live channels and the dashboard showed no active calls. Both were correct. The test was making local echo calls through real browser sessions; the dashboard counted customer calls owned by the dialer workflow.

That distinction came up repeatedly while working on the dialer. A successful provisioning request sat alongside a failed softphone connection. A clean recording sat alongside broken browser audio. A completed call still said Needs AVMD review.

Each pair looked contradictory until we named what each measurement actually described. We ended up separating product state, PBX runtime, browser media and post-call review.

Product state tells the agent what they can do

The database owns the business lifecycle: which agent has the call, which contact is being attempted, whether the call ended and whether the next lead can be offered. The agent desk should use that state to decide which controls are available.

It needs clear transition rules. Duplicate events must not release an agent twice. Late callbacks must not reopen completed work. A provider rejection must finish the attempt even if FreeSWITCH never creates a normal customer channel.

A simplified lifecycle looks like this:

EXAMPLE

requested       -> dialing
dialing         -> bridged
bridged         -> voicemail_handoff
voicemail_handoff -> completed
*               -> terminal

ESL events supply evidence for those transitions. Updating the call record directly from every callback leaves the final result dependent on arrival order.

This gives the agent a consistent workflow. It still doesn't tell us how many channels are alive in FreeSWITCH.

PBX state tells us what is running

We had two separate load-test paths. The read-only Agent Desk path exercised polling and SSE:

EXAMPLE

GET /agent/desk
GET /agent/events

That tested authentication, database reads, response time, SSE connections and frontend refresh behavior.

The SIP/RTP path launched browser sessions, registered SIP.js over WSS and made local FreeSWITCH echo calls. It exercised registration, INVITE/answer, ICE, DTLS, SRTP and RTP without using the public SIP trunk.

Those echo calls were real PBX work, but they weren't customer calls in the product database. We needed separate counters:

EXAMPLE

outbound_dialer_freeswitch_active_channels
outbound_dialer_freeswitch_registrations

Even for normal traffic, channels and calls aren't interchangeable: a bridged call can have multiple legs. Labelling both numbers “active calls” invites the wrong comparison.

Provisioning succeeded; WSS returned 502

The UI showed Phone unavailable, although the provisioning endpoint returned 200. Provisioning had returned SIP credentials and connection settings successfully. The browser still had to open a WebSocket through this path:

EXAMPLE

/freeswitch-ws -> proxy -> internal FreeSWITCH WSS upstream

The proxy rejected the upstream's internal self-signed certificate and returned 502. SIP registration couldn't begin.

Looking only at the API response sent the investigation toward the softphone. Checking the WSS request located it at the proxy/upstream connection. This needed a deployment fix; more SIP.js retries would just repeat the failed connection.

The recording missed the broken audio path

In another incident, pre-answer audio sounded slowed and intermittent in the browser. The customer-leg recording was clean, and the provider's RTP looked healthy.

We eventually traced this to pacing on the browser-bound early-media path. I've kept the packet counts and timer fix in the audio-dropout write-up.

For observability, the missing piece was browser evidence. RTCPeerConnection.getStats() can report received and lost packets, jitter, jitter-buffer delay, concealed samples, codec, RTT and the selected ICE candidate pair. Those observations describe what reached the agent's browser, downstream of the server recording.

Attaching a sample to whatever call the UI currently considered active was unreliable. Polling and hangup timing could change that state while telemetry was being collected. Instead, the internal agent INVITE carried the call identity:

EXAMPLE

X-Outbound-Dialer-Call-ID: <call-id>

SIP.js could then upload cumulative summaries periodically and at termination. The backend stored one bounded row per call. That gave operators call-level evidence without retaining an unlimited stream of raw samples.

Record the microphone settings that actually took effect

Browser capture settings belong in that evidence too. In this dialer, we avoided forcing sampleRate: 8000 just because the trunk ultimately used narrowband PCMU. We requested:

EXAMPLE

channelCount: 1
echoCancellation: true
noiseSuppression: true
autoGainControl: true

These were the defaults chosen for this application, not a prescription for every browser or microphone. The useful operational detail was recording the settings the browser actually applied. A requested constraint and the resulting capture configuration can differ.

Review state describes unfinished analysis

Needs AVMD review caused a different misunderstanding. It sounded like live call processing was waiting on the detector.

In this system, it meant the call had been answered and ended, AVMD had been attempted, and nobody had supplied a manual review label yet. The label fed coverage, precision, recall and confusion-matrix calculations.

It didn't change the completed outcome, requeue the contact, alter voicemail drop or tune the detector. The remaining work was human review for analytics. Putting that label next to live call state without explaining it made completed work look blocked.

The refresh mechanism also needed debugging

We also found a feedback loop in the dashboard itself. The browser subscribed over SSE; PostgreSQL pg_notify told it when to refresh.

An Agent Desk refresh could execute an UPDATE agents ... that affected zero rows. A statement-level trigger still sent a notification. The browser refreshed, performed another zero-row update and received another notification.

The requests kept repeating:

EXAMPLE

GET /agent/desk
GET /admin/overview
GET /admin/csv-imports

Changing the notification trigger to row-level behavior stopped zero-row updates from announcing a change. We kept a regression test for that case. Slowing the polling interval wouldn't have removed the loop.

Give each dashboard number a question to answer

The resulting set of measurements was more useful than one overall status:

EXAMPLE

Product active calls
  Calls currently owned by the dialer workflow.

FreeSWITCH active channels
  Channels currently alive in the PBX runtime.

FreeSWITCH registrations
  Browser SIP endpoints currently registered over WSS.

Media stats coverage
  Completed answered calls with server-side media evidence.

Browser media coverage
  Calls with browser getStats evidence attached.

AVMD review backlog
  Answered calls that need a human label for detector analytics.

Terminal latency
  Time between source hangup/progress events and durable call finalization.

When those numbers disagree, there is something specific to investigate. A flat product count during an echo test is expected. Live customer channels with no owning product record deserve attention. A completed call without browser telemetry means the evidence is missing; it doesn't establish that the audio was good or bad.

That was the useful change: the dashboard could tell an operator what was known, where it was observed and what still needed checking.