The Agent Performance table on the team dashboard was bucketing by raw
call.agentName — the field that holds Ozonetel's transfer-chain string
("RamaiahAdmin -> GlobalHealthX") and collides for distinct AgentIDs
that share a Full Name. Result: 7 rows for 3 real agents.
Now buckets by call.agent.id when the CDR enrichment has populated it,
falls back to legacy agentName grouping otherwise. Calls without any
agent info are dropped from the agent rollup (instead of being
collapsed under "Unknown").
Pulls agent { id name ozonetelAgentId } + transferredTo + transferType
on CALLS_QUERY.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Helix Engage — Frontend
Call center CRM frontend for healthcare lead management. Built on the FortyTwo platform.
Owner: Mouli
Architecture
┌─────────────────────┐ ┌──────────────────────┐ ┌─────────────────────┐
│ helix-engage │ │ helix-engage-server │ │ FortyTwo Platform │
│ (this repo) │────▶│ (sidecar) │────▶│ (backend) │
│ React frontend │ │ NestJS REST API │ │ GraphQL API │
│ Port 5173 (dev) │ │ Port 4100 │ │ Port 4000 │
└─────────────────────┘ └──────────────────────┘ └─────────────────────┘
│ │
│ SIP/WebRTC │ Ozonetel CloudAgent APIs
▼ ▼
┌───────────┐ ┌──────────────┐
│ Ozonetel │ │ Ozonetel │
│ SIP (444) │ │ REST APIs │
└───────────┘ └──────────────┘
Three repos:
| Repo | Purpose | Owner |
|---|---|---|
helix-engage (this) |
React frontend | Mouli |
helix-engage-server |
NestJS sidecar — Ozonetel + Platform bridge | Karthik |
helix-engage-app |
FortyTwo SDK app — entity schemas (Call, Lead, etc.) | Shared |
Getting Started
npm install
npm run dev # http://localhost:5173
npm run build # TypeScript check + production build
Environment Variables (set at build time or in .env)
| Variable | Purpose | Dev Default | Production |
|---|---|---|---|
VITE_API_URL |
Platform GraphQL | http://localhost:4000 |
https://engage-api.srv1477139.hstgr.cloud |
VITE_SIDECAR_URL |
Sidecar REST API | http://localhost:4100 |
https://engage-api.srv1477139.hstgr.cloud |
VITE_SIP_URI |
Ozonetel SIP URI | — | sip:523590@blr-pub-rtc4.ozonetel.com |
VITE_SIP_PASSWORD |
SIP password | — | 523590 |
VITE_SIP_WS_SERVER |
SIP WebSocket | — | wss://blr-pub-rtc4.ozonetel.com:444 |
Production build command:
VITE_API_URL=https://engage-api.srv1477139.hstgr.cloud \
VITE_SIDECAR_URL=https://engage-api.srv1477139.hstgr.cloud \
VITE_SIP_URI=sip:523590@blr-pub-rtc4.ozonetel.com \
VITE_SIP_PASSWORD=523590 \
VITE_SIP_WS_SERVER=wss://blr-pub-rtc4.ozonetel.com:444 \
npm run build
Tech Stack
- React 19 + TypeScript + Vite
- Tailwind CSS 4 with semantic color tokens (
text-primary,bg-brand-section— never raw colors liketext-gray-900) - React Aria Components for accessibility (imports always prefixed
Aria*) - Jotai for SIP/call state
- React Context for auth, data, theme
- FontAwesome Pro Duotone icons
- Untitled UI component library (
src/components/base/,src/components/application/)
Project Structure
src/
├── pages/ # Route-level pages
│ ├── call-desk.tsx # Main CC agent workspace — THE CORE PAGE
│ ├── login.tsx # Auth page (centered card on blue bg)
│ ├── call-history.tsx # CDR log viewer
│ ├── my-performance.tsx # Agent KPI dashboard
│ ├── team-dashboard.tsx # Supervisor overview
│ ├── all-leads.tsx # Lead master table
│ └── campaigns.tsx # Campaign listing
│
├── components/
│ ├── call-desk/ # ⚡ Call center components — WHERE MOST WORK HAPPENS
│ │ ├── active-call-card.tsx # In-call UI + post-call disposition flow
│ │ ├── worklist-panel.tsx # Agent task queue with tabs + sub-tabs
│ │ ├── context-panel.tsx # AI assistant + Lead 360 sidebar
│ │ ├── disposition-form.tsx # Post-call outcome selector
│ │ ├── appointment-form.tsx # Book appointment during/after call
│ │ ├── agent-status-toggle.tsx # Ready/Break/Training/Offline toggle
│ │ ├── transfer-dialog.tsx # Call transfer
│ │ ├── enquiry-form.tsx # General enquiry capture
│ │ ├── live-transcript.tsx # Real-time transcription (Deepgram)
│ │ └── phone-action-cell.tsx # Click-to-call in table rows
│ ├── base/ # Untitled UI primitives (Button, Input, Select, Badge)
│ ├── application/ # Complex UI (Table, Modal, Tabs, DatePicker, Nav)
│ ├── layout/ # Sidebar — role-based navigation
│ └── dashboard/ # KPI cards, charts, missed queue widget
│
├── providers/
│ ├── sip-provider.tsx # SIP WebRTC — call lifecycle management
│ ├── auth-provider.tsx # User session, roles (executive/admin/cc-agent)
│ ├── data-provider.tsx # Bulk entity loader (leads, campaigns, calls)
│ └── theme-provider.tsx # Light/dark mode
│
├── hooks/
│ ├── use-worklist.ts # Polls sidecar /api/worklist every 30s
│ ├── use-call-assist.ts # Live transcript via Socket.IO
│ └── use-sip-phone.ts # Low-level SIP.js wrapper
│
├── lib/
│ ├── api-client.ts # REST + GraphQL client (auth, queries, sidecar calls)
│ ├── queries.ts # Platform GraphQL query strings
│ └── format.ts # Phone/date formatting
│
├── state/
│ └── sip-state.ts # Jotai atoms (callState, callerNumber, isMuted, etc.)
│
└── types/
└── entities.ts # Lead, Patient, Call, Appointment, etc.
Troubleshooting Guide — Where to Look
"The call desk isn't working"
File: src/pages/call-desk.tsx
This is the orchestrator. It uses useSip() for call state, useWorklist() for the task queue, and renders either ActiveCallCard (in-call) or WorklistPanel (idle). Start here, then drill into whichever child component is misbehaving.
"Calls aren't connecting / SIP errors"
File: src/providers/sip-provider.tsx + src/state/sip-state.ts
Check VITE_SIP_* env vars. Ozonetel SIP WebSocket runs on port 444 — VPNs block it. If WebSocket hangs at "connecting", turn off VPN. Also check browser console for SIP.js registration errors.
"Worklist not loading / empty"
File: src/hooks/use-worklist.ts
This polls GET /api/worklist on the sidecar every 30s. Open browser Network tab → filter for /api/worklist. Common causes: sidecar is down, auth token expired, or agent name doesn't match any assigned leads.
"Missed calls not appearing / sub-tabs empty"
File: src/components/call-desk/worklist-panel.tsx
Missed calls come from the sidecar worklist response. The sub-tabs filter by callbackstatus field. If all sub-tabs are empty, the sidecar ingestion may not be running (check sidecar logs for MissedQueueService).
"Disposition / appointment not saving"
File: src/components/call-desk/active-call-card.tsx → handleDisposition()
Posts to sidecar POST /api/ozonetel/dispose. Errors are caught silently (non-blocking). Check browser Network tab for the dispose request/response, then check sidecar logs.
"Login broken / Failed to fetch"
File: src/pages/login.tsx + src/lib/api-client.ts
Login calls apiClient.login() → sidecar /auth/login → platform GraphQL. Most common cause: wrong VITE_API_URL (built with localhost instead of production URL). Always set env vars at build time.
"UI component looks wrong"
Files: src/components/base/ (primitives), src/components/application/ (complex)
These come from the Untitled UI library. Design tokens are in src/styles/theme.css. Brand colors were rebuilt from logo blue rgb(32, 96, 160).
"Navigation / role-based access"
File: src/components/layout/sidebar.tsx
Navigation groups are defined per role (admin, cc-agent, executive). Routes are registered in src/main.tsx.
Data Flow
User action
│
▼
Component (e.g. ActiveCallCard)
│
├──▶ Sidecar REST API (via apiClient.post/get)
│ e.g. /api/ozonetel/dispose, /api/worklist
│
├──▶ Platform GraphQL (via apiClient.graphql)
│ e.g. leads, appointments, patients queries
│
└──▶ SIP.js (via useSip() hook)
Call control: answer, hangup, mute, hold
Key pattern: The frontend talks to TWO backends:
- Sidecar (REST) — for Ozonetel telephony operations and worklist
- Platform (GraphQL) — for entity CRUD (leads, appointments, patients)
Conventions
- File naming: kebab-case (
worklist-panel.tsx) - Colors: Semantic tokens only (
text-primary,bg-brand-section) - Icons:
@fortawesome/pro-duotone-svg-icons+faIcon()wrapper insrc/lib/icon-wrapper.ts - React Aria: Always prefix imports (
Button as AriaButton) - Transitions:
transition duration-100 ease-linear
Git Workflow
dev— active developmentmaster— stable baseline- Always build with production env vars before deploying