saridsa2 517b2661b0 chore: move widget source into sidecar repo (widget-src/)
Widget builds from widget-src/ → public/widget.js
Vite outDir updated to ../public
.gitignore excludes node_modules

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 06:59:54 +05:30

Helix Engage Server — Sidecar Backend

NestJS sidecar that bridges Ozonetel telephony APIs with the FortyTwo platform. Handles agent auth, call control, disposition, missed call queue, worklist aggregation, AI enrichment, and live call assist.

Owner: Karthik

Architecture

┌─────────────────────┐     ┌──────────────────────┐     ┌─────────────────────┐
│   helix-engage      │     │ helix-engage-server   │     │  FortyTwo Platform  │
│   React frontend    │────▶│ (this repo)           │────▶│  GraphQL API        │
│                     │     │ Port 4100             │     │  Port 4000          │
└─────────────────────┘     └──────────────────────┘     └─────────────────────┘
                                     │
                                     │  Ozonetel CloudAgent APIs
                                     ▼
                              ┌──────────────┐
                              │ Ozonetel     │
                              │ in1-ccaas-api│
                              └──────────────┘

This server has no database. All persistent data flows to/from the FortyTwo platform via GraphQL. Ozonetel is the telephony provider (CloudAgent APIs).

Three repos:

Repo Purpose Owner
helix-engage React frontend Mouli
helix-engage-server (this) NestJS sidecar Karthik
helix-engage-app FortyTwo SDK app — entity schemas Shared

Getting Started

npm install
npm run start:dev    # http://localhost:4100 (watch mode)
npm run build        # Production build
npm run start:prod   # Run production build

Environment Variables

Variable Purpose Default
PORT Server port 4100
CORS_ORIGIN Allowed frontend origin http://localhost:5173
PLATFORM_GRAPHQL_URL FortyTwo GraphQL endpoint http://localhost:4000/graphql
PLATFORM_API_KEY FortyTwo API key (server-to-server)
EXOTEL_API_KEY Ozonetel API key
EXOTEL_API_TOKEN Ozonetel API token
EXOTEL_ACCOUNT_SID Ozonetel account SID
OZONETEL_AGENT_ID Default agent ID agent3
OZONETEL_AGENT_PASSWORD Default agent password
OZONETEL_SIP_ID Default SIP extension 521814
OZONETEL_DID Inbound DID number 918041763265
OZONETEL_CAMPAIGN_NAME Default campaign Inbound_918041763265
MISSED_QUEUE_POLL_INTERVAL_MS Missed call ingestion interval 30000
OPENAI_API_KEY For AI enrichment / call assist
ANTHROPIC_API_KEY Alternative AI provider
DEEPGRAM_API_KEY Live transcription (STT)

Module Structure

src/
├── ozonetel/               # ⚡ Ozonetel telephony — WHERE MOST WORK HAPPENS
│   ├── ozonetel-agent.controller.ts   # REST endpoints for agent operations
│   ├── ozonetel-agent.service.ts      # Ozonetel API wrapper (token, CDR, abandon calls)
│   ├── ozonetel-agent.module.ts       # Module wiring
│   └── kookoo-ivr.controller.ts       # IVR callback handler (XML responses)
│
├── worklist/               # Agent task queue + missed call queue
│   ├── worklist.controller.ts         # GET /api/worklist, missed queue endpoints
│   ├── worklist.service.ts            # Aggregates leads + missed calls + follow-ups
│   ├── missed-queue.service.ts        # Ingestion, dedup, auto-assignment
│   ├── missed-call-webhook.controller.ts  # Webhook receiver
│   └── kookoo-callback.controller.ts  # Kookoo webhook
│
├── call-events/            # Real-time call event processing
│   ├── call-events.service.ts     # Incoming call handling, AI enrichment, disposition logging
│   ├── call-events.gateway.ts     # WebSocket push to frontend (Socket.IO)
│   └── call-lookup.controller.ts  # Reverse phone lookup + AI enrichment
│
├── platform/               # FortyTwo platform GraphQL client
│   ├── platform-graphql.service.ts    # query() for server-to-server, queryWithAuth() for user JWT
│   ├── platform.types.ts             # Lead, Call, Activity types
│   └── platform.module.ts
│
├── search/                 # Cross-entity search
│   └── search.controller.ts          # GET /api/search — leads + patients + appointments
│
├── call-assist/            # Live call assistance
│   └── (Socket.IO namespace /call-assist, Deepgram STT, AI suggestions)
│
├── ai/                     # AI enrichment (lead summaries, suggested actions)
├── auth/                   # User auth proxy
├── graphql-proxy/          # GraphQL passthrough to platform
├── health/                 # Health check endpoint
├── config/
│   └── configuration.ts    # All env var loading
├── app.module.ts           # Root module — imports all feature modules
└── main.ts                 # NestJS bootstrap (port 4100, CORS)

API Endpoints

Ozonetel Agent (/api/ozonetel/)

