Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/chad-1964_resyncs-openclaw-mission-control-panel.json
T
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
76 KiB
JSON

{"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<string, string>;\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<string, ModelConfig>;\n // Step 4: Chat Providers\n chatProviders: Record<string, ChatProviderConfig>;\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<string, ModelConfig> = {};\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<string, ChatProviderConfig> = {};\n for (const c of CHAT_PROVIDER_DEFS) {\n const fields: Record<string, string> = {};\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 <div className=\"relative\">\n <Input\n type={visible ? \"text\" : \"password\"}\n value={value}\n onChange={(e) => 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 <button\n type=\"button\"\n onClick={() => 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 ? <EyeOff className=\"w-3.5 h-3.5\" /> : <Eye className=\"w-3.5 h-3.5\" />}\n </button>\n </div>\n );\n}\n\n// ── Progress Bar ──────────────────────────────────────────\n\nfunction StepProgressBar({ currentStep }: { currentStep: number }) {\n return (\n <div className=\"w-full px-4 sm:px-8 py-6\" data-testid=\"step-progress-bar\">\n <div className=\"max-w-3xl mx-auto\">\n <div className=\"flex items-center justify-between relative\">\n {/* Connecting line (background) */}\n <div className=\"absolute top-4 left-4 right-4 h-[2px] bg-[hsl(217,14%,15%)]\" />\n {/* Connecting line (filled) */}\n <div\n className=\"absolute top-4 left-4 h-[2px] bg-primary transition-all duration-500 ease-out\"\n style={{\n width: `calc(${(currentStep / (STEPS.length - 1)) * 100}% - 32px)`,\n }}\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 <div\n key={step.label}\n className=\"flex flex-col items-center relative z-10\"\n data-testid={`step-indicator-${i}`}\n >\n <div\n className={`w-8 h-8 rounded-full flex items-center justify-center transition-all duration-300 ${\n isComplete\n ? \"bg-primary text-primary-foreground\"\n : isCurrent\n ? \"bg-primary/20 text-primary border-2 border-primary\"\n : \"bg-[hsl(217,14%,14%)] text-muted-foreground border border-[hsl(217,14%,20%)]\"\n }`}\n >\n {isComplete ? (\n <Check className=\"w-4 h-4\" />\n ) : (\n <Icon className=\"w-3.5 h-3.5\" />\n )}\n </div>\n <span\n className={`text-[10px] mt-1.5 font-medium transition-colors duration-300 hidden sm:block ${\n isCurrent\n ? \"text-primary\"\n : isComplete\n ? \"text-foreground/70\"\n : \"text-muted-foreground/50\"\n }`}\n >\n {step.label}\n </span>\n </div>\n );\n })}\n </div>\n </div>\n </div>\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<WizardData>) => void;\n}) {\n const [detection, setDetection] = useState<OpenClawDetection | null>(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 <div className=\"text-center space-y-8\" data-testid=\"step-welcome\">\n {/* Animated logo */}\n <div className=\"relative inline-flex items-center justify-center\">\n <div className=\"absolute inset-0 w-24 h-24 mx-auto rounded-full bg-primary/10 animate-pulse\" />\n <div className=\"relative w-24 h-24 rounded-2xl bg-primary/10 border border-primary/20 flex items-center justify-center mx-auto\">\n <Zap className=\"w-12 h-12 text-primary\" />\n </div>\n </div>\n\n <div className=\"space-y-3\">\n <h1 className=\"text-2xl font-bold tracking-tight\" data-testid=\"text-welcome-heading\">\n Welcome to Mission Control\n </h1>\n <p className=\"text-sm text-primary font-medium\">\n Powered by OpenClaw — AI Agent Orchestration\n </p>\n <p className=\"text-sm text-muted-foreground max-w-md mx-auto leading-relaxed\">\n This wizard will guide you through setting up your AI command center.\n It takes about 5 minutes.\n </p>\n </div>\n\n {/* OpenClaw Auto-Detection */}\n <div className=\"max-w-sm mx-auto text-left\">\n {detecting ? (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground p-3 rounded-lg border border-border/50\">\n <Loader2 className=\"w-3.5 h-3.5 animate-spin\" />\n Scanning for existing OpenClaw installation...\n </div>\n ) : detection?.installed ? (\n <div className=\"p-3 rounded-lg border border-emerald-500/30 bg-emerald-500/5 space-y-2\">\n <div className=\"flex items-center gap-2\">\n <Check className=\"w-4 h-4 text-emerald-400\" />\n <span className=\"text-sm font-medium text-emerald-400\">OpenClaw detected!</span>\n {detection.version && <span className=\"text-[10px] text-muted-foreground\">v{detection.version}</span>}\n </div>\n <div className=\"text-[11px] text-muted-foreground space-y-1\">\n {detection.agents.length > 0 && (\n <p>\n <span className=\"text-foreground font-medium\">{detection.agents.length}</span> agent{detection.agents.length !== 1 ? \"s\" : \"\"} found\n {detection.agents.length <= 4 && (\n <span className=\"text-muted-foreground\"> — {detection.agents.map(a => a.name || a.id).join(\", \")}</span>\n )}\n </p>\n )}\n {detection.providers.length > 0 && (\n <p>\n <span className=\"text-foreground font-medium\">{detection.providers.length}</span> API key{detection.providers.length !== 1 ? \"s\" : \"\"} configured\n <span className=\"text-muted-foreground\"> — {detection.providers.map(p => p.provider).join(\", \")}</span>\n </p>\n )}\n </div>\n <p className=\"text-[10px] text-emerald-400/70\">\n Your existing agents and API keys will be auto-imported during setup.\n </p>\n </div>\n ) : (\n <div className=\"flex items-center gap-2 text-xs text-muted-foreground p-3 rounded-lg border border-border/50\">\n <Info className=\"w-3.5 h-3.5\" />\n No OpenClaw installation detected — we'll set one up for you.\n </div>\n )}\n </div>\n\n </div>\n );\n}\n\n// ── Step 1: Database ──────────────────────────────────────\n\nfunction StepDatabase({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial<WizardData>) => 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 <div className=\"space-y-6\" data-testid=\"step-database\">\n <div>\n <h2 className=\"text-xl font-semibold flex items-center gap-2\">\n <Database className=\"w-5 h-5 text-primary\" />\n Database Configuration\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Configure your database — or use built-in SQLite for zero-config setup\n </p>\n </div>\n\n <div className=\"space-y-4\">\n <div className=\"rounded-lg border border-[hsl(217,14%,15%)] bg-[hsl(220,14%,9%)]/80 p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <Server className=\"w-4 h-4 text-primary\" />\n <div>\n <p className=\"text-sm font-medium\">Database Type</p>\n <p className=\"text-[10px] text-muted-foreground\">\n Select your DB engine — auto-detected after Test Connection\n </p>\n </div>\n {data.dbDetectedType && (\n <span className=\"ml-auto text-[10px] px-2 py-0.5 rounded-full bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-mono\">\n Auto-detected\n </span>\n )}\n </div>\n <div className=\"grid grid-cols-4 gap-2\">\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 <button\n key={opt.key}\n type=\"button\"\n disabled={opt.disabled}\n onClick={() => !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 <span className=\"font-semibold\">{opt.label}</span>\n <span className=\"text-[10px] opacity-60\">{opt.sub}</span>\n </button>\n ))}\n </div>\n </div>\n\n {data.dbType === \"sqlite\" && (\n <div className=\"rounded-lg border border-emerald-500/20 bg-emerald-500/5 p-4\">\n <div className=\"flex items-center gap-2 text-sm\">\n <CheckCircle2 className=\"w-4 h-4 text-emerald-400\" />\n <span className=\"text-emerald-400 font-medium\">SQLite — Zero Configuration</span>\n </div>\n <p className=\"text-xs text-muted-foreground mt-2\">\n Data will be stored locally in <code className=\"text-[10px] bg-[hsl(220,16%,5%)]/60 px-1 py-0.5 rounded\">.data/mc.sqlite</code>.\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 </p>\n </div>\n )}\n\n {data.dbType !== \"sqlite\" && (<>\n <div className=\"space-y-3 rounded-lg border border-[hsl(217,14%,15%)] bg-[hsl(220,16%,5%)]/40 p-4\">\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">Host</Label>\n <Input type=\"text\" value={data.dbHost} onChange={(e) => 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 </div>\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">Port</Label>\n <Input type=\"number\" value={data.dbPort} onChange={(e) => 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 </div>\n </div>\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">Database Name</Label>\n <Input type=\"text\" value={data.dbName} onChange={(e) => 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 </div>\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">Username</Label>\n <Input type=\"text\" value={data.dbUser} onChange={(e) => 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 </div>\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">Password</Label>\n <SecureInput value={data.dbPassword} onChange={(v) => onChange({ dbPassword: v })} placeholder=\"Database password\" testId=\"input-db-password\" />\n </div>\n </div>\n\n <div className=\"flex items-center gap-3 flex-wrap\">\n <Button variant=\"secondary\" onClick={testConnection} disabled={data.dbTestStatus === \"testing\"} data-testid=\"button-test-db\">\n {data.dbTestStatus === \"testing\" ? (\n <><Loader2 className=\"w-4 h-4 animate-spin\" />Testing...</>\n ) : (\n <><Database className=\"w-4 h-4\" />Test Connection</>\n )}\n </Button>\n {data.dbTestStatus === \"success\" && (\n <Badge className=\"text-xs bg-emerald-500/15 text-emerald-400 border-emerald-500/25\" data-testid=\"badge-db-success\">\n <CheckCircle2 className=\"w-3 h-3 mr-1\" />Connected\n </Badge>\n )}\n {data.dbTestStatus === \"failed\" && (\n <Badge variant=\"destructive\" className=\"text-xs\" data-testid=\"badge-db-failed\">\n <AlertCircle className=\"w-3 h-3 mr-1\" />Failed\n </Badge>\n )}\n </div>\n {data.dbTestMessage && data.dbTestStatus === \"failed\" && (\n <p className=\"text-[10px] text-red-400\" data-testid=\"text-db-error\">{data.dbTestMessage}</p>\n )}\n </>)}\n </div>\n </div>\n );\n}\n\n// ── Step 2: Admin Account ─────────────────────────────────\n\nfunction StepAdmin({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial<WizardData>) => 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 <div className=\"space-y-6\" data-testid=\"step-admin\">\n <div>\n <h2 className=\"text-xl font-semibold flex items-center gap-2\">\n <Shield className=\"w-5 h-5 text-primary\" />\n Create Admin Account\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n This will be the primary administrator for Mission Control\n </p>\n </div>\n\n <div className=\"space-y-4\">\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n Full Name\n </Label>\n <Input\n type=\"text\"\n value={data.adminName}\n onChange={(e) => 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 </div>\n\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n Email Address\n </Label>\n <Input\n type=\"email\"\n value={data.adminEmail}\n onChange={(e) => 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 </div>\n\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n Password\n </Label>\n <SecureInput\n value={data.adminPassword}\n onChange={(v) => onChange({ adminPassword: v })}\n placeholder=\"Minimum 8 characters\"\n testId=\"input-admin-password\"\n />\n {/* Password strength indicator */}\n {data.adminPassword.length > 0 && (\n <div className=\"mt-2 space-y-1\" data-testid=\"password-strength\">\n <div className=\"flex gap-1\">\n {[1, 2, 3, 4].map((level) => (\n <div\n key={level}\n className={`h-1 flex-1 rounded-full transition-all duration-300 ${\n level <= strength.level\n ? strength.color\n : \"bg-[hsl(217,14%,18%)]\"\n }`}\n />\n ))}\n </div>\n <p\n className={`text-[10px] ${\n strength.level <= 1\n ? \"text-red-400\"\n : strength.level === 2\n ? \"text-orange-400\"\n : strength.level === 3\n ? \"text-yellow-400\"\n : \"text-emerald-400\"\n }`}\n >\n {strength.label}\n </p>\n </div>\n )}\n </div>\n\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n Confirm Password\n </Label>\n <SecureInput\n value={data.adminConfirmPassword}\n onChange={(v) => onChange({ adminConfirmPassword: v })}\n placeholder=\"Re-enter your password\"\n testId=\"input-admin-confirm-password\"\n />\n {passwordsMatch && (\n <p className=\"text-[10px] text-emerald-400 mt-1 flex items-center gap-1\">\n <Check className=\"w-3 h-3\" /> Passwords match\n </p>\n )}\n {passwordsMismatch && (\n <p className=\"text-[10px] text-red-400 mt-1 flex items-center gap-1\">\n <AlertCircle className=\"w-3 h-3\" /> Passwords do not match\n </p>\n )}\n </div>\n </div>\n </div>\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<ModelConfig>) => 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 <div\n className={`rounded-lg border transition-all duration-200 overflow-hidden ${\n config.configured\n ? \"border-primary/30 bg-primary/[0.03]\"\n : \"border-[hsl(217,14%,15%)] bg-[hsl(220,14%,9%)]/80\"\n }`}\n data-testid={`model-card-${def.key}`}\n >\n <button\n className=\"flex items-center gap-3 w-full px-4 py-3 hover:bg-white/[0.02] transition-colors text-left\"\n onClick={() => setExpanded(!expanded)}\n data-testid={`toggle-model-${def.key}`}\n >\n <span\n className=\"w-6 h-6 flex items-center justify-center shrink-0\"\n style={{ color: def.iconColor }}\n >\n <Icon className=\"w-5 h-5\" />\n </span>\n <span className=\"text-sm font-medium flex-1\">{def.name}</span>\n {config.configured && (\n <Badge className=\"text-[10px] bg-emerald-500/15 text-emerald-400 border-emerald-500/25 mr-2\">\n <Check className=\"w-2.5 h-2.5 mr-1\" />\n Ready\n </Badge>\n )}\n {expanded ? (\n <ChevronDown className=\"w-4 h-4 text-muted-foreground\" />\n ) : (\n <ChevronRight className=\"w-4 h-4 text-muted-foreground\" />\n )}\n </button>\n\n {expanded && (\n <div className=\"px-4 pb-4 space-y-3 border-t border-[hsl(217,14%,13%)]\">\n <div className=\"pt-3 space-y-3\">\n {def.fields.map((field) => (\n <div key={field.key}>\n <Label className=\"text-[11px] text-muted-foreground mb-1 block\">\n {field.label}\n </Label>\n {/* Plain text for non-secret fields; SecureInput for API keys */}\n {field.key === \"orgId\" || field.key === \"providerName\" ? (\n <Input\n type=\"text\"\n value={(config as any)[field.key] || \"\"}\n onChange={(e) => 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 <Input\n type=\"url\"\n value={(config as any)[field.key] || \"\"}\n onChange={(e) => 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 <SecureInput\n value={(config as any)[field.key] || \"\"}\n onChange={(v) => onUpdate({ [field.key]: v })}\n placeholder={field.placeholder}\n testId={`input-model-${def.key}-${field.key}`}\n />\n )}\n </div>\n ))}\n </div>\n\n <div className=\"flex items-center gap-2 pt-1\">\n <Button\n size=\"sm\"\n variant=\"secondary\"\n onClick={testModel}\n disabled={!hasValue || config.testStatus === \"testing\"}\n data-testid={`button-test-model-${def.key}`}\n >\n {config.testStatus === \"testing\" ? (\n <>\n <Loader2 className=\"w-3 h-3 animate-spin\" />\n Testing...\n </>\n ) : (\n <>\n <Zap className=\"w-3 h-3\" />\n Test\n </>\n )}\n </Button>\n {config.testStatus === \"success\" && (\n <span className=\"text-[10px] text-emerald-400 flex items-center gap-1\">\n <CheckCircle2 className=\"w-3 h-3\" /> Connection verified\n </span>\n )}\n {config.testStatus === \"failed\" && (\n <span className=\"text-[10px] text-red-400 flex items-center gap-1\">\n <AlertCircle className=\"w-3 h-3\" /> Test failed\n </span>\n )}\n </div>\n </div>\n )}\n </div>\n );\n}\n\nfunction StepModels({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial<WizardData>) => 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<ModelConfig>) => {\n onChange({\n models: {\n ...data.models,\n [key]: { ...data.models[key], ...update },\n },\n });\n };\n\n return (\n <div className=\"space-y-6\" data-testid=\"step-models\">\n <div>\n <h2 className=\"text-xl font-semibold flex items-center gap-2\">\n <Bot className=\"w-5 h-5 text-primary\" />\n Connect AI Models\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Connect at least one AI model to power your agents\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {MODEL_DEFS.map((def) => (\n <ModelCard\n key={def.key}\n def={def}\n config={data.models[def.key]}\n onUpdate={(u) => updateModel(def.key, u)}\n />\n ))}\n </div>\n </div>\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<string, string>) => 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 <div\n className={`rounded-lg border transition-all duration-200 overflow-hidden ${\n hasAnyValue\n ? \"border-primary/30 bg-primary/[0.03]\"\n : \"border-[hsl(217,14%,15%)] bg-[hsl(220,14%,9%)]/80\"\n }`}\n data-testid={`chat-card-${def.key}`}\n >\n <button\n className=\"flex items-center gap-3 w-full px-4 py-3 hover:bg-white/[0.02] transition-colors text-left\"\n onClick={() => setExpanded(!expanded)}\n data-testid={`toggle-chat-${def.key}`}\n >\n <span\n className=\"w-6 h-6 flex items-center justify-center shrink-0\"\n style={{ color: def.iconColor }}\n >\n <Icon className=\"w-5 h-5\" />\n </span>\n <span className=\"text-sm font-medium flex-1\">{def.name}</span>\n {hasAnyValue && (\n <Badge className=\"text-[10px] bg-primary/15 text-primary border-primary/25 mr-2\">\n Configured\n </Badge>\n )}\n {expanded ? (\n <ChevronDown className=\"w-4 h-4 text-muted-foreground\" />\n ) : (\n <ChevronRight className=\"w-4 h-4 text-muted-foreground\" />\n )}\n </button>\n\n {expanded && (\n <div className=\"px-4 pb-4 space-y-3 border-t border-[hsl(217,14%,13%)] pt-3\">\n {def.fields.map((field) => (\n <div key={field.key}>\n <Label className=\"text-[11px] text-muted-foreground mb-1 block\">\n {field.label}\n </Label>\n {field.secret ? (\n <SecureInput\n value={config.fields[field.key] || \"\"}\n onChange={(v) =>\n onUpdate({ ...config.fields, [field.key]: v })\n }\n placeholder={field.placeholder}\n testId={`input-chat-${def.key}-${field.key}`}\n />\n ) : (\n <Input\n type=\"text\"\n value={config.fields[field.key] || \"\"}\n onChange={(e) =>\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 </div>\n ))}\n </div>\n )}\n </div>\n );\n}\n\nfunction StepChat({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial<WizardData>) => void;\n}) {\n const updateProvider = (key: string, fields: Record<string, string>) => {\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 <div className=\"space-y-6\" data-testid=\"step-chat\">\n <div>\n <h2 className=\"text-xl font-semibold flex items-center gap-2\">\n <MessageCircle className=\"w-5 h-5 text-primary\" />\n Chat Providers\n <Badge variant=\"secondary\" className=\"text-[10px] ml-1\">\n Optional\n </Badge>\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Connect messaging platforms for agent notifications\n </p>\n </div>\n\n <div className=\"grid grid-cols-1 sm:grid-cols-2 gap-3\">\n {CHAT_PROVIDER_DEFS.map((def) => (\n <ChatProviderCard\n key={def.key}\n def={def}\n config={data.chatProviders[def.key]}\n onUpdate={(fields) => updateProvider(def.key, fields)}\n />\n ))}\n </div>\n </div>\n );\n}\n\n// ── Step 5: Agent Configuration ───────────────────────────\n\nfunction StepAgents({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial<WizardData>) => void;\n}) {\n return (\n <div className=\"space-y-6\" data-testid=\"step-agents\">\n <div>\n <h2 className=\"text-xl font-semibold flex items-center gap-2\">\n <Users className=\"w-5 h-5 text-primary\" />\n Configure Your Agents\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Choose a preset team or customize your own\n </p>\n </div>\n\n <div className=\"space-y-3\">\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 <button\n key={key}\n className={`w-full text-left rounded-lg border p-4 transition-all duration-200 ${\n isSelected\n ? \"border-primary/40 bg-primary/[0.05] ring-1 ring-primary/20\"\n : \"border-[hsl(217,14%,15%)] bg-[hsl(220,14%,9%)]/80 hover:bg-white/[0.02]\"\n }`}\n onClick={() => onChange({ agentPreset: key as WizardData[\"agentPreset\"] })}\n data-testid={`button-preset-${key}`}\n >\n <div className=\"flex items-start gap-3\">\n <div\n className={`p-2 rounded-lg transition-colors ${\n isSelected\n ? \"bg-primary/15 text-primary\"\n : \"bg-[hsl(217,14%,14%)] text-muted-foreground\"\n }`}\n >\n <Icon className=\"w-5 h-5\" />\n </div>\n <div className=\"flex-1 min-w-0\">\n <div className=\"flex items-center gap-2\">\n <span className=\"text-sm font-semibold\">{preset.label}</span>\n {key === \"business\" && (\n <Badge className=\"text-[9px] bg-primary/15 text-primary border-primary/25\">\n Recommended\n </Badge>\n )}\n </div>\n <p className=\"text-[11px] text-muted-foreground mt-0.5\">\n {preset.description}\n </p>\n {preset.agents.length > 0 && isSelected && (\n <div className=\"flex flex-wrap gap-1.5 mt-3\">\n {preset.agents.map((agent) => (\n <Badge\n key={agent}\n variant=\"secondary\"\n className=\"text-[10px] bg-[hsl(217,14%,14%)] text-foreground/80 border-[hsl(217,14%,18%)]\"\n >\n {agent}\n </Badge>\n ))}\n </div>\n )}\n </div>\n <div\n className={`w-5 h-5 rounded-full border-2 flex items-center justify-center shrink-0 transition-all ${\n isSelected\n ? \"border-primary bg-primary\"\n : \"border-[hsl(217,14%,25%)]\"\n }`}\n >\n {isSelected && <Check className=\"w-3 h-3 text-primary-foreground\" />}\n </div>\n </div>\n </button>\n );\n }\n )}\n </div>\n </div>\n );\n}\n\n// ── Step 6: Domain & SSL ──────────────────────────────────\n\nfunction StepDomain({\n data,\n onChange,\n}: {\n data: WizardData;\n onChange: (d: Partial<WizardData>) => 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 <div className=\"space-y-6\" data-testid=\"step-domain\">\n <div>\n <h2 className=\"text-xl font-semibold flex items-center gap-2\">\n <Globe className=\"w-5 h-5 text-primary\" />\n Domain & SSL Setup\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Configure how users will access your Mission Control\n </p>\n </div>\n\n <div className=\"space-y-4\">\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n Domain Name\n </Label>\n <Input\n type=\"text\"\n value={data.domain}\n onChange={(e) => 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 </div>\n\n <div className=\"flex items-center justify-between rounded-lg border border-[hsl(217,14%,15%)] bg-[hsl(220,14%,9%)]/80 p-4\">\n <div className=\"flex items-center gap-3\">\n <Lock className=\"w-4 h-4 text-primary\" />\n <div>\n <p className=\"text-sm font-medium\">Auto-provision SSL</p>\n <p className=\"text-[10px] text-muted-foreground\">\n Let's Encrypt will be used for certificate provisioning\n </p>\n </div>\n </div>\n <Switch\n checked={data.autoSsl}\n onCheckedChange={(checked) => onChange({ autoSsl: checked })}\n data-testid=\"switch-auto-ssl\"\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-3\">\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n HTTP Port\n </Label>\n <Input\n type=\"number\"\n value={data.httpPort}\n onChange={(e) => 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 </div>\n <div>\n <Label className=\"text-xs text-muted-foreground mb-1.5 block\">\n HTTPS Port\n </Label>\n <Input\n type=\"number\"\n value={data.httpsPort}\n onChange={(e) => 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 </div>\n </div>\n\n {/* Server info & DNS note */}\n <div className=\"rounded-lg border border-[hsl(217,14%,15%)] bg-[hsl(220,16%,5%)]/40 p-4 space-y-3\">\n <div className=\"flex items-start gap-2\">\n <Info className=\"w-4 h-4 text-primary shrink-0 mt-0.5\" />\n <p className=\"text-xs text-muted-foreground leading-relaxed\">\n DNS must be pointed to this server's IP before SSL can be provisioned.\n </p>\n </div>\n {data.serverIp && (\n <div className=\"flex items-center gap-3 text-xs\">\n <span className=\"text-muted-foreground\">Server IP:</span>\n <code className=\"font-mono text-foreground bg-[hsl(217,14%,14%)] px-2 py-0.5 rounded\" data-testid=\"text-server-ip\">\n {data.serverIp}\n </code>\n </div>\n )}\n </div>\n </div>\n </div>\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 <div\n className=\"flex items-center gap-3 py-3 border-b border-[hsl(217,14%,13%)] last:border-0\"\n data-testid={testId}\n >\n <div\n className={`w-6 h-6 rounded-full flex items-center justify-center shrink-0 ${\n status === \"complete\"\n ? \"bg-emerald-500/15 text-emerald-400\"\n : status === \"partial\"\n ? \"bg-primary/15 text-primary\"\n : \"bg-[hsl(217,14%,16%)] text-muted-foreground\"\n }`}\n >\n {status === \"complete\" ? (\n <CheckCircle2 className=\"w-3.5 h-3.5\" />\n ) : status === \"partial\" ? (\n <Icon className=\"w-3 h-3\" />\n ) : (\n <div className=\"w-2 h-2 rounded-full bg-muted-foreground/30\" />\n )}\n </div>\n <div className=\"flex-1 min-w-0\">\n <p className=\"text-sm font-medium\">{label}</p>\n <p className=\"text-[11px] text-muted-foreground truncate\">{value}</p>\n </div>\n <Button\n variant=\"ghost\"\n size=\"sm\"\n onClick={onEdit}\n className=\"text-xs text-primary hover:text-primary/80\"\n data-testid={`button-edit-${testId}`}\n >\n Edit\n </Button>\n </div>\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 <div className=\"space-y-6\" data-testid=\"step-review\">\n <div className=\"text-center\">\n <h2 className=\"text-xl font-semibold flex items-center gap-2 justify-center\">\n <Rocket className=\"w-5 h-5 text-primary\" />\n Ready to Launch\n </h2>\n <p className=\"text-xs text-muted-foreground mt-1\">\n Review your configuration before launching Mission Control\n </p>\n </div>\n\n <div className=\"rounded-lg border border-[hsl(217,14%,15%)] bg-[hsl(220,14%,9%)]/80 overflow-hidden\">\n <div className=\"px-5 py-2\">\n {data.dbTestStatus === \"success\" && (\n <SummaryRow\n icon={Database}\n label=\"Database\"\n value={data.dbType === \"sqlite\" ? \"SQLite (Built-in)\" : `${data.dbName} @ ${data.dbHost} (${data.dbDetectedType ?? data.dbType})`}\n status=\"complete\"\n onEdit={() => onGoToStep(1)}\n testId=\"summary-database\"\n />\n )}\n <SummaryRow\n icon={Shield}\n label=\"Admin Account\"\n value={`${data.adminName} — ${data.adminEmail}`}\n status=\"complete\"\n onEdit={() => onGoToStep(2)}\n testId=\"summary-admin\"\n />\n <SummaryRow\n icon={Bot}\n label=\"AI Models\"\n value={\n configuredModels.length > 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 <SummaryRow\n icon={MessageCircle}\n label=\"Chat Providers\"\n value={\n configuredProviders.length > 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 <SummaryRow\n icon={Users}\n label=\"Agent Team\"\n value={`${presetInfo.label}${presetInfo.agents.length > 0 ? ` — ${presetInfo.agents.length} agents` : \"\"}`}\n status=\"complete\"\n onEdit={() => onGoToStep(5)}\n testId=\"summary-agents\"\n />\n <SummaryRow\n icon={Globe}\n label=\"Domain & SSL\"\n value={data.domain || \"Not configured\"}\n status={data.domain ? \"complete\" : \"skipped\"}\n onEdit={() => onGoToStep(6)}\n testId=\"summary-domain\"\n />\n </div>\n </div>\n\n {/* Launch button */}\n <div className=\"text-center pt-2\">\n <Button\n size=\"lg\"\n onClick={onLaunch}\n disabled={isLaunching}\n className=\"px-10 py-3 text-base font-semibold gap-2 relative overflow-hidden\"\n data-testid=\"button-launch\"\n >\n {isLaunching ? (\n <>\n <Loader2 className=\"w-5 h-5 animate-spin\" />\n Launching...\n </>\n ) : (\n <>\n <Rocket className=\"w-5 h-5\" />\n Launch Mission Control\n </>\n )}\n </Button>\n {isLaunching && (\n <div className=\"mt-4 max-w-xs mx-auto\">\n <div className=\"h-1.5 rounded-full bg-[hsl(217,14%,15%)] overflow-hidden\">\n <div\n className=\"h-full bg-primary rounded-full\"\n style={{ width: \"90%\", animation: \"launch-progress 3s ease-out forwards\" }}\n />\n </div>\n <p className=\"text-[10px] text-muted-foreground mt-2\">\n Setting up your environment...\n </p>\n </div>\n )}\n </div>\n </div>\n );\n}\n\n// ── Confetti Effect ───────────────────────────────────────\n\nfunction LaunchProgressScreen() {\n const [messages, setMessages] = useState<string[]>([\"Initializing Mission Control...\"]);\n const [statusData, setStatusData] = useState<any>(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 <div className=\"flex-1 flex items-center justify-center p-8\">\n <div className=\"w-full max-w-lg\">\n {/* Header */}\n <div className=\"text-center mb-6\">\n <div className=\"inline-flex items-center justify-center w-16 h-16 rounded-2xl bg-primary/10 border border-primary/20 mb-4\">\n <Zap className=\"w-8 h-8 text-primary animate-pulse\" />\n </div>\n <h1 className=\"text-xl font-bold\">Launching Mission Control</h1>\n <p className=\"text-sm text-muted-foreground mt-1\">Setting up your AI team — this takes a moment</p>\n </div>\n\n {/* Spinner + status */}\n <div className=\"flex items-center justify-center gap-3 mb-4\">\n <div className=\"w-5 h-5 border-2 border-primary border-t-transparent rounded-full animate-spin\" />\n <span className=\"text-xs text-muted-foreground\">{statusData?.summary || \"Starting...\"}</span>\n <span className=\"text-[10px] text-muted-foreground ml-auto\">{elapsed}s</span>\n </div>\n\n {/* Log output — scrolling messages like the PHP installer */}\n <div className=\"bg-[hsl(220,16%,6%)] border border-border rounded-lg p-4 h-[250px] overflow-y-auto font-mono text-xs\">\n {messages.map((msg, i) => (\n <div key={i} className={`py-0.5 ${msg.startsWith(\"✅\") ? \"text-emerald-400\" : \"text-muted-foreground\"}`}>\n <span className=\"text-zinc-600 mr-2\">[{String(Math.floor((elapsed - (messages.length - i) * 3) / 60)).padStart(2, \"0\")}:{String(Math.abs((elapsed - (messages.length - i) * 3) % 60)).padStart(2, \"0\")}]</span>\n {msg}\n </div>\n ))}\n <div className=\"py-0.5 text-teal-400 animate-pulse\">▌</div>\n </div>\n\n {/* Status checklist */}\n {statusData?.steps && (\n <div className=\"mt-4 space-y-1\">\n {statusData.steps.map((step: any, i: number) => (\n <div key={i} className=\"flex items-center gap-2 text-xs\">\n {step.done ? (\n <CheckCircle2 className=\"w-3.5 h-3.5 text-emerald-400\" />\n ) : (\n <div className=\"w-3.5 h-3.5 rounded-full border border-zinc-600\" />\n )}\n <span className={step.done ? \"text-foreground\" : \"text-muted-foreground\"}>{step.name}</span>\n {step.done && <span className=\"text-[9px] text-muted-foreground ml-auto\">{step.count}</span>}\n </div>\n ))}\n </div>\n )}\n </div>\n </div>\n );\n}\n\nfunction Confetti() {\n return (\n <div className=\"fixed inset-0 pointer-events-none z-50 overflow-hidden\" data-testid=\"confetti\">\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 <div\n key={i}\n className=\"absolute -top-2\"\n style={{\n left: `${left}%`,\n width: size,\n height: size * 1.5,\n backgroundColor: color,\n borderRadius: \"2px\",\n animation: `confetti-fall ${duration}s ease-in ${delay}s forwards`,\n transform: `rotate(${rotation}deg)`,\n opacity: 0,\n }}\n />\n );\n })}\n <style>{`\n @keyframes confetti-fall {\n 0% { opacity: 1; transform: translateY(0) rotate(0deg); }\n 100% { opacity: 0; transform: translateY(100vh) rotate(720deg); }\n }\n `}</style>\n </div>\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<WizardData>(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<HTMLDivElement>(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<WizardData>) => {\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<string, unknown> = {\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 <LaunchProgressScreen />;\n\n switch (currentStep) {\n case 0:\n return <StepWelcome data={wizardData} onChange={updateData} />;\n case 1:\n return <StepDatabase data={wizardData} onChange={updateData} />;\n case 2:\n return <StepAdmin data={wizardData} onChange={updateData} />;\n case 3:\n return <StepModels data={wizardData} onChange={updateData} />;\n case 4:\n return <StepChat data={wizardData} onChange={updateData} />;\n case 5:\n return <StepAgents data={wizardData} onChange={updateData} />;\n case 6:\n return <StepDomain data={wizardData} onChange={updateData} />;\n case 7:\n return (\n <StepReview\n data={wizardData}\n onGoToStep={goToStep}\n onLaunch={handleLaunch}\n isLaunching={isLaunching}\n />\n );\n default:\n return null;\n }\n };\n\n return (\n <div\n className=\"min-h-screen bg-background flex flex-col\"\n data-testid=\"page-setup-wizard\"\n >\n {showConfetti && <Confetti />}\n\n {/* Step progress bar — show full (step 7) when launching */}\n <StepProgressBar currentStep={showLaunchProgress ? STEPS.length - 1 : currentStep} />\n\n {/* Step content */}\n <div className=\"flex-1 flex items-start justify-center px-4 pb-32\" ref={contentRef}>\n <div\n className={`w-full max-w-2xl transition-all duration-300 ${\n isTransitioning\n ? direction === \"forward\"\n ? \"opacity-0 translate-x-4\"\n : \"opacity-0 -translate-x-4\"\n : \"opacity-100 translate-x-0\"\n }`}\n >\n <div className=\"rounded-xl border border-[hsl(217,14%,13%)] bg-[hsl(220,14%,9%)] p-6 sm:p-8\">\n {renderStep()}\n </div>\n </div>\n </div>\n\n {/* Bottom navigation bar — hidden during launch */}\n {currentStep < 7 && !showLaunchProgress && (\n <div className=\"fixed bottom-0 left-0 right-0 border-t border-[hsl(217,14%,13%)] bg-background z-40\">\n <div className=\"max-w-2xl mx-auto px-4 sm:px-8 py-4 flex items-center justify-between\">\n <div>\n {currentStep > 0 && (\n <Button\n variant=\"ghost\"\n onClick={prevStep}\n className=\"gap-2\"\n data-testid=\"button-back\"\n >\n <ArrowLeft className=\"w-4 h-4\" />\n Back\n </Button>\n )}\n </div>\n <div className=\"flex items-center gap-3\">\n {/* Skip button for optional steps */}\n {(currentStep === 4 || currentStep === 6) && (\n <Button\n variant=\"ghost\"\n onClick={nextStep}\n className=\"text-muted-foreground\"\n data-testid=\"button-skip\"\n >\n Skip for now\n </Button>\n )}\n <Button\n onClick={nextStep}\n disabled={!canContinue()}\n className=\"gap-2 min-w-[140px]\"\n data-testid=\"button-continue\"\n >\n {currentStep === 0 ? (\n <>\n Get Started\n <ArrowRight className=\"w-4 h-4\" />\n </>\n ) : (\n <>\n Continue\n <ArrowRight className=\"w-4 h-4\" />\n </>\n )}\n </Button>\n </div>\n </div>\n </div>\n )}\n </div>\n );\n}\n"}}}}