Dashboard — Call Pickup Time¶
The Pickup Time card on /dashboard/<orgId>/overview shows the average seconds from queue entry to agent pickup for answered inbound queue calls in the selected time window. Hospitals use it to gauge how quickly their agents respond to patient calls.
Status: shipped 2026-05-21 via PRs #267 / #269 / #271 / #272 (promoted to main via #273).
Pickup Time is one of four range-scoped cards (Total Calls, Pickup Time, Open Tickets [live], Avg Duration). The time selector at the page header drives all four cards + the Call Volume chart so the visible scope is always one consistent window.
What it measures¶
For each answered inbound queue call:
- Parent CDR (
PJSIP/<trunk> → Queue(...)):duration = billsecbecause the dialplan callsAnswer()immediately for queue MOH. There's no ring-time signal in the parent row. - Answered queue child leg (
Local/qm…@<org>__qmem → PJSIP/<agent>):duration = ring + talkandbillsec = talk. Sochild.duration - child.billsec= the agent's actual ring seconds before pickup.
For RingAll queues (the hospital default) this equals the caller's wait time exactly. It matches what Asterisk announces to the agent on connect: "Hold time: N seconds."
The SQL¶
In api/src/server.js → /api/v1/calls/stats:
SELECT
ROUND(AVG(child.duration - child.billsec), 1) AS avg_pickup_secs,
COUNT(*) AS sample_calls
FROM asterisk_cdr parent
JOIN asterisk_cdr child
ON child.linkedid = parent.linkedid
AND child.id != parent.id
WHERE (parent.accountcode = ? OR parent.peeraccount = ? OR parent.channel LIKE ?)
AND parent.dcontext LIKE '%incoming%'
AND parent.disposition = 'ANSWERED' AND parent.billsec > 0
AND child.disposition = 'ANSWERED' AND child.billsec > 0
AND child.dstchannel LIKE 'PJSIP/%'
AND child.channel LIKE 'Local/qm%qmem%'
AND parent.calldate >= DATE_SUB(CURDATE(), INTERVAL <days> DAY);
Required signals (in order of restrictiveness):
- Parent must be inbound (
dcontext LIKE '%incoming%') and ANSWERED withbillsec > 0— excludes IVR-only calls that never reached an agent. - Child must be ANSWERED with
billsec > 0— picks the agent leg that actually had talk time, not the parallel agents who only rang then gave up. - Child channel is the Local queue-member dial-out (
Local/qm…@<org>__qmem), and child dstchannel is the agent's PJSIP endpoint — rules out non-queue Local channels. - Time window filters on the parent's
calldate(the inbound channel timestamp).
The same accountcode/peeraccount/channel-prefix filter the rest of /api/v1/calls/stats uses isolates per-org.
Accuracy by queue strategy¶
| Strategy | Pickup Time accuracy | Notes |
|---|---|---|
| ringall | ✅ Exact | Every agent rings simultaneously. Answered child's ring time = caller's holdtime. Matches Hold time: N seconds announcement. |
| leastrecent / fewestcalls / random | ✅ Exact | Single agent rings at a time; the answered child's ring time = total caller wait. |
| sequential / linear | ⚠️ Under-counts | Multiple agents ring sequentially. Each agent's ring is a separate child row. We pick the answered child (the last one), but the caller actually waited through all the prior rings. The metric still bounds pickup-time from below — so a "30s pickup" display means at least 30s, possibly more. The framing "X or worse" remains sound. |
| rrmemory / rrordered | ✅ Approximately exact | Similar to ringall in that the typical agent rotation rings one or a few; in practice indistinguishable from ringall for the avg metric. |
Current prod hospital orgs all use ringall. If a customer switches to a sequential strategy, the under-count caveat applies and we should revisit (see #270 for the queue_log realtime path that would fix this).
Three-tier badge¶
Thresholds set 2026-05-21 by Operations:
| Pickup avg | Tier | Badge variant | Subtitle |
|---|---|---|---|
≤ 20s | Healthy | outline | "Agents are responsive" |
20 – 30s | Moderate | secondary | "Acceptable — worth watching" |
> 30s | Needs intervention | destructive (red) | "Operationally too slow — escalate" |
| no answered calls in window | (badge: "no data") | outline | "No answered queue calls yet" |
Wording note: "Moderate" is deliberately used instead of "Average" — the card already displays an average value, so "Average" as a tier label reads ambiguously. Hovering the subtitle row shows the thresholds in a tooltip so an operator who sees "Moderate" can hover and immediately know the band.
Default time window¶
Pages land on Last 7 days by default (PR #272). Operator dashboards prioritise this-week operational health over 90-day historical trends. The page-level toggle lets operators expand to 30d or 90d when they want broader context. All four cards + the Call Volume chart respect whichever window is selected.
Why this approach and not queue_log realtime¶
queue_log is Asterisk's own per-event log (ENTERQUEUE / CONNECT / ABANDON / COMPLETE*). The CONNECT.data1 field is the canonical holdtime — strictly correct for every queue strategy. We could enable realtime ingestion via:
But on Asterisk 20.18 (tested 2026-05-21 on staging) this mapping alone is not sufficient — app_queue.so doesn't pick up the realtime mapping even after module unload + module load. The file /var/log/asterisk/queue_log keeps receiving events but the table stays empty. Needs more investigation; tracked in #270.
The CDR child-leg approach used here:
- ✅ Works on existing data (no Asterisk config change)
- ✅ No ingestion gap — historical calls have the metric immediately
- ✅ Same SQL engine as the rest of the stats endpoint (no new connection, no new pool)
- ⚠️ Under-counts for sequential queues (acceptable today; not used by current customers)
When #270 resolves and queue_log realtime ingestion is flowing, the metric can switch to AVG(CAST(data1 AS UNSIGNED)) WHERE event = 'CONNECT' against the queue_log table for strict correctness across all strategies.
Verified on prod CDR (last 90 days at promotion time)¶
All plausible hospital pickup times. Spot-checked 15 individual rows — all between 4-31s, no negatives, no NULLs, no outliers.
Related¶
- Concurrent Call Cap Architecture — sister dashboard work shipped same week (org + trunk caps, CDR cap_rejected userfield, call-logs badge).
- Queue Architecture — broader queue dialplan generation reference.
- Issue #270 — queue_log realtime activation gap. Unblocks sequential-queue correctness when resolved.
- PR #271 — the Pickup Time card. PR body documents the CDR-child-leg SQL discovery + verification.
- PR #272 — three-tier thresholds + 7d default.
- Issue #259 — API reference docs auto-gen (Scalar/Mintlify) for partner integrations. Unrelated to Pickup Time but the partner-docs roadmap.