Commit Graph

8 Commits

Author SHA1 Message Date
048545317d fix: set platform name on every entity create — patients/appts/calls/etc no longer "Untitled"
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>
2026-04-15 09:32:28 +05:30
fbe782b5ac fix+feat: morning QA fixes, worklist pagination, misc sidecar improvements
- caller-resolution: drop cache, use indexed phone filter (lead.contactPhone.primaryPhoneNumber.like)
- worklist: externalize page size (WORKLIST_PAGE_SIZE × WORKLIST_MAX_PAGES), paginate getMissedCalls/getAssignedLeads/getPendingFollowUps
- maint: unlock-agent, force-ready, backfill-caller-resolution, clear-analysis-cache, fix-timestamps
- ozonetel agent.service: force logout+re-login on "already logged in"
- ai chat: context expansion
- livekit-agent: updates
- widget: session handling
- masterdata: clinic list cache

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 06:49:02 +05:30
b6b597fdda fix: clinicId on all appointment paths + startedAt on call records
- AI chat book_appointment tool: accepts optional clinicId
- Widget booking: passes clinicId from request
- LiveKit agent: passes clinicId from doctor context if available
- Dispose endpoint: sets startedAt/endedAt on outbound call records
  (computed from durationSec). Fixes null timestamps in call history.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 14:52:30 +05:30
695f119c2b feat: team module, multi-stage Dockerfile, doctor utils, AI config overhaul
- Team module: POST /api/team/members (in-place employee creation with
  temp password + Redis cache), PUT /api/team/members/:id, GET temp
  password endpoint. Uses signUpInWorkspace — no email invites.
- Dockerfile: rewritten as multi-stage build (builder + runtime) so
  native modules compile for target arch. Fixes darwin→linux crash.
- .dockerignore: exclude dist, node_modules, .env, .git, data/
- package-lock.json: regenerated against public npmjs.org (was
  pointing at localhost:4873 Verdaccio — broke docker builds)
- Doctor utils: shared DOCTOR_VISIT_SLOTS_FRAGMENT + normalizeDoctors
  helper for visit-slot-aware queries across 6 consumers
- AI config: full admin CRUD (GET/PUT/POST reset), workspace-scoped
  setup-state with workspace ID isolation, AI prompt defaults overhaul
- Agent config: camelCase field fix for SDK-synced workspaces
- Session service: workspace-scoped Redis key prefixing for setup state
- Recordings/supervisor/widget services: updated to use doctor-utils
  shared fragments instead of inline visitingHours queries

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:37:58 +05:30
619e9ab405 feat(onboarding/phase-1): admin-editable telephony, ai, and setup-state config
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>
2026-04-07 07:02:07 +05:30
e6c8d950ea feat: widget config via admin-editable data/widget.json
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>
2026-04-06 17:33:25 +05:30
aa41a2abb7 feat: widget chat with generative UI, branch selection, captcha gate, lead dedup
- 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>
2026-04-06 16:04:46 +05:30
76fa6f51de feat: website widget + omnichannel lead webhooks
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>
2026-04-06 06:49:02 +05:30