feat(web): drive incident flow summaries from status chain
All checks were successful
Code Review / ai-code-review (push) Successful in 16s
CD Pipeline / tests (push) Successful in 4m12s
CD Pipeline / build-and-deploy (push) Successful in 4m34s
CD Pipeline / post-deploy-checks (push) Successful in 1m48s

This commit is contained in:
Your Name
2026-05-20 12:11:41 +08:00
parent 1d6636cd0d
commit 5bc346b97e
4 changed files with 153 additions and 7 deletions

View File

@@ -34,8 +34,10 @@ import { PendingApprovalsCard } from '@/components/shared/pending-approvals-card
import { AIModelStatus } from '@/components/shared/ai-model-status'
import { FlywheelKPICard } from '@/components/dashboard/flywheel-kpi-card'
import { AutomationEvidenceCard } from '@/components/dashboard/automation-evidence-card'
import type { AwoooPStatusChain } from '@/components/awooop/status-chain'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
const STATUS_CHAIN_PREFETCH_LIMIT = 25
// =============================================================================
// Tab 2: 告警 & 授權 (串接真實 API)
@@ -128,7 +130,9 @@ function ActivityStreamTab() {
try {
const data = JSON.parse(e.data)
setEvents(prev => [{ ...data, _time: new Date().toLocaleTimeString('zh-TW', { hour: '2-digit', minute: '2-digit', second: '2-digit' }) }, ...prev].slice(0, 50))
} catch {}
} catch {
return
}
}
es.onerror = () => setConnected(false)
return () => es.close()
@@ -600,10 +604,59 @@ export default function Home({ params }: { params: { locale: string } }) {
incidents,
isLoading: isIncidentsLoading,
error: incidentsError,
lastUpdated: incidentsLastUpdated,
} = useIncidents({
pollInterval: 15000,
enablePolling: true,
})
const statusChainIncidentKey = incidents
?.slice(0, STATUS_CHAIN_PREFETCH_LIMIT)
.map(incident => incident.incident_id)
.join('|') ?? ''
const [statusChains, setStatusChains] = useState<Record<string, AwoooPStatusChain | null>>({})
useEffect(() => {
const incidentIds = statusChainIncidentKey
? statusChainIncidentKey.split('|').filter(Boolean)
: []
if (incidentIds.length === 0) {
setStatusChains({})
return
}
const controller = new AbortController()
const timeout = window.setTimeout(() => controller.abort(), 12000)
Promise.all(
incidentIds.map(async (incidentId): Promise<[string, AwoooPStatusChain | null]> => {
const params = new URLSearchParams({ project_id: 'awoooi' })
params.append('incident_id', incidentId)
try {
const response = await fetch(`${API_BASE}/api/v1/platform/status-chain?${params.toString()}`, {
cache: 'no-store',
signal: controller.signal,
})
if (!response.ok) return [incidentId, null]
return [incidentId, await response.json() as AwoooPStatusChain]
} catch {
return [incidentId, null]
}
})
)
.then(entries => {
if (controller.signal.aborted) return
setStatusChains(Object.fromEntries(entries))
})
.catch(() => {
if (!controller.signal.aborted) setStatusChains({})
})
.finally(() => window.clearTimeout(timeout))
return () => {
window.clearTimeout(timeout)
controller.abort()
}
}, [statusChainIncidentKey, incidentsLastUpdated])
// ── Metrics 計算 ────────────────────────────────────────────────────────────
@@ -889,6 +942,7 @@ export default function Home({ params }: { params: { locale: string } }) {
key={incident.incident_id}
incident={incident}
decision={incident.decision}
statusChain={statusChains[incident.incident_id] ?? null}
/>
))}
</div>