- LogStreamService: singleton that extends ConsoleLogger, captures all
NestJS log output into an RxJS Subject while preserving stdout
- main.ts: uses LogStreamService.instance as app logger
- supervisor.controller.ts: new @Sse('logs/stream') endpoint pipes
log entries (timestamp, level, context, message) to connected clients
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New worklist SSE stream replaces the 30s frontend poll. When the
missed-call webhook creates a Call record, it emits a worklist-updated
event via the supervisor's worklistSubject. All connected agents
receive the event immediately.
- supervisor.service.ts: worklistSubject + emitWorklistUpdate()
- supervisor.controller.ts: @Sse('worklist/stream') broadcast endpoint
- missed-call-webhook.controller.ts: emits after createCall() with
callerPhone + callerName for toast notification
- worklist.module.ts: imports SupervisorModule (forwardRef)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three related fixes:
1. Disposition for answered inbound calls
Previously the dispose endpoint sent the agent's choice to Ozonetel
but never wrote it back to the platform Call record. The webhook's
pre-disposition value ("General Enquiry" → INFO_PROVIDED) persisted.
Now: dispose endpoint finds the Call by UCID and updates disposition
to the agent's actual selection.
2. SLA timing wiring (assignedAt / answeredAt / responseTimeS)
patchCallTiming() existed but was never called. Now wired into
handleCallEvent:
- "Calling" event → writes assignedAt (ring start)
- "Answered" event → writes answeredAt + computes responseTimeS
(answeredAt - startedAt = caller wait time)
Uses patchCallTimingByUcid helper that looks up Call by UCID.
3. Backfill maint endpoint: POST /api/maint/backfill-call-disposition-timing
Walks calls for a given date, joins to CDR by UCID (both legs),
patches disposition (from CDR's mapped value, always overwrites),
timing fields (answeredAt, assignedAt, responseTimeS from CDR),
and CDR-specific durations (handlingTimeS, acwDurationS, holdDurationS).
Idempotent — safe to run multiple times.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bug 560: Live Call Monitor showed ghost calls with runaway timers when
the agent wasn't on a call. Cause — activeCalls Map only added on
'Answered' and deleted on 'Disconnect'; a missed Disconnect (sidecar
restart, Ozonetel subscription hiccup, network blip) left the entry
lingering forever.
getActiveCalls() now sweeps stale entries before returning:
- drop if startTime is older than 30 minutes
- drop if the mapped agent is currently ready / offline / paused
(agent can't be on a call in any of those states)
Each sweep logs the reason so we can track how often this fires.
Audited all 23 sidecar create-mutation call sites; 7 were missing the
top-level data.name field that the platform uses as record title:
- caller-resolution.service.ts createPatient — full name from first/last
- maint.controller.ts createPatient (backfill-lead-patient-links) — same
- widget.service.ts createPatient (chat path + booking path) — full name
- widget.service.ts createAppointment — "<Patient> — <date>"
- worklist/missed-queue.service.ts createCall — "Missed — <phone>"
- rules-engine/actions/escalate.action.ts createPerformanceAlert —
"<agent>: <message> (<value>)"
- supervisor/agent-history.service.ts createAgentEvent / createAgentSession
Cosmetic only — the app fetches fullName/agentName for display, so
end users never saw "Untitled". Fixes platform-side admin browsing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase A+B of the alerts overhaul:
- New PerformanceFactsProvider exposes agent.idleMinutes (from
AgentSession), agent.busyMinutes, agent.totalCallsToday,
agent.bookedCallsToday, agent.conversionPercent
- Implement EscalateActionHandler (was a stub): persists a
PerformanceAlert row, dedupes per agent+type+IST date so a 5-min
cron can't spam, updates value if it changes
- New PerformanceConsumer: setInterval every 5 min, reads on_schedule
rules referencing agent.* facts, evaluates per agent, dispatches
escalate actions
- Two starter rules in hospital-starter.json: excessive-idle (>60min)
and low-conversion (<15% with >10 calls today). NPS deferred — no
source signal exists yet
- New PerformanceAlertsController: GET /api/supervisor/performance-alerts
(active list), POST /:id/dismiss, POST /dismiss-all
- Rules engine now injects EscalateActionHandler via DI so the action
has access to PlatformGraphqlService for persistence
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 3 — wire the dashboard to the new metrics path without touching
the frontend. getTeamPerformance now:
1. Fetches AgentSession rows for the given IST date (keyed by agent UUID)
2. For each agent: uses AgentSession data rendered as HH:MM:SS if a row
exists, otherwise falls back to Ozonetel summaryReport
3. Returns timeBreakdownSource so the frontend can optionally show which
source was used
Frontend continues to parse the existing HH:MM:SS shape via parseTime()
— no UI change needed. Historical dates without AgentSession rows still
render via Ozonetel.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
CALL and ACW overlap: an agent enters ACW before the CALL_END webhook
arrives. With a single shared pending slot, ACW_START would clobber the
pending CALL_START and CALL_END would compute 0-second duration against
the ACW_START timestamp. Verified in production data — 4/4 CALL_END rows
on Global had durationS=0.
Fix: one slot per category (pause/call/acw). Each END reads and clears
its own slot. READY and LOGOUT defensively flush all slots to avoid
leaking state across sessions.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New AgentHistoryService: persistAgentEvent pairs START/END for durationS, patchCallTiming updates Call SLA fields
- Supervisor service wires handleCallEvent (CALL_START on Answered, CALL_END on Disconnect) and handleAgentEvent (LOGIN/LOGOUT/PAUSE/RESUME/ACW_START/ACW_END/READY) via priorState-aware mapping
- setInterval-based nightly-ish rollup: every 15min aggregates AgentEvent into AgentSession per IST day (idempotent upsert by agentId+date)
- Ozonetel dispose flow extracts HandlingTime/WrapupDuration/HoldDuration from CDR, patches Call timing fields
- Field names match platform truncation: durationS, loginDurationS, busyTimeS, idleTimeS, pauseTimeS, wrapupTimeS, avgHandlingTimeS, handlingTimeS, acwDurationS, holdDurationS, responseTimeS, sessionDate → date
- Skips cleanly on workspaces where AgentEvent entity isn't synced
Known issue: pending-pair map has single slot per agent, so ACW_START overwrites pending CALL_START and CALL_END computes 0s duration. Fix in followup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ozonetel sends action: 'pause' via webhook when agent is paused, but
mapOzonetelAction only handled 'AUX'. The 'pause' action fell through
to default (null), so the break SSE event was never emitted. The agent
UI stayed on 'Ready' while Ozonetel had the agent PAUSED.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Endpoints:
- GET /api/supervisor/barge/sip-credentials — fetch SIP number from pool
- POST /api/supervisor/barge — initiate barge via Ozonetel apiId 63
- POST /api/supervisor/barge/mode — update mode (listen/whisper/barge)
- POST /api/supervisor/barge/end — cleanup session + Redis
SupervisorService extended with barge session tracking (in-memory Map).
Mode changes emit SSE events to agent: supervisor-whisper, supervisor-barge,
supervisor-left. Listen mode is silent (no event to agent).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
#536: Performance endpoint now accepts agentId query param and filters
CDR to that agent only. Previously returned all agents' calls as one
agent's total. Fixed 'Unanswered' → 'NotAnswered' status filter.
#538: Team performance now includes per-agent call metrics (total,
inbound, outbound, answered, missed) from CDR data + teamTotals
aggregate. Previously only returned Ozonetel time breakdown without
any call counts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When Ozonetel sends an ACW event, starts a 30-second timer. If no
/api/ozonetel/dispose call arrives within that window (frontend
crashed, tab closed, page refreshed), auto-disposes with "General
Enquiry" + autoRelease:true. Agent exits ACW automatically.
Timer is cancelled when:
- Frontend submits disposition normally (cancelAcwTimer in controller)
- Agent transitions to Ready or Offline
- Agent logs out
Wiring: OzonetelAgentModule now imports SupervisorModule (forwardRef
for circular dep), controller injects SupervisorService to cancel
the timer on successful dispose.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Team module: POST /api/team/members (in-place employee creation with
temp password + Redis cache), PUT /api/team/members/:id, GET temp
password endpoint. Uses signUpInWorkspace — no email invites.
- Dockerfile: rewritten as multi-stage build (builder + runtime) so
native modules compile for target arch. Fixes darwin→linux crash.
- .dockerignore: exclude dist, node_modules, .env, .git, data/
- package-lock.json: regenerated against public npmjs.org (was
pointing at localhost:4873 Verdaccio — broke docker builds)
- Doctor utils: shared DOCTOR_VISIT_SLOTS_FRAGMENT + normalizeDoctors
helper for visit-slot-aware queries across 6 consumers
- AI config: full admin CRUD (GET/PUT/POST reset), workspace-scoped
setup-state with workspace ID isolation, AI prompt defaults overhaul
- Agent config: camelCase field fix for SDK-synced workspaces
- Session service: workspace-scoped Redis key prefixing for setup state
- Recordings/supervisor/widget services: updated to use doctor-utils
shared fragments instead of inline visitingHours queries
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SSE agent state stream: supervisor maintains state map from Ozonetel webhooks, streams via /api/supervisor/agent-state/stream
- Force-logout via SSE: distinct force-logout event type avoids conflict with normal login cycle
- Maint module (/api/maint): OTP-guarded endpoints for force-ready, unlock-agent, backfill-missed-calls, fix-timestamps
- Fix Ozonetel IST→UTC timestamp conversion: istToUtc() in webhook controller and missed-queue service
- Missed call lead lookup: ingestion queries leads by phone, stores leadId + leadName on Call entity
- Timestamp backfill endpoint: throttled at 700ms/mutation, idempotent (skips already-fixed records)
- Structured logging: full JSON payloads for agent/call webhooks, [DISPOSE] trace with agentId
- Fix dead code: agent-state endpoint auto-assign was after return statement
- Export SupervisorService for cross-module injection
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SupervisorService: aggregates Ozonetel agent summary across all agents,
tracks active calls from real-time events
- GET /api/supervisor/team-performance — per-agent time breakdown + thresholds
- GET /api/supervisor/active-calls — current active call map
- POST /api/supervisor/call-event — Ozonetel event webhook
- POST /api/supervisor/agent-event — Ozonetel agent event webhook
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>