Fix: milestone routes use milestone_code, config check via backend
- Milestone navigation now uses milestone_code instead of numeric id - MilestoneDetailPage uses milestone.id (numeric) for project-scoped API calls - App.tsx checks /config/status on backend first, falls back to wizard - Added milestone_code to Milestone type - Fixed MilestoneDetailPage to use fetched milestone.id for sub-queries
This commit is contained in:
22
src/App.tsx
22
src/App.tsx
@@ -24,6 +24,10 @@ 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}`
|
const WIZARD_BASE = `http://127.0.0.1:${WIZARD_PORT}`
|
||||||
|
|
||||||
|
const getApiBase = () => {
|
||||||
|
return localStorage.getItem('HF_BACKEND_BASE_URL') || import.meta.env.VITE_API_BASE || 'http://127.0.0.1:8000'
|
||||||
|
}
|
||||||
|
|
||||||
type AppState = 'checking' | 'setup' | 'ready'
|
type AppState = 'checking' | 'setup' | 'ready'
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@@ -35,6 +39,22 @@ export default function App() {
|
|||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const checkInitialized = async () => {
|
const checkInitialized = async () => {
|
||||||
|
// First try the backend /config/status endpoint (reads from config volume directly)
|
||||||
|
try {
|
||||||
|
const res = await axios.get(`${getApiBase()}/config/status`, { 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')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Backend unreachable — fall through to wizard check
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: try the wizard directly (needed during initial setup before backend starts)
|
||||||
try {
|
try {
|
||||||
const res = await axios.get(`${WIZARD_BASE}/api/v1/config/harborforge.json`, {
|
const res = await axios.get(`${WIZARD_BASE}/api/v1/config/harborforge.json`, {
|
||||||
timeout: 5000,
|
timeout: 5000,
|
||||||
@@ -49,7 +69,7 @@ export default function App() {
|
|||||||
setAppState('setup')
|
setAppState('setup')
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// Wizard unreachable or config doesn't exist → setup needed
|
// Neither backend nor wizard reachable → setup needed
|
||||||
setAppState('setup')
|
setAppState('setup')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,14 +57,14 @@ export default function MilestoneDetailPage() {
|
|||||||
setProjectCode(proj.project_code || '')
|
setProjectCode(proj.project_code || '')
|
||||||
})
|
})
|
||||||
api.get<ProjectMember[]>(`/projects/${data.project_id}/members`).then(({ data }) => setMembers(data)).catch(() => {})
|
api.get<ProjectMember[]>(`/projects/${data.project_id}/members`).then(({ data }) => setMembers(data)).catch(() => {})
|
||||||
fetchPreflight(data.project_id)
|
fetchPreflight(data.project_id, data.id)
|
||||||
}
|
}
|
||||||
|
api.get<MilestoneProgress>(`/milestones/${data.id}/progress`).then(({ data: prog }) => setProgress(prog)).catch(() => {})
|
||||||
})
|
})
|
||||||
api.get<MilestoneProgress>(`/milestones/${id}/progress`).then(({ data }) => setProgress(data)).catch(() => {})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const fetchPreflight = (projectId: number) => {
|
const fetchPreflight = (projectId: number, milestoneId: number) => {
|
||||||
api.get(`/projects/${projectId}/milestones/${id}/actions/preflight`)
|
api.get(`/projects/${projectId}/milestones/${milestoneId}/actions/preflight`)
|
||||||
.then(({ data }) => setPreflight(data))
|
.then(({ data }) => setPreflight(data))
|
||||||
.catch(() => setPreflight(null))
|
.catch(() => setPreflight(null))
|
||||||
}
|
}
|
||||||
@@ -74,23 +74,23 @@ export default function MilestoneDetailPage() {
|
|||||||
}, [id])
|
}, [id])
|
||||||
|
|
||||||
const refreshMilestoneItems = () => {
|
const refreshMilestoneItems = () => {
|
||||||
if (!projectCode || !id) return
|
if (!projectCode || !milestone) return
|
||||||
api.get<MilestoneTask[]>(`/tasks/${projectCode}/${id}`).then(({ data }) => setTasks(data)).catch(() => {})
|
api.get<MilestoneTask[]>(`/tasks/${projectCode}/${milestone.id}`).then(({ data }) => setTasks(data)).catch(() => {})
|
||||||
api.get<any[]>(`/supports/${projectCode}/${id}`).then(({ data }) => setSupports(data)).catch(() => {})
|
api.get<any[]>(`/supports/${projectCode}/${milestone.id}`).then(({ data }) => setSupports(data)).catch(() => {})
|
||||||
api.get<any[]>(`/meetings/${projectCode}/${id}`).then(({ data }) => setMeetings(data)).catch(() => {})
|
api.get<any[]>(`/meetings/${projectCode}/${milestone.id}`).then(({ data }) => setMeetings(data)).catch(() => {})
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refreshMilestoneItems()
|
refreshMilestoneItems()
|
||||||
}, [projectCode, id])
|
}, [projectCode, milestone?.id])
|
||||||
|
|
||||||
const createItem = async (type: 'supports' | 'meetings') => {
|
const createItem = async (type: 'supports' | 'meetings') => {
|
||||||
if (!newTitle.trim() || !projectCode) return
|
if (!newTitle.trim() || !projectCode || !milestone) return
|
||||||
const payload = {
|
const payload = {
|
||||||
title: newTitle,
|
title: newTitle,
|
||||||
description: newDesc || null,
|
description: newDesc || null,
|
||||||
}
|
}
|
||||||
await api.post(`/${type}/${projectCode}/${id}`, payload)
|
await api.post(`/${type}/${projectCode}/${milestone.id}`, payload)
|
||||||
setNewTitle('')
|
setNewTitle('')
|
||||||
setNewDesc('')
|
setNewDesc('')
|
||||||
setShowCreateSupport(false)
|
setShowCreateSupport(false)
|
||||||
@@ -122,7 +122,7 @@ export default function MilestoneDetailPage() {
|
|||||||
await api.post(`/projects/${project.id}/milestones/${milestone.id}/actions/${action}`, body ?? {})
|
await api.post(`/projects/${project.id}/milestones/${milestone.id}/actions/${action}`, body ?? {})
|
||||||
fetchMilestone()
|
fetchMilestone()
|
||||||
refreshMilestoneItems()
|
refreshMilestoneItems()
|
||||||
fetchPreflight(project.id)
|
fetchPreflight(project.id, milestone.id)
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const detail = err?.response?.data?.detail
|
const detail = err?.response?.data?.detail
|
||||||
setActionError(typeof detail === 'string' ? detail : `${action} failed`)
|
setActionError(typeof detail === 'string' ? detail : `${action} failed`)
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export default function ProjectDetailPage() {
|
|||||||
{canEditProject && <button className="btn-sm" onClick={() => setShowMilestoneModal(true)}>+ New</button>}
|
{canEditProject && <button className="btn-sm" onClick={() => setShowMilestoneModal(true)}>+ New</button>}
|
||||||
</h3>
|
</h3>
|
||||||
{milestones.map((ms) => (
|
{milestones.map((ms) => (
|
||||||
<div key={ms.id} className="milestone-item" onClick={() => navigate(`/milestones/${ms.id}`)}>
|
<div key={ms.id} className="milestone-item" onClick={() => navigate(`/milestones/${ms.milestone_code || ms.id}`)}>
|
||||||
<span className={`badge status-${ms.status === 'open' ? 'open' : ms.status === 'closed' ? 'closed' : 'in_progress'}`}>{ms.status}</span>
|
<span className={`badge status-${ms.status === 'open' ? 'open' : ms.status === 'closed' ? 'closed' : 'in_progress'}`}>{ms.status}</span>
|
||||||
<span className="milestone-title">{ms.title}</span>
|
<span className="milestone-title">{ms.title}</span>
|
||||||
{ms.due_date && <span className="text-dim"> · Due {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
|
{ms.due_date && <span className="text-dim"> · Due {dayjs(ms.due_date).format('YYYY-MM-DD')}</span>}
|
||||||
|
|||||||
Reference in New Issue
Block a user