Merge pull request 'fix/wizard-init-flow' (#1) from fix/wizard-init-flow into main
Reviewed-on: #1
This commit was merged in pull request #1.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
34
src/App.tsx
34
src/App.tsx
@@ -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,40 @@ 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,
|
||||
})
|
||||
const cfg = res.data || {}
|
||||
if (cfg.backend_url) {
|
||||
localStorage.setItem('HF_BACKEND_BASE_URL', cfg.backend_url)
|
||||
}
|
||||
if (cfg.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>
|
||||
|
||||
@@ -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={{
|
||||
@@ -58,7 +58,7 @@ export default function DashboardPage() {
|
||||
<tr><th>ID</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{stats.recent_issues.map((i) => (
|
||||
{(stats.recent_issues || []).map((i) => (
|
||||
<tr key={i.id}>
|
||||
<td>#{i.id}</td>
|
||||
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
|
||||
|
||||
@@ -2,32 +2,28 @@ 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
|
||||
backend_base_url: string
|
||||
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 +38,13 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
||||
db_user: 'harborforge',
|
||||
db_password: 'harborforge_pass',
|
||||
db_database: 'harborforge',
|
||||
backend_url: '',
|
||||
backend_base_url: 'http://127.0.0.1:8000',
|
||||
project_name: 'Default',
|
||||
project_description: '默认项目',
|
||||
})
|
||||
|
||||
const wizardApi = axios.create({
|
||||
baseURL: `http://127.0.0.1:${wizardPort}`,
|
||||
baseURL: wizardBase,
|
||||
timeout: 5000,
|
||||
})
|
||||
|
||||
@@ -63,7 +59,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 +68,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 +82,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
||||
password: form.db_password,
|
||||
database: form.db_database,
|
||||
},
|
||||
backend_url: form.backend_url || undefined,
|
||||
backend_url: form.backend_base_url || undefined,
|
||||
default_project: form.project_name
|
||||
? { name: form.project_name, description: form.project_description }
|
||||
: undefined,
|
||||
@@ -95,8 +92,9 @@ 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(() => {})
|
||||
if (form.backend_base_url) {
|
||||
localStorage.setItem('HF_BACKEND_BASE_URL', form.backend_base_url)
|
||||
}
|
||||
|
||||
setStep(4)
|
||||
} catch (err: any) {
|
||||
@@ -129,7 +127,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 <wizard_port>:127.0.0.1:<wizard_port> user@your-server</code>
|
||||
</div>
|
||||
<button className="btn-primary" onClick={checkWizard}>
|
||||
连接 Wizard 开始配置
|
||||
@@ -187,6 +185,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
||||
<h2>默认项目(可选)</h2>
|
||||
<p className="text-dim">创建一个初始项目,也可以跳过</p>
|
||||
<div className="setup-form">
|
||||
<label>后端 Base URL <input value={form.backend_base_url} onChange={(e) => set('backend_base_url', e.target.value)} placeholder="http://127.0.0.1:8000" /></label>
|
||||
<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>
|
||||
@@ -204,10 +203,11 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
||||
<div className="setup-step-content">
|
||||
<div className="setup-done">
|
||||
<h2>✅ 初始化完成!</h2>
|
||||
<p>配置已保存到 AbstractWizard,Wizard 已切换为只读模式。</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}>
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
import axios from 'axios'
|
||||
|
||||
const getApiBase = () => {
|
||||
return localStorage.getItem('HF_BACKEND_BASE_URL') || import.meta.env.VITE_API_BASE || 'http://127.0.0.1:8000'
|
||||
}
|
||||
|
||||
const api = axios.create({
|
||||
baseURL: import.meta.env.VITE_API_BASE || '/api',
|
||||
baseURL: getApiBase(),
|
||||
})
|
||||
|
||||
api.interceptors.request.use((config) => {
|
||||
config.baseURL = getApiBase()
|
||||
const token = localStorage.getItem('token')
|
||||
if (token) {
|
||||
config.headers.Authorization = `Bearer ${token}`
|
||||
|
||||
Reference in New Issue
Block a user