diff --git a/Dockerfile b/Dockerfile index 8407916..55964ba 100644 --- a/Dockerfile +++ b/Dockerfile @@ -5,7 +5,9 @@ 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 # Production stage — lightweight static server, no nginx diff --git a/src/App.tsx b/src/App.tsx index 473aaca..f3674ed 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,9 @@ +import { useState, useEffect } from 'react' 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 DashboardPage from '@/pages/DashboardPage' import IssuesPage from '@/pages/IssuesPage' import IssueDetailPage from '@/pages/IssueDetailPage' @@ -11,12 +13,41 @@ 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' + +const WIZARD_PORT = Number(import.meta.env.VITE_WIZARD_PORT) || 18080 + +type AppState = 'checking' | 'setup' | 'ready' export default function App() { + const [appState, setAppState] = useState('checking') const { user, loading, login, logout } = useAuth() + const checkBackend = async () => { + try { + await api.get('/health') + setAppState('ready') + } catch { + setAppState('setup') + } + } + + useEffect(() => { checkBackend() }, []) + + // Checking backend availability + if (appState === 'checking') { + return
检查后端状态...
+ } + + // Backend not ready — show setup wizard + if (appState === 'setup') { + return + } + + // Backend ready but auth loading if (loading) return
加载中...
+ // Not logged in if (!user) { return ( diff --git a/src/index.css b/src/index.css index bf60b36..adc51c4 100644 --- a/src/index.css +++ b/src/index.css @@ -160,3 +160,26 @@ dd { font-size: .9rem; } /* Text dim helper */ .text-dim { color: var(--text-dim); font-size: .85rem; } + +/* Setup Wizard */ +.setup-wizard { display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; } +.setup-container { background: var(--bg-card); border: 1px solid var(--border); border-radius: 16px; padding: 40px; max-width: 600px; width: 100%; } +.setup-header { text-align: center; margin-bottom: 32px; } +.setup-header h1 { font-size: 1.5rem; margin-bottom: 20px; } +.setup-steps { display: flex; justify-content: center; gap: 8px; flex-wrap: wrap; } +.setup-step { font-size: .8rem; color: var(--text-dim); padding: 4px 10px; border-radius: 12px; border: 1px solid var(--border); } +.setup-step.active { color: var(--accent); border-color: var(--accent); background: rgba(59,130,246,.1); } +.setup-step.done { color: var(--success); border-color: var(--success); } +.setup-step-content { animation: fadeIn .2s ease; } +.setup-step-content h2 { margin-bottom: 8px; font-size: 1.2rem; } +.setup-form { display: flex; flex-direction: column; gap: 12px; margin: 20px 0; } +.setup-form label { display: flex; flex-direction: column; gap: 4px; font-size: .85rem; color: var(--text-dim); } +.setup-form input { padding: 10px 12px; border: 1px solid var(--border); border-radius: 6px; background: var(--bg); color: var(--text); font-size: .95rem; } +.setup-nav { display: flex; justify-content: space-between; margin-top: 24px; } +.setup-error { background: rgba(239,68,68,.1); border: 1px solid var(--danger); color: var(--danger); padding: 12px 16px; border-radius: 8px; margin-bottom: 16px; font-size: .9rem; white-space: pre-line; } +.setup-info { background: rgba(59,130,246,.08); border: 1px solid rgba(59,130,246,.2); padding: 16px; border-radius: 8px; margin: 16px 0; } +.setup-info code { display: block; background: var(--bg); padding: 8px 12px; border-radius: 4px; margin-top: 8px; font-size: .85rem; color: var(--accent); word-break: break-all; } +.setup-hint { color: var(--warning); font-size: .85rem; margin-top: 8px; } +.setup-done { text-align: center; } +.setup-done h2 { color: var(--success); margin-bottom: 12px; } +@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: none; } } diff --git a/src/pages/SetupWizardPage.tsx b/src/pages/SetupWizardPage.tsx new file mode 100644 index 0000000..bffd16c --- /dev/null +++ b/src/pages/SetupWizardPage.tsx @@ -0,0 +1,222 @@ +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(null) + const [form, setForm] = useState({ + 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 ( +
+
+
+

⚓ HarborForge 初始化向导

+
+ {STEPS.map((s, i) => ( + + {i < step ? '✓' : i + 1}. {s} + + ))} +
+
+ + {error &&
{error}
} + + {/* Step 0: Welcome */} + {step === 0 && ( +
+

欢迎使用 HarborForge

+

Agent/人类协同任务管理平台

+
+

⚠️ 初始化向导通过 SSH 隧道连接 AbstractWizard,请确保已映射端口:

+ ssh -L {wizardPort}:127.0.0.1:{wizardPort} user@your-server +
+ + {wizardOk === false && ( +

连接失败,请检查 SSH 隧道是否正确建立。

+ )} +
+ )} + + {/* Step 1: Database */} + {step === 1 && ( +
+

数据库配置

+

配置 MySQL 连接信息(使用 docker-compose 内置 MySQL 可保持默认值)

+
+ + + + + +
+
+ + +
+
+ )} + + {/* Step 2: Admin */} + {step === 2 && ( +
+

管理员账号

+

创建首个管理员用户

+
+ + + + +
+
+ + +
+
+ )} + + {/* Step 3: Project */} + {step === 3 && ( +
+

默认项目(可选)

+

创建一个初始项目,也可以跳过

+
+ + +
+
+ + +
+
+ )} + + {/* Step 4: Done */} + {step === 4 && ( +
+
+

✅ 初始化完成!

+

配置已保存到 AbstractWizard,Wizard 已切换为只读模式。

+

后端将在检测到配置后自动启动。

+
+

请等待后端启动完成,然后刷新页面进入登录界面。

+

管理员账号: {form.admin_username}

+
+ +
+
+ )} +
+
+ ) +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..dd6f269 --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE: string + readonly VITE_WIZARD_PORT: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +}