{"repo": "chad-1964/resyncs-openclaw-mission-control-panel", "files": {"client/src/pages/SetupWizard.tsx": {"74f527292c896aa5919b5e7ae4f45507cbba2db5": {"t": 1786068555.4340887, "n": 72678, "h": "258aafd5c68f0735", "c": "import { useState, useEffect, useCallback, useRef } from \"react\";\nimport { apiRequest, queryClient } from \"@/lib/queryClient\";\nimport { useToast } from \"@/hooks/use-toast\";\nimport { Button } from \"@/components/ui/button\";\nimport { Input } from \"@/components/ui/input\";\nimport { Label } from \"@/components/ui/label\";\nimport { Switch } from \"@/components/ui/switch\";\nimport { Badge } from \"@/components/ui/badge\";\nimport {\n Shield, Database, Bot, MessageCircle, Users, Globe,\n CheckCircle2, ArrowRight, ArrowLeft, Eye, EyeOff,\n Loader2, Zap, Settings2, Rocket, ChevronDown, ChevronRight,\n AlertCircle, Check, Info, Key, Sparkles, Server, Lock,\n Search, Share2,\n} from \"lucide-react\";\nimport {\n SiAnthropic, SiOpenai, SiGoogle, SiWhatsapp, SiTelegram,\n SiDiscord, SiSlack, SiOllama, SiSignal,\n} from \"react-icons/si\";\nimport { FaXTwitter } from \"react-icons/fa6\";\n\n// ── Types ─────────────────────────────────────────────────\n\ninterface ModelConfig {\n apiKey: string;\n orgId?: string;\n endpointUrl?: string;\n providerName?: string; // used by the \"custom\" provider card\n configured: boolean;\n tested: boolean;\n testStatus: \"idle\" | \"testing\" | \"success\" | \"failed\";\n}\n\ninterface ChatProviderConfig {\n fields: Record;\n configured: boolean;\n}\n\ninterface WizardData {\n // Step 1: Database (skipped when ?installer=1 — PHP already configured DB)\n dbType: \"mariadb\" | \"mysql\" | \"postgresql\" | \"sqlite\";\n dbHost: string;\n dbPort: number;\n dbName: string;\n dbUser: string;\n dbPassword: string;\n dbTestStatus: \"idle\" | \"testing\" | \"success\" | \"failed\";\n dbTestMessage: string;\n dbDetectedType: \"mariadb\" | \"mysql\" | null;\n // Step 2: Admin\n adminName: string;\n adminEmail: string;\n adminPassword: string;\n adminConfirmPassword: string;\n // Step 2: AI Models\n models: Record;\n // Step 4: Chat Providers\n chatProviders: Record;\n // Step 5: Agent Configuration\n agentPreset: \"business\" | \"development\" | \"seo\" | \"social\" | \"custom\";\n // Step 6: Domain & SSL\n domain: string;\n autoSsl: boolean;\n httpPort: number;\n httpsPort: number;\n // Server info\n serverIp: string;\n serverHostname: string;\n}\n\nconst STEPS = [\n { label: \"Welcome\", icon: Zap },\n { label: \"Database\", icon: Database },\n { label: \"Admin\", icon: Shield },\n { label: \"AI Models\", icon: Bot },\n { label: \"Chat\", icon: MessageCircle },\n { label: \"Agents\", icon: Users },\n { label: \"Domain\", icon: Globe },\n { label: \"Review\", icon: CheckCircle2 },\n];\n\nconst MODEL_DEFS = [\n {\n key: \"anthropic\",\n name: \"Anthropic (Claude)\",\n icon: SiAnthropic,\n iconColor: \"#D4A574\",\n fields: [{ key: \"apiKey\", label: \"API Key\", placeholder: \"sk-ant-api03-...\" }],\n },\n {\n key: \"openai\",\n name: \"OpenAI (GPT-4/5)\",\n icon: SiOpenai,\n iconColor: \"#ffffff\",\n fields: [\n { key: \"apiKey\", label: \"API Key\", placeholder: \"sk-proj-...\" },\n { key: \"orgId\", label: \"Organization ID (optional)\", placeholder: \"org-xxxxxxxx\" },\n ],\n },\n {\n key: \"google\",\n name: \"Google (Gemini)\",\n icon: SiGoogle,\n iconColor: \"#4285F4\",\n fields: [{ key: \"apiKey\", label: \"API Key\", placeholder: \"AIzaSy...\" }],\n },\n {\n key: \"xai\",\n name: \"xAI (Grok)\",\n icon: FaXTwitter,\n iconColor: \"#ffffff\",\n fields: [{ key: \"apiKey\", label: \"API Key\", placeholder: \"xai-...\" }],\n },\n {\n key: \"perplexity\",\n name: \"Perplexity\",\n icon: Bot,\n iconColor: \"#20B8CD\",\n fields: [{ key: \"apiKey\", label: \"API Key\", placeholder: \"pplx-...\" }],\n },\n {\n // OpenRouter — unified gateway to 200+ models (OpenAI-compatible API)\n key: \"openrouter\",\n name: \"OpenRouter\",\n icon: Sparkles,\n iconColor: \"#6366F1\",\n fields: [\n { key: \"apiKey\", label: \"API Key\", placeholder: \"sk-or-v1-...\" },\n { key: \"endpointUrl\", label: \"Endpoint URL\", placeholder: \"https://openrouter.ai/api/v1\" },\n ],\n },\n {\n key: \"ollama\",\n name: \"Ollama (Local)\",\n icon: SiOllama,\n iconColor: \"#ffffff\",\n fields: [{ key: \"endpointUrl\", label: \"Endpoint URL\", placeholder: \"http://localhost:11434\" }],\n },\n {\n // Generic fallback for any OpenAI-compatible provider not listed above\n // (e.g. Mistral, Groq, Cohere, Together AI, DeepSeek, etc.)\n key: \"custom\",\n name: \"Custom / Other\",\n icon: Settings2,\n iconColor: \"#94A3B8\",\n fields: [\n { key: \"providerName\", label: \"Provider Name\", placeholder: \"e.g. Mistral, Groq, DeepSeek...\" },\n { key: \"endpointUrl\", label: \"API Endpoint URL\", placeholder: \"https://api.example.com/v1\" },\n { key: \"apiKey\", label: \"API Key\", placeholder: \"Your API key\" },\n ],\n },\n];\n\nconst CHAT_PROVIDER_DEFS = [\n {\n key: \"whatsapp\",\n name: \"WhatsApp\",\n icon: SiWhatsapp,\n iconColor: \"#25D366\",\n fields: [\n { key: \"phoneNumberId\", label: \"Phone Number ID\", placeholder: \"1xxxxxxxxxx\" },\n { key: \"businessAccountId\", label: \"Business Account ID\", placeholder: \"1xxxxxxxxxx\" },\n { key: \"accessToken\", label: \"Access Token\", placeholder: \"EAAxxxxxxxxx...\", secret: true },\n ],\n },\n {\n key: \"telegram\",\n name: \"Telegram\",\n icon: SiTelegram,\n iconColor: \"#26A5E4\",\n fields: [\n { key: \"botToken\", label: \"Bot Token\", placeholder: \"123456789:ABCdefGHI...\", secret: true },\n { key: \"chatId\", label: \"Chat ID\", placeholder: \"-1001234567890\" },\n ],\n },\n {\n key: \"discord\",\n name: \"Discord\",\n icon: SiDiscord,\n iconColor: \"#5865F2\",\n fields: [\n { key: \"botToken\", label: \"Bot Token\", placeholder: \"MTxxxxxxxx...\", secret: true },\n { key: \"channelId\", label: \"Channel ID\", placeholder: \"123456789012345678\" },\n { key: \"webhookUrl\", label: \"Webhook URL\", placeholder: \"https://discord.com/api/webhooks/...\" },\n ],\n },\n {\n key: \"slack\",\n name: \"Slack\",\n icon: SiSlack,\n iconColor: \"#4A154B\",\n fields: [\n { key: \"botToken\", label: \"Bot Token\", placeholder: \"xoxb-xxxxxxxxxxxx-xxxxxxxx\", secret: true },\n { key: \"webhookUrl\", label: \"Webhook URL\", placeholder: \"https://hooks.slack.com/services/...\" },\n ],\n },\n {\n key: \"signal\",\n name: \"Signal\",\n icon: SiSignal,\n iconColor: \"#3A76F0\",\n fields: [\n { key: \"signalCliUrl\", label: \"Signal CLI URL\", placeholder: \"http://localhost:8080\" },\n ],\n },\n];\n\nconst AGENT_PRESETS = {\n business: {\n label: \"Business Operations\",\n description: \"Full business operations team for managing your company\",\n icon: Sparkles,\n agents: [\"CEO\", \"Operations\", \"Accountant\", \"Marketing\", \"Customer Success\", \"Market Intelligence\"],\n },\n development: {\n label: \"Development Team\",\n description: \"Complete development team for software projects\",\n icon: Settings2,\n agents: [\"Project Manager\", \"Frontend Dev\", \"Backend Dev\", \"QA Engineer\", \"DevOps\", \"Technical Writer\"],\n },\n seo: {\n label: \"SEO & Content\",\n description: \"SEO specialists for organic growth, keyword research, and content optimization\",\n icon: Search,\n agents: [\"CEO\", \"SEO Strategist\", \"Content Writer\", \"Market Intelligence\"],\n },\n social: {\n label: \"Social Media\",\n description: \"Content creation, scheduling, and engagement across social platforms\",\n icon: Share2,\n agents: [\"CEO\", \"Social Media Manager\", \"Creative Director\", \"Marketing\"],\n },\n custom: {\n label: \"Custom\",\n description: \"Start with an empty roster and add agents later\",\n icon: Users,\n agents: [],\n },\n};\n\nfunction defaultWizardData(): WizardData {\n const models: Record = {};\n for (const m of MODEL_DEFS) {\n models[m.key] = {\n apiKey: \"\",\n orgId: \"\",\n endpointUrl: m.key === \"ollama\" ? \"http://localhost:11434\"\n : m.key === \"openrouter\" ? \"https://openrouter.ai/api/v1\"\n : \"\",\n configured: false,\n tested: false,\n testStatus: \"idle\",\n };\n }\n const chatProviders: Record = {};\n for (const c of CHAT_PROVIDER_DEFS) {\n const fields: Record = {};\n for (const f of c.fields) fields[f.key] = \"\";\n chatProviders[c.key] = { fields, configured: false };\n }\n return {\n dbType: \"mariadb\",\n dbHost: \"localhost\",\n dbPort: 3306,\n dbName: \"\",\n dbUser: \"\",\n dbPassword: \"\",\n dbTestStatus: \"idle\",\n dbTestMessage: \"\",\n dbDetectedType: null,\n adminName: \"\",\n adminEmail: \"\",\n adminPassword: \"\",\n adminConfirmPassword: \"\",\n models,\n chatProviders,\n agentPreset: \"business\",\n domain: \"\",\n autoSsl: true,\n httpPort: 80,\n httpsPort: 443,\n serverIp: \"\",\n serverHostname: \"\",\n };\n}\n\n// ── Helpers ───────────────────────────────────────────────\n\nfunction getPasswordStrength(pw: string): { level: number; label: string; color: string } {\n if (!pw) return { level: 0, label: \"\", color: \"\" };\n let score = 0;\n if (pw.length >= 8) score++;\n if (pw.length >= 12) score++;\n if (/[A-Z]/.test(pw) && /[a-z]/.test(pw)) score++;\n if (/\\d/.test(pw)) score++;\n if (/[^A-Za-z0-9]/.test(pw)) score++;\n\n if (score <= 1) return { level: 1, label: \"Weak\", color: \"bg-red-500\" };\n if (score === 2) return { level: 2, label: \"Fair\", color: \"bg-orange-500\" };\n if (score === 3) return { level: 3, label: \"Strong\", color: \"bg-yellow-500\" };\n return { level: 4, label: \"Excellent\", color: \"bg-emerald-500\" };\n}\n\n// ── SecureInput Component ─────────────────────────────────\n\nfunction SecureInput({\n value,\n onChange,\n placeholder,\n testId,\n}: {\n value: string;\n onChange: (v: string) => void;\n placeholder: string;\n testId?: string;\n}) {\n const [visible, setVisible] = useState(false);\n return (\n
\n onChange(e.target.value)}\n placeholder={placeholder}\n className=\"text-xs pr-9 font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid={testId}\n />\n setVisible(!visible)}\n className=\"absolute right-2.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground transition-colors\"\n data-testid={testId ? `${testId}-toggle` : undefined}\n >\n {visible ? : }\n \n
\n );\n}\n\n// ── Progress Bar ──────────────────────────────────────────\n\nfunction StepProgressBar({ currentStep }: { currentStep: number }) {\n return (\n
\n
\n
\n {/* Connecting line (background) */}\n
\n {/* Connecting line (filled) */}\n \n\n {STEPS.map((step, i) => {\n const Icon = step.icon;\n const isComplete = i < currentStep;\n const isCurrent = i === currentStep;\n return (\n \n \n {isComplete ? (\n \n ) : (\n \n )}\n
\n
\n );\n })}\n
\n
\n \n );\n}\n\n// ── Step 0: Welcome ───────────────────────────────────────\n\ninterface OpenClawDetection {\n installed: boolean;\n version: string | null;\n agents: { id: string; hasSoul: boolean; name?: string }[];\n providers: { provider: string; hasKey: boolean }[];\n gatewayToken: string | null;\n gatewayUrl: string;\n}\n\nfunction StepWelcome({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n const [detection, setDetection] = useState(null);\n const [detecting, setDetecting] = useState(true);\n\n useEffect(() => {\n (async () => {\n try {\n const res = await fetch(\"/api/setup/detect-openclaw\");\n const d = await res.json();\n setDetection(d);\n } catch { /* skip */ }\n setDetecting(false);\n })();\n }, []);\n\n return (\n
\n {/* Animated logo */}\n
\n
\n
\n \n
\n
\n\n
\n

