fix(web): resolve interface mismatch + add defensive null checks
- P0/P1/P2 now map to 'alert' status (was P0/P1 only) - Tier mapping: P0=Tier3, P1=Tier2, P2=Tier1 - Added null/undefined guards in mapToDualState() - Optional chaining on incidents array access - Safe fallback for missing serviceName, message, timestamp Fixes frontend warroom showing no cards despite API returning data. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ import type { IncidentResponse } from '@/lib/api-client'
|
||||
// Utility: Map IncidentResponse to DualStateIncidentCard props
|
||||
// =============================================================================
|
||||
|
||||
function mapToDualState(incident: IncidentResponse): {
|
||||
function mapToDualState(incident: IncidentResponse | null | undefined): {
|
||||
id: string
|
||||
serviceName: string
|
||||
status: 'normal' | 'alert'
|
||||
@@ -48,32 +48,65 @@ function mapToDualState(incident: IncidentResponse): {
|
||||
message: string
|
||||
timestamp: string
|
||||
} {
|
||||
// P0/P1 視為異常 (alert),P2/P3 視為正常 (normal)
|
||||
const isAlert = incident.severity === 'P0' || incident.severity === 'P1'
|
||||
|
||||
// Tier 判定: proposal_count > 0 且為 P0 = Tier 3, P1 = Tier 2, else Tier 1
|
||||
let tier: 1 | 2 | 3 | undefined = undefined
|
||||
if (isAlert && incident.proposal_count > 0) {
|
||||
tier = incident.severity === 'P0' ? 3 : 2
|
||||
} else if (isAlert) {
|
||||
tier = 1
|
||||
// 防禦性檢查: 若 incident 無效則返回預設值
|
||||
if (!incident) {
|
||||
return {
|
||||
id: 'unknown',
|
||||
serviceName: 'Unknown Service',
|
||||
status: 'normal',
|
||||
tier: undefined,
|
||||
message: '資料載入中...',
|
||||
timestamp: '-',
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化時間
|
||||
const date = new Date(incident.created_at)
|
||||
const timestamp = date.toLocaleString('zh-TW', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
// P0/P1/P2 視為異常 (alert),只有 P3 視為正常 (normal)
|
||||
const severity = incident.severity || 'P3'
|
||||
const isAlert = severity === 'P0' || severity === 'P1' || severity === 'P2'
|
||||
|
||||
// Tier 判定: P0 = Tier 3 (親核), P1 = Tier 2 (授權), P2+ = Tier 1 (自主)
|
||||
let tier: 1 | 2 | 3 | undefined = undefined
|
||||
if (isAlert) {
|
||||
if (severity === 'P0') {
|
||||
tier = 3
|
||||
} else if (severity === 'P1') {
|
||||
tier = 2
|
||||
} else {
|
||||
tier = 1
|
||||
}
|
||||
}
|
||||
|
||||
// 安全提取服務名稱
|
||||
const services = incident.affected_services || []
|
||||
const serviceName = services.length > 0
|
||||
? services[0]
|
||||
: incident.incident_id?.split('-')[2] || 'Unknown Service'
|
||||
|
||||
// 格式化時間 (安全處理)
|
||||
let timestamp = '-'
|
||||
try {
|
||||
const date = new Date(incident.created_at || Date.now())
|
||||
timestamp = date.toLocaleString('zh-TW', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
} catch {
|
||||
timestamp = 'Invalid Date'
|
||||
}
|
||||
|
||||
// 安全生成訊息
|
||||
const signalCount = incident.signal_count ?? 0
|
||||
const status = incident.status || 'unknown'
|
||||
const message = `[${severity}] ${signalCount} 筆告警 | ${status}`
|
||||
|
||||
return {
|
||||
id: incident.incident_id,
|
||||
serviceName: incident.affected_services[0] || 'unknown',
|
||||
id: incident.incident_id || 'unknown',
|
||||
serviceName,
|
||||
status: isAlert ? 'alert' : 'normal',
|
||||
tier,
|
||||
message: `${incident.signal_count} 筆告警 | ${incident.status}`,
|
||||
message,
|
||||
timestamp,
|
||||
}
|
||||
}
|
||||
@@ -156,7 +189,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
{/* Active Incidents Section (Phase 7: 真實血脈 + Phase 6.5b 雙態卡片) */}
|
||||
<DataPincerPanel
|
||||
title={t('incident.activeIncidents')}
|
||||
status={incidents.length > 0 ? 'critical' : 'healthy'}
|
||||
status={(incidents?.length || 0) > 0 ? 'critical' : 'healthy'}
|
||||
>
|
||||
{isIncidentsLoading ? (
|
||||
<div className="flex items-center justify-center py-8">
|
||||
@@ -170,7 +203,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<AlertTriangle className="w-5 h-5 mb-2" />
|
||||
<span className="font-mono text-sm">{incidentsError}</span>
|
||||
</div>
|
||||
) : incidents.length === 0 ? (
|
||||
) : (incidents?.length || 0) === 0 ? (
|
||||
/* Nothing.tech 風格平靜態: 系統穩定 */
|
||||
<div className="flex flex-col items-center justify-center py-12 text-center">
|
||||
<div className="w-3 h-3 rounded-full bg-green-500 mb-4 animate-pulse" />
|
||||
@@ -185,11 +218,11 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
<div className="space-y-4">
|
||||
{/* Phase 6.5b: 雙態戰情室卡片 (脈衝雷達 + Tier 決策層) */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
{incidents.map((incident) => {
|
||||
{(incidents || []).map((incident, index) => {
|
||||
const dualProps = mapToDualState(incident)
|
||||
return (
|
||||
<DualStateIncidentCard
|
||||
key={`dual-${incident.incident_id}`}
|
||||
key={`dual-${incident?.incident_id || index}`}
|
||||
{...dualProps}
|
||||
/>
|
||||
)
|
||||
@@ -205,9 +238,9 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
status="thinking"
|
||||
>
|
||||
<ThinkingTerminal
|
||||
decisionChain={incidents.length > 0 ? DEMO_DECISION_CHAIN : null}
|
||||
incidentId={incidents.length > 0 ? incidents[0].incident_id : undefined}
|
||||
autoPlay={incidents.length > 0}
|
||||
decisionChain={(incidents?.length || 0) > 0 ? DEMO_DECISION_CHAIN : null}
|
||||
incidentId={(incidents?.length || 0) > 0 ? incidents?.[0]?.incident_id : undefined}
|
||||
autoPlay={(incidents?.length || 0) > 0}
|
||||
maxHeight="300px"
|
||||
/>
|
||||
</DataPincerPanel>
|
||||
|
||||
Reference in New Issue
Block a user