218 lines
8.0 KiB
TypeScript
218 lines
8.0 KiB
TypeScript
import { useState } from 'react'
|
||
import axios from 'axios'
|
||
|
||
interface Props {
|
||
wizardBase: string
|
||
onComplete: () => void
|
||
}
|
||
|
||
interface SetupForm {
|
||
admin_username: string
|
||
admin_password: string
|
||
admin_email: string
|
||
admin_full_name: string
|
||
db_host: string
|
||
db_port: number
|
||
db_user: string
|
||
db_password: string
|
||
db_database: string
|
||
backend_base_url: string
|
||
project_name: string
|
||
project_description: string
|
||
}
|
||
|
||
const STEPS = ['Welcome', 'Database', 'Admin', 'Backend', 'Finish']
|
||
|
||
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
||
const [step, setStep] = useState(0)
|
||
const [error, setError] = useState('')
|
||
const [saving, setSaving] = useState(false)
|
||
const [wizardOk, setWizardOk] = useState<boolean | null>(null)
|
||
const [form, setForm] = useState<SetupForm>({
|
||
admin_username: 'admin',
|
||
admin_password: '',
|
||
admin_email: '',
|
||
admin_full_name: 'Admin',
|
||
db_host: 'mysql',
|
||
db_port: 3306,
|
||
db_user: 'harborforge',
|
||
db_password: 'harborforge_pass',
|
||
db_database: 'harborforge',
|
||
backend_base_url: 'http://backend:8000',
|
||
project_name: '',
|
||
project_description: '',
|
||
})
|
||
|
||
const wizardApi = axios.create({
|
||
baseURL: wizardBase,
|
||
timeout: 5000,
|
||
})
|
||
|
||
const set = (key: keyof SetupForm, value: string | number) =>
|
||
setForm((f) => ({ ...f, [key]: value }))
|
||
|
||
const checkWizard = async () => {
|
||
setError('')
|
||
try {
|
||
await wizardApi.get('/health')
|
||
setWizardOk(true)
|
||
setStep(1)
|
||
} catch {
|
||
setWizardOk(false)
|
||
setError(`Unable to connect to AbstractWizard (${wizardBase}).\nPlease ensure the SSH tunnel is configured:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
|
||
}
|
||
}
|
||
|
||
const saveConfig = async () => {
|
||
setError('')
|
||
setSaving(true)
|
||
try {
|
||
const config = {
|
||
initialized: true,
|
||
admin: {
|
||
username: form.admin_username,
|
||
password: form.admin_password,
|
||
email: form.admin_email,
|
||
full_name: form.admin_full_name,
|
||
},
|
||
database: {
|
||
host: form.db_host,
|
||
port: form.db_port,
|
||
user: form.db_user,
|
||
password: form.db_password,
|
||
database: form.db_database,
|
||
},
|
||
backend_url: form.backend_base_url || undefined,
|
||
}
|
||
|
||
await wizardApi.put('/api/v1/config/harborforge.json', config, {
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
|
||
if (form.backend_base_url) {
|
||
localStorage.setItem('HF_BACKEND_BASE_URL', form.backend_base_url)
|
||
}
|
||
|
||
setStep(4)
|
||
} catch (err: any) {
|
||
setError(`Failed to save configuration: ${err.message}`)
|
||
} finally {
|
||
setSaving(false)
|
||
}
|
||
}
|
||
|
||
return (
|
||
<div className="setup-wizard">
|
||
<div className="setup-container">
|
||
<div className="setup-header">
|
||
<h1>⚓ HarborForge Setup Wizard</h1>
|
||
<div className="setup-steps">
|
||
{STEPS.map((s, i) => (
|
||
<span key={i} className={`setup-step ${i === step ? 'active' : i < step ? 'done' : ''}`}>
|
||
{i < step ? '✓' : i + 1}. {s}
|
||
</span>
|
||
))}
|
||
</div>
|
||
</div>
|
||
|
||
{error && <div className="setup-error">{error}</div>}
|
||
|
||
{/* Step 0: Welcome */}
|
||
{step === 0 && (
|
||
<div className="setup-step-content">
|
||
<h2>Welcome to HarborForge</h2>
|
||
<p>Agent/Human collaborative task management platform</p>
|
||
<div className="setup-info">
|
||
<p>⚠️ The setup wizard connects to AbstractWizard via SSH tunnel. Ensure the port is forwarded:</p>
|
||
<code>ssh -L <wizard_port>:127.0.0.1:<wizard_port> user@your-server</code>
|
||
</div>
|
||
<button className="btn-primary" onClick={checkWizard}>
|
||
Connect to Wizard
|
||
</button>
|
||
{wizardOk === false && (
|
||
<p className="setup-hint">Connection failed. Check the SSH tunnel.</p>
|
||
)}
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 1: Database */}
|
||
{step === 1 && (
|
||
<div className="setup-step-content">
|
||
<h2>Database configuration</h2>
|
||
<p className="text-dim">Configure MySQL connection (docker-compose defaults are fine if using the bundled MySQL).</p>
|
||
<div className="setup-form">
|
||
<label>Host <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
|
||
<label>Port <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
|
||
<label>Username <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
|
||
<label>Password <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
|
||
<label>Database <input value={form.db_database} onChange={(e) => set('db_database', e.target.value)} /></label>
|
||
</div>
|
||
<div className="setup-nav">
|
||
<button className="btn-back" onClick={() => setStep(0)}>Back</button>
|
||
<button className="btn-primary" onClick={() => setStep(2)}>Next</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 2: Admin */}
|
||
{step === 2 && (
|
||
<div className="setup-step-content">
|
||
<h2>Admin account</h2>
|
||
<p className="text-dim">Create the first admin user</p>
|
||
<div className="setup-form">
|
||
<label>Username <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
|
||
<label>Password <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="Set admin password" /></label>
|
||
<label>Email <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
|
||
<label>Full name <input value={form.admin_full_name} onChange={(e) => set('admin_full_name', e.target.value)} /></label>
|
||
</div>
|
||
<div className="setup-nav">
|
||
<button className="btn-back" onClick={() => setStep(1)}>Back</button>
|
||
<button className="btn-primary" onClick={() => {
|
||
if (!form.admin_password) { setError('Please set an admin password'); return }
|
||
setError('')
|
||
setStep(3)
|
||
}}>Next</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 3: Backend */}
|
||
{step === 3 && (
|
||
<div className="setup-step-content">
|
||
<h2>Backend URL</h2>
|
||
<p className="text-dim">Configure the HarborForge backend API URL</p>
|
||
<div className="setup-form">
|
||
<label>Backend Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://backend:8000" /></label>
|
||
</div>
|
||
<div className="setup-nav">
|
||
<button className="btn-back" onClick={() => setStep(2)}>Back</button>
|
||
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
|
||
{saving ? 'Saving...' : 'Finish setup'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Step 4: Done */}
|
||
{step === 4 && (
|
||
<div className="setup-step-content">
|
||
<div className="setup-done">
|
||
<h2>✅ Setup complete!</h2>
|
||
<p>Configuration saved to AbstractWizard.</p>
|
||
<div className="setup-info">
|
||
<p>Restart services on the server:</p>
|
||
<code>docker compose restart</code>
|
||
<p style={{ marginTop: '1rem' }}>After the backend starts, refresh this page to go to login.</p>
|
||
<p>Admin account: <strong>{form.admin_username}</strong></p>
|
||
</div>
|
||
<button className="btn-primary" onClick={onComplete}>
|
||
Refresh to check
|
||
</button>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|