fix(web): monitoring 頁 QA 修復 — NAN% + HostGrid + i18n
- HostGrid 接 useHosts() SSE 數據(不再傳空陣列) - HealthSummary NAN% 修復:total_count=0 時顯示 0% 而非 NaN% - 8 處硬編碼中文改 i18n (正常/警告/異常/黃金指標/主機狀態/服務清單/表頭) - 新增 monitoring namespace i18n keys (11 keys × 2 langs) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -708,5 +708,18 @@
|
||||
"entries": "entries",
|
||||
"empty": "No knowledge entries yet",
|
||||
"emptyDescription": "Entries will be auto-extracted from incidents, or you can create them manually"
|
||||
},
|
||||
"monitoring": {
|
||||
"healthy": "Healthy",
|
||||
"warning": "Warning",
|
||||
"critical": "Critical",
|
||||
"goldMetrics": "GOLD METRICS",
|
||||
"hostStatus": "HOST STATUS (FOUR-HOST ARCHITECTURE)",
|
||||
"serviceList": "SERVICE LIST",
|
||||
"serviceName": "Service",
|
||||
"status": "Status",
|
||||
"latency": "Latency",
|
||||
"uptime": "Uptime",
|
||||
"lastCheck": "Last Check"
|
||||
}
|
||||
}
|
||||
@@ -709,5 +709,18 @@
|
||||
"entries": "筆",
|
||||
"empty": "尚未建立任何知識條目",
|
||||
"emptyDescription": "知識庫將自動從 Incident 中萃取案例,你也可以手動新增"
|
||||
},
|
||||
"monitoring": {
|
||||
"healthy": "正常",
|
||||
"warning": "警告",
|
||||
"critical": "異常",
|
||||
"goldMetrics": "黃金指標 (GOLD METRICS)",
|
||||
"hostStatus": "主機狀態 (FOUR-HOST ARCHITECTURE)",
|
||||
"serviceList": "服務清單",
|
||||
"serviceName": "服務名稱",
|
||||
"status": "狀態",
|
||||
"latency": "延遲",
|
||||
"uptime": "可用率",
|
||||
"lastCheck": "最後檢查"
|
||||
}
|
||||
}
|
||||
@@ -4,18 +4,16 @@
|
||||
* Monitoring Page - 服務監控
|
||||
* ===========================
|
||||
* 黃金指標 (Gold Metrics) + 四主機狀態 + 服務健康
|
||||
* 資料來源:
|
||||
* GET /api/v1/metrics/gold
|
||||
* GET /api/v1/dashboard/hosts
|
||||
* GET /api/v1/dashboard (SSE 快照)
|
||||
*
|
||||
* @created 2026-04-01 ogt
|
||||
* @updated 2026-04-02 Claude Code — QA 修復: i18n + useHosts + NaN
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
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'
|
||||
@@ -74,21 +72,23 @@ const STATUS_BADGE = {
|
||||
// Sub-component: Health Summary
|
||||
// =============================================================================
|
||||
|
||||
function HealthSummary({ data }: { data: DashboardResponse }) {
|
||||
const total = data.total_count || 1
|
||||
function HealthSummary({ data, t }: { data: DashboardResponse; t: (key: string) => string }) {
|
||||
const total = data.total_count || 0
|
||||
const pct = (count: number) => total > 0 ? `${Math.round(count / total * 100)}%` : '0%'
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-3 mb-6">
|
||||
{[
|
||||
{ label: '正常', count: data.healthy_count, color: 'text-status-healthy', bg: 'bg-status-healthy/10' },
|
||||
{ label: '警告', count: data.warning_count, color: 'text-status-warning', bg: 'bg-status-warning/10' },
|
||||
{ label: '異常', count: data.critical_count, color: 'text-status-critical', bg: 'bg-status-critical/10' },
|
||||
{ label: t('healthy'), count: data.healthy_count, color: 'text-status-healthy', bg: 'bg-status-healthy/10' },
|
||||
{ label: t('warning'), count: data.warning_count, color: 'text-status-warning', bg: 'bg-status-warning/10' },
|
||||
{ label: t('critical'), count: data.critical_count, color: 'text-status-critical', bg: 'bg-status-critical/10' },
|
||||
].map(item => (
|
||||
<div key={item.label} className={cn('rounded-lg border p-4 text-center', item.bg, 'border-transparent')}>
|
||||
<div className={cn('text-3xl font-bold font-body tabular-nums', item.color)}>
|
||||
{item.count}
|
||||
</div>
|
||||
<div className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-wider mt-1">
|
||||
{item.label} · {Math.round(item.count / total * 100)}%
|
||||
{item.label} · {pct(item.count)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -101,7 +101,11 @@ function HealthSummary({ data }: { data: DashboardResponse }) {
|
||||
// =============================================================================
|
||||
|
||||
export default function MonitoringPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations()
|
||||
const t = useTranslations('monitoring')
|
||||
const tNav = useTranslations('nav')
|
||||
const tCommon = useTranslations('common')
|
||||
const tAlerts = useTranslations('alerts')
|
||||
const hosts = useHosts()
|
||||
|
||||
const { metrics, isLoading: metricsLoading } = useGlobalPulseMetrics({
|
||||
pollInterval: 30000,
|
||||
@@ -153,10 +157,10 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
<div>
|
||||
<h2 className="font-heading text-2xl font-bold text-nothing-black flex items-center gap-2">
|
||||
<Monitor className="w-6 h-6" />
|
||||
{t('nav.monitoring')}
|
||||
{tNav('monitoring')}
|
||||
</h2>
|
||||
<p className="mt-1 text-sm text-nothing-gray-500 font-body">
|
||||
{t('alerts.autoRefresh', { seconds: 30 })}
|
||||
{tAlerts('autoRefresh', { seconds: 30 })}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
@@ -170,7 +174,7 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={cn('w-3.5 h-3.5', isLoading && 'animate-spin')} />
|
||||
{t('common.refresh')}
|
||||
{tCommon('refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -183,12 +187,12 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
)}
|
||||
|
||||
{/* Health Summary */}
|
||||
{dashboard && <HealthSummary data={dashboard} />}
|
||||
{dashboard && <HealthSummary data={dashboard} t={t} />}
|
||||
|
||||
{/* Gold Metrics */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
黃金指標 (Gold Metrics)
|
||||
{t('goldMetrics')}
|
||||
</h3>
|
||||
{metricsLoading && metrics.length === 0 ? (
|
||||
<div className="h-32 rounded-lg bg-nothing-gray-50 border border-nothing-gray-200 flex items-center justify-center">
|
||||
@@ -202,22 +206,22 @@ export default function MonitoringPage({ params }: { params: { locale: string }
|
||||
{/* Host Grid */}
|
||||
<div className="mb-6">
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
主機狀態 (Four-Host Architecture)
|
||||
{t('hostStatus')}
|
||||
</h3>
|
||||
<HostGrid hosts={[]} />
|
||||
<HostGrid hosts={hosts} />
|
||||
</div>
|
||||
|
||||
{/* Service Table */}
|
||||
{dashboard && dashboard.services && dashboard.services.length > 0 && (
|
||||
<div>
|
||||
<h3 className="text-[11px] font-body text-nothing-gray-500 uppercase tracking-widest mb-3">
|
||||
服務清單
|
||||
{t('serviceList')}
|
||||
</h3>
|
||||
<div className="rounded-lg border border-nothing-gray-200 overflow-hidden">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="bg-nothing-gray-50 border-b border-nothing-gray-200">
|
||||
{['服務名稱', '狀態', '延遲', '可用率', '最後檢查'].map(h => (
|
||||
{[t('serviceName'), t('status'), t('latency'), t('uptime'), t('lastCheck')].map(h => (
|
||||
<th key={h} className="px-4 py-2.5 text-left font-body text-[10px] uppercase text-nothing-gray-500 tracking-wider">
|
||||
{h}
|
||||
</th>
|
||||
|
||||
Reference in New Issue
Block a user