\n Welcome to Mission Control\n

\n

\n Powered by OpenClaw — AI Agent Orchestration\n

\n

\n This wizard will guide you through setting up your AI command center.\n It takes about 5 minutes.\n

\n
\n\n {/* OpenClaw Auto-Detection */}\n
\n {detecting ? (\n
\n \n Scanning for existing OpenClaw installation...\n
\n ) : detection?.installed ? (\n
\n
\n \n OpenClaw detected!\n {detection.version && v{detection.version}}\n
\n
\n {detection.agents.length > 0 && (\n

\n {detection.agents.length} agent{detection.agents.length !== 1 ? \"s\" : \"\"} found\n {detection.agents.length <= 4 && (\n — {detection.agents.map(a => a.name || a.id).join(\", \")}\n )}\n

\n )}\n {detection.providers.length > 0 && (\n

\n {detection.providers.length} API key{detection.providers.length !== 1 ? \"s\" : \"\"} configured\n — {detection.providers.map(p => p.provider).join(\", \")}\n

\n )}\n
\n

\n Your existing agents and API keys will be auto-imported during setup.\n

\n
\n ) : (\n
\n \n No OpenClaw installation detected — we'll set one up for you.\n
\n )}\n
\n\n
\n );\n}\n\n// ── Step 1: Database ──────────────────────────────────────\n\nfunction StepDatabase({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n const testConnection = async () => {\n onChange({ dbTestStatus: \"testing\", dbTestMessage: \"\", dbDetectedType: null });\n try {\n const body = { host: data.dbHost, port: data.dbPort, user: data.dbUser, password: data.dbPassword, database: data.dbName };\n const res = await apiRequest(\"POST\", \"/api/setup/test-db\", body);\n const json = await res.json();\n if (json.success) {\n const detected = json.detectedType || \"mariadb\";\n onChange({ dbTestStatus: \"success\", dbTestMessage: json.message || \"Connection successful\", dbDetectedType: detected, dbType: detected });\n } else {\n onChange({ dbTestStatus: \"failed\", dbTestMessage: json.message || \"Connection failed\", dbDetectedType: null });\n }\n } catch (err: any) {\n onChange({ dbTestStatus: \"failed\", dbTestMessage: err.message || \"Connection failed\", dbDetectedType: null });\n }\n };\n\n return (\n
\n
\n

