Mirrors the existing theme config pattern so website widget settings can be
edited from the admin portal instead of baked into frontend env vars. Fixes
the current symptom where the staging widget is silently disabled because
VITE_WIDGET_KEY is missing from .env.production.
Backend (sidecar):
- src/config/widget.defaults.ts — WidgetConfig type + defaults
(enabled, key, siteId, url, allowedOrigins, hospitalName,
embed.loginPage, version, updatedAt)
- src/config/widget-config.service.ts — file-backed load / update /
rotate-key / reset with backups, mirroring ThemeService. On module init:
* first boot → auto-generates an HMAC-signed site key via
WidgetKeysService, persists both to data/widget.json and to Redis
* subsequent boots → re-registers the key in Redis if missing (handles
Redis flushes so validateKey() keeps working without admin action)
- src/config/widget-config.controller.ts — new endpoints under /api/config:
GET /api/config/widget public subset {enabled, key, url, embed}
GET /api/config/widget/admin full config for the settings UI
PUT /api/config/widget admin update (partial merge)
POST /api/config/widget/rotate-key revoke old siteId + mint a new key
POST /api/config/widget/reset reset to defaults + regenerate
- Move src/widget/widget-keys.service.ts → src/config/widget-keys.service.ts
(it's a config-layer concern now, not widget-layer). config-theme.module
becomes the owner, imports AuthModule for SessionService, and exports
WidgetKeysService + WidgetConfigService alongside ThemeService.
- widget.module stops providing WidgetKeysService (it imports ConfigThemeModule
already, so the guard + controller still get it via DI).
- .gitignore data/widget.json + data/widget-backups/ so each environment
auto-generates its own instance-specific key instead of sharing one via git.
TODO (flagged, out of scope for this pass):
- Protect admin endpoints with an auth guard when settings UI ships.
- Set WIDGET_SECRET env var in staging (currently falls back to the
hardcoded default in widget-keys.service.ts).
- Admin portal settings page for editing widget config (mirror branding-settings).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Streaming AI chat via Vercel AI SDK v6 UI message stream — tool-based
generative UI (pick_branch, list_departments, show_clinic_timings,
show_doctors, show_doctor_slots, suggest_booking). Typing indicator,
markdown suppressed, text parts hidden when widgets are rendered.
- Centralized Preact store (store.tsx) for visitor, leadId, captchaToken,
bookingPrefill, doctors roster, branches, selectedBranch — replaces prop
drilling across chat/book/contact tabs.
- Cloudflare Turnstile captcha gate rendered via light-DOM portal so it
renders correctly inside the shadow DOM (Turnstile CSS doesn't cross
shadow boundaries).
- Lead dedup helper (findOrCreateLeadByPhone, 24h phone window) shared
across chat-start / book / contact so one visitor == one lead. Booking
upgrades existing lead status NEW → APPOINTMENT_SET via updateLeadStatus.
- Pre-chat name+phone form captures the visitor; chat transcript logged
to leadActivity records after each stream.
- Booking wizard gains a branch step 0 (skipped for single-branch
hospitals); departments + doctors filtered by selectedBranch. Chat slot
picks prefill the booking details step and lock the branch.
- Window-level captcha gate, modal maximize mode, header badge showing
selected branch, widget font inherits from host page (fix :host { all:
initial } override).
- 23 FA Pro 7.1 duotone icons bundled — medical departments, nav, actions,
hospital/location-dot for branch context.
- main.ts: resolve public/ from process.cwd() so widget.js serves in both
dev and prod. tsconfig: exclude widget-src/public/data from server tsc.
- captcha.guard: switch from reCAPTCHA v3 to Cloudflare Turnstile verify.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Widget (embeddable):
- Preact + Vite library mode → 35KB IIFE bundle served from sidecar
- Shadow DOM for CSS isolation, themed from sidecar theme API
- AI chatbot (streaming), appointment booking (4-step wizard), lead capture form
- FontAwesome Pro duotone SVGs bundled as inline strings
- HMAC-signed site keys (Redis storage, origin validation)
- Captcha guard (Cloudflare Turnstile ready)
Sidecar endpoints:
- GET/PUT/DELETE /api/widget/keys/* — site key management
- GET /api/widget/init — theme + config (key-gated)
- GET /api/widget/doctors, /slots — doctor list + availability
- POST /api/widget/book — appointment booking (captcha-gated)
- POST /api/widget/lead — lead capture (captcha-gated)
Omnichannel webhooks:
- POST /api/webhook/facebook — Meta Lead Ads (verification + lead ingestion)
- POST /api/webhook/google — Google Ads lead form extension
- POST /api/webhook/whatsapp — Ozonetel WhatsApp callback (receiver ready)
- POST /api/webhook/sms — Ozonetel SMS callback (receiver ready)
Infrastructure:
- SessionService.setCachePersistent() for non-expiring Redis keys
- Static file serving from /public (widget.js)
- WidgetModule registered in AppModule
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- 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>