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:
zhi
2026-03-11 10:09:33 +00:00
parent f8fac48fcc
commit c92e399218
4 changed files with 30 additions and 34 deletions

View File

@@ -4,9 +4,7 @@ WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm install
COPY . .
ARG VITE_API_BASE=/api
ARG VITE_WIZARD_PORT=18080
ENV VITE_API_BASE=$VITE_API_BASE
ENV VITE_WIZARD_PORT=$VITE_WIZARD_PORT
RUN npm run build

View File

@@ -13,9 +13,10 @@ import ProjectDetailPage from '@/pages/ProjectDetailPage'
import MilestonesPage from '@/pages/MilestonesPage'
import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
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_BASE = `http://127.0.0.1:${WIZARD_PORT}`
type AppState = 'checking' | 'setup' | 'ready'
@@ -23,31 +24,36 @@ export default function App() {
const [appState, setAppState] = useState<AppState>('checking')
const { user, loading, login, logout } = useAuth()
const checkBackend = async () => {
useEffect(() => {
checkInitialized()
}, [])
const checkInitialized = async () => {
try {
await api.get('/health')
setAppState('ready')
const res = await axios.get(`${WIZARD_BASE}/api/v1/config/harborforge.json`, {
timeout: 5000,
})
if (res.data && res.data.initialized === true) {
setAppState('ready')
} else {
setAppState('setup')
}
} catch {
// Wizard unreachable or config doesn't exist → setup needed
setAppState('setup')
}
}
useEffect(() => { checkBackend() }, [])
// Checking backend availability
if (appState === 'checking') {
return <div className="loading">...</div>
return <div className="loading">...</div>
}
// Backend not ready — show setup wizard
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>
// Not logged in
if (!user) {
return (
<BrowserRouter>

View File

@@ -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={{

View File

@@ -2,32 +2,27 @@ import { useState } from 'react'
import axios from 'axios'
interface Props {
wizardPort: number
wizardBase: string
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) {
export default function SetupWizardPage({ wizardBase, onComplete }: Props) {
const [step, setStep] = useState(0)
const [error, setError] = useState('')
const [saving, setSaving] = useState(false)
@@ -42,13 +37,12 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
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}`,
baseURL: wizardBase,
timeout: 5000,
})
@@ -63,7 +57,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
setStep(1)
} catch {
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)
try {
const config = {
initialized: true,
admin: {
username: form.admin_username,
password: form.admin_password,
@@ -85,7 +80,6 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
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,
@@ -95,9 +89,6 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
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}`)
@@ -129,7 +120,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
<p>Agent/</p>
<div className="setup-info">
<p> SSH AbstractWizard</p>
<code>ssh -L {wizardPort}:127.0.0.1:{wizardPort} user@your-server</code>
<code>ssh -L &lt;wizard_port&gt;:127.0.0.1:&lt;wizard_port&gt; user@your-server</code>
</div>
<button className="btn-primary" onClick={checkWizard}>
Wizard
@@ -204,10 +195,11 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
<div className="setup-step-content">
<div className="setup-done">
<h2> </h2>
<p> AbstractWizardWizard </p>
<p></p>
<p> AbstractWizard</p>
<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>
</div>
<button className="btn-primary" onClick={onComplete}>