Compare commits
5 Commits
f8fac48fcc
...
d3562582b4
| Author | SHA1 | Date | |
|---|---|---|---|
| d3562582b4 | |||
| dbb7d9013a | |||
| bd4a206e76 | |||
| d3ca13108b | |||
| c92e399218 |
@@ -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
|
||||||
|
|
||||||
|
|||||||
34
src/App.tsx
34
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,40 @@ 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,
|
||||||
|
})
|
||||||
|
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 {
|
} 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={{
|
||||||
@@ -58,7 +58,7 @@ export default function DashboardPage() {
|
|||||||
<tr><th>ID</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th></tr>
|
<tr><th>ID</th><th>标题</th><th>状态</th><th>优先级</th><th>类型</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{stats.recent_issues.map((i) => (
|
{(stats.recent_issues || []).map((i) => (
|
||||||
<tr key={i.id}>
|
<tr key={i.id}>
|
||||||
<td>#{i.id}</td>
|
<td>#{i.id}</td>
|
||||||
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
|
<td><a href={`/issues/${i.id}`}>{i.title}</a></td>
|
||||||
|
|||||||
@@ -2,32 +2,28 @@ 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_base_url: string
|
||||||
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 +38,13 @@ 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: '',
|
backend_base_url: 'http://127.0.0.1:8000',
|
||||||
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 +59,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 +68,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 +82,7 @@ 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,
|
backend_url: form.backend_base_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,8 +92,9 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
})
|
})
|
||||||
|
|
||||||
// Switch wizard to readonly after setup
|
if (form.backend_base_url) {
|
||||||
await wizardApi.put('/api/v1/mode', { mode: 'readonly' }).catch(() => {})
|
localStorage.setItem('HF_BACKEND_BASE_URL', form.backend_base_url)
|
||||||
|
}
|
||||||
|
|
||||||
setStep(4)
|
setStep(4)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
@@ -129,7 +127,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 开始配置
|
||||||
@@ -187,6 +185,7 @@ export default function SetupWizardPage({ wizardPort, onComplete }: Props) {
|
|||||||
<h2>默认项目(可选)</h2>
|
<h2>默认项目(可选)</h2>
|
||||||
<p className="text-dim">创建一个初始项目,也可以跳过</p>
|
<p className="text-dim">创建一个初始项目,也可以跳过</p>
|
||||||
<div className="setup-form">
|
<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_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>
|
<label>项目描述 <input value={form.project_description} onChange={(e) => set('project_description', e.target.value)} /></label>
|
||||||
</div>
|
</div>
|
||||||
@@ -204,10 +203,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}>
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import axios from 'axios'
|
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({
|
const api = axios.create({
|
||||||
baseURL: import.meta.env.VITE_API_BASE || '/api',
|
baseURL: getApiBase(),
|
||||||
})
|
})
|
||||||
|
|
||||||
api.interceptors.request.use((config) => {
|
api.interceptors.request.use((config) => {
|
||||||
|
config.baseURL = getApiBase()
|
||||||
const token = localStorage.getItem('token')
|
const token = localStorage.getItem('token')
|
||||||
if (token) {
|
if (token) {
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
config.headers.Authorization = `Bearer ${token}`
|
||||||
|
|||||||
Reference in New Issue
Block a user