Files
helix-engage/docs/superpowers/plans/2026-04-06-hospital-onboarding-self-service.md
saridsa2 f57fbc1f24 feat(onboarding/phase-6): setup wizard polish, seed script alignment, doctor visit slots
- Setup wizard: 3-pane layout with right-side live previews, resume
  banner, edit/copy icons on team step, AI prompt configuration
- Forms: employee-create replaces invite-member (no email invites),
  clinic form with address/hours/payment, doctor form with visit slots
- Seed script: aligned to current SDK schema — doctors created as
  workspace members (HelixEngage Manager role), visitingHours replaced
  by doctorVisitSlot entity, clinics seeded, portalUserId linked
  dynamically, SUB/ORIGIN/GQL configurable via env vars
- Pages: clinics + doctors CRUD updated for new schema, team settings
  with temp password + role assignment
- New components: time-picker, day-selector, wizard-right-panes,
  wizard-layout-context, resume-setup-banner
- Removed: invite-member-form (replaced by employee-create-form per
  no-email-invites rule)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 08:37:34 +05:30

21 KiB

Hospital Onboarding & Self-Service Setup

Date: 2026-04-06 Status: Plan — pending implementation Owner: helix-engage


Goal

Make onboarding a new hospital a one-command devops action plus a guided self-service flow inside the staff portal. After running the script, the hospital admin should be able to log into a fresh workspace and reach a fully operational call center by filling in 6 setup pages — without anyone touching env vars, JSON files, or running shell commands a second time.

Non-goals

  • Per-tenant secrets management (env vars stay infra-owned for now).
  • Self-service Cloudflare Turnstile / Ozonetel account provisioning. Operator pastes pre-existing credentials.
  • Multi-hospital routing inside one sidecar. One sidecar per workspace; multi-tenancy is handled by the platform.
  • Bulk CSV import of doctors / staff. Single-row form CRUD only.
  • Email infrastructure for invitations beyond what core already does.

User journey

T0 — devops, one-command bootstrap (~30 seconds)

./onboard-hospital.sh \
  --create \
  --display-name "Care Hospital" \
  --subdomain care \
  --admin-email admin@carehospital.com \
  --admin-password 'TempCare#2026'

Script signs up the admin user, creates and activates the workspace, syncs the helix-engage SDK, mints an API key, writes a sidecar .env, and prints a credentials handoff block. Done.

T1 — hospital admin first login (~10 minutes)

