Frontend no longer has any wizard flow. Backend URL is baked into the bundle at build time via VITE_HF_BACKEND_BASE_URL (forwarded as a Dockerfile ARG from compose). - src/App.tsx: drop SetupWizardPage import + appState='setup' fallback + HF_WIZARD_PORT-via-localStorage probe. getApiBase() now reads import.meta.env.VITE_HF_BACKEND_BASE_URL with localStorage as an escape hatch for dev. When /config/status reports no admin yet, show a card prompting the operator to run `docker exec hf_backend hf-cli admin create-user ...`. - src/pages/SetupWizardPage.tsx: deleted (~250 lines) - src/index.css: drop .setup-wizard + .setup-* styles (~36 lines) - src/vite-env.d.ts: add VITE_HF_BACKEND_BASE_URL to ImportMetaEnv - Dockerfile: ARG VITE_HF_BACKEND_BASE_URL → ENV → npm run build Build the prod image with: docker build --build-arg VITE_HF_BACKEND_BASE_URL=https://hf-api.hangman-lab.top \ -t git.hangman-lab.top/zhi/harborforge-frontend:latest . Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
163 lines
6.6 KiB
TypeScript
163 lines
6.6 KiB
TypeScript
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 DashboardPage from '@/pages/DashboardPage'
|
|
import TasksPage from '@/pages/TasksPage'
|
|
import TaskDetailPage from '@/pages/TaskDetailPage'
|
|
import ProjectsPage from '@/pages/ProjectsPage'
|
|
import ProjectDetailPage from '@/pages/ProjectDetailPage'
|
|
import MilestonesPage from '@/pages/MilestonesPage'
|
|
import MilestoneDetailPage from '@/pages/MilestoneDetailPage'
|
|
import NotificationsPage from '@/pages/NotificationsPage'
|
|
import RoleEditorPage from '@/pages/RoleEditorPage'
|
|
import MonitorPage from '@/pages/MonitorPage'
|
|
import ProposalsPage from '@/pages/ProposalsPage'
|
|
import ProposalDetailPage from '@/pages/ProposalDetailPage'
|
|
import UsersPage from '@/pages/UsersPage'
|
|
import CalendarPage from '@/pages/CalendarPage'
|
|
import SupportDetailPage from '@/pages/SupportDetailPage'
|
|
import MeetingDetailPage from '@/pages/MeetingDetailPage'
|
|
import OidcCallbackPage from '@/pages/OidcCallbackPage'
|
|
import OidcSettingsPage from '@/pages/OidcSettingsPage'
|
|
import axios from 'axios'
|
|
|
|
// Backend URL is baked in at build time via VITE_HF_BACKEND_BASE_URL (the
|
|
// docker-compose hf-frontend service passes it as a build ARG). Falling
|
|
// back to a same-origin call only makes sense in dev with the Vite proxy.
|
|
// localStorage override is kept as an escape hatch for one-off pointing
|
|
// (e.g. dev pointing the prod build at a local backend).
|
|
const getApiBase = (): string => {
|
|
const ls = localStorage.getItem('HF_BACKEND_BASE_URL')
|
|
if (ls) return ls
|
|
const baked = import.meta.env.VITE_HF_BACKEND_BASE_URL
|
|
return baked || ''
|
|
}
|
|
|
|
type AppState = 'checking' | 'no-admin' | 'ready'
|
|
|
|
export default function App() {
|
|
const [appState, setAppState] = useState<AppState>('checking')
|
|
const [errorMessage, setErrorMessage] = useState<string>('')
|
|
const { user, loading, login, loginWithToken, logout } = useAuth()
|
|
|
|
useEffect(() => {
|
|
checkInitialized()
|
|
}, [])
|
|
|
|
const checkInitialized = async () => {
|
|
try {
|
|
const res = await axios.get(`${getApiBase()}/config/status`, { timeout: 5000 })
|
|
const cfg = res.data || {}
|
|
if (cfg.initialized === true) {
|
|
setAppState('ready')
|
|
return
|
|
}
|
|
setAppState('no-admin')
|
|
} catch (err) {
|
|
const msg = err instanceof Error ? err.message : String(err)
|
|
setErrorMessage(`Backend unreachable at ${getApiBase() || '<same origin>'} — ${msg}`)
|
|
setAppState('no-admin')
|
|
}
|
|
}
|
|
|
|
if (appState === 'checking') {
|
|
return <div className="loading">Checking deployment status…</div>
|
|
}
|
|
|
|
if (appState === 'no-admin') {
|
|
return (
|
|
<div className="login-page">
|
|
<div className="login-card">
|
|
<h1>⚓ HarborForge</h1>
|
|
{errorMessage ? (
|
|
<>
|
|
<p className="text-dim">Cannot reach the backend.</p>
|
|
<pre style={{ whiteSpace: 'pre-wrap', fontSize: '0.85em' }}>{errorMessage}</pre>
|
|
<p className="text-dim">
|
|
Set <code>VITE_HF_BACKEND_BASE_URL</code> at build time
|
|
(e.g. <code>https://hf-api.example.com</code>) in the
|
|
frontend container's compose entry.
|
|
</p>
|
|
</>
|
|
) : (
|
|
<>
|
|
<p className="text-dim">
|
|
No admin user found. Bootstrap the deployment by running, on the host:
|
|
</p>
|
|
<pre style={{ whiteSpace: 'pre-wrap', fontSize: '0.85em' }}>
|
|
{`docker exec hf-backend hf-cli admin create-user \\
|
|
--email you@example.com \\
|
|
--password '...' \\
|
|
# ...or in OIDC_ONLY mode:
|
|
--oidc-issuer https://login.example.com/realms/your-realm \\
|
|
--oidc-subject <sub-from-idp>`}
|
|
</pre>
|
|
<button className="btn-primary" onClick={checkInitialized}>
|
|
Recheck
|
|
</button>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
if (loading) return <div className="loading">Loading...</div>
|
|
|
|
if (!user) {
|
|
return (
|
|
<BrowserRouter>
|
|
<div className="app-layout">
|
|
<Sidebar user={null} onLogout={logout} />
|
|
<main className="main-content">
|
|
<Routes>
|
|
<Route path="/roles" element={<RoleEditorPage />} />
|
|
<Route path="/users" element={<UsersPage />} />
|
|
<Route path="/monitor" element={<MonitorPage />} />
|
|
<Route path="/login" element={<LoginPage onLogin={login} />} />
|
|
<Route path="/oidc/callback" element={<OidcCallbackPage onToken={loginWithToken} />} />
|
|
<Route path="*" element={<Navigate to="/monitor" />} />
|
|
</Routes>
|
|
</main>
|
|
</div>
|
|
</BrowserRouter>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<BrowserRouter>
|
|
<div className="app-layout">
|
|
<Sidebar user={user} onLogout={logout} />
|
|
<main className="main-content">
|
|
<Routes>
|
|
<Route path="/" element={<DashboardPage />} />
|
|
<Route path="/tasks" element={<TasksPage />} />
|
|
<Route path="/tasks/:taskCode" element={<TaskDetailPage />} />
|
|
<Route path="/projects" element={<ProjectsPage />} />
|
|
<Route path="/projects/:id" element={<ProjectDetailPage />} />
|
|
<Route path="/milestones" element={<MilestonesPage />} />
|
|
<Route path="/milestones/:milestoneCode" element={<MilestoneDetailPage />} />
|
|
<Route path="/proposals" element={<ProposalsPage />} />
|
|
<Route path="/proposals/:proposalCode" element={<ProposalDetailPage />} />
|
|
<Route path="/calendar" element={<CalendarPage />} />
|
|
{/* Legacy routes for backward compatibility */}
|
|
<Route path="/proposes" element={<ProposalsPage />} />
|
|
<Route path="/proposes/:proposalCode" element={<ProposalDetailPage />} />
|
|
<Route path="/meetings/:meetingCode" element={<MeetingDetailPage />} />
|
|
<Route path="/supports/:supportCode" element={<SupportDetailPage />} />
|
|
<Route path="/notifications" element={<NotificationsPage />} />
|
|
<Route path="/roles" element={<RoleEditorPage />} />
|
|
<Route path="/users" element={<UsersPage />} />
|
|
<Route path="/monitor" element={<MonitorPage />} />
|
|
{user?.is_admin && <Route path="/settings/oidc" element={<OidcSettingsPage />} />}
|
|
<Route path="/oidc/callback" element={<OidcCallbackPage onToken={loginWithToken} />} />
|
|
<Route path="*" element={<Navigate to="/" />} />
|
|
</Routes>
|
|
</main>
|
|
</div>
|
|
</BrowserRouter>
|
|
)
|
|
}
|