diff --git a/apps/web/src/components/dashboard/flywheel-diagram.tsx b/apps/web/src/components/dashboard/flywheel-diagram.tsx new file mode 100644 index 00000000..c062a46f --- /dev/null +++ b/apps/web/src/components/dashboard/flywheel-diagram.tsx @@ -0,0 +1,138 @@ +'use client' + +/** + * FlywheelDiagram — ADR-073-C C4 + * + * 飛輪六節點圖(監控→去重→診斷→推理→執行→學習) + * 人工介入路徑視覺化: + * TYPE-3(人工審核):紅色虛線 推理→人工處理中心 + * TYPE-4(根因確認):橙色虛線 推理→TYPE-4 根因確認 + * + * Props: currentFlow — 來自 GET /api/v1/stats/flywheel 的 current_flow 陣列 + * + * 2026-04-12 ogt (ADR-073-C C4) + */ + +export interface FlowItem { + incident_id: string + alertname: string + current_node: string + ts: string | null +} + +interface FlywheelDiagramProps { + currentFlow?: FlowItem[] + /** active node names from node_stats */ + activeNodes?: Record +} + +const NODES = [ + { id: 'monitoring', label: '監控', x: 10, y: 50 }, + { id: 'deduplication', label: '去重', x: 26, y: 25 }, + { id: 'diagnosis', label: '診斷', x: 50, y: 12 }, + { id: 'reasoning', label: '推理匹配', x: 74, y: 25 }, + { id: 'execution', label: '執行', x: 90, y: 50 }, + { id: 'learning', label: '學習', x: 74, y: 75 }, +] + +// SVG viewport: 100 × 100 (percentage-based) +const R = 5 // node circle radius +const W = 400 +const H = 160 + +function toSVG(pct: { x: number; y: number }) { + return { cx: (pct.x / 100) * W, cy: (pct.y / 100) * H } +} + +export function FlywheelDiagram({ currentFlow = [], activeNodes = {} }: FlywheelDiagramProps) { + // Count active incidents per node + const nodeCounts: Record = {} + for (const f of currentFlow) { + nodeCounts[f.current_node] = (nodeCounts[f.current_node] ?? 0) + 1 + } + + 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 + + // Build arc path through nodes in order + const pts = NODES.map(n => toSVG(n)) + const arcD = pts.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p.cx} ${p.cy}`).join(' ') + + // Reasoning node coords (index 3) + const reasoningPt = toSVG(NODES[3]) + // Manual intervention point (right of SVG) + const manualPt = { cx: W * 0.98, cy: H * 0.50 } + // TYPE-4 point (below reasoning) + const type4Pt = { cx: W * 0.74, cy: H * 0.92 } + + return ( + + {/* Main flywheel arc */} + + + {/* Return arc (learning → monitoring) */} + + + {/* TYPE-3 intervention path */} + {hasType3 && ( + <> + + + 人工 + + )} + + {/* TYPE-4 intervention path */} + {hasType4 && ( + <> + + + 根因確認 + + )} + + {/* Nodes */} + {NODES.map(node => { + const { cx, cy } = toSVG(node) + const count = nodeCounts[node.id] ?? 0 + const isActive = activeNodes[node.id]?.status === 'active' || count > 0 + const fill = isActive ? '#d97757' : '#fff' + const stroke = isActive ? '#d97757' : '#c5c2b8' + + return ( + + + {count > 0 && ( + {count} + )} + + {node.label} + + + ) + })} + + ) +} diff --git a/apps/web/src/components/dashboard/flywheel-kpi-card.tsx b/apps/web/src/components/dashboard/flywheel-kpi-card.tsx index 8796305f..086ee812 100644 --- a/apps/web/src/components/dashboard/flywheel-kpi-card.tsx +++ b/apps/web/src/components/dashboard/flywheel-kpi-card.tsx @@ -11,6 +11,7 @@ */ import { useEffect, useRef, useState } from 'react' +import { FlywheelDiagram, type FlowItem } from './flywheel-diagram' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' // ws(s):// mirror of NEXT_PUBLIC_API_URL @@ -30,6 +31,7 @@ interface FlywheelSummary { export function FlywheelKPICard() { const [data, setData] = useState(null) + const [flowData, setFlowData] = useState<{ current_flow: FlowItem[]; nodes: Record } | null>(null) const [error, setError] = useState(false) const wsRef = useRef(null) @@ -45,6 +47,12 @@ export function FlywheelKPICard() { .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(() => {}) + load() // C3: WebSocket — upgrades from polling when available @@ -162,6 +170,14 @@ export function FlywheelKPICard() { ))} + {/* C4: 飛輪節點圖 + 人工介入路徑 */} +
+ +
+ {/* Stuck incidents warning */} {data?.incidents_stuck != null && data.incidents_stuck > 0 && (