\n \n Database Configuration\n

\n

\n Configure your database — or use built-in SQLite for zero-config setup\n

\n
\n\n
\n
\n
\n \n
\n

Database Type

\n

\n Select your DB engine — auto-detected after Test Connection\n

\n
\n {data.dbDetectedType && (\n \n Auto-detected\n \n )}\n
\n
\n {([\n { key: \"sqlite\", label: \"SQLite\", sub: \"Built-in / Zero config\", port: 0 },\n { key: \"mariadb\", label: \"MariaDB\", sub: \"10.x / 11.x\", port: 3306 },\n { key: \"mysql\", label: \"MySQL\", sub: \"5.7 / 8.x\", port: 3306 },\n { key: \"postgresql\", label: \"PostgreSQL\", sub: \"Coming soon\", port: 5432, disabled: true },\n ] as const).map((opt) => (\n !opt.disabled && onChange({\n dbType: opt.key, dbPort: opt.port,\n dbTestStatus: opt.key === \"sqlite\" ? \"success\" : \"idle\",\n dbTestMessage: opt.key === \"sqlite\" ? \"SQLite — no configuration needed\" : \"\",\n })}\n className={`flex flex-col items-center gap-1 p-3 rounded-lg border text-xs transition-all ${\n opt.disabled\n ? \"border-[hsl(217,14%,13%)] text-muted-foreground/40 cursor-not-allowed\"\n : data.dbType === opt.key\n ? \"border-primary bg-primary/10 text-primary\"\n : \"border-[hsl(217,14%,18%)] text-muted-foreground hover:border-primary/40 hover:text-foreground\"\n }`}\n >\n {opt.label}\n {opt.sub}\n \n ))}\n
\n
\n\n {data.dbType === \"sqlite\" && (\n
\n
\n \n SQLite — Zero Configuration\n
\n

\n Data will be stored locally in .data/mc.sqlite.\n No database server required. Perfect for local development, testing, and single-server deployments.\n You can migrate to MariaDB or MySQL later from Settings.\n

\n
\n )}\n\n {data.dbType !== \"sqlite\" && (<>\n
\n
\n
\n \n onChange({ dbHost: e.target.value })} placeholder=\"localhost\"\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\" />\n
\n
\n \n onChange({ dbPort: parseInt(e.target.value) || 3306 })} placeholder=\"3306\"\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\" />\n
\n
\n
\n \n onChange({ dbName: e.target.value })} placeholder=\"mission_control\"\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\" />\n
\n
\n \n onChange({ dbUser: e.target.value })} placeholder=\"root\"\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\" />\n
\n
\n \n onChange({ dbPassword: v })} placeholder=\"Database password\" testId=\"input-db-password\" />\n
\n
\n\n
\n \n {data.dbTestStatus === \"success\" && (\n \n Connected\n \n )}\n {data.dbTestStatus === \"failed\" && (\n \n Failed\n \n )}\n
\n {data.dbTestMessage && data.dbTestStatus === \"failed\" && (\n

{data.dbTestMessage}

\n )}\n )}\n
\n
\n );\n}\n\n// ── Step 2: Admin Account ─────────────────────────────────\n\nfunction StepAdmin({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n const strength = getPasswordStrength(data.adminPassword);\n const passwordsMatch =\n data.adminConfirmPassword.length > 0 &&\n data.adminPassword === data.adminConfirmPassword;\n const passwordsMismatch =\n data.adminConfirmPassword.length > 0 &&\n data.adminPassword !== data.adminConfirmPassword;\n\n return (\n
\n
\n

