fix(web): stabilize homepage live status
All checks were successful
Code Review / ai-code-review (push) Successful in 10s
CD Pipeline / tests (push) Successful in 1m8s
CD Pipeline / build-and-deploy (push) Successful in 3m30s
CD Pipeline / post-deploy-checks (push) Successful in 1m22s

This commit is contained in:
Your Name
2026-05-19 11:12:09 +08:00
parent 504d038a9e
commit 10f2f1abaf
6 changed files with 112 additions and 49 deletions

View File

@@ -5,7 +5,7 @@
*
* 飛輪健康度 KPI 面板。
* C2: 初始載入 GET /api/v1/stats/summaryHTTP fallback
* C3: WebSocket /api/v1/stats/flywheel/ws 即時推送(10s 更新
* C3: WebSocket /api/v1/stats/flywheel/ws 即時推送(旗標開啟時
*
* 2026-04-12 ogt (ADR-073-C C2 + C3)
*/
@@ -16,6 +16,7 @@ import { FlywheelDiagram, type FlowItem } from './flywheel-diagram'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
// ws(s):// mirror of NEXT_PUBLIC_API_URL
const WS_BASE = API_BASE.replace(/^https/, 'wss').replace(/^http/, 'ws')
const ENABLE_FLYWHEEL_WS = process.env.NEXT_PUBLIC_ENABLE_FLYWHEEL_WS === 'true'
interface FlywheelSummary {
playbook_count: number
@@ -37,23 +38,29 @@ export function FlywheelKPICard() {
const [error, setError] = useState(false)
const wsRef = useRef<WebSocket | null>(null)
// C2: HTTP fallback (initial load + 30s poll when WS unavailable)
// C2: HTTP fallback (initial load + 30s poll)
useEffect(() => {
let cancelled = false
let pollId: ReturnType<typeof setInterval> | null = null
const load = () => {
const loadSummary = () => {
fetch(`${API_BASE}/api/v1/stats/summary`)
.then(r => r.ok ? r.json() : Promise.reject(r.status))
.then(d => { if (!cancelled) { setData(d); setError(false) } })
.catch(() => { if (!cancelled) setError(true) })
}
// 載入飛輪節點狀態C4 用)
fetch(`${API_BASE}/api/v1/stats/flywheel`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (!cancelled && d) setFlowData(d) })
.catch(() => {})
const loadFlow = () => {
fetch(`${API_BASE}/api/v1/stats/flywheel`)
.then(r => r.ok ? r.json() : null)
.then(d => { if (!cancelled && d) setFlowData(d) })
.catch(() => {})
}
const load = () => {
loadSummary()
loadFlow()
}
load()
@@ -63,7 +70,7 @@ export function FlywheelKPICard() {
let wsRetryTimer: ReturnType<typeof setTimeout> | null = null
const connectWS = () => {
if (!WS_BASE || cancelled) return
if (!ENABLE_FLYWHEEL_WS || !WS_BASE || cancelled) return
const ws = new WebSocket(`${WS_BASE}/api/v1/stats/flywheel/ws`)
wsRef.current = ws
@@ -94,7 +101,7 @@ export function FlywheelKPICard() {
}
connectWS()
// Also start polling as backup until WS opens
// Production default: HTTP polling stays authoritative unless WS is enabled explicitly.
pollId = setInterval(load, 30_000)
return () => {