fix(web): monitoring 頁 QA 修復 — NAN% + HostGrid + i18n
Some checks failed
E2E Health Check / e2e-health (push) Successful in 17s
CD Pipeline / build-and-deploy (push) Has been cancelled

- 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:
OG T
2026-04-02 13:55:29 +08:00
parent 6ce82ff883
commit f0f9cc87a1
3 changed files with 50 additions and 20 deletions

View File

@@ -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"
}
}

View File

@@ -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": "最後檢查"
}
}

View File

@@ -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>