Admin opens the workspace URL, signs in with the temp password. App detects an unconfigured workspace and routes them to /setup. A 6-step wizard walks them through:

  1. Hospital identity — confirm display name, upload logo, pick brand colors → writes to theme.json
  2. Clinics — add at least one branch (name, address, phone, timings) → creates Clinic records on platform
  3. Doctors — add at least one doctor (name, specialty, clinic, visiting hours) → creates Doctor records on platform
  4. Team — create supervisors and CC agents in place (name, email, temp password, role). If the role is HelixEngage User the form also shows a SIP seat dropdown so the admin links the new employee to an Agent profile in the same step. Posts to sidecar POST /api/team/members which chains signUpInWorkspace (using the workspace's own inviteHash server-side — no email is sent) → updateWorkspaceMemberupdateWorkspaceMemberRole → optional updateAgent. Never uses sendInvitations — see feedback-no-invites memory for the absolute rule.
  5. Telephony — read-only summary of which workspace members own which SIP seats. Seats themselves are seeded during onboarding (onboard-hospital.sh step 5b) and linked to members in step 4. Admin just confirms and advances.
  6. AI assistant — pick provider (OpenAI / Anthropic), model, optional system prompt override → writes to ai.json

After step 6, admin clicks "Finish setup" and lands on the home dashboard. Setup state is recorded in setup-state.json so the wizard never auto-shows again.

T2 — hospital admin returns later (any time)

Each setup page is also accessible standalone via the Settings menu. Admin can edit any of them at any time. Settings hub shows green checkmarks for completed sections and yellow badges for sections still using defaults.

T3 — agents and supervisors join

The admin hands each employee their email + temp password directly (WhatsApp, in-person, etc.). Employees sign in, land on the home dashboard, and change their password from their profile. They're already role-assigned and (if CC agents) SIP-linked from T1 step 4, so they see the right pages — and can take calls — immediately.


Architecture decisions

1. Script does identity. Portal does configuration.

  • In script: anything requiring platform-admin credentials (signup, workspace activation, SDK sync, API key creation). One-time, devops-only.
  • In staff portal: anything that operates inside the workspace (clinics, doctors, team, sidecar config files). Self-serve, repeatable.

This keeps the script's blast radius small and means the hospital admin never needs platform-admin access.

2. Two distinct frontend → backend patterns

Pattern A — Direct GraphQL to platform (for entities the platform owns)

  • Clinics, Doctors, Workspace Members
  • Frontend uses apiClient.graphql<any>(...) with the user's JWT
  • Already established by settings.tsx for member listing
  • No sidecar code needed

Pattern B — Sidecar admin endpoints (for sidecar-owned config files)

  • Theme (theme.json), Widget (widget.json), Telephony (telephony.json), AI (ai.json), Setup state (setup-state.json)
  • Frontend uses apiClient.fetch('/api/config/...')
  • Sidecar persists to disk via *ConfigService mirroring ThemeService
  • Already established by branding-settings.tsx and WidgetConfigService

Rule: if it lives in a workspace schema on the platform, use Pattern A. If it's a sidecar config file, use Pattern B. Don't mix.

3. Telephony config moves out of env vars

OZONETEL_*, SIP_*, EXOTEL_* env vars become bootstrap defaults that seed data/telephony.json on first boot, then never read again. All runtime reads go through TelephonyConfigService.getConfig(). Six read sites refactor (auth.controller, ozonetel-agent.service, ozonetel-agent.controller, kookoo-ivr.controller, agent-config.service, maint.controller).

4. AI config moves out of env vars

Same pattern. ANTHROPIC_API_KEY / OPENAI_API_KEY stay in env (true secrets), but AI_PROVIDER / AI_MODEL move to data/ai.json. WidgetChatService and any other AI-using services read from AiConfigService.

5. Setup state lives in its own file

data/setup-state.json tracks completion status for each of the 6 setup steps + a global wizardDismissed flag. Frontend reads it on app load to decide whether to show the setup wizard. Each setup page marks its step complete on save.

{
  "version": 1,
  "wizardDismissed": false,
  "steps": {
    "identity":  { "completed": false, "completedAt": null },
    "clinics":   { "completed": false, "completedAt": null },
    "doctors":   { "completed": false, "completedAt": null },
    "team":      { "completed": false, "completedAt": null },
    "telephony": { "completed": false, "completedAt": null },
    "ai":        { "completed": false, "completedAt": null }
  }
}

6. Members are created in place — never via email invitation

Absolute rule (see feedback-no-invites in memory): Helix Engage does not use the platform's sendInvitations flow for any reason, ever. Hospital admins are expected to onboard employees in person or over WhatsApp, hand out login credentials directly, and have the employee change the password on first login.

The sidecar exposes POST /api/team/members taking { firstName, lastName, email, password, roleId, agentId? }. Server-side it chains:

  1. signUpInWorkspace(email, password, workspaceId, workspaceInviteHash) — the platform's isPublicInviteLinkEnabled + inviteHash values are read once per boot and used to authorize the create. The hash is a server-side secret, never surfaced to the admin UI, and no email is sent.
  2. updateWorkspaceMember — set first name / last name (the signUp mutation doesn't take them).
  3. updateWorkspaceMemberRole — assign the role the admin picked.
  4. updateAgent (optional) — link the new workspace member to the chosen Agent profile if the admin selected a SIP seat.

The Team wizard step and the /settings/team slideout both call this endpoint via the new EmployeeCreateForm component. The old InviteMemberForm and all sendInvitations call sites have been deleted.

7. Roles are auto-synced by SDK

HelixEngage Manager, HelixEngage Supervisor, and HelixEngage User roles are defined in FortyTwoApps/helix-engage/src/roles/ and created automatically by yarn app:sync. The frontend's role dropdown in the team form queries the platform via getRoles and uses real role IDs (no email-pattern hacks). The "is this person a CC agent, so show the SIP seat dropdown?" check matches by the exact label HelixEngage User — see CC_AGENT_ROLE_LABEL in wizard-step-team.tsx / team-settings.tsx.


Backend changes (helix-engage-server)

New services / files

File Purpose
src/config/setup-state.defaults.ts Type + defaults for data/setup-state.json
src/config/setup-state.service.ts Load / get / mark step complete / dismiss wizard
src/config/telephony.defaults.ts Type + defaults for data/telephony.json (Ozonetel + Exotel + SIP)
src/config/telephony-config.service.ts File-backed CRUD; onModuleInit seeds from env vars on first boot
src/config/ai.defaults.ts Type + defaults for data/ai.json
src/config/ai-config.service.ts File-backed CRUD; seeds from env on first boot
src/config/setup-state.controller.ts GET /api/config/setup-state, PUT /api/config/setup-state/steps/:step, POST /api/config/setup-state/dismiss
src/config/telephony-config.controller.ts GET/PUT /api/config/telephony with secret masking on GET
src/config/ai-config.controller.ts GET/PUT /api/config/ai with secret masking

Modified files

File Change
src/config/config-theme.module.ts Register the 3 new services + 3 new controllers
src/config/widget.defaults.ts Drop hospitalName field (the long-standing duplicate)
src/config/widget-config.service.ts Inject ThemeService, read brand.hospitalName from theme at the 2 generateKey call sites
src/widget/widget.service.ts getInitData() reads captcha site key from WidgetConfigService instead of process.env.RECAPTCHA_SITE_KEY
src/auth/agent-config.service.ts:49 Read OZONETEL_CAMPAIGN_NAME from TelephonyConfigService
src/auth/auth.controller.ts:141, 255 Read OZONETEL_AGENT_PASSWORD from TelephonyConfigService
src/ozonetel/ozonetel-agent.service.ts:199, 235, 236 Read OZONETEL_DID, OZONETEL_SIP_ID from TelephonyConfigService
src/ozonetel/ozonetel-agent.controller.ts:39, 42, 192 Same
src/ozonetel/kookoo-ivr.controller.ts:11, 12 Same
src/maint/maint.controller.ts:27 Same
src/widget/widget-chat.service.ts Read provider and model from AiConfigService instead of ConfigService
src/ai/ai-provider.ts Same — provider/model from config file, API keys still from env

Frontend changes (helix-engage)

New pages

Page Path Purpose
setup/setup-wizard.tsx /setup 6-step wizard, auto-shown on first login when setup incomplete
pages/clinics.tsx /settings/clinics List + add/edit clinic records (slideout pattern)
pages/doctors.tsx /settings/doctors List + add/edit doctors, assign to clinics
pages/team-settings.tsx /settings/team Member list + invite form + role editor (replaces current settings.tsx member view)
pages/telephony-settings.tsx /settings/telephony Ozonetel + Exotel + SIP form (consumes /api/config/telephony)
pages/ai-settings.tsx /settings/ai AI provider/model/prompt form (consumes /api/config/ai)
pages/widget-settings.tsx /settings/widget Widget enabled/embed/captcha form (consumes /api/config/widget)
pages/settings-hub.tsx /settings Index page listing all setup sections with completion badges. Replaces current settings.tsx.

Modified pages

File Change
src/pages/login.tsx After successful login, fetch /api/config/setup-state. If incomplete and user is workspace admin, redirect to /setup. Otherwise existing flow.
src/pages/branding-settings.tsx On save, mark identity step complete via PUT /api/config/setup-state/steps/identity
src/components/layout/sidebar.tsx Add Settings hub entry; remove direct links to individual settings pages from main nav (move them under Settings)
src/providers/router-provider.tsx Register the 7 new routes
src/pages/integrations.tsx Remove the Ozonetel + Exotel cards (functionality moves to telephony-settings.tsx); keep WhatsApp/FB/Google/website cards for now

New shared components

File Purpose
src/components/setup/wizard-shell.tsx Layout: progress bar, step navigation, footer with prev/next
src/components/setup/wizard-step.tsx Single-step container — title, description, content slot, validation hook
src/components/setup/section-card.tsx Settings hub section card with status badge
src/components/forms/clinic-form.tsx Reused by clinics page + setup wizard step 2
src/components/forms/doctor-form.tsx Reused by doctors page + setup wizard step 3
src/components/forms/invite-member-form.tsx Reused by team page + setup wizard step 4
src/components/forms/telephony-form.tsx Reused by telephony settings + setup wizard step 5
src/components/forms/ai-form.tsx Reused by ai settings + setup wizard step 6

The pattern: each settings page renders the same form component the wizard step renders. Wizard steps just wrap the form in <WizardStep> and add prev/next navigation. Standalone settings pages wrap the form in a normal page layout. Form is the source of truth; wizard and settings page are two presentations of the same thing.


Onboarding script changes

onboard-hospital.sh is already 90% there. Three minor changes:

  1. Drop the --sidecar-env-out default behavior — print a structured "credentials handoff" block at the end with admin email, temp password, workspace URL, sidecar .env content. Operator copies what they need.
  2. Change the credentials block format — make it copy-pasteable as a single email body so the operator can email it to the hospital owner directly.
  3. Add setup-state.json initialization — the script writes a fresh setup-state.json to the sidecar's data/ directory as part of step 6, so the first frontend load knows nothing is configured yet.

Phasing

Each phase is a coherent commit. Don't ship phases out of order.

Phase 1 — Backend foundations (config services + endpoints)

Files: 9 new + 4 modified backend files. No frontend.

  • New services: setup-state, telephony-config, ai-config
  • New defaults files for each
  • New controllers for each
  • Module wiring
  • Drop widget.json.hospitalName (the original duplicate that started this whole thread)
  • Migrate the 6 Ozonetel read sites to TelephonyConfigService
  • Migrate the AI provider/model reads to AiConfigService
  • First-boot env-var seeding: each new service reads its respective env vars on onModuleInit and writes them to its config file if the file doesn't exist

Verifies: sidecar still serves all existing endpoints, env-var-driven Ozonetel still works (because the seeding picks up the same values), data/telephony.json and data/ai.json exist on first boot.

Estimate: 4-5 hours.

Phase 2 — Settings hub + first-run detection

Files: 2 new pages + 4 modified frontend files + new shared section-card component.

  • settings-hub.tsx replaces settings.tsx as the /settings route
  • Move the existing member-list view from settings.tsx into a new team-settings.tsx (read-only for now; invite + role editing comes in Phase 3)
  • login.tsx fetches setup-state after successful login and redirects to /setup if incomplete
  • setup/setup-wizard.tsx shell renders the 6 step containers (with placeholder content for now)
  • Sidebar redesign: collapse all settings into one Settings entry that opens the hub
  • Router updates to register the new routes

Verifies: clean login → setup wizard appearance for fresh workspace; Settings hub navigates to existing pages; nothing breaks for already-set-up workspaces.

Estimate: 3-4 hours.

Phase 3 — Entity CRUD pages (Pattern A — direct platform GraphQL)

Files: 3 new pages + 3 new form components + 1 modified team page.

  • clinics.tsx + clinic-form.tsx — list with add/edit slideout
  • doctors.tsx + doctor-form.tsx — list with add/edit, clinic dropdown sourced from clinics
  • team-settings.tsx becomes interactive — employees are created in place via the sidecar's POST /api/team/members endpoint (see architecture decision 6), real role dropdown via getRoles, role assignment via updateWorkspaceMemberRole. Never uses sendInvitations.

Verifies: admin can create clinics, doctors, and invite team members from the staff portal without touching the database.

Estimate: 5-6 hours.

Phase 4 — Sidecar-config CRUD pages (Pattern B — sidecar admin endpoints)

Files: 3 new pages + 3 new form components.

  • telephony-settings.tsx + telephony-form.tsx — Ozonetel + Exotel + SIP fields
  • ai-settings.tsx + ai-form.tsx — provider, model, temperature, system prompt
  • widget-settings.tsx + widget-form.tsx — wraps the existing widget config endpoint with a real form

Verifies: admin can edit telephony, AI, and widget config from the staff portal. Changes take effect without sidecar restart (since services use in-memory cache + file write).

Estimate: 4-5 hours.

Phase 5 — Wizard step composition

Files: 6 wizard step components, each thin wrappers around the Phase 3/4 forms.

  • wizard-step-identity.tsx
  • wizard-step-clinics.tsx
  • wizard-step-doctors.tsx
  • wizard-step-team.tsx
  • wizard-step-telephony.tsx
  • wizard-step-ai.tsx

Each wraps the corresponding form, adds wizard validation (required fields enforced for setup completion), and on save calls PUT /api/config/setup-state/steps/<step> to mark the step complete.

Verifies: admin can complete the entire setup wizard end-to-end on a fresh workspace. After step 6, redirected to home dashboard. Setup state file shows all 6 steps complete.

Estimate: 2-3 hours.

Phase 6 — Polish

  • Onboarding script credentials handoff block format
  • "Resume setup" CTA on home dashboard if any step is incomplete
  • Loading states, error toasts, optimistic updates
  • Setup-state badges on the Settings hub
  • Validation: clinic count > 0 required for booking flow, doctor count > 0 required for booking flow, etc.
  • E2E smoke test against the Care Hospital workspace I already created

Estimate: 2-3 hours.


Total estimate

20-26 hours of focused implementation work spanning ~30 new files and ~15 modified files. Realistic over 3-4 working days with checkpoints at each phase boundary.


Out of scope (explicit)

  • Self-service Cloudflare Turnstile signup (operator pastes existing site key)
  • Self-service Ozonetel account creation (operator pastes credentials)
  • Bulk import of doctors / staff (single-row form only)
  • Per-tenant secrets management (env vars stay infra-owned for AI keys, captcha secret, HMAC secret)
  • Workspace deletion / archival
  • Multi-hospital admin (one admin per workspace; switching workspaces is platform-level)
  • Hospital templates ("clone from Ramaiah") — useful follow-up but not required for the first real onboarding
  • Self-service password reset for employees (handled by the existing platform reset-password flow)
  • Onboarding analytics / metrics

Open questions before phase 1

  1. Sidecar config file hot-reload — when an admin updates telephony.json via the new endpoint, does the change need to take effect immediately (in-memory cache invalidation, no restart) or is a sidecar restart acceptable? Decision affects whether services need a "refresh" hook. Recommendation: in-memory cache only, no restart needed — already how ThemeService works.

  2. Setup state visibility — should the setup-state file be a simple flag set or should it track who completed each step and when? Recommendation: track completedAt timestamp + completedBy user id for audit trail.

  3. Auto-mark "identity" step complete from existing branding — if the workspace already has a theme.json with a non-default brand.hospitalName, should the wizard auto-skip step 1? Recommendation: yes — don't make admins re-confirm something they already configured.

  4. What if the admin tries to create an employee whose email already exists on the platform? signUpInWorkspace will surface the platform's "email already exists" error, which the sidecar's TeamService.extractGraphqlMessage passes through to the toast. No "find or link existing user" path yet — if this comes up in practice, add a findUserByEmail preflight lookup before the signUpInWorkspace call.

  5. Logo upload — do we accept a URL only (admin pastes a CDN link) or do we need real file upload to MinIO? Recommendation: URL only for Phase 1, file upload as Phase 6 polish.


Risks

  • yarn app:sync may sometimes fail to register HelixEngage roles cleanly if a workspace was activated but never had its first sync — this would block the team page's role dropdown. Mitigation: script runs sync immediately after activation, before exiting.
  • Frontend role queries require user JWT, not API keysettings.tsx already noted this with the "Roles are only accessible via user JWT" comment. The team-settings page has to use direct GraphQL with user auth, not the sidecar proxy.
  • Migrating Ozonetel env vars to a config file mid-session can break a running sidecar if someone's actively using the call desk during deploy. Mitigation: deploy during low-usage window; the new service falls back to env vars if the config file is missing.
  • Setup wizard auto-redirect could trap users in a loop if setup-state.json write fails. Mitigation: wizard always has a "Skip for now" link in the top right that sets wizardDismissed: true.