Method Path Purpose
POST /agent-login Agent login to Ozonetel
POST /agent-logout Agent logout
POST /agent-state Change state (Ready/Pause) + auto-assign missed call on Ready
POST /agent-ready Force ready (logout + login)
POST /dispose Submit call disposition + update missed call status + auto-assign next
POST /dial Manual outbound dial
POST /call-control CONFERENCE, HOLD, UNHOLD, MUTE, UNMUTE, KICK_CALL
POST /recording Pause/unpause recording
GET /missed-calls Raw Ozonetel abandon calls
GET /call-history?date= CDR for a date
GET /performance?date= Aggregated agent metrics

Worklist (/api/worklist/)

Method Path Purpose
GET / Agent's worklist (missed calls + follow-ups + leads)
GET /missed-queue Missed calls grouped by callback status
PATCH /missed-queue/:id/status Update callback status on a missed call

Other

Method Path Purpose
GET /api/search?q= Cross-entity search (leads, patients, appointments)
POST /api/call/lookup Reverse phone lookup + AI enrichment
GET /api/health Health check
POST /graphql GraphQL proxy to platform

Troubleshooting Guide — Where to Look

"Agent can't log in to Ozonetel"

File: src/ozonetel/ozonetel-agent.controller.tsagentLogin() Service: src/ozonetel/ozonetel-agent.service.tsloginAgent() Uses HTTP Basic auth to Ozonetel's AgentAuthenticationV2 endpoint. "Already logged in" responses have status: "error" but are not real errors. Check OZONETEL_AGENT_ID and OZONETEL_AGENT_PASSWORD env vars.

"Disposition failing / ACW not releasing"

File: src/ozonetel/ozonetel-agent.controller.tsdispose() Service: src/ozonetel/ozonetel-agent.service.tssetDisposition() All dispositions currently map to 'General Enquiry' (campaign limitation). Uses autoRelease: 'true' to end ACW. If agent stays in ACW, the Ozonetel campaign's wrapup time (8s) may not have elapsed.

"Missed calls not being ingested"

File: src/worklist/missed-queue.service.tsingest() Runs on a 30s interval (onModuleInit). Polls Ozonetel abandonCalls API for the last 5 minutes. Look for log lines with [MissedQueueService]. Common issues: Ozonetel token expired (55-min cache), platform API key missing, phone number format mismatch.

"Auto-assignment not working"

File: src/worklist/missed-queue.service.tsassignNext() Triggered from two places: dispose() and agent-state() in ozonetel-agent.controller.ts. Queries platform for oldest PENDING_CALLBACK call with empty agentName. Uses a mutex to prevent race conditions. If no calls are assigned, check that callbackstatus field exists on the Call entity (custom field, all-lowercase in GraphQL).

"Worklist returning empty"

File: src/worklist/worklist.service.ts Three parallel queries: getMissedCalls(), getPendingFollowUps(), getAssignedLeads(). All filter by agentName. If the agent name from the JWT doesn't match what's stored in lead/call records, results will be empty. Check resolveAgentName() in worklist.controller.ts.

"Call events / webhooks not arriving"

File: src/call-events/call-events.service.ts Ozonetel sends webhooks to the sidecar. Check that the webhook URL is configured in the Ozonetel dashboard and that the sidecar is reachable from the internet (Caddy reverse proxy on the VPS).

"AI enrichment / call assist broken"

Files: src/ai/, src/call-assist/ Live transcription uses Deepgram Nova STT via raw WebSocket. AI suggestions use OpenAI gpt-4o-mini. Check DEEPGRAM_API_KEY and OPENAI_API_KEY env vars. The call-assist gateway uses Socket.IO namespace /call-assist.

"Search not finding records"

File: src/search/search.controller.ts Runs three parallel GraphQL queries (leads, patients, appointments), filters client-side. Requires minimum 2 characters. Uses the user's JWT (passed from frontend auth header).

Key Technical Patterns

Two Auth Models

  1. User JWT passthroughplatform.queryWithAuth(query, vars, authHeader) — for user-facing endpoints (worklist, search). The frontend sends its JWT and the sidecar forwards it.
  2. Server API keyplatform.query(query, vars) — for server-to-server operations (missed call ingestion, auto-assignment). Uses PLATFORM_API_KEY.

Ozonetel Token Caching

ozonetel-agent.service.tsgetToken() caches the bearer token for 55 minutes (tokens expire at 60 min). All CloudAgent API calls use this cached token.

Custom Field Naming

Fields added via the FortyTwo admin portal use all-lowercase GraphQL names:

  • callbackstatus (not callbackStatus)
  • callsourcenumber (not callSourceNumber)
  • missedcallcount (not missedCallCount)
  • callbackattemptedat (not callbackAttemptedAt)

App-defined (managed) fields keep camelCase: callStatus, agentName, etc.

Error Handling Pattern

Ozonetel endpoints return { status: 'error', message } instead of throwing — this prevents UI from blocking on telephony failures. The frontend catches errors silently on disposition and recording.

Deployment

npm run build
# Then tar + scp + docker cp + restart (see deploy script in project docs)

The sidecar runs inside a Docker container (fortytwo-staging-sidecar-1) on the staging VPS.

Git Workflow

  • dev — active development
  • master — stable baseline
Description
No description provided
Readme 727 KiB
Languages
TypeScript 99.7%
JavaScript 0.3%