---
phase: 03-operator-experience
plan: 02
type: execute
wave: 2
depends_on: ["03-01"]
files_modified:
- packages/portal/app/api/slack/callback/route.ts
- packages/portal/app/(dashboard)/onboarding/page.tsx
- packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx
- packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx
- packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx
- packages/portal/app/(dashboard)/settings/api-keys/page.tsx
- packages/portal/lib/queries.ts
- packages/portal/components/onboarding-stepper.tsx
autonomous: false
requirements:
- PRTA-03
- PRTA-04
- LLM-03
must_haves:
truths:
- "Operator can click Add to Slack and complete OAuth flow to connect their workspace"
- "Operator can paste WhatsApp credentials and have them validated and stored"
- "After connecting a channel, operator can send a test message that verifies end-to-end connectivity"
- "Agent goes live automatically after test message succeeds"
- "Operator completes the full onboarding sequence (connect -> configure -> test) in a guided wizard"
- "Operator can add, view, and delete BYO LLM API keys from a settings page"
artifacts:
- path: "packages/portal/app/api/slack/callback/route.ts"
provides: "Next.js Route Handler for Slack OAuth redirect"
- path: "packages/portal/app/(dashboard)/onboarding/page.tsx"
provides: "Onboarding wizard with 3-step stepper"
- path: "packages/portal/app/(dashboard)/settings/api-keys/page.tsx"
provides: "BYO API key management page"
key_links:
- from: "packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx"
to: "/api/portal/channels/slack/install"
via: "fetch to get OAuth URL, then window.location redirect"
pattern: "channels/slack/install"
- from: "packages/portal/app/api/slack/callback/route.ts"
to: "/api/portal/channels/slack/callback"
via: "proxy the OAuth callback to FastAPI backend"
pattern: "channels/slack/callback"
- from: "packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx"
to: "/api/portal/channels/{tenant_id}/test"
via: "POST to send test message"
pattern: "channels.*test"
---
Channel connection wizard (Slack OAuth + WhatsApp manual setup), onboarding flow with 3-step stepper, and BYO API key management page.
Purpose: Operators can connect their messaging channels and onboard their AI employee through a guided wizard, plus manage their own LLM API keys -- all from the portal UI.
Output: Onboarding wizard (connect channel -> configure agent -> test message), Slack OAuth callback handler, WhatsApp manual connect form, BYO key settings page.
@/home/adelorenzo/.claude/get-shit-done/workflows/execute-plan.md
@/home/adelorenzo/.claude/get-shit-done/templates/summary.md
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/03-operator-experience/03-CONTEXT.md
@.planning/phases/03-operator-experience/03-RESEARCH.md
@.planning/phases/03-operator-experience/03-01-SUMMARY.md
From packages/shared/shared/api/channels.py (created in Plan 01):
```python
# GET /api/portal/channels/slack/install?tenant_id={id} -> { "authorize_url": "https://slack.com/oauth/v2/authorize?..." }
# GET /api/portal/channels/slack/callback?code={code}&state={state} -> { "success": true, "workspace_name": "..." }
# POST /api/portal/channels/whatsapp/connect -> { "success": true } (body: { tenant_id, phone_number_id, waba_id, system_user_token })
# POST /api/portal/channels/{tenant_id}/test -> { "success": true, "message": "Test message sent" } (body: { channel_type })
```
From packages/shared/shared/api/billing.py (created in Plan 01):
```python
# POST /api/portal/billing/checkout -> { "checkout_url": "https://checkout.stripe.com/..." }
# POST /api/portal/billing/portal -> { "portal_url": "https://billing.stripe.com/..." }
```
From packages/shared/shared/crypto.py (created in Plan 01):
```python
class KeyEncryptionService:
def encrypt(self, plaintext: str) -> str: ...
def decrypt(self, ciphertext: str) -> str: ...
def rotate(self, ciphertext: str) -> str: ...
```
From packages/portal/lib/api.ts:
```typescript
// API client for FastAPI backend — use this for all portal API calls
```
From packages/portal/lib/queries.ts:
```typescript
// TanStack Query hooks — add new hooks for channels, billing, usage here
```
Established patterns:
- shadcn/ui components
- react-hook-form + zod v4 + standardSchemaResolver
- TanStack Query for data fetching
- App Router with (dashboard) route group
- proxy.ts for auth protection (Next.js 16 pattern)
Task 1: Slack OAuth callback route, onboarding wizard with 3-step stepper, and channel connection forms
packages/portal/app/api/slack/callback/route.ts,
packages/portal/app/(dashboard)/onboarding/page.tsx,
packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx,
packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx,
packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx,
packages/portal/components/onboarding-stepper.tsx,
packages/portal/lib/queries.ts
1. Create `packages/portal/app/api/slack/callback/route.ts`:
- Next.js Route Handler (GET) that receives the OAuth redirect from Slack
- Extract `code` and `state` from searchParams
- Forward to FastAPI backend: GET /api/portal/channels/slack/callback?code={code}&state={state}
- On success: redirect to /onboarding?step=2&channel=slack&connected=true
- On error: redirect to /onboarding?step=1&error=slack_auth_failed
2. Create `packages/portal/components/onboarding-stepper.tsx`:
- 3-step stepper component using shadcn/ui: "Connect Channel" -> "Configure Agent" -> "Test Message"
- Visual progress indicator (numbered steps with active/complete/pending states)
- Step state managed via URL searchParams (step=1|2|3) for shareable/refreshable URLs
- Each step renders the corresponding step component
3. Create `packages/portal/app/(dashboard)/onboarding/page.tsx`:
- Reads `step` from searchParams (default 1)
- Reads `tenant_id` from session or query param
- Renders OnboardingStepper with the appropriate step component
- Guards: if no tenant selected, redirect to tenant selection
4. Create `packages/portal/app/(dashboard)/onboarding/steps/connect-channel.tsx`:
- Two cards: "Add to Slack" and "Connect WhatsApp"
- **Slack card:** Button that fetches /api/portal/channels/slack/install?tenant_id={id}, then does window.location.href = authorize_url (external redirect to Slack)
- **WhatsApp card:** Expandable form with 3 fields (Phone Number ID, WhatsApp Business Account ID, System User Token) + step-by-step instructions text
- On submit: POST /api/portal/channels/whatsapp/connect
- Validation with zod + react-hook-form
- On successful connection (either channel): advance to step 2 automatically
- If returning from Slack OAuth (connected=true in searchParams): show success toast and advance
5. Create `packages/portal/app/(dashboard)/onboarding/steps/configure-agent.tsx`:
- If tenant already has an agent: show existing agent summary with "Edit" link to Agent Designer
- If no agent: redirect to Agent Designer page with return URL back to onboarding?step=3
- Minimal step — the Agent Designer (from Phase 1) handles all agent configuration
- "Next" button enabled only when at least one active agent exists for the tenant
6. Create `packages/portal/app/(dashboard)/onboarding/steps/test-message.tsx`:
- Shows connected channel(s) with a "Send Test Message" button per channel
- On click: POST /api/portal/channels/{tenant_id}/test with {channel_type}
- Shows loading state while test runs, then success/failure result
- On success: show "Your AI employee is live!" celebration message
- Agent goes live automatically (is_active already true by default) — NO separate "Go Live" button per user decision
- Per user decision: test message step is REQUIRED, not skippable
7. Add TanStack Query hooks to `packages/portal/lib/queries.ts`:
- useSlackInstallUrl(tenantId) — GET /channels/slack/install
- useConnectWhatsApp() — mutation POST /channels/whatsapp/connect
- useSendTestMessage() — mutation POST /channels/{tenantId}/test
- useChannelConnections(tenantId) — GET to list existing connections
cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20
- Slack OAuth callback route handler proxies to FastAPI and redirects appropriately
- Onboarding page renders 3-step stepper with progress indicator
- Connect Channel step shows Slack OAuth button and WhatsApp manual form
- Configure Agent step links to existing Agent Designer
- Test Message step sends test and shows result
- Agent goes live automatically after successful test (no Go Live button)
- All TanStack Query hooks defined
- Portal builds without errors
Task 2: BYO API key management settings page
packages/portal/app/(dashboard)/settings/api-keys/page.tsx,
packages/portal/lib/queries.ts
1. Create `packages/portal/app/(dashboard)/settings/api-keys/page.tsx`:
- Tenant-level settings page (per user decision — simpler than per-agent for v1)
- List existing BYO keys: show provider name, label, created date (NOT the key itself — never display decrypted keys)
- "Add API Key" button opens a form:
- Provider: select dropdown (OpenAI, Anthropic, Custom)
- Label: text input (human-readable name, e.g., "Production OpenAI key")
- API Key: password input (masked by default)
- Submit: POST to /api/portal/tenants/{tenant_id}/llm-keys (needs endpoint added to Plan 01's channels.py or a new file)
- Delete button per key with confirmation dialog
- Use shadcn/ui Card, Table, Dialog, Button, Input, Select components
- react-hook-form + zod for validation (provider required, label 3-100 chars, key not empty)
2. Add TanStack Query hooks to `packages/portal/lib/queries.ts`:
- useLlmKeys(tenantId) — GET /api/portal/tenants/{tenant_id}/llm-keys
- useAddLlmKey() — mutation POST /api/portal/tenants/{tenant_id}/llm-keys
- useDeleteLlmKey() — mutation DELETE /api/portal/tenants/{tenant_id}/llm-keys/{keyId}
3. Add navigation link to settings/api-keys in the dashboard layout sidebar (if sidebar exists) or in the tenant detail page.
Note: The backend endpoints for LLM key CRUD may need to be added to Plan 01's API if not already included. If the endpoint doesn't exist yet, create it in this task: add routes to packages/shared/shared/api/channels.py or create a separate packages/shared/shared/api/llm_keys.py with GET (list, redacted), POST (encrypt + store), DELETE.
cd /home/adelorenzo/repos/konstruct/packages/portal && npx next build 2>&1 | tail -20
- BYO API key settings page renders with list of existing keys (redacted)
- Add key form validates and submits to backend
- Delete key with confirmation dialog works
- Portal builds without errors
Task 3: Verify onboarding wizard and BYO key management
n/a
Human verifies the onboarding wizard and BYO key management UI:
1. Start the portal: `cd packages/portal && npm run dev`
2. Navigate to /onboarding — verify 3-step stepper displays
3. Step 1: Verify "Add to Slack" button and WhatsApp form are present
4. Step 2: Verify it links to Agent Designer or shows existing agent
5. Step 3: Verify "Send Test Message" button is present
6. Navigate to /settings/api-keys — verify key list and add form render
7. Try adding a BYO key with the form — verify it submits without JS errors
8. Confirm there is NO separate "Go Live" button — agent goes live after test
Human visual inspection of onboarding flow and settings page
Operator confirms onboarding wizard works through all 3 steps and BYO key page renders correctly
- Portal builds successfully: `cd packages/portal && npx next build`
- Onboarding wizard navigable through all 3 steps
- BYO API key page renders and accepts input
- No separate "Go Live" button exists (per user decision)
- Test message step is required (not skippable)
- Operator can initiate Slack OAuth from the portal
- Operator can paste WhatsApp credentials via guided form
- Onboarding wizard completes in 3 steps: connect -> configure -> test
- Agent goes live automatically after successful test message
- Operator can manage BYO API keys from settings page
- Portal builds without errors