\n \n Create Admin Account\n

\n

\n This will be the primary administrator for Mission Control\n

\n
\n\n
\n
\n \n onChange({ adminName: e.target.value })}\n placeholder=\"John Smith\"\n className=\"text-sm bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid=\"input-admin-name\"\n />\n
\n\n
\n \n onChange({ adminEmail: e.target.value })}\n placeholder=\"admin@yourcompany.com\"\n className=\"text-sm bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid=\"input-admin-email\"\n />\n
\n\n
\n \n onChange({ adminPassword: v })}\n placeholder=\"Minimum 8 characters\"\n testId=\"input-admin-password\"\n />\n {/* Password strength indicator */}\n {data.adminPassword.length > 0 && (\n
\n
\n {[1, 2, 3, 4].map((level) => (\n \n ))}\n
\n \n {strength.label}\n

\n
\n )}\n
\n\n
\n \n onChange({ adminConfirmPassword: v })}\n placeholder=\"Re-enter your password\"\n testId=\"input-admin-confirm-password\"\n />\n {passwordsMatch && (\n

\n Passwords match\n

\n )}\n {passwordsMismatch && (\n

\n Passwords do not match\n

\n )}\n
\n
\n
\n );\n}\n\n// ── Step 2: AI Models ─────────────────────────────────────\n\nfunction ModelCard({\n def,\n config,\n onUpdate,\n}: {\n def: (typeof MODEL_DEFS)[0];\n config: ModelConfig;\n onUpdate: (c: Partial) => void;\n}) {\n const [expanded, setExpanded] = useState(false);\n const Icon = def.icon;\n\n const hasValue = def.fields.some((f) => {\n const val = (config as any)[f.key];\n return val && val.trim().length > 0;\n });\n\n const testModel = async () => {\n onUpdate({ testStatus: \"testing\" });\n const t0 = Date.now();\n try {\n const res = await fetch(\"/api/setup/test-model\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ provider: def.key, apiKey: config.apiKey, endpointUrl: config.endpointUrl }),\n });\n const json = await res.json();\n console.log(`[test-model] ${def.key}: ${Date.now() - t0}ms — ${json.success ? \"OK\" : \"FAIL\"}: ${json.message}`);\n if (json.success) {\n onUpdate({ testStatus: \"success\", tested: true, configured: true });\n } else {\n onUpdate({ testStatus: \"failed\" });\n }\n } catch (err: any) {\n console.log(`[test-model] ${def.key}: ${Date.now() - t0}ms — ERROR: ${err.message}`);\n onUpdate({ testStatus: \"failed\" });\n }\n };\n\n return (\n \n setExpanded(!expanded)}\n data-testid={`toggle-model-${def.key}`}\n >\n \n \n \n {def.name}\n {config.configured && (\n \n \n Ready\n \n )}\n {expanded ? (\n \n ) : (\n \n )}\n \n\n {expanded && (\n
\n
\n {def.fields.map((field) => (\n
\n \n {/* Plain text for non-secret fields; SecureInput for API keys */}\n {field.key === \"orgId\" || field.key === \"providerName\" ? (\n onUpdate({ [field.key]: e.target.value })}\n placeholder={field.placeholder}\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid={`input-model-${def.key}-${field.key}`}\n />\n ) : field.key === \"endpointUrl\" ? (\n onUpdate({ [field.key]: e.target.value })}\n placeholder={field.placeholder}\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid={`input-model-${def.key}-${field.key}`}\n />\n ) : (\n onUpdate({ [field.key]: v })}\n placeholder={field.placeholder}\n testId={`input-model-${def.key}-${field.key}`}\n />\n )}\n
\n ))}\n
\n\n
\n \n {config.testStatus === \"testing\" ? (\n <>\n \n Testing...\n \n ) : (\n <>\n \n Test\n \n )}\n \n {config.testStatus === \"success\" && (\n \n Connection verified\n \n )}\n {config.testStatus === \"failed\" && (\n \n Test failed\n \n )}\n
\n
\n )}\n
\n );\n}\n\nfunction StepModels({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n // Warm up HTTPS connection so first API test click is fast (no cold SSL negotiation)\n useEffect(() => {\n fetch(\"/api/setup/server-info\", { method: \"HEAD\" }).catch(() => {});\n }, []);\n\n const updateModel = (key: string, update: Partial) => {\n onChange({\n models: {\n ...data.models,\n [key]: { ...data.models[key], ...update },\n },\n });\n };\n\n return (\n
\n
\n

\n \n Connect AI Models\n

\n

\n Connect at least one AI model to power your agents\n

\n
\n\n
\n {MODEL_DEFS.map((def) => (\n updateModel(def.key, u)}\n />\n ))}\n
\n
\n );\n}\n\n// ── Step 4: Chat Providers ────────────────────────────────\n\nfunction ChatProviderCard({\n def,\n config,\n onUpdate,\n}: {\n def: (typeof CHAT_PROVIDER_DEFS)[0];\n config: ChatProviderConfig;\n onUpdate: (fields: Record) => void;\n}) {\n const [expanded, setExpanded] = useState(false);\n const Icon = def.icon;\n\n const hasAnyValue = Object.values(config.fields).some((v) => v.trim().length > 0);\n\n return (\n \n setExpanded(!expanded)}\n data-testid={`toggle-chat-${def.key}`}\n >\n \n \n \n {def.name}\n {hasAnyValue && (\n \n Configured\n \n )}\n {expanded ? (\n \n ) : (\n \n )}\n \n\n {expanded && (\n
\n {def.fields.map((field) => (\n
\n \n {field.secret ? (\n \n onUpdate({ ...config.fields, [field.key]: v })\n }\n placeholder={field.placeholder}\n testId={`input-chat-${def.key}-${field.key}`}\n />\n ) : (\n \n onUpdate({ ...config.fields, [field.key]: e.target.value })\n }\n placeholder={field.placeholder}\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid={`input-chat-${def.key}-${field.key}`}\n />\n )}\n
\n ))}\n
\n )}\n \n );\n}\n\nfunction StepChat({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n const updateProvider = (key: string, fields: Record) => {\n const configured = Object.values(fields).some((v) => v.trim().length > 0);\n onChange({\n chatProviders: {\n ...data.chatProviders,\n [key]: { fields, configured },\n },\n });\n };\n\n return (\n
\n
\n

\n \n Chat Providers\n \n Optional\n \n

\n

\n Connect messaging platforms for agent notifications\n

\n
\n\n
\n {CHAT_PROVIDER_DEFS.map((def) => (\n updateProvider(def.key, fields)}\n />\n ))}\n
\n
\n );\n}\n\n// ── Step 5: Agent Configuration ───────────────────────────\n\nfunction StepAgents({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n return (\n
\n
\n

