From 4b51f9b60d0bf6d1fe09d3529a544d7ac7fdece3 Mon Sep 17 00:00:00 2001 From: OG T Date: Sun, 12 Apr 2026 15:39:10 +0800 Subject: [PATCH] =?UTF-8?q?feat(c2):=20ADR-073-C=20C2=20=E2=80=94=20?= =?UTF-8?q?=E5=89=8D=E7=AB=AF=E9=A3=9B=E8=BC=AA=20KPI=20=E5=85=83=E4=BB=B6?= =?UTF-8?q?=E6=8E=A5=E7=9C=9F=E5=AF=A6=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 FlywheelKPICard 元件 - 消費 GET /api/v1/stats/summary,30 秒輪詢 - 顯示 Playbooks、修復成功率、今日轉化數、KM 向量化率 - 卡住 Incident 警示條 - 插入首頁右欄 PendingApprovalsCard 之後 2026-04-12 ogt (ADR-073-C C2) Co-Authored-By: Claude Sonnet 4.6 --- apps/web/src/app/[locale]/page.tsx | 4 + .../dashboard/flywheel-kpi-card.tsx | 136 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 apps/web/src/components/dashboard/flywheel-kpi-card.tsx 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 +
+ )} +
+ ) +}