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 (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>