@@ -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 . t otal > 0 ) {
return ` ${ Math . round ( dispositionRate . auto_rate * 100 ) } % `
if ( evaluatedAutomationT otal > 0 ) {
return ` ${ Math . round ( ( verifiedAutomationTotal / evaluatedAutomationTotal ) * 100 ) } % `
}
return '--'
} ) ( )
// 自動處置率數值 (for progress bar)
const autoRemediationPct = ( ( ) = > {
if ( dispositionRate && dispositionRate . t otal > 0 ) {
return Math . round ( dispositionRate . auto_rate * 100 )
if ( evaluatedAutomationT otal > 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 ( 'acti veIncidents' ) } < / div >
< div style = { { fontSize : 10 , textTransform : 'uppercase' , letterSpacing : '0.5px' , color : '#87867f' , fontWeight : 500 } } > { tDashboard ( 'unresol ved Incidents' ) } < / 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 ( 'acti veIncidents' ) }
{ tDashboard ( 'unresol ved Incidents' ) }
< / 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 } } >