diff --git a/apps/web/src/app/[locale]/page.tsx b/apps/web/src/app/[locale]/page.tsx index e7470142..43a92bc0 100644 --- a/apps/web/src/app/[locale]/page.tsx +++ b/apps/web/src/app/[locale]/page.tsx @@ -32,6 +32,7 @@ import { DispositionMini } from '@/components/shared/disposition-mini' import { RecentActivity } from '@/components/shared/recent-activity' import { PendingApprovalsCard } from '@/components/shared/pending-approvals-card' import { AIModelStatus } from '@/components/shared/ai-model-status' +import { FlywheelKPICard } from '@/components/dashboard/flywheel-kpi-card' const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? '' @@ -924,6 +925,9 @@ export default function Home({ params }: { params: { locale: string } }) { {/* 待審批任務 (S7) */} + {/* 飛輪健康度 KPI (ADR-073-C C2) */} + + {/* 基礎架構 — Toggle: 拓撲圖 / 主機網格 */}
(null) + const [error, setError] = useState(false) + + useEffect(() => { + let cancelled = false + + const load = () => { + 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) }) + } + + load() + const id = setInterval(load, 30_000) + return () => { cancelled = true; clearInterval(id) } + }, []) + + const fmt = (n: number | undefined, digits = 0) => + n == null ? '--' : n.toLocaleString(undefined, { maximumFractionDigits: digits }) + + const pct = (n: number | undefined) => + n == null ? '--' : `${Math.round(n * 100)}%` + + const kpis = [ + { + label: 'Playbooks', + value: fmt(data?.playbook_count), + color: data?.playbook_count != null && data.playbook_count >= 20 ? '#22C55E' : '#d97757', + hint: '目標 ≥ 20', + }, + { + label: '修復成功率', + value: pct(data?.execution_success_rate), + color: data?.execution_success_rate != null && data.execution_success_rate >= 0.3 ? '#22C55E' : '#F59E0B', + hint: '目標 ≥ 30%', + }, + { + label: '今日轉化', + value: fmt(data?.flywheel_conversions_today), + color: '#4A90D9', + hint: '今日新增 KM', + }, + { + label: 'KM 向量化率', + value: pct(data?.km_vectorized_rate), + color: data?.km_vectorized_rate != null && data.km_vectorized_rate >= 0.95 ? '#22C55E' : '#F59E0B', + hint: '目標 ≥ 95%', + }, + ] + + return ( +
+ {/* Header */} +
+
+ 飛輪健康度 + {error && ( + API 離線 + )} + {data && !error && ( + + {new Date(data.computed_at).toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit' })} + + )} +
+ + {/* KPI Grid */} +
+ {kpis.map(({ label, value, color, hint }) => ( +
+
+ {label} +
+
{value}
+
{hint}
+
+ ))} +
+ + {/* Stuck incidents warning */} + {data?.incidents_stuck != null && data.incidents_stuck > 0 && ( +
+ {data.incidents_stuck} 筆 Incident 卡在 INVESTIGATING {'>'} 24h +
+ )} +
+ ) +}