Skip to content

Live Calls (event-driven) & call display pipeline

How the Live Calls page gets its data, why it's event-driven, and the verified Asterisk facts every display heuristic must respect. Shipped via astradial-platform PRs #505/#506 (2026-07-06/07), staging-verified.

Architecture

Asterisk ──AMI events──> eventListenerService.amiManager (boot-time, persistent)
                              │ 'channelEvent' (Newchannel/Newstate/Newexten/
                              │  NewCallerid/NewConnectedLine/NewAccountCode/
                              │  Hangup/Bridge*/Rename)
                    liveCallsService (api/src/services/liveCallsService.js)
                    • in-memory Map<channel, record> mirroring CoreShowChannel fields
                    • CoreShowChannels resync every 60s + after AMI rewire
                    • per-org views: filter → dedup by linkedid → enrich → qm-resolve
              ┌───────────────┼──────────────────────────┐
              ▼               ▼                          ▼
   GET /calls/live   GET /calls/live/stream (SSE)   POST /calls/automation-count
   (memory, <1ms;    push on change (500ms debounce,  (workflow-engine
    poll fallback)    15s heartbeat, 15min max age)    concurrency gate)
  • Passive by design: the service only listens. No Asterisk config, no dialplan, no AMI actions on call paths. Worst-case failure = wrong page, never an affected call.
  • Readiness gate: until the first successful snapshot resync, the state is unknown — /calls/live and the stream return 503 (clients keep showing existing data), automation-count returns {count:0} (the old AMI-failure semantics). A 5s maintenance tick retries the first resync and re-wires the event listener if eventListenerService rebuilt its AMI manager. Never serve an empty map as "no active calls".
  • Editor (live-calls/page.tsx + lib/pbx/client.ts liveStream): fetch-based SSE (Bearer auth; EventSource can't send headers), backoff reconnect, falls back to 10s polling only on 404/405/501 or two consecutive silent connects (buffering proxy); hidden tabs tear the stream down; durations tick client-side.

Verified Asterisk facts (AMI capture, staging, 2026-07-07)

Every one of these was learned the hard way — display heuristics that guessed differently shipped wrong From/To/CallerID:

  1. Trunk endpoints are NOT named "trunk". Generated names: org_<prefix>_tata, org_<prefix>_trunk<ts>, shared tata_gateway. Any channel-name test must match trunk|tata|gateway (TRUNK_LEG_RE in liveCallsService; trunkDial() in api/src/lib/callDirection.js; pollCdr's TRUNK_NAME). New provider slug ⇒ extend all three (long-term: source names from the SipTrunk table).
  2. The trunk leg's CallerIDNum is the DIALED party. The presented DID is stamped on the caller leg via NewCallerid a few ms after creation. Reading the trunk leg's CID displays "CallerID == To".
  3. Newexten reports the channel's BASE context (org_x__internal) even while executing the included __outbound context. Context can never detect outbound on extension legs — use the creation-time exten (external-length number ⇒ outbound).
  4. Inbound gateway legs are org-invisible at ring time (shared context tata-inbound, empty accountcode). Set(CDR(accountcode)=org.id) emits NewAccountCode ~2ms later (Asterisk 20) — org attribution must accept AccountCode === org.id, not just name-prefix matching.
  5. The DID a caller dialed = the channel's creation-time exten (_firstExten); the live Exten mutates every dialplan step (s, qm<hex> tokens). Only DID-shaped values (7+ digits) qualify.
  6. Dial(...@target): the @-target is a trunk only when it's a trunk NAME. Local-channel dials target dialplan contexts (…@org_x__internal) — a bare %@% outbound classifier misfires on them (90-day CDR audit: 117 such rows, 0 false positives with name-based patterns).

Debug method that works (use it before changing display logic)

  1. Passive AMI capture on staging: connect a second AMI client (creds in /opt/astrapbx/.env, AMI_USER/AMI_SECRET), send Events: EventMask: on, dump timestamped blocks to a file.
  2. Reproduce the call while capturing.
  3. Replay the captured events through liveCallsService._applyEvent() in a local harness (stub-models fixture) and print getOrgView() after each event — this deterministically reproduces exactly what the dashboard rendered, for the deployed version AND the candidate fix.
  4. For Call Logs (CDR pipeline): run the classifier CASE from callDirection.js directly against asterisk_cdr on the known linkedids.
  • Call Logs From/To/Direction share the same trunk-naming facts — classifier lives in api/src/lib/callDirection.js (single source of truth for /calls, /export, /history, /stats; pollCdr has an aligned JS copy feeding webhooks/auto-tickets).
  • Troubleshooting entries: Error 86 (Live Calls display), Error 87 (Call Logs display) in troubleshooting.