fix(web): align homepage automation truth metrics
All checks were successful
CD Pipeline / tests (push) Successful in 1m18s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 3m26s
CD Pipeline / post-deploy-checks (push) Successful in 1m20s

This commit is contained in:
Your Name
2026-05-25 15:30:00 +08:00
parent d6d7c27152
commit ffe479dbcc
4 changed files with 164 additions and 14 deletions

View File

@@ -36,6 +36,14 @@ const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
const STATUS_CHAIN_PREFETCH_LIMIT = 25
const HOMEPAGE_INCIDENT_LIMIT = STATUS_CHAIN_PREFETCH_LIMIT
interface HomepageAutomationQualitySummary {
evaluated_total?: number
verified_auto_repair_total?: number
production_claim?: {
can_claim_full_auto_repair?: boolean
}
}
// =============================================================================
// Tab 2: 告警 & 授權 (串接真實 API)
// =============================================================================
@@ -512,7 +520,11 @@ export default function Home({ params }: { params: { locale: string } }) {
pollInterval: 15000,
enablePolling: true,
})
const { statusChains } = useIncidentStatusChains({
const {
statusChains,
requestedIncidentIds,
isLoading: isStatusChainsLoading,
} = useIncidentStatusChains({
incidentIds: incidents?.map(incident => incident.incident_id) ?? [],
limit: STATUS_CHAIN_PREFETCH_LIMIT,
refreshKey: incidentsLastUpdated?.toISOString() ?? null,
@@ -534,6 +546,7 @@ export default function Home({ params }: { params: { locale: string } }) {
// 2026-04-07 Claude Code: Sprint 4 E2 — 從 disposition API 取得真實自動化率
const [dispositionRate, setDispositionRate] = useState<{ auto_rate: number; total: number } | null>(null)
const [automationQuality, setAutomationQuality] = useState<HomepageAutomationQualitySummary | null>(null)
useEffect(() => {
fetch(`${API_BASE}/api/v1/stats/disposition`)
.then(r => r.json())
@@ -542,22 +555,57 @@ export default function Home({ params }: { params: { locale: string } }) {
})
.catch(() => {})
}, [])
useEffect(() => {
const controller = new AbortController()
fetch(`${API_BASE}/api/v1/platform/truth-chain/quality/summary?project_id=awoooi&hours=24&limit=30`, {
signal: controller.signal,
})
.then(r => r.ok ? r.json() : null)
.then(d => {
if (!d) return
setAutomationQuality({
evaluated_total: typeof d.evaluated_total === 'number' ? d.evaluated_total : undefined,
verified_auto_repair_total: typeof d.verified_auto_repair_total === 'number' ? d.verified_auto_repair_total : undefined,
production_claim: d.production_claim,
})
})
.catch(() => {})
return () => controller.abort()
}, [])
// 自動處置率 — 優先使用 disposition APIfallback 到 incidents 推算
// 自動處置率 — 首頁 KPI 使用 24h truth-chain 驗證率,避免把歷史 disposition 總表誤讀成今日閉環。
const evaluatedAutomationTotal = automationQuality?.evaluated_total ?? 0
const verifiedAutomationTotal = automationQuality?.verified_auto_repair_total ?? 0
const autoRemediationRate = (() => {
if (dispositionRate && dispositionRate.total > 0) {
return `${Math.round(dispositionRate.auto_rate * 100)}%`
if (evaluatedAutomationTotal > 0) {
return `${Math.round((verifiedAutomationTotal / evaluatedAutomationTotal) * 100)}%`
}
return '--'
})()
// 自動處置率數值 (for progress bar)
const autoRemediationPct = (() => {
if (dispositionRate && dispositionRate.total > 0) {
return Math.round(dispositionRate.auto_rate * 100)
if (evaluatedAutomationTotal > 0) {
return Math.round((verifiedAutomationTotal / evaluatedAutomationTotal) * 100)
}
return 0
})()
const autoRemediationTone = automationQuality?.production_claim?.can_claim_full_auto_repair
? '#22C55E'
: evaluatedAutomationTotal > 0
? '#F59E0B'
: '#141413'
const autoRemediationDetail = evaluatedAutomationTotal > 0
? tDashboard('autoRepairVerifiedCount', {
verified: verifiedAutomationTotal,
evaluated: evaluatedAutomationTotal,
})
: dispositionRate && dispositionRate.total > 0
? tDashboard('autoRepairAllTime', {
pct: Math.round(dispositionRate.auto_rate * 100),
total: dispositionRate.total.toLocaleString(),
})
: null
// ── 5 KPI Cards (Sprint 5R 設計稿批准版) ────────────────────────────────────
@@ -567,6 +615,16 @@ export default function Home({ params }: { params: { locale: string } }) {
const p2Count = incidents?.filter(i => i.severity === 'P2').length ?? 0
const visibleIncidents = incidents?.slice(0, HOMEPAGE_INCIDENT_LIMIT) ?? []
const hiddenIncidentCount = Math.max(incidentCount - visibleIncidents.length, 0)
const truthChainCoveredCount = requestedIncidentIds.filter((incidentId) => {
const chain = statusChains[incidentId]
return Boolean(chain && !chain.fetch_error && chain.source_id)
}).length
const truthChainCoverageLabel = isStatusChainsLoading
? tDashboard('truthChainLoading')
: tDashboard('truthChainCoverage', {
loaded: truthChainCoveredCount,
shown: requestedIncidentIds.length,
})
const liveTopologyGroups = Object.values(
hosts.reduce<Record<string, {
role: string
@@ -676,21 +734,30 @@ export default function Home({ params }: { params: { locale: string } }) {
</div>
{/* 活動事件 */}
<div style={{ flex: 1, background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 8, padding: '8px 12px' }}>
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.5px', color: '#87867f', fontWeight: 500 }}>{tDashboard('activeIncidents')}</div>
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.5px', color: '#87867f', fontWeight: 500 }}>{tDashboard('unresolvedIncidents')}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginTop: 2 }}>
<span style={{ fontSize: 22, fontWeight: 700, color: incidentCount > 0 ? '#d97757' : '#141413' }}>{incidentCount || '--'}</span>
{incidentCount > 0 && <span style={{ fontSize: 9, color: '#87867f' }}>P1:{p1Count} P2:{p2Count}</span>}
{incidentCount > 0 && (
<span style={{ fontSize: 9, color: '#87867f' }}>
{tDashboard('severityBreakdown', { p1: p1Count, p2: p2Count })}
</span>
)}
</div>
{visibleIncidents.length > 0 && (
<div style={{ marginTop: 4, fontSize: 9, color: '#87867f' }}>
{tDashboard('latestIncidentWindow', { shown: visibleIncidents.length })}
</div>
)}
</div>
{/* 自動修復率 */}
<div style={{ flex: 1, background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 8, padding: '8px 12px' }}>
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.5px', color: '#87867f', fontWeight: 500 }}>{tDashboard('autoRemediationRate')}</div>
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: '0.5px', color: '#87867f', fontWeight: 500 }}>{tDashboard('autoRepairVerified24h')}</div>
<div style={{ display: 'flex', alignItems: 'baseline', gap: 6, marginTop: 2 }}>
<span style={{ fontSize: 22, fontWeight: 700, color: '#22C55E' }}>{autoRemediationRate}</span>
{autoRemediationPct > 0 && <span style={{ fontSize: 10, fontWeight: 700, color: '#22C55E' }}>{tDashboard('trendUp', { pct: autoRemediationPct })}</span>}
<span style={{ fontSize: 22, fontWeight: 700, color: autoRemediationTone }}>{autoRemediationRate}</span>
{autoRemediationDetail && <span style={{ fontSize: 10, fontWeight: 700, color: autoRemediationTone }}>{autoRemediationDetail}</span>}
</div>
<div style={{ height: 3, borderRadius: 2, background: '#ebe8df', marginTop: 4, overflow: 'hidden' }}>
<div style={{ width: `${autoRemediationPct}%`, height: '100%', borderRadius: 2, background: 'linear-gradient(90deg,#22C55E,#4ade80)' }} />
<div style={{ width: `${autoRemediationPct}%`, height: '100%', borderRadius: 2, background: autoRemediationTone }} />
</div>
</div>
{/* 待審批 */}
@@ -738,7 +805,7 @@ export default function Home({ params }: { params: { locale: string } }) {
}}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757', flexShrink: 0 }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>
{tDashboard('activeIncidents')}
{tDashboard('unresolvedIncidents')}
</span>
{(incidents?.length ?? 0) > 0 && (
<span style={{
@@ -749,6 +816,19 @@ export default function Home({ params }: { params: { locale: string } }) {
{incidents?.length}
</span>
)}
{requestedIncidentIds.length > 0 && (
<span style={{
fontSize: 11,
color: truthChainCoveredCount === requestedIncidentIds.length && !isStatusChainsLoading ? '#17602a' : '#8a5a08',
background: truthChainCoveredCount === requestedIncidentIds.length && !isStatusChainsLoading ? '#f0faf2' : '#fff7e8',
border: '0.5px solid #e0ddd4',
padding: '2px 7px',
borderRadius: 10,
fontWeight: 700,
}}>
{truthChainCoverageLabel}
</span>
)}
<a
href={`/${locale}/alerts`}
style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500, textDecoration: 'none' }}
@@ -765,7 +845,12 @@ export default function Home({ params }: { params: { locale: string } }) {
) : (incidents?.length ?? 0) === 0 ? (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22C55E' }} />
<span style={{ fontSize: 13, color: '#87867f' }}>{tDashboard('stable')} · 0 {tDashboard('activeIncidents')}</span>
<span style={{ fontSize: 13, color: '#87867f' }}>
{tDashboard('stableUnresolved', {
stable: tDashboard('stable'),
label: tDashboard('unresolvedIncidents'),
})}
</span>
</div>
) : (
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>