fix: check wizard config for initialized flag instead of backend health
- App checks wizard API for harborforge.json config with initialized=true - If not initialized, show embedded setup wizard (talks to wizard API via CORS) - Setup saves config with initialized:true to wizard config volume - After restart, backend reads config and starts, frontend sees initialized=true - Remove VITE_API_BASE build arg (not needed, api.ts uses /api relative path) - Fix Object.entries null crash in DashboardPage
This commit is contained in:
@@ -4,9 +4,7 @@ WORKDIR /app
|
|||||||
COPY package.json package-lock.json* ./
|
COPY package.json package-lock.json* ./
|
||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
ARG VITE_API_BASE=/api
|
|
||||||
ARG VITE_WIZARD_PORT=18080
|
ARG VITE_WIZARD_PORT=18080
|
||||||
ENV VITE_API_BASE=$VITE_API_BASE
|
|
||||||
ENV VITE_WIZARD_PORT=$VITE_WIZARD_PORT
|
ENV VITE_WIZARD_PORT=$VITE_WIZARD_PORT
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
|
|||||||
30
src/App.tsx
30
src/App.tsx
@@ -13,9 +13,10 @@ import ProjectDetailPage from '@/pages/ProjectDetailPage'
|
|||||||
import MilestonesPage from '@/pages/MilestonesPage'
|
import MilestonesPage from '@/pages/MilestonesPage'
|
||||||
import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
|
import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
|
||||||
import NotificationsPage from '@/pages/NotificationsPage'
|
import NotificationsPage from '@/pages/NotificationsPage'
|
||||||
import api from '@/services/api'
|
import axios from 'axios'
|
||||||
|
|
||||||
const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080
|
const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080
|
||||||
|
const WIZARD_BASE = `http://127.0.0.1:${WIZARD_PORT}`
|
||||||
|
|
||||||
type AppState = 'checking' | 'setup' | 'ready'
|
type AppState = 'checking' | 'setup' | 'ready'
|
||||||
|
|
||||||
@@ -23,31 +24,36 @@ export default function App() {
|
|||||||
const [appState, setAppState] = useState<AppState>('checking')
|
const [appState, setAppState] = useState<AppState>('checking')
|
||||||
const { user, loading, login, logout } = useAuth()
|
const { user, loading, login, logout } = useAuth()
|
||||||
|
|
||||||
const checkBackend = async () => {
|
useEffect(() => {
|
||||||
|
checkInitialized()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const checkInitialized = async () => {
|
||||||
try {
|
try {
|
||||||
await api.get('/health')
|
const res = await axios.get(`${WIZARD_BASE}/api/v1/config/harborforge.json`, {
|
||||||
setAppState('ready')
|
timeout: 5000,
|
||||||
|
})
|
||||||
|
if (res.data && res.data.initialized === true) {
|
||||||
|
setAppState('ready')
|
||||||
|
} else {
|
||||||
|
setAppState('setup')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
// Wizard unreachable or config doesn't exist → setup needed
|
||||||
setAppState('setup')
|
setAppState('setup')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => { checkBackend() }, [])
|
|
||||||
|
|
||||||
// Checking backend availability
|
|
||||||
if (appState === 'checking') {
|
if (appState === 'checking') {
|
||||||
return <div className="loading">检查后端状态...</div>
|
return <div className="loading">检查配置状态...</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend not ready — show setup wizard
|
|
||||||
if (appState === 'setup') {
|
if (appState === 'setup') {
|
||||||
return <SetupWizardPage wizardPort={WIZARD_PORT} onComplete={checkBackend} />
|
return <SetupWizardPage wizardBase={WIZARD_BASE} onComplete={checkInitialized} />
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend ready but auth loading
|
|
||||||
if (loading) return <div className="loading">加载中...</div>
|
if (loading) return <div className="loading">加载中...</div>
|
||||||
|
|
||||||
// Not logged in
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return (
|
return (
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function DashboardPage() {
|
|||||||
<span className="stat-number">{stats.total_issues}</span>
|
<span className="stat-number">{stats.total_issues}</span>
|
||||||
<span className="stat-label">总 Issues</span>
|
<span className="stat-label">总 Issues</span>
|
||||||
</div>
|
</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' }}>
|
<div className="stat-card" key={k} style={{ borderLeftColor: statusColors[k] || '#ccc' }}>
|
||||||
<span className="stat-number">{v}</span>
|
<span className="stat-number">{v}</span>
|
||||||
<span className="stat-label">{k}</span>
|
<span className="stat-label">{k}</span>
|
||||||
@@ -39,7 +39,7 @@ export default function DashboardPage() {
|
|||||||
<div className="section">
|
<div className="section">
|
||||||
<h3>按优先级</h3>
|
<h3>按优先级</h3>
|
||||||
<div className="bar-chart">
|
<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}>
|
<div className="bar-row" key={k}>
|
||||||
<span className="bar-label">{k}</span>
|
<span className="bar-label">{k}</span>
|
||||||
<div className="bar" style={{
|
<div className="bar" style={{
|
||||||
|
|||||||
@@ -2,32 +2,27 @@ import { useState } from 'react'
|
|||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
wizardPort: number
|
wizardBase: string
|
||||||
onComplete: () => void
|
onComplete: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SetupForm {
|
interface SetupForm {
|
||||||
// Admin
|
|
||||||
admin_username: string
|
admin_username: string
|
||||||
admin_password: string
|
admin_password: string
|
||||||
admin_email: string
|
admin_email: string
|
||||||
admin_full_name: string
|
admin_full_name: string
|
||||||
// Database
|
|
||||||
db_host: string
|
db_host: string
|
||||||
db_port: number
|
db_port: number
|
||||||
db_user: string
|
db_user: string
|
||||||
db_password: string
|
db_password: string
|
||||||
db_database: string
|
db_database: string
|
||||||
// Backend
|
|
||||||
backend_url: string
|
|
||||||
// Default project
|
|
||||||
project_name: string
|
project_name: string
|
||||||
project_description: string
|
project_description: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const STEPS = ['欢迎', '数据库', '管理员', '项目', '完成']
|
const STEPS = ['欢迎', '数据库', '管理员', '项目', '完成']
|
||||||
|
|
||||||
export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
|
||||||
const [step, setStep] = useState(0)
|
const [step, setStep] = useState(0)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
const [saving, setSaving] = useState(false)
|
const [saving, setSaving] = useState(false)
|
||||||
@@ -42,13 +37,12 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
db_user: 'harborforge',
|
db_user: 'harborforge',
|
||||||
db_password: 'harborforge_pass',
|
db_password: 'harborforge_pass',
|
||||||
db_database: 'harborforge',
|
db_database: 'harborforge',
|
||||||
backend_url: '',
|
|
||||||
project_name: 'Default',
|
project_name: 'Default',
|
||||||
project_description: '默认项目',
|
project_description: '默认项目',
|
||||||
})
|
})
|
||||||
|
|
||||||
const wizardApi = axios.create({
|
const wizardApi = axios.create({
|
||||||
baseURL: `http://127.0.0.1:${wizardPort}`,
|
baseURL: wizardBase,
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -63,7 +57,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
setStep(1)
|
setStep(1)
|
||||||
} catch {
|
} catch {
|
||||||
setWizardOk(false)
|
setWizardOk(false)
|
||||||
setError(`无法连接 AbstractWizard (127.0.0.1:${wizardPort})。请确认已通过 SSH 隧道映射端口:\nssh -L ${wizardPort}:127.0.0.1:${wizardPort} user@server`)
|
setError(`无法连接 AbstractWizard (${wizardBase})。\n请确认已通过 SSH 隧道映射端口:\nssh -L <wizard_port>:127.0.0.1:<wizard_port> user@server`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,6 +66,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
setSaving(true)
|
setSaving(true)
|
||||||
try {
|
try {
|
||||||
const config = {
|
const config = {
|
||||||
|
initialized: true,
|
||||||
admin: {
|
admin: {
|
||||||
username: form.admin_username,
|
username: form.admin_username,
|
||||||
password: form.admin_password,
|
password: form.admin_password,
|
||||||
@@ -85,7 +80,6 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
password: form.db_password,
|
password: form.db_password,
|
||||||
database: form.db_database,
|
database: form.db_database,
|
||||||
},
|
},
|
||||||
backend_url: form.backend_url || undefined,
|
|
||||||
default_project: form.project_name
|
default_project: form.project_name
|
||||||
? { name: form.project_name, description: form.project_description }
|
? { name: form.project_name, description: form.project_description }
|
||||||
: undefined,
|
: undefined,
|
||||||
@@ -95,9 +89,6 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Switch wizard to readonly after setup
|
|
||||||
await wizardApi.put('/api/v1/mode', { mode: 'readonly' }).catch(() => {})
|
|
||||||
|
|
||||||
setStep(4)
|
setStep(4)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
setError(`保存配置失败: ${err.message}`)
|
setError(`保存配置失败: ${err.message}`)
|
||||||
@@ -129,7 +120,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
<p>Agent/人类协同任务管理平台</p>
|
<p>Agent/人类协同任务管理平台</p>
|
||||||
<div className="setup-info">
|
<div className="setup-info">
|
||||||
<p>⚠️ 初始化向导通过 SSH 隧道连接 AbstractWizard,请确保已映射端口:</p>
|
<p>⚠️ 初始化向导通过 SSH 隧道连接 AbstractWizard,请确保已映射端口:</p>
|
||||||
<code>ssh -L {wizardPort}:127.0.0.1:{wizardPort} user@your-server</code>
|
<code>ssh -L <wizard_port>:127.0.0.1:<wizard_port> user@your-server</code>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-primary" onClick={checkWizard}>
|
<button className="btn-primary" onClick={checkWizard}>
|
||||||
连接 Wizard 开始配置
|
连接 Wizard 开始配置
|
||||||
@@ -204,10 +195,11 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
<div className="setup-step-content">
|
<div className="setup-step-content">
|
||||||
<div className="setup-done">
|
<div className="setup-done">
|
||||||
<h2>✅ 初始化完成!</h2>
|
<h2>✅ 初始化完成!</h2>
|
||||||
<p>配置已保存到 AbstractWizard,Wizard 已切换为只读模式。</p>
|
<p>配置已保存到 AbstractWizard。</p>
|
||||||
<p>后端将在检测到配置后自动启动。</p>
|
|
||||||
<div className="setup-info">
|
<div className="setup-info">
|
||||||
<p>请等待后端启动完成,然后刷新页面进入登录界面。</p>
|
<p>请在服务器上重启服务:</p>
|
||||||
|
<code>docker compose restart</code>
|
||||||
|
<p style={{ marginTop: '1rem' }}>后端启动完成后,刷新本页面进入登录界面。</p>
|
||||||
<p>管理员账号: <strong>{form.admin_username}</strong></p>
|
<p>管理员账号: <strong>{form.admin_username}</strong></p>
|
||||||
</div>
|
</div>
|
||||||
<button className="btn-primary" onClick={onComplete}>
|
<button className="btn-primary" onClick={onComplete}>
|
||||||
|
|||||||
Reference in New Issue
Block a user