fix(web): align homepage automation truth metrics
This commit is contained in:
@@ -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 API,fallback 到 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 }}>
|
||||
|
||||
Reference in New Issue
Block a user