Three changes:
1. Await Ozonetel logout in /auth/logout — prevents race condition when
agent re-logs in quickly via "Remember me". The fire-and-forget
logoutAgent() left a window where the next loginAgent() arrived
while Ozonetel was still processing the previous logout, leaving
the agent stuck in "Telephony Unavailable". (#559)
2. Use agentConfig.sipPassword (from Agent entity) instead of
OZONETEL_AGENT_PASSWORD env var for login/logout/force-ready.
The env var was a single shared credential that ignored per-agent
passwords. Removed hardcoded "Test123$" fallback. Force-ready
now looks up the Agent entity by ozonetelAgentId to get the
correct sipPassword + sipExtension.
3. Missed-calls worklist query now fetches campaign { id campaignName }
so the frontend Branch column can show the campaign name instead
of the raw DID phone number.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Supervisor users were getting 'executive' role because only 'HelixEngage
Manager' was mapped to admin. This broke admin route access after the
RequireAdmin guard was added.
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>
- 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>
- 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>
- 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>
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>