feat(flywheel-c2-c3): C2 hasType4接真實API + C3 WebSocket指數退避重連
Some checks failed
CD Pipeline / build-and-deploy (push) Failing after 3m41s

C2: flywheel_stats_service 加 type4_count query → API 回傳
    flywheel-diagram.tsx hasType4 改由 type4Count prop 驅動(非 false)
    flywheel-kpi-card.tsx 傳入 type4Count={flowData?.type4_count}

C3: WebSocket onclose 加指數退避重連 (1s→2s→4s→最大30s)
    cancelled 旗標確保 unmount 後不重連
    wsRetryTimer 加入 cleanup

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-12 18:45:40 +08:00
parent 079d0e89b9
commit 65a5220e16
3 changed files with 40 additions and 13 deletions

View File

@@ -24,6 +24,8 @@ interface FlywheelDiagramProps {
currentFlow?: FlowItem[]
/** active node names from node_stats */
activeNodes?: Record<string, { status: string }>
/** TYPE-4 active incident count from API (ADR-073-C C2) */
type4Count?: number
}
const NODES = [
@@ -44,7 +46,7 @@ function toSVG(pct: { x: number; y: number }) {
return { cx: (pct.x / 100) * W, cy: (pct.y / 100) * H }
}
export function FlywheelDiagram({ currentFlow = [], activeNodes = {} }: FlywheelDiagramProps) {
export function FlywheelDiagram({ currentFlow = [], activeNodes = {}, type4Count = 0 }: FlywheelDiagramProps) {
// Count active incidents per node
const nodeCounts: Record<string, number> = {}
for (const f of currentFlow) {
@@ -52,7 +54,7 @@ export function FlywheelDiagram({ currentFlow = [], activeNodes = {} }: Flywheel
}
const hasType3 = currentFlow.some(f => f.current_node === 'reasoning' || f.current_node === 'execution')
const hasType4 = false // TYPE-4 shown when diagnosis node has stuck items
const hasType4 = type4Count > 0
// Build arc path through nodes in order
const pts = NODES.map(n => toSVG(n))

View File

@@ -31,7 +31,7 @@ interface FlywheelSummary {
export function FlywheelKPICard() {
const [data, setData] = useState<FlywheelSummary | null>(null)
const [flowData, setFlowData] = useState<{ current_flow: FlowItem[]; nodes: Record<string, { status: string }> } | null>(null)
const [flowData, setFlowData] = useState<{ current_flow: FlowItem[]; nodes: Record<string, { status: string }>; type4_count?: number } | null>(null)
const [error, setError] = useState(false)
const wsRef = useRef<WebSocket | null>(null)
@@ -55,14 +55,18 @@ export function FlywheelKPICard() {
load()
// C3: WebSocket — upgrades from polling when available
// C3: WebSocket — upgrades from polling when available, with reconnect (ADR-073-C C3)
// 指數退避重連1s → 2s → 4s → 8s → 最大 30s
let wsRetryDelay = 1000
let wsRetryTimer: ReturnType<typeof setTimeout> | null = null
const connectWS = () => {
if (!WS_BASE) return
if (!WS_BASE || cancelled) return
const ws = new WebSocket(`${WS_BASE}/api/v1/stats/flywheel/ws`)
wsRef.current = ws
ws.onopen = () => {
// WS connected — stop HTTP polling
wsRetryDelay = 1000 // 連線成功 — 重置退避
if (pollId) { clearInterval(pollId); pollId = null }
}
ws.onmessage = (e) => {
@@ -75,10 +79,14 @@ export function FlywheelKPICard() {
} catch { /* ignore malformed */ }
}
ws.onclose = () => {
// WS closed — fall back to polling
if (!cancelled && !pollId) {
pollId = setInterval(load, 30_000)
}
if (cancelled) return
// 退回 HTTP polling
if (!pollId) pollId = setInterval(load, 30_000)
// 指數退避重連
wsRetryTimer = setTimeout(() => {
wsRetryDelay = Math.min(wsRetryDelay * 2, 30_000)
connectWS()
}, wsRetryDelay)
}
ws.onerror = () => ws.close()
}
@@ -90,6 +98,7 @@ export function FlywheelKPICard() {
return () => {
cancelled = true
if (pollId) clearInterval(pollId)
if (wsRetryTimer) clearTimeout(wsRetryTimer)
wsRef.current?.close()
}
}, [])
@@ -175,6 +184,7 @@ export function FlywheelKPICard() {
<FlywheelDiagram
currentFlow={flowData?.current_flow ?? []}
activeNodes={flowData?.nodes ?? {}}
type4Count={flowData?.type4_count ?? 0}
/>
</div>