- ThemeService: read/write/validate theme.json, auto-backup on save
- ThemeController: GET/PUT/POST /api/config/theme (public GET, versioned PUT)
- ThemeConfig type with version + updatedAt fields
- Default theme: Global Hospital blue scale
- ConfigThemeModule registered in AppModule
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /api/caller/invalidate — clears Redis cache for a phone number
- WorklistController: resolves agent name from login cache (avoids currentUser query)
- AuthController: caches agent name in Redis during login (keyed by token suffix)
- WorklistModule: imports AuthModule (forwardRef for circular dep)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- POST /api/ai/stream: streamText with tools, streams response via toTextStreamResponse
- Tools: lookup_patient, lookup_appointments, lookup_doctor (same as existing chat endpoint)
- Uses stopWhen(stepCountIs(5)) for multi-step tool execution
- Streams response body to Express response manually (NestJS + AI SDK v6 compatibility)
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>
- Token cache reduced from 55min to 10min (Ozonetel expires in ~15min)
- All API methods invalidate cached token on 401
- loginAgent forces logout + re-login when "already logged in"
to refresh SIP phone mapping
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Reduced token cache from 55min to 10min (Ozonetel expires in ~15min)
- All API methods invalidate cached token on 401 response
- Force-refresh token on CC agent login
- Removed unused withTokenRetry wrapper
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>
- SessionService stores JSON { memberId, ip, lockedAt } instead of plain memberId
- Auth controller extracts client IP from x-forwarded-for header
- Lockout error message includes IP of blocking device
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Block any login attempt when a session exists, regardless of user identity.
Same user on second device is blocked until logout or TTL expiry.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
POST /auth/refresh exchanges refresh token for new access token
via platform's renewToken mutation.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- MissedQueueService: polls Ozonetel abandonCalls every 30s, dedup by phone
- Auto-assigns oldest PENDING_CALLBACK call on agent Ready (dispose + state change)
- GET /api/worklist/missed-queue, PATCH /api/worklist/missed-queue/:id/status
- Worklist query updated with callback fields and FIFO ordering
- PlatformGraphqlService.query() made public for server-to-server ops
- forwardRef circular dependency resolution between WorklistModule and OzonetelAgentModule
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix Call record field names (recording, callerNumber, durationSec)
- Add POST /api/ozonetel/agent-ready using logout+login for Force Ready
- Add callerNumber to kookoo callback
- Better error logging with response body
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The toolbar uses TWO WebSocket connections:
1. CloudAgent WS (mdlConnection.php) for control events (newCall, busyAgent)
2. SIP WS (blr-pub-rtc4) for audio
tbManualDial requires browserSessionId + usId from the CloudAgent WS session.
Without these, CloudAgent doesn't route the SIP INVITE to our browser.
Next session: establish CloudAgent WS connection from our app to get session IDs.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Kookoo <dial> only routes to PSTN numbers, not SIP extensions.
Tested: 523590, 0523590 — both result in not_answered.
The bridge from Kookoo to browser SIP requires either:
- Ozonetel enabling /ca_apis/ auth (CloudAgent API)
- Or a way to route Kookoo calls to internal SIP extensions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When customer answers outbound call, Kookoo hits /kookoo/ivr which
returns <dial record="true">523590</dial> to bridge the call to
the agent's SIP extension. Agent's browser rings, both connect.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace CloudAgent V3 tbManualDial with Kookoo outbound.php
- Simple HTTP GET with api_key — no auth issues
- Kookoo callback endpoint: POST /webhooks/kookoo/callback
- Creates Call record in platform
- Matches caller to Lead by phone
- Remove agent login requirement before dial
- Tested: call queued successfully, phone rang
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Parse Ozonetel POST payload (CallerID, Status, Duration, Recording URL)
- Create Call record in platform via API key auth
- Match caller to Lead by phone number
- Create LeadActivity timeline entry
- Update lead contactAttempts and lastContacted
- Map Ozonetel dispositions to platform enum values
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Update default campaign to Inbound_918041763265
- Add Dockerfile and .dockerignore for container deployment
- Sidecar image: 043728036361.dkr.ecr.ap-south-1.amazonaws.com/fortytwo-eap/helix-engage-sidecar:alpha
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Replace raw @anthropic-ai/sdk with Vercel AI SDK (generateText, tool, generateObject)
- Add provider abstraction (ai-provider.ts) — swap OpenAI/Anthropic via env var
- AI chat controller: dynamic KB from platform (clinics, packages, insurance), zero hardcoding
- AI enrichment service: use generateObject with Zod schema instead of manual JSON parsing
- Worklist: resolve agent name from platform currentUser API instead of JWT decode
- Worklist: fix GraphQL field names to match platform remapping (source, status, direction, etc.)
- Config: add AI_PROVIDER, AI_MODEL, OPENAI_API_KEY env vars
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- CallEventsService orchestrates: Exotel webhook → lead lookup → AI enrichment → WebSocket push
- CallEventsGateway (Socket.IO /call-events namespace) with agent room registration and disposition handling
- EnrichedCallEvent/DispositionPayload types for frontend contract
- Disposition flow: creates Call record, updates lead status, logs lead activity
- Wired ExotelController to forward answered/ended events to CallEventsService
- forwardRef used to resolve circular dependency between gateway and service
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds POST /auth/login and POST /auth/tokens endpoints that proxy
GraphQL mutations to the fortytwo-eap-core platform, letting the
frontend use only the sidecar URL for authentication.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>