Populates clinicId on historical Appointments that predate clinicId
persistence. Infers from DoctorVisitSlot records:
1. Pull all appointments with clinicId=NULL
2. Pre-load visit slots per unique doctorId (batched)
3. For each appointment: match slots by weekday + time-window
4. Single clinic → use it
5. Multiple candidates → pick lex-order first (deterministic; logged)
6. Last resort → any slot's clinic
No CDR or rate-limited calls — pure platform queries. Safe to re-run;
idempotent (only patches rows still missing clinicId).
POST /api/maint/backfill-appointment-clinics
Header: x-maint-otp: <OTP>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
Inbound webhook rows store agentName as Ozonetel's display string
("Ganesh Bandi", "GlobalHealthX") which doesn't match either
ozonetelAgentId or platform Agent.name. Add ozonetelDisplayName as a
third index — populated on each platform Agent with the Full Name from
the Ozonetel admin UI.
After this + setting display names on Global + Ramaiah agents:
backfill matched 122/136 (~90%) on Global; remaining are calls handled
by agents that don't exist on this workspace.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Historical Calls pre-date UCID persistence, so the CDR-join enrichment
can't reach them. Fallback: parse agentName (may be "A -> B -> C"
transfer chain), take the final hop, resolve to Agent by ozonetelAgentId
(case-insensitive) or by Full Name. Preserve the full chain string in
transferredTo when it was actually chained.
No rate limit — pure platform queries, no CDR.
POST /api/maint/backfill-call-agents-by-name
Header: x-maint-otp: <OTP>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Ozonetel's webhook AgentName is a transfer-chain display string — same
display can collide (two agents both named "GlobalHealthX" with distinct
agent IDs), and chained like "RamaiahAdmin -> Ganesh Bandi -> GlobalHealthX".
Team Performance was bucketing every unique raw string as a separate
"agent", producing 7 rows for 3 real agents.
Fix — authoritative agent link via CDR AgentID (unique):
- New AgentLookupService (platform module): case-insensitive
ozonetelAgentId → Agent UUID cache, shared across webhook / dispose /
enrichment / backfill paths
- Webhook + outbound-dispose now persist UCID on Call so CDR can join
- Outbound dispose resolves agent relation at create time and overwrites
from CDR AgentID post-hoc (catches dial transfers)
- New CdrEnrichmentService: every 30 min fetches today + yesterday CDR,
patches Calls missing agentId / transferredTo / transferType by UCID
join. Well under Ozonetel's 2 req/min cap.
- Historical backfill maint endpoint: /api/maint/enrich-call-agents
with configurable day window (default 2, max 15). Rate-limited at 35s
between dates.
Call schema additions (synced on Global + Ramaiah): agent relation,
ucid, transferredTo, transferType. agentName remains for legacy/display.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Recomputes durationS on existing AgentEvent rows using the fixed
per-category pairing logic and re-runs the session rollup for every
affected date. Fixes the 0-second CALL_END durations written before the
slot-split fix.
Idempotent — only patches rows whose stored durationS differs from the
newly computed value. Safe to re-run.
POST /api/maint/backfill-agent-event-durations
Headers: x-maint-otp: <OTP>
Body: { "date": "YYYY-MM-DD" } (optional; defaults to today IST; use "all" to backfill every row)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
agent-state, dispose, dial, performance, force-ready, unlock-agent
all required agentId from the request body now. No silent fallback
to OZONETEL_AGENT_ID env var which caused cross-tenant operations
in multi-agent setups (Ramaiah operations hitting Global's agent).
OZONETEL_AGENT_ID removed from telephony env seed list. Hardcoded
fallbacks (agent3, Test123$, 521814) deleted.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Phase 1 of hospital onboarding & self-service plan
(docs/superpowers/plans/2026-04-06-hospital-onboarding-self-service.md).
Backend foundations to support the upcoming staff-portal Settings hub and
6-step setup wizard. No frontend in this phase.
New config services (mirroring ThemeService / WidgetConfigService):
- SetupStateService — tracks completion of 6 wizard steps; isWizardRequired()
drives the post-login redirect
- TelephonyConfigService — Ozonetel + Exotel + SIP, replaces 8 env vars,
seeds from env on first boot, masks secrets on GET,
'***masked***' sentinel on PUT means "keep existing"
- AiConfigService — provider, model, temperature, system prompt addendum;
API keys remain in env
New endpoints under /api/config:
- GET /api/config/setup-state returns state + wizardRequired flag
- PUT /api/config/setup-state/steps/:step mark step complete/incomplete
- POST /api/config/setup-state/dismiss dismiss wizard
- POST /api/config/setup-state/reset
- GET /api/config/telephony masked
- PUT /api/config/telephony
- POST /api/config/telephony/reset
- GET /api/config/ai
- PUT /api/config/ai
- POST /api/config/ai/reset
ConfigThemeModule is now @Global() so the new sidecar config services are
injectable from AuthModule, OzonetelAgentModule, MaintModule without creating
a circular dependency (ConfigThemeModule already imports AuthModule for
SessionService).
Migrated 11 env-var read sites to use the new services:
- ozonetel-agent.service: exotel API + ozonetel did/sipId via read-through getters
- ozonetel-agent.controller: defaultAgentId/Password/SipId via getters
- kookoo-ivr.controller: sipId/callerId via getters
- auth.controller: OZONETEL_AGENT_PASSWORD (login + logout)
- agent-config.service: sipDomain/wsPort/campaignName via getters
- maint.controller: forceReady + unlockAgent
- ai-provider: createAiModel and isAiConfigured refactored to pure factories
taking AiProviderOpts; no more ConfigService dependency
- widget-chat.service, recordings.service, ai-enrichment.service,
ai-chat.controller, ai-insight.consumer, call-assist.service: each builds
the AI model from AiConfigService.getConfig() + ConfigService API keys
Hot-reload guarantee: every consumer reads via a getter or builds per-call,
so admin updates take effect without sidecar restart. WidgetChatService
specifically rebuilds the model on each streamReply().
Bug fix bundled: dropped widget.json.hospitalName field (the original
duplicate that started this whole thread). WidgetConfigService now reads
brand.hospitalName from ThemeService at the 2 generateKey call sites.
Single source of truth for hospital name is workspace branding.
First-boot env seeding: TelephonyConfigService and AiConfigService both
copy their respective env vars into a fresh data/*.json on onModuleInit if
the file doesn't exist. Existing deployments auto-migrate without manual
intervention.
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>