\n \n Configure Your Agents\n

\n

\n Choose a preset team or customize your own\n

\n
\n\n
\n {(Object.entries(AGENT_PRESETS) as [keyof typeof AGENT_PRESETS, (typeof AGENT_PRESETS)[keyof typeof AGENT_PRESETS]][]).map(\n ([key, preset]) => {\n const Icon = preset.icon;\n const isSelected = data.agentPreset === key;\n return (\n onChange({ agentPreset: key as WizardData[\"agentPreset\"] })}\n data-testid={`button-preset-${key}`}\n >\n
\n \n \n
\n
\n
\n {preset.label}\n {key === \"business\" && (\n \n Recommended\n \n )}\n
\n

\n {preset.description}\n

\n {preset.agents.length > 0 && isSelected && (\n
\n {preset.agents.map((agent) => (\n \n {agent}\n \n ))}\n
\n )}\n
\n \n {isSelected && }\n
\n
\n \n );\n }\n )}\n \n \n );\n}\n\n// ── Step 6: Domain & SSL ──────────────────────────────────\n\nfunction StepDomain({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial) => void;\n}) {\n // Fetch server info on mount + auto-fill domain from current URL\n useEffect(() => {\n // Auto-fill domain from the browser's current hostname\n if (!data.domain && window.location.hostname && window.location.hostname !== \"localhost\") {\n onChange({ domain: window.location.hostname });\n }\n async function fetchServerInfo() {\n try {\n const res = await apiRequest(\"GET\", \"/api/setup/server-info\");\n const json = await res.json();\n onChange({ serverIp: json.ip || \"\", serverHostname: json.hostname || \"\" });\n } catch {\n // silently fail\n }\n }\n fetchServerInfo();\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n return (\n
\n
\n

\n \n Domain & SSL Setup\n

\n

\n Configure how users will access your Mission Control\n

\n
\n\n
\n
\n \n onChange({ domain: e.target.value })}\n placeholder=\"control.yourdomain.com\"\n className=\"text-sm font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid=\"input-domain\"\n />\n
\n\n
\n
\n \n
\n

Auto-provision SSL

\n

\n Let's Encrypt will be used for certificate provisioning\n

\n
\n
\n onChange({ autoSsl: checked })}\n data-testid=\"switch-auto-ssl\"\n />\n
\n\n
\n
\n \n onChange({ httpPort: parseInt(e.target.value) || 80 })}\n placeholder=\"80\"\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid=\"input-http-port\"\n />\n
\n
\n \n onChange({ httpsPort: parseInt(e.target.value) || 443 })}\n placeholder=\"443\"\n className=\"text-xs font-mono bg-[hsl(220,16%,5%)]/60 border-[hsl(217,14%,18%)] focus:border-[hsl(173,58%,44%)] focus:ring-1 focus:ring-[hsl(173,58%,44%)]/30 transition-all\"\n data-testid=\"input-https-port\"\n />\n
\n
\n\n {/* Server info & DNS note */}\n
\n
\n \n

\n DNS must be pointed to this server's IP before SSL can be provisioned.\n

\n
\n {data.serverIp && (\n
\n Server IP:\n \n {data.serverIp}\n \n
\n )}\n
\n
\n
\n );\n}\n\n// ── Step 7: Final Verification ────────────────────────────\n\nfunction SummaryRow({\n icon: Icon,\n label,\n value,\n status,\n onEdit,\n testId,\n}: {\n icon: any;\n label: string;\n value: string;\n status: \"complete\" | \"partial\" | \"skipped\";\n onEdit: () => void;\n testId: string;\n}) {\n return (\n \n \n {status === \"complete\" ? (\n \n ) : status === \"partial\" ? (\n \n ) : (\n
\n )}\n
\n
\n

{label}

\n

{value}

\n
\n \n Edit\n \n \n );\n}\n\nfunction StepReview({\n data,\n onGoToStep,\n onLaunch,\n isLaunching,\n}: {\n data: WizardData;\n onGoToStep: (s: number) => void;\n onLaunch: () => void;\n isLaunching: boolean;\n}) {\n const configuredModels = Object.entries(data.models)\n .filter(([, c]) => c.configured)\n .map(([k]) => MODEL_DEFS.find((d) => d.key === k)?.name || k);\n\n const configuredProviders = Object.entries(data.chatProviders)\n .filter(([, c]) => c.configured)\n .map(([k]) => CHAT_PROVIDER_DEFS.find((d) => d.key === k)?.name || k);\n\n const presetInfo = AGENT_PRESETS[data.agentPreset];\n\n return (\n
\n
\n

\n \n Ready to Launch\n

\n

\n Review your configuration before launching Mission Control\n

\n
\n\n
\n
\n {data.dbTestStatus === \"success\" && (\n onGoToStep(1)}\n testId=\"summary-database\"\n />\n )}\n onGoToStep(2)}\n testId=\"summary-admin\"\n />\n 0\n ? configuredModels.join(\", \")\n : \"No models configured\"\n }\n status={configuredModels.length > 0 ? \"complete\" : \"skipped\"}\n onEdit={() => onGoToStep(3)}\n testId=\"summary-models\"\n />\n 0\n ? configuredProviders.join(\", \")\n : \"None configured\"\n }\n status={configuredProviders.length > 0 ? \"partial\" : \"skipped\"}\n onEdit={() => onGoToStep(4)}\n testId=\"summary-chat\"\n />\n 0 ? ` — ${presetInfo.agents.length} agents` : \"\"}`}\n status=\"complete\"\n onEdit={() => onGoToStep(5)}\n testId=\"summary-agents\"\n />\n onGoToStep(6)}\n testId=\"summary-domain\"\n />\n
\n
\n\n {/* Launch button */}\n
\n \n {isLaunching ? (\n <>\n \n Launching...\n \n ) : (\n <>\n \n Launch Mission Control\n \n )}\n \n {isLaunching && (\n
\n
\n \n
\n

\n Setting up your environment...\n

\n
\n )}\n
\n
\n );\n}\n\n// ── Confetti Effect ───────────────────────────────────────\n\nfunction LaunchProgressScreen() {\n const [messages, setMessages] = useState([\"Initializing Mission Control...\"]);\n const [statusData, setStatusData] = useState(null);\n const [elapsed, setElapsed] = useState(0);\n\n const funMessages = [\n \"Warming up the AI engines...\",\n \"Teaching agents their new roles...\",\n \"Organizing the task board...\",\n \"Setting up your command center...\",\n \"Calibrating the skill library...\",\n \"Configuring security policies...\",\n \"Tuning the memory systems...\",\n \"Preparing your dashboard...\",\n \"Brewing coffee for the CEO agent...\",\n \"Training the marketing department...\",\n \"Sharpening the analytics tools...\",\n \"Polishing the approval queue...\",\n \"Loading the schedule planner...\",\n \"Wiring up the integrations...\",\n \"Almost there — final checks...\",\n \"Connecting to OpenClaw gateway...\",\n \"Your AI team is getting ready...\",\n ];\n\n useEffect(() => {\n // Elapsed timer\n const timer = setInterval(() => setElapsed(e => e + 1), 1000);\n\n // Fun rotating messages every 3 seconds\n let msgIdx = 0;\n const msgTimer = setInterval(() => {\n msgIdx = (msgIdx + 1) % funMessages.length;\n setMessages(prev => [...prev.slice(-15), funMessages[msgIdx]]);\n }, 3000);\n\n // Poll real status every 2 seconds\n const statusTimer = setInterval(async () => {\n try {\n const res = await fetch(\"/api/setup/launch-status\");\n const data = await res.json();\n setStatusData(data);\n // Add real step completions to the log\n if (data.steps) {\n for (const step of data.steps) {\n if (step.done) {\n setMessages(prev => {\n const msg = `✅ ${step.name} (${step.count})`;\n if (prev.includes(msg)) return prev;\n return [...prev.slice(-15), msg];\n });\n }\n }\n }\n } catch { /* keep polling */ }\n }, 2000);\n\n return () => { clearInterval(timer); clearInterval(msgTimer); clearInterval(statusTimer); };\n }, []);\n\n // Time-based progress: +5% every 5 seconds, capped at 90% until truly done.\n // The backend steps complete almost instantly but the actual build takes 60+s.\n const [timedProgress, setTimedProgress] = useState(0);\n const allDone = statusData?.allDone === true;\n\n useEffect(() => {\n const progressTimer = setInterval(() => {\n setTimedProgress(prev => {\n if (prev >= 90) return 90; // cap at 90% until app is actually ready\n return prev + 5;\n });\n }, 5000);\n return () => clearInterval(progressTimer);\n }, []);\n\n // When backend signals allDone, jump to 100%\n const progressPercent = allDone ? 100 : timedProgress;\n\n return (\n
\n
\n {/* Header */}\n
\n
\n \n
\n

Launching Mission Control

\n

Setting up your AI team — this takes a moment

\n
\n\n {/* Spinner + status */}\n
\n
\n {statusData?.summary || \"Starting...\"}\n {elapsed}s\n
\n\n {/* Log output — scrolling messages like the PHP installer */}\n
\n {messages.map((msg, i) => (\n
\n [{String(Math.floor((elapsed - (messages.length - i) * 3) / 60)).padStart(2, \"0\")}:{String(Math.abs((elapsed - (messages.length - i) * 3) % 60)).padStart(2, \"0\")}]\n {msg}\n
\n ))}\n
▌
\n
\n\n {/* Status checklist */}\n {statusData?.steps && (\n
\n {statusData.steps.map((step: any, i: number) => (\n
\n {step.done ? (\n \n ) : (\n
\n )}\n {step.name}\n {step.done && {step.count}}\n
\n ))}\n
\n )}\n
\n
\n );\n}\n\nfunction Confetti() {\n return (\n
\n {Array.from({ length: 60 }).map((_, i) => {\n const left = Math.random() * 100;\n const delay = Math.random() * 0.8;\n const duration = 1.5 + Math.random() * 2;\n const size = 4 + Math.random() * 6;\n const colors = [\n \"hsl(173, 58%, 44%)\",\n \"hsl(262, 83%, 68%)\",\n \"hsl(43, 74%, 60%)\",\n \"hsl(340, 75%, 60%)\",\n \"hsl(221, 83%, 65%)\",\n ];\n const color = colors[i % colors.length];\n const rotation = Math.random() * 360;\n return (\n \n );\n })}\n \n
\n );\n}\n\n// ── Main Wizard Component ─────────────────────────────────\n\nexport default function SetupWizard() {\n const { toast } = useToast();\n const [currentStep, setCurrentStep] = useState(0);\n const [wizardData, setWizardData] = useState(defaultWizardData);\n const [isLaunching, setIsLaunching] = useState(false);\n const [showConfetti, setShowConfetti] = useState(false);\n const [showLaunchProgress, setShowLaunchProgress] = useState(false);\n const [direction, setDirection] = useState<\"forward\" | \"back\">(\"forward\");\n const [isTransitioning, setIsTransitioning] = useState(false);\n const contentRef = useRef(null);\n\n // When coming from the PHP installer (?installer=1), skip Welcome (0)\n // and Database (1) — PHP already configured the DB — start at Admin (2).\n useEffect(() => {\n const params = new URLSearchParams(window.location.search);\n if (params.get(\"installer\") === \"1\") {\n setCurrentStep(2);\n window.history.replaceState({}, \"\", window.location.pathname + window.location.hash);\n }\n }, []);\n\n const updateData = useCallback(\n (partial: Partial) => {\n setWizardData((prev) => ({ ...prev, ...partial }));\n },\n []\n );\n\n // ── Validation per step ────────────────────────────────\n\n const canContinue = useCallback((): boolean => {\n switch (currentStep) {\n case 0: // Welcome — no required input\n return true;\n case 1: // Database — must pass test connection\n return wizardData.dbTestStatus === \"success\";\n case 2: { // Admin\n const pw = wizardData.adminPassword;\n return (\n wizardData.adminName.trim().length > 0 &&\n wizardData.adminEmail.trim().length > 0 &&\n /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(wizardData.adminEmail) &&\n pw.length >= 8 &&\n wizardData.adminConfirmPassword === pw\n );\n }\n case 3: // AI Models — at least one configured\n return Object.values(wizardData.models).some((m) => m.configured);\n case 4: // Chat — always optional\n return true;\n case 5: // Agents — always has a selection\n return true;\n case 6: // Domain — optional\n return true;\n case 7: // Review\n return true;\n default:\n return false;\n }\n }, [currentStep, wizardData]);\n\n const goToStep = useCallback((step: number) => {\n setDirection(step > currentStep ? \"forward\" : \"back\");\n setIsTransitioning(true);\n setTimeout(() => {\n setCurrentStep(step);\n setIsTransitioning(false);\n // Scroll to top of content\n contentRef.current?.scrollTo({ top: 0, behavior: \"smooth\" });\n }, 150);\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [currentStep]);\n\n const nextStep = useCallback(() => {\n if (currentStep < STEPS.length - 1 && canContinue()) {\n // Pre-seed agents when leaving the Agents step (step 5)\n // This creates agents in DB while user fills Domain + Review steps\n if (currentStep === 5 && wizardData.agentPreset !== \"custom\") {\n fetch(\"/api/setup/pre-seed\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ preset: wizardData.agentPreset }),\n }).catch(() => {}); // fire-and-forget\n }\n goToStep(currentStep + 1);\n }\n }, [currentStep, canContinue, goToStep, wizardData.agentPreset]);\n\n const prevStep = useCallback(() => {\n if (currentStep > 0) {\n goToStep(currentStep - 1);\n }\n }, [currentStep, goToStep]);\n\n const handleLaunch = useCallback(async () => {\n setIsLaunching(true);\n try {\n // Include DB config only if the user configured it in the wizard\n // (direct install). When coming from PHP installer, dbTestStatus is\n // \"idle\" and DB is already in .env — skip to avoid overwriting it.\n const payload: Record = {\n admin: {\n fullName: wizardData.adminName,\n email: wizardData.adminEmail,\n password: wizardData.adminPassword,\n },\n models: wizardData.models,\n chatProviders: wizardData.chatProviders,\n agentPreset: wizardData.agentPreset,\n domain: {\n domainName: wizardData.domain,\n autoSsl: wizardData.autoSsl,\n httpPort: wizardData.httpPort,\n httpsPort: wizardData.httpsPort,\n },\n };\n // Only send DB config if the wizard collected it (direct install path)\n if (wizardData.dbTestStatus === \"success\") {\n payload.database = {\n host: wizardData.dbHost,\n port: wizardData.dbPort,\n name: wizardData.dbName,\n user: wizardData.dbUser,\n password: wizardData.dbPassword,\n type: wizardData.dbType,\n };\n }\n console.log(\"[launch] starting setup/complete...\");\n const t0 = Date.now();\n\n // Direct fetch — no apiRequest wrapper, no body read wait\n const setupRes = await fetch(\"/api/setup/complete\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(payload),\n });\n console.log(`[launch] setup/complete: ${Date.now() - t0}ms (${setupRes.status})`);\n\n // Show launch progress screen — replaces the wizard\n setShowLaunchProgress(true);\n\n // Auto-login in background\n fetch(\"/api/auth/login\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify({ email: wizardData.adminEmail, password: wizardData.adminPassword }),\n }).then(() => {\n queryClient.setQueryData([\"/api/auth/me\"], { authenticated: true, email: wizardData.adminEmail });\n }).catch(() => {});\n\n // Poll launch status until everything is seeded, then transfer\n const pollInterval = setInterval(async () => {\n try {\n const res = await fetch(\"/api/setup/launch-status\");\n const data = await res.json();\n if (data.allDone) {\n clearInterval(pollInterval);\n // Add completion message to log\n setMessages(prev => [...prev, \"🎉 All systems ready — launching dashboard!\"]);\n setShowConfetti(true);\n // Hold confetti for 4 seconds so user sees it\n setTimeout(() => {\n window.location.href = window.location.origin + window.location.pathname + \"#/\";\n window.location.reload();\n }, 4000);\n }\n } catch { /* keep polling */ }\n }, 2000);\n\n // Safety: force transfer after 120 seconds — skip confetti on timeout\n setTimeout(() => {\n clearInterval(pollInterval);\n window.location.href = window.location.origin + window.location.pathname + \"#/\";\n window.location.reload();\n }, 120000);\n } catch (err: any) {\n toast({\n title: \"Setup failed\",\n description: err.message || \"Something went wrong. Please try again.\",\n variant: \"destructive\",\n });\n setIsLaunching(false);\n }\n }, [wizardData, toast]);\n\n // ── Render step content ────────────────────────────────\n\n const renderStep = () => {\n // Show launch progress inside the wizard frame\n if (showLaunchProgress) return ;\n\n switch (currentStep) {\n case 0:\n return ;\n case 1:\n return ;\n case 2:\n return ;\n case 3:\n return ;\n case 4:\n return ;\n case 5:\n return ;\n case 6:\n return ;\n case 7:\n return (\n \n );\n default:\n return null;\n }\n };\n\n return (\n \n {showConfetti && }\n\n {/* Step progress bar — show full (step 7) when launching */}\n \n\n {/* Step content */}\n
\n \n
\n {renderStep()}\n
\n
\n
\n\n {/* Bottom navigation bar — hidden during launch */}\n {currentStep < 7 && !showLaunchProgress && (\n
\n
\n
\n {currentStep > 0 && (\n \n \n Back\n \n )}\n
\n
\n {/* Skip button for optional steps */}\n {(currentStep === 4 || currentStep === 6) && (\n \n Skip for now\n \n )}\n \n {currentStep === 0 ? (\n <>\n Get Started\n \n \n ) : (\n <>\n Continue\n \n \n )}\n \n
\n
\n
\n )}\n
\n );\n}\n"}}}}