fix: replace embedded wizard with redirect to wizard port
- Frontend no longer makes cross-origin AJAX calls to wizard - checkBackend validates response is actual JSON (not serve SPA fallback) - When backend is down, show simple redirect page to wizard URL - After wizard config + service restart, frontend detects backend and shows login - Remove SetupWizardPage (embedded wizard), add SetupRedirectPage - Fix Object.entries null crash in DashboardPage
This commit is contained in:
24
src/App.tsx
24
src/App.tsx
@@ -3,7 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import Sidebar from '@/components/Sidebar'
|
||||
import LoginPage from '@/pages/LoginPage'
|
||||
import SetupWizardPage from '@/pages/SetupWizardPage'
|
||||
import SetupRedirectPage from '@/pages/SetupRedirectPage'
|
||||
import DashboardPage from '@/pages/DashboardPage'
|
||||
import IssuesPage from '@/pages/IssuesPage'
|
||||
import IssueDetailPage from '@/pages/IssueDetailPage'
|
||||
@@ -23,31 +23,35 @@ export default function App() {
|
||||
const [appState, setAppState] = useState<AppState>('checking')
|
||||
const { user, loading, login, logout } = useAuth()
|
||||
|
||||
useEffect(() => {
|
||||
checkBackend()
|
||||
}, [])
|
||||
|
||||
const checkBackend = async () => {
|
||||
try {
|
||||
await api.get('/health')
|
||||
setAppState('ready')
|
||||
const res = await api.get('/health')
|
||||
// Verify the response is actually from the backend (JSON with status field),
|
||||
// not serve's SPA fallback returning index.html as 200
|
||||
if (res.data && typeof res.data === 'object' && res.data.status === 'healthy') {
|
||||
setAppState('ready')
|
||||
} else {
|
||||
setAppState('setup')
|
||||
}
|
||||
} catch {
|
||||
setAppState('setup')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => { checkBackend() }, [])
|
||||
|
||||
// Checking backend availability
|
||||
if (appState === 'checking') {
|
||||
return <div className="loading">检查后端状态...</div>
|
||||
}
|
||||
|
||||
// Backend not ready — show setup wizard
|
||||
if (appState === 'setup') {
|
||||
return <SetupWizardPage wizardPort={WIZARD_PORT} onComplete={checkBackend} />
|
||||
return <SetupRedirectPage wizardPort={WIZARD_PORT} />
|
||||
}
|
||||
|
||||
// Backend ready but auth loading
|
||||
if (loading) return <div className="loading">加载中...</div>
|
||||
|
||||
// Not logged in
|
||||
if (!user) {
|
||||
return (
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -28,7 +28,7 @@ export default function DashboardPage() {
|
||||
<span className="stat-number">{stats.total_issues}</span>
|
||||
<span className="stat-label">总 Issues</span>
|
||||
</div>
|
||||
{Object.entries(stats.by_status).map(([k, v]) => (
|
||||
{Object.entries(stats.by_status || {}).map(([k, v]) => (
|
||||
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
||||
<span className="stat-number">{v}</span>
|
||||
<span className="stat-label">{k}</span>
|
||||
@@ -39,7 +39,7 @@ export default function DashboardPage() {
|
||||
<div className="section">
|
||||
<h3>按优先级</h3>
|
||||
<div className="bar-chart">
|
||||
{Object.entries(stats.by_priority).map(([k, v]) => (
|
||||
{Object.entries(stats.by_priority || {}).map(([k, v]) => (
|
||||
<div className="bar-row" key={k}>
|
||||
<span className="bar-label">{k}</span>
|
||||
<div className="bar" style={{
|
||||
|
||||
44
src/pages/SetupRedirectPage.tsx
Normal file
44
src/pages/SetupRedirectPage.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
interface Props {
|
||||
wizardPort: number
|
||||
}
|
||||
|
||||
export default function SetupRedirectPage({ wizardPort }: Props) {
|
||||
const wizardUrl = `http://127.0.0.1:${wizardPort}`
|
||||
|
||||
return (
|
||||
<div className="setup-wizard">
|
||||
<div className="setup-container">
|
||||
<div className="setup-header">
|
||||
<h1>⚓ HarborForge 初始化</h1>
|
||||
</div>
|
||||
|
||||
<div className="setup-step-content">
|
||||
<h2>后端尚未配置</h2>
|
||||
<p>请先通过 AbstractWizard 完成初始化配置。</p>
|
||||
|
||||
<div className="setup-info">
|
||||
<p>⚠️ 需要通过 SSH 隧道访问 Wizard:</p>
|
||||
<code>ssh -L {wizardPort}:127.0.0.1:{wizardPort} user@your-server</code>
|
||||
</div>
|
||||
|
||||
<a
|
||||
href={wizardUrl}
|
||||
className="btn-primary"
|
||||
style={{ display: 'inline-block', textDecoration: 'none', marginTop: '1rem' }}
|
||||
>
|
||||
打开 AbstractWizard →
|
||||
</a>
|
||||
|
||||
<div className="setup-info" style={{ marginTop: '2rem' }}>
|
||||
<h3>配置完成后:</h3>
|
||||
<ol style={{ textAlign: 'left', lineHeight: '2' }}>
|
||||
<li>在服务器上重启服务:<code>docker compose restart</code></li>
|
||||
<li>等待后端启动完成</li>
|
||||
<li>刷新本页面进入登录界面</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,222 +0,0 @@
|
||||
import { useState } from 'react'
|
||||
import axios from 'axios'
|
||||
|
||||
interface Props {
|
||||
wizardPort: number
|
||||
onComplete: () => void
|
||||
}
|
||||
|
||||
interface SetupForm {
|
||||
// Admin
|
||||
admin_username: string
|
||||
admin_password: string
|
||||
admin_email: string
|
||||
admin_full_name: string
|
||||
// Database
|
||||
db_host: string
|
||||
db_port: number
|
||||
db_user: string
|
||||
db_password: string
|
||||
db_database: string
|
||||
// Backend
|
||||
backend_url: string
|
||||
// Default project
|
||||
project_name: string
|
||||
project_description: string
|
||||
}
|
||||
|
||||
const STEPS = ['欢迎', '数据库', '管理员', '项目', '完成']
|
||||
|
||||
export default function SetupWizardPage({ wizardPort, 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_url: '',
|
||||
project_name: 'Default',
|
||||
project_description: '默认项目',
|
||||
})
|
||||
|
||||
const wizardApi = axios.create({
|
||||
baseURL: `http://127.0.0.1:${wizardPort}`,
|
||||
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(`无法连接 AbstractWizard (127.0.0.1:${wizardPort})。请确认已通过 SSH 隧道映射端口:\nssh -L ${wizardPort}:127.0.0.1:${wizardPort} user@server`)
|
||||
}
|
||||
}
|
||||
|
||||
const saveConfig = async () => {
|
||||
setError('')
|
||||
setSaving(true)
|
||||
try {
|
||||
const config = {
|
||||
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_url || undefined,
|
||||
default_project: form.project_name
|
||||
? { name: form.project_name, description: form.project_description }
|
||||
: undefined,
|
||||
}
|
||||
|
||||
await wizardApi.put('/api/v1/config/harborforge.json', config, {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
})
|
||||
|
||||
// Switch wizard to readonly after setup
|
||||
await wizardApi.put('/api/v1/mode', { mode: 'readonly' }).catch(() => {})
|
||||
|
||||
setStep(4)
|
||||
} catch (err: any) {
|
||||
setError(`保存配置失败: ${err.message}`)
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="setup-wizard">
|
||||
<div className="setup-container">
|
||||
<div className="setup-header">
|
||||
<h1>⚓ HarborForge 初始化向导</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>欢迎使用 HarborForge</h2>
|
||||
<p>Agent/人类协同任务管理平台</p>
|
||||
<div className="setup-info">
|
||||
<p>⚠️ 初始化向导通过 SSH 隧道连接 AbstractWizard,请确保已映射端口:</p>
|
||||
<code>ssh -L {wizardPort}:127.0.0.1:{wizardPort} user@your-server</code>
|
||||
</div>
|
||||
<button className="btn-primary" onClick={checkWizard}>
|
||||
连接 Wizard 开始配置
|
||||
</button>
|
||||
{wizardOk === false && (
|
||||
<p className="setup-hint">连接失败,请检查 SSH 隧道是否正确建立。</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 1: Database */}
|
||||
{step === 1 && (
|
||||
<div className="setup-step-content">
|
||||
<h2>数据库配置</h2>
|
||||
<p className="text-dim">配置 MySQL 连接信息(使用 docker-compose 内置 MySQL 可保持默认值)</p>
|
||||
<div className="setup-form">
|
||||
<label>主机 <input value={form.db_host} onChange={(e) => set('db_host', e.target.value)} /></label>
|
||||
<label>端口 <input type="number" value={form.db_port} onChange={(e) => set('db_port', Number(e.target.value))} /></label>
|
||||
<label>用户名 <input value={form.db_user} onChange={(e) => set('db_user', e.target.value)} /></label>
|
||||
<label>密码 <input type="password" value={form.db_password} onChange={(e) => set('db_password', e.target.value)} /></label>
|
||||
<label>数据库名 <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)}>上一步</button>
|
||||
<button className="btn-primary" onClick={() => setStep(2)}>下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 2: Admin */}
|
||||
{step === 2 && (
|
||||
<div className="setup-step-content">
|
||||
<h2>管理员账号</h2>
|
||||
<p className="text-dim">创建首个管理员用户</p>
|
||||
<div className="setup-form">
|
||||
<label>用户名 <input value={form.admin_username} onChange={(e) => set('admin_username', e.target.value)} required /></label>
|
||||
<label>密码 <input type="password" value={form.admin_password} onChange={(e) => set('admin_password', e.target.value)} required placeholder="设置管理员密码" /></label>
|
||||
<label>邮箱 <input type="email" value={form.admin_email} onChange={(e) => set('admin_email', e.target.value)} placeholder="admin@example.com" /></label>
|
||||
<label>姓名 <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)}>上一步</button>
|
||||
<button className="btn-primary" onClick={() => {
|
||||
if (!form.admin_password) { setError('请设置管理员密码'); return }
|
||||
setError('')
|
||||
setStep(3)
|
||||
}}>下一步</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 3: Project */}
|
||||
{step === 3 && (
|
||||
<div className="setup-step-content">
|
||||
<h2>默认项目(可选)</h2>
|
||||
<p className="text-dim">创建一个初始项目,也可以跳过</p>
|
||||
<div className="setup-form">
|
||||
<label>项目名称 <input value={form.project_name} onChange={(e) => set('project_name', e.target.value)} placeholder="留空则跳过" /></label>
|
||||
<label>项目描述 <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
|
||||
</div>
|
||||
<div className="setup-nav">
|
||||
<button className="btn-back" onClick={() => setStep(2)}>上一步</button>
|
||||
<button className="btn-primary" onClick={saveConfig} disabled={saving}>
|
||||
{saving ? '保存中...' : '完成配置'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Step 4: Done */}
|
||||
{step === 4 && (
|
||||
<div className="setup-step-content">
|
||||
<div className="setup-done">
|
||||
<h2>✅ 初始化完成!</h2>
|
||||
<p>配置已保存到 AbstractWizard,Wizard 已切换为只读模式。</p>
|
||||
<p>后端将在检测到配置后自动启动。</p>
|
||||
<div className="setup-info">
|
||||
<p>请等待后端启动完成,然后刷新页面进入登录界面。</p>
|
||||
<p>管理员账号: <strong>{form.admin_username}</strong></p>
|
||||
</div>
|
||||
<button className="btn-primary" onClick={onComplete}>
|
||||
刷新检查
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user