fix(i18n): MonitoringTools 硬編碼中文 → i18n keys + MTTR 趨勢改為真實計算
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

- MonitoringTools: 載入中/無法連線/觸發/正常/離線/版本/統計/更新 → useTranslations
- MTTR 趨勢: '↓2m' hardcode → 前半/後半 resolved incidents 真實比較
- zh-TW.json + en.json: 新增 connectionError/monitoringStatus.firing/metaVersion/metaStats/metaUpdatedAt

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-03 14:36:46 +08:00
parent 778d3cc2e4
commit 7ff0c5c304
3 changed files with 48 additions and 21 deletions

View File

@@ -148,8 +148,14 @@
"monitoringStatus": {
"up": "OK",
"down": "Down",
"unknown": "Unknown"
}
"unknown": "Unknown",
"firing": "firing",
"alert": "alerts"
},
"connectionError": "Connection failed",
"metaVersion": "Version",
"metaStats": "Stats",
"metaUpdatedAt": "Updated"
},
"openclaw": {
"name": "OpenClaw",
@@ -911,4 +917,4 @@
"noData": "--",
"comingSoon": "Integration pending"
}
}
}

View File

@@ -149,8 +149,14 @@
"monitoringStatus": {
"up": "正常",
"down": "離線",
"unknown": "未知"
}
"unknown": "未知",
"firing": "觸發",
"alert": "告警"
},
"connectionError": "無法連線",
"metaVersion": "版本",
"metaStats": "統計",
"metaUpdatedAt": "更新"
},
"openclaw": {
"name": "OpenClaw",
@@ -912,4 +918,4 @@
"noData": "--",
"comingSoon": "資料尚未整合"
}
}
}

View File

@@ -109,6 +109,8 @@ const TOOL_EMOJI: Record<string, string> = {
}
function MonitoringTools() {
const tDash = useTranslations('dashboard')
const tCommon = useTranslations('common')
const [tools, setTools] = useState<MonitoringTool[]>([])
const [loading, setLoading] = useState(true)
@@ -125,10 +127,10 @@ function MonitoringTools() {
}, [])
if (loading) return (
<div style={{ padding: '12px 14px', fontSize: 12, color: '#87867f' }}>...</div>
<div style={{ padding: '12px 14px', fontSize: 12, color: '#87867f' }}>{tCommon('loading')}</div>
)
if (tools.length === 0) return (
<div style={{ padding: '12px 14px', fontSize: 12, color: '#cc2200' }}></div>
<div style={{ padding: '12px 14px', fontSize: 12, color: '#cc2200' }}>{tDash('connectionError')}</div>
)
return (
@@ -137,7 +139,7 @@ function MonitoringTools() {
const isUp = tool.status === 'up'
const hasFiring = (tool.firing_count ?? 0) > 0
const statusColor = isUp ? (hasFiring ? '#F59E0B' : '#22C55E') : '#cc2200'
const statusText = isUp ? (hasFiring ? `${tool.firing_count} 觸發` : '正常') : '離線'
const statusText = isUp ? (hasFiring ? `${tool.firing_count} ${tDash('monitoringStatus.firing')}` : tDash('monitoringStatus.up')) : tDash('monitoringStatus.down')
const accentColor = TOOL_ACCENT_COLOR[tool.name] ?? '#b0ad9f'
const emoji = TOOL_EMOJI[tool.name] ?? '🔧'
const link = TOOL_LINKS[tool.name] ?? '#'
@@ -218,18 +220,18 @@ function MonitoringTools() {
}}>
{tool.version && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 14, fontSize: 10 }}>
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}></span>
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>{tDash('metaVersion')}</span>
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>v{tool.version}</span>
</div>
)}
{tool.stats && (
<div style={{ display: 'flex', alignItems: 'center', gap: 4, paddingRight: 14, fontSize: 10 }}>
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}></span>
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>{tDash('metaStats')}</span>
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>{tool.stats}</span>
</div>
)}
<div style={{ marginLeft: 'auto', display: 'flex', alignItems: 'center', gap: 4, fontSize: 10 }}>
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}></span>
<span style={{ color: '#b0ad9f', whiteSpace: 'nowrap' }}>{tDash('metaUpdatedAt')}</span>
<span style={{ color: '#141413', fontWeight: 600, whiteSpace: 'nowrap' }}>{timeStr}</span>
</div>
</div>
@@ -386,16 +388,29 @@ export default function Home({ params }: { params: { locale: string } }) {
return Math.round((resolved / incidents.length) * 100)
})()
// MTTR 均值
const mttrAvg = (() => {
if (!incidents?.length) return '--'
// MTTR 均值 + 趨勢(前半 vs 後半比較)
const { mttrAvg, mttrTrend } = (() => {
if (!incidents?.length) return { mttrAvg: '--', mttrTrend: undefined }
const resolved = incidents.filter(i => i.updated_at && (i.status === 'resolved' || i.status === 'closed'))
if (!resolved.length) return '--'
const avgMs = resolved.reduce((sum, i) =>
sum + (new Date(i.updated_at).getTime() - new Date(i.created_at).getTime()), 0
) / resolved.length
if (!resolved.length) return { mttrAvg: '--', mttrTrend: undefined }
const durs = resolved.map(i => new Date(i.updated_at).getTime() - new Date(i.created_at).getTime())
const avgMs = durs.reduce((a, b) => a + b, 0) / durs.length
const mins = Math.round(avgMs / 60000)
return mins < 60 ? `${mins}m` : `${(mins / 60).toFixed(1)}h`
const avgStr = mins < 60 ? `${mins}m` : `${(mins / 60).toFixed(1)}h`
// 趨勢比較前半與後半需至少4筆
let trend: { text: string; color: string } | undefined
if (durs.length >= 4) {
const half = Math.floor(durs.length / 2)
const older = durs.slice(0, half).reduce((a, b) => a + b, 0) / half
const newer = durs.slice(half).reduce((a, b) => a + b, 0) / (durs.length - half)
const diffMins = Math.round((newer - older) / 60000)
if (Math.abs(diffMins) >= 1) {
trend = diffMins < 0
? { text: `${Math.abs(diffMins)}m`, color: '#22C55E' }
: { text: `${diffMins}m`, color: '#d97757' }
}
}
return { mttrAvg: avgStr, mttrTrend: trend }
})()
// (pulseMetrics reserved for future sparklines)
@@ -483,7 +498,7 @@ export default function Home({ params }: { params: { locale: string } }) {
{
label: tDashboard('mttrAvg'),
value: mttrAvg,
trend: mttrAvg !== '--' ? { text: '↓2m', color: '#22C55E' } : undefined,
trend: mttrTrend,
extra: (
<svg width="60" height="12" viewBox="0 0 60 12" fill="none">
<polyline points="0,2 10,3 20,2 30,5 40,4 50,7 60,9" stroke="#22C55E" strokeWidth="1.5" fill="none" strokeLinecap="round" strokeLinejoin="round"/>