'use client' /** * useGlobalPulseMetrics - 全局脈搏真實數據 Hook * ============================================= * 專案鐵律: 禁止假數據!所有指標必須來自 production 真實資料源 * * Features: * - Fail Fast 錯誤處理 * - 自動輪詢 (30 秒) * - 誠實渲染 (無數據顯示 "--") * - 專案鐵律: 禁止 Hardcode API URL */ import { useState, useEffect, useCallback, useRef } from 'react' import type { PulseMetric } from '@/components/charts/global-pulse-chart' // ============================================================================= // Types (對應後端 Response) // ============================================================================= interface GoldMetricItem { label: string value: number | string unit?: string trend: number[] status: 'healthy' | 'warning' | 'critical' } interface GoldMetricsResponse { timestamp: string service_name: string metrics: GoldMetricItem[] raw_data?: Record } interface UseGlobalPulseMetricsOptions { /** 輪詢間隔 (ms),預設 30000 */ pollInterval?: number /** 是否啟用自動輪詢,預設 true */ enablePolling?: boolean /** 服務名稱,預設 "awoooi-api" */ serviceName?: string } interface UseGlobalPulseMetricsResult { /** 指標數據 (格式化給 GlobalPulseChart 使用) */ metrics: PulseMetric[] /** 是否載入中 */ isLoading: boolean /** 錯誤訊息 */ error: string | null /** 最後更新時間 */ lastUpdated: Date | null /** 手動刷新 */ refresh: () => Promise /** 是否有真實數據 (非全 0) */ hasRealData: boolean } // ============================================================================= // API Helper (專案鐵律: 禁止 Hardcode) // ============================================================================= const getApiBaseUrl = (): string => { if (typeof window === 'undefined') return '' const url = process.env.NEXT_PUBLIC_API_URL if (!url) { console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL configuration.') return '' } return url } // ============================================================================= // Hook Implementation // ============================================================================= export function useGlobalPulseMetrics( options: UseGlobalPulseMetricsOptions = {} ): UseGlobalPulseMetricsResult { const { pollInterval = 30000, enablePolling = true, serviceName = 'awoooi-api', } = options // State const [metrics, setMetrics] = useState([]) const [isLoading, setIsLoading] = useState(true) const [error, setError] = useState(null) const [lastUpdated, setLastUpdated] = useState(null) const [hasRealData, setHasRealData] = useState(false) // Polling timer ref const pollTimerRef = useRef(null) // ========================================================================== // Fetch Metrics // ========================================================================== const fetchMetrics = useCallback(async () => { const apiBaseUrl = getApiBaseUrl() // Fail Fast: 無 API URL 則報錯 if (!apiBaseUrl) { setError('NEXT_PUBLIC_API_URL 未設定') setIsLoading(false) setHasRealData(false) // 誠實渲染: 顯示 "--" setMetrics(createEmptyMetrics()) return } try { setError(null) const response = await fetch( `${apiBaseUrl}/api/v1/metrics/gold?service_name=${encodeURIComponent(serviceName)}`, { headers: { 'Content-Type': 'application/json' }, // 10 秒超時 signal: AbortSignal.timeout(10000), } ) if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`) } const data: GoldMetricsResponse = await response.json() // 轉換為 PulseMetric 格式 const pulseMetrics: PulseMetric[] = data.metrics.map((m) => ({ label: m.label, value: m.value, unit: m.unit, trend: m.trend, status: m.status, })) setMetrics(pulseMetrics) setLastUpdated(new Date()) // 檢查是否有真實數據 (非全 0) const hasReal = pulseMetrics.some( (m) => typeof m.value === 'number' && m.value > 0 ) setHasRealData(hasReal) console.log('[GlobalPulse] Metrics fetched:', pulseMetrics.length, 'items') } catch (err) { const message = err instanceof Error ? err.message : 'Unknown error' console.error('[GlobalPulse] Fetch error:', message) setError(message) setHasRealData(false) // 誠實渲染: 錯誤時顯示 "--" setMetrics(createEmptyMetrics()) } finally { setIsLoading(false) } }, [serviceName]) // ========================================================================== // Polling Effect // ========================================================================== useEffect(() => { // Initial fetch fetchMetrics() // Start polling if enabled if (enablePolling && pollInterval > 0) { pollTimerRef.current = setInterval(fetchMetrics, pollInterval) } // Cleanup return () => { if (pollTimerRef.current) { clearInterval(pollTimerRef.current) pollTimerRef.current = null } } }, [fetchMetrics, enablePolling, pollInterval]) return { metrics, isLoading, error, lastUpdated, refresh: fetchMetrics, hasRealData, } } // ============================================================================= // Helper: Create Empty Metrics (誠實渲染用) // ============================================================================= function createEmptyMetrics(): PulseMetric[] { return [ { label: 'RPS', value: '--', unit: 'req/s', trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], status: 'critical', }, { label: 'Error Rate', value: '--', unit: '%', trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], status: 'critical', }, { label: 'P99 Latency', value: '--', unit: 'ms', trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], status: 'critical', }, { label: 'AI Success', value: '--', unit: '%', trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0], status: 'critical', }, ] } // ============================================================================= // Export // ============================================================================= export default useGlobalPulseMetrics