Files
helix-engage/docs/superpowers/plans/2026-04-06-hospital-onboarding-self-service.md
saridsa2 c1b636cb6d feat(onboarding/phase-2): settings hub, setup wizard shell, first-run redirect
Phase 2 of hospital onboarding & self-service plan
(docs/superpowers/plans/2026-04-06-hospital-onboarding-self-service.md
checked in here for the helix-engage repo).

Frontend foundations for the staff-portal Settings hub and 6-step setup
wizard. Backend was Phase 1 (helix-engage-server commit).

New shared components (src/components/setup/):
- wizard-shell.tsx — fullscreen layout with left step navigator, progress
  bar, and Skip-for-now affordance
- wizard-step.tsx — single-step wrapper with Mark Complete + Prev/Next/
  Finish navigation, completion badge
- section-card.tsx — Settings hub card with title/description/icon, links
  to a section page, optional status badge mirroring setup-state

New pages:
- pages/setup-wizard.tsx — top-level /setup route, fullscreen (no AppShell),
  loads setup-state from sidecar, renders the active step. Each step has a
  placeholder body for now; Phase 5 swaps placeholders for real form
  components from the matching settings pages. Already functional end-to-end:
  Mark Complete writes to PUT /api/config/setup-state/steps/<step>, Skip
  posts to /dismiss, Finish navigates to /.
- pages/team-settings.tsx — moved the existing workspace member listing out
  of the old monolithic settings.tsx into its own /settings/team route. No
  functional change; Phase 3 will add the invite form + role editor here.
- pages/settings-placeholder.tsx — generic "Coming in Phase X" stub used by
  routes for clinics, doctors, telephony, ai, widget until those pages land.

Modified pages:
- pages/settings.tsx — rewritten as the Settings hub (the new /settings
  route). Renders SectionCards in 3 groups (Hospital identity, Care
  delivery, Channels & automation) with completion badges sourced from
  /api/config/setup-state. The hub links to existing pages (/branding,
  /rules) and to placeholder pages for the not-yet-built sections.
- pages/login.tsx — after successful login, calls getSetupState() and
  redirects to /setup if wizardRequired. Failures fall through to / so an
  older sidecar without the setup-state endpoint still works.
- components/layout/sidebar.tsx — collapsed the Configuration group
  (Rules Engine + Branding standalone entries) into the single Settings
  entry that opens the hub. Removes the IconSlidersUp import that's no
  longer used.

New types and helpers (src/lib/setup-state.ts):
- SetupState / SetupStepName / SetupStepStatus types mirroring the sidecar
  shape
- SETUP_STEP_NAMES constant + SETUP_STEP_LABELS map (title + description
  per step) — single source of truth used by the wizard, hub, and any
  future surface that wants to render step metadata
- getSetupState / markSetupStepComplete / markSetupStepIncomplete /
  dismissSetupWizard / resetSetupState helpers wrapping the api-client

Other:
- lib/api-client.ts — added apiClient.put() helper for the setup-state
  step update mutations (PUT was the only verb missing from the existing
  get/post/graphql helpers)
- main.tsx — registered new routes:
    /setup                       (fullscreen, no AppShell)
    /settings                    (the hub, replaces old settings.tsx)
    /settings/team               (moved member listing)
    /settings/clinics            (placeholder, Phase 3)
    /settings/doctors            (placeholder, Phase 3)
    /settings/telephony          (placeholder, Phase 4)
    /settings/ai                 (placeholder, Phase 4)
    /settings/widget             (placeholder, Phase 4)

Tested via npx tsc --noEmit and npm run build (clean, only pre-existing
chunk-size and dynamic-import warnings unrelated to this change).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 07:13:25 +05:30

19 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 — invite supervisors and CC agents by email → triggers core's sendInvitations mutation
  5. Telephony — paste Ozonetel + Exotel credentials, default DID, default campaign → writes to telephony.json
  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

Supervisors and agents accept their invitation emails, set their own passwords, and land on the home dashboard. They're already role-assigned by the admin during T1 step 4, so they see the right pages 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. Member invitation stays email-based

The platform only supports email-based invitations (sendInvitations → email link → invitee sets password). We use this as-is. No new core mutation. Trade-off: admin can't bulk-create users with pre-set passwords, but the email flow is acceptable for hospital onboarding (admin types in the team's emails once, they each get a setup link).

7. Roles are auto-synced by SDK

HelixEngage Manager 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 invite form queries the platform for the workspace's roles via getRoles and uses real role IDs (no email-pattern hacks).


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 — add invite form via sendInvitations, real role dropdown via getRoles, role assignment via updateWorkspaceMemberRole

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 invited members (handled by the existing platform reset-password flow)
  • Onboarding analytics / metrics
  • Email branding for invitation emails (uses platform default for now)

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 invites a team member who already exists on the platform? Does sendInvitations add them to the workspace, or fail? Need to verify before Phase 3. If it fails, we may need a "find or invite" wrapper.

  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.