Files
awoooi/apps/web/src/hooks/useIncidents.ts
OG T 196d269b92 feat: add all application source code
- apps/api: FastAPI backend with Dockerfile
- apps/web: Next.js frontend with Dockerfile
- apps/sensor: Signal collection agent
- packages: shared packages

Co-Authored-By: Claude <noreply@anthropic.com>
2026-03-22 18:57:44 +08:00

138 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use client'
/**
* useIncidents - Phase 7 事件訂閱 Hook
* =====================================
*
* 功能:
* - 從 /api/v1/incidents 獲取活躍事件
* - 從 /api/v1/approvals/pending 獲取待簽核提案
* - 支援輪詢更新 (Smart Polling)
*
* 統帥鐵律: 禁止假數據!所有資料必須來自真實 API
*/
import { useState, useEffect, useCallback, useRef } from 'react'
import {
apiClient,
type IncidentResponse,
type ApprovalResponse,
} from '@/lib/api-client'
// =============================================================================
// Types
// =============================================================================
interface UseIncidentsOptions {
/** 輪詢間隔 (毫秒)0 則不輪詢 */
pollInterval?: number
/** 是否啟用輪詢 */
enablePolling?: boolean
}
interface UseIncidentsResult {
/** 活躍事件清單 */
incidents: IncidentResponse[]
/** 待簽核提案清單 */
pendingApprovals: ApprovalResponse[]
/** 載入中狀態 */
isLoading: boolean
/** 錯誤訊息 */
error: string | null
/** 手動刷新 */
refresh: () => Promise<void>
/** 最後更新時間 */
lastUpdated: Date | null
}
// =============================================================================
// Hook
// =============================================================================
export function useIncidents(options: UseIncidentsOptions = {}): UseIncidentsResult {
const { pollInterval = 15000, enablePolling = true } = options
const [incidents, setIncidents] = useState<IncidentResponse[]>([])
const [pendingApprovals, setPendingApprovals] = useState<ApprovalResponse[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const mountedRef = useRef(true)
const pollTimeoutRef = useRef<NodeJS.Timeout | null>(null)
const fetchData = useCallback(async () => {
try {
// 並行請求 incidents 和 pending approvals
const [incidentsRes, approvalsRes] = await Promise.all([
apiClient.listIncidents(),
apiClient.getPendingApprovals(),
])
if (!mountedRef.current) return
setIncidents(incidentsRes.incidents)
setPendingApprovals(approvalsRes.approvals)
setError(null)
setLastUpdated(new Date())
} catch (err) {
if (!mountedRef.current) return
const message = err instanceof Error ? err.message : 'Unknown error'
setError(message)
// eslint-disable-next-line no-console
console.error('[useIncidents] Fetch error:', message)
} finally {
if (mountedRef.current) {
setIsLoading(false)
}
}
}, [])
const refresh = useCallback(async () => {
setIsLoading(true)
await fetchData()
}, [fetchData])
// Initial fetch and polling
useEffect(() => {
mountedRef.current = true
fetchData()
if (enablePolling && pollInterval > 0) {
const poll = () => {
pollTimeoutRef.current = setTimeout(async () => {
if (mountedRef.current) {
await fetchData()
poll()
}
}, pollInterval)
}
poll()
}
return () => {
mountedRef.current = false
if (pollTimeoutRef.current) {
clearTimeout(pollTimeoutRef.current)
}
}
}, [fetchData, enablePolling, pollInterval])
return {
incidents,
pendingApprovals,
isLoading,
error,
refresh,
lastUpdated,
}
}
// =============================================================================
// Export
// =============================================================================
export type { UseIncidentsOptions, UseIncidentsResult }