diff --git a/apps/web/messages/en.json b/apps/web/messages/en.json
index 2ee713b4..01446622 100644
--- a/apps/web/messages/en.json
+++ b/apps/web/messages/en.json
@@ -1327,10 +1327,74 @@
"hostStatus": "主機狀態 (FOUR-HOST ARCHITECTURE)",
"serviceList": "服務清單",
"serviceName": "服務名稱",
+ "serviceHealth": "服務健康",
+ "service": "服務",
"status": "狀態",
"latency": "延遲",
"uptime": "可用率",
- "lastCheck": "最後檢查"
+ "lastCheck": "最後檢查",
+ "incidentFocus": {
+ "emptyValue": "尚無資料",
+ "title": "焦點 Incident 監控證據鏈",
+ "subtitle": "{incidentId}|{incidentTitle}",
+ "loading": "正在讀取 Incident status-chain 與 timeline...",
+ "loadFailed": "焦點 Incident 資料讀取失敗:{error}",
+ "boundary": "此區塊只讀取監控證據,不會自動標記 Sentry/SigNoz 已匹配,也不會觸發修復或靜音告警。最新 PlayBook:{playbook};executor:{executor}。",
+ "human": {
+ "yes": "需要人工",
+ "no": "未要求人工"
+ },
+ "links": {
+ "workItems": "工作項",
+ "runs": "Runs",
+ "approvals": "審批",
+ "authorizations": "授權",
+ "tickets": "Tickets"
+ },
+ "sourceStatuses": {
+ "linked": "已匹配 provider event",
+ "candidateFound": "找到候選但未套用",
+ "providerFreshNoMatch": "Provider 有心跳但未匹配此 Incident",
+ "missing": "缺少 provider 證據",
+ "noIncidentContext": "缺少 Incident context",
+ "fetchFailed": "讀取 provider 證據失敗"
+ },
+ "sourceReasons": {
+ "providerHeartbeatNoMatch": "Sentry / SigNoz 有心跳,但此 Incident 尚未匹配 provider event",
+ "noMatchingProviderSourceEvent": "沒有找到可對應此 Incident 的 provider source event",
+ "noIncidentIds": "缺少 Incident ID,無法比對 provider",
+ "incidentNotFound": "找不到此 Incident 的 provider 關聯",
+ "fetchFailed": "provider 關聯查詢失敗"
+ },
+ "tiles": {
+ "sourceRefs": "Source refs",
+ "sourceRefsValue": "{inbound} 入站 / {outbound} 出站",
+ "sourceRefsDetail": "direct {direct}、candidate {candidate}、applied {applied}",
+ "provider": "Sentry / SigNoz",
+ "providerDetail": "{reason};provider events {providerEvents}",
+ "mcp": "MCP Gateway",
+ "mcpValue": "{success} / {total}",
+ "mcpDetail": "failed {failed}、blocked {blocked}、policy {policy}",
+ "ansible": "Ansible",
+ "ansibleDetail": "mode {mode}、rc {rc}、apply {apply}",
+ "km": "KM",
+ "kmDetail": "verification {verification};next {next}",
+ "handoff": "交接狀態"
+ },
+ "providerEvidence": {
+ "title": "Provider 匹配狀態",
+ "rawIdsHidden": "raw id 已收斂",
+ "summary": "目前判斷:{status}。原因:{reason}。",
+ "counts": "direct {direct} / candidate {candidate} / applied {applied}",
+ "latest": "latest event {event};heartbeat {heartbeat}"
+ },
+ "timeline": {
+ "title": "Incident Timeline 寫入證據",
+ "summary": "status {status};severity {severity};stages {stages}",
+ "sourceTable": "source_table:{table}",
+ "empty": "此 Incident 尚未回傳 timeline stage"
+ }
+ }
},
"services": {
"title": "服務目錄",
diff --git a/apps/web/messages/zh-TW.json b/apps/web/messages/zh-TW.json
index 2ee713b4..01446622 100644
--- a/apps/web/messages/zh-TW.json
+++ b/apps/web/messages/zh-TW.json
@@ -1327,10 +1327,74 @@
"hostStatus": "主機狀態 (FOUR-HOST ARCHITECTURE)",
"serviceList": "服務清單",
"serviceName": "服務名稱",
+ "serviceHealth": "服務健康",
+ "service": "服務",
"status": "狀態",
"latency": "延遲",
"uptime": "可用率",
- "lastCheck": "最後檢查"
+ "lastCheck": "最後檢查",
+ "incidentFocus": {
+ "emptyValue": "尚無資料",
+ "title": "焦點 Incident 監控證據鏈",
+ "subtitle": "{incidentId}|{incidentTitle}",
+ "loading": "正在讀取 Incident status-chain 與 timeline...",
+ "loadFailed": "焦點 Incident 資料讀取失敗:{error}",
+ "boundary": "此區塊只讀取監控證據,不會自動標記 Sentry/SigNoz 已匹配,也不會觸發修復或靜音告警。最新 PlayBook:{playbook};executor:{executor}。",
+ "human": {
+ "yes": "需要人工",
+ "no": "未要求人工"
+ },
+ "links": {
+ "workItems": "工作項",
+ "runs": "Runs",
+ "approvals": "審批",
+ "authorizations": "授權",
+ "tickets": "Tickets"
+ },
+ "sourceStatuses": {
+ "linked": "已匹配 provider event",
+ "candidateFound": "找到候選但未套用",
+ "providerFreshNoMatch": "Provider 有心跳但未匹配此 Incident",
+ "missing": "缺少 provider 證據",
+ "noIncidentContext": "缺少 Incident context",
+ "fetchFailed": "讀取 provider 證據失敗"
+ },
+ "sourceReasons": {
+ "providerHeartbeatNoMatch": "Sentry / SigNoz 有心跳,但此 Incident 尚未匹配 provider event",
+ "noMatchingProviderSourceEvent": "沒有找到可對應此 Incident 的 provider source event",
+ "noIncidentIds": "缺少 Incident ID,無法比對 provider",
+ "incidentNotFound": "找不到此 Incident 的 provider 關聯",
+ "fetchFailed": "provider 關聯查詢失敗"
+ },
+ "tiles": {
+ "sourceRefs": "Source refs",
+ "sourceRefsValue": "{inbound} 入站 / {outbound} 出站",
+ "sourceRefsDetail": "direct {direct}、candidate {candidate}、applied {applied}",
+ "provider": "Sentry / SigNoz",
+ "providerDetail": "{reason};provider events {providerEvents}",
+ "mcp": "MCP Gateway",
+ "mcpValue": "{success} / {total}",
+ "mcpDetail": "failed {failed}、blocked {blocked}、policy {policy}",
+ "ansible": "Ansible",
+ "ansibleDetail": "mode {mode}、rc {rc}、apply {apply}",
+ "km": "KM",
+ "kmDetail": "verification {verification};next {next}",
+ "handoff": "交接狀態"
+ },
+ "providerEvidence": {
+ "title": "Provider 匹配狀態",
+ "rawIdsHidden": "raw id 已收斂",
+ "summary": "目前判斷:{status}。原因:{reason}。",
+ "counts": "direct {direct} / candidate {candidate} / applied {applied}",
+ "latest": "latest event {event};heartbeat {heartbeat}"
+ },
+ "timeline": {
+ "title": "Incident Timeline 寫入證據",
+ "summary": "status {status};severity {severity};stages {stages}",
+ "sourceTable": "source_table:{table}",
+ "empty": "此 Incident 尚未回傳 timeline stage"
+ }
+ }
},
"services": {
"title": "服務目錄",
diff --git a/apps/web/src/components/panels/MonitoringPanel.tsx b/apps/web/src/components/panels/MonitoringPanel.tsx
index 1820f6b1..7adfc932 100644
--- a/apps/web/src/components/panels/MonitoringPanel.tsx
+++ b/apps/web/src/components/panels/MonitoringPanel.tsx
@@ -11,15 +11,36 @@
*/
import { useState, useEffect, useCallback, useRef } from 'react'
+import { useSearchParams } from 'next/navigation'
import { useTranslations } from 'next-intl'
import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics'
import { useHosts } from '@/stores/dashboard.store'
import { GlobalPulseChart } from '@/components/charts/global-pulse-chart'
import { HostGrid } from '@/components/infra/host-grid'
import { cn } from '@/lib/utils'
+import { Link } from '@/i18n/routing'
import {
- Monitor, RefreshCw, AlertCircle,
- CheckCircle2, XCircle, Minus,
+ AwoooPStatusChainPanel,
+ type AwoooPStatusChain,
+} from '@/components/awooop/status-chain'
+import type { IncidentTimelineResponse } from '@/lib/api-client'
+import {
+ Activity,
+ BookOpenCheck,
+ CheckCircle2,
+ ExternalLink,
+ GitBranch,
+ Link2,
+ ListChecks,
+ Minus,
+ Monitor,
+ RadioTower,
+ RefreshCw,
+ ShieldCheck,
+ TriangleAlert,
+ Wrench,
+ AlertCircle,
+ XCircle,
} from 'lucide-react'
// =============================================================================
@@ -43,6 +64,15 @@ interface DashboardResponse {
timestamp: string
}
+interface IncidentFocusState {
+ chain: AwoooPStatusChain | null
+ timeline: IncidentTimelineResponse | null
+ loading: boolean
+ error: string | null
+}
+
+type EvidenceTone = 'success' | 'warning' | 'blocked' | 'neutral'
+
// =============================================================================
// Helpers
// =============================================================================
@@ -52,6 +82,50 @@ const getApiBaseUrl = () => {
return process.env.NEXT_PUBLIC_API_URL ?? ''
}
+async function fetchJson {label}
+ {value}
+
+ {detail}
+
+ {t('subtitle', {
+ incidentId,
+ incidentTitle: timeline?.title ?? valueOrEmpty(chain?.source_id, emptyLabel),
+ })}
+
+ {t('providerEvidence.summary', {
+ status: sourceStatusLabel,
+ reason: sourceReasonLabel,
+ })}
+ {providerLabel(provider)}
+ {t('providerEvidence.counts', {
+ direct: stats?.direct_ref_total ?? 0,
+ candidate: stats?.candidate_total ?? 0,
+ applied: stats?.applied_link_total ?? 0,
+ })}
+
+ {t('providerEvidence.latest', {
+ event: formatDateTime(stats?.latest_event_at, emptyLabel),
+ heartbeat: formatDateTime(stats?.latest_heartbeat_at, emptyLabel),
+ })}
+
+ {t('timeline.summary', {
+ status: valueOrEmpty(timeline?.status, emptyLabel),
+ severity: valueOrEmpty(timeline?.severity, emptyLabel),
+ stages: timeline?.timeline?.length ?? 0,
+ })}
+
+ {stage.label || stage.stage}
+
+ {t('timeline.sourceTable', { table: valueOrEmpty(stage.source_table, emptyLabel) })}
+
+ {t('title')}
+
+ {t('providerEvidence.title')}
+
+ {t('providerEvidence.rawIdsHidden')}
+
+ {t('timeline.title')}
+