fix(review): 架構審查修復 — P0 import crash + i18n 零 hardcode + 靜默錯誤

P0:
- proposal_service.py: 補 get_redis + INCIDENT_KEY_PREFIX import
  (修前: resolve_incident_after_approval 必 NameError crash)

P1 i18n:
- page.tsx: 拓撲群組移除 emoji,改用 tTopo() i18n key
- page.tsx: 主機標籤 (DevOps金庫等) 改 tTopo() i18n
- ai-model-status.tsx: 加 useTranslations,AI 模型狀態 → t('aiModelStatus')
- disposition-mini.tsx: 查看完整報表 → t('viewAllReport')
- recent-activity.tsx: 查看活動串流 → t('viewAllAlerts')

P2 品質:
- pending-approvals-card.tsx: approve/reject 加 r.ok 檢查+錯誤顯示,查看全部授權加路由+i18n
- page-tabs.tsx: TabSkeleton 載入中... → t('loading')
- page.tsx: ↑5% → tDashboard('trendUp', {pct}) 動態值
- page.tsx: Prometheus '23' hardcode → '-- targets'

i18n 新增 key (zh-TW + en 同步):
- dashboard: viewAllAlerts/viewAllAuth/viewAllReport/aiModelStatus/loading/trendUp
- topology: groupExternal/allReachable/investigating/hostDevops/hostAiData/hostK3sMaster/hostK3sWorker

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 18:34:50 +08:00
parent 309fe04698
commit 890e2a9568
9 changed files with 68 additions and 22 deletions

View File

@@ -7,6 +7,7 @@
*/
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -17,6 +18,7 @@ interface ModelInfo {
}
export function AIModelStatus() {
const t = useTranslations('dashboard')
const [models, setModels] = useState<ModelInfo[]>([
{ name: 'OpenClaw Nemo', tag: 'local', healthy: false },
{ name: 'Ollama gemma3', tag: 'local', healthy: false },
@@ -50,7 +52,7 @@ export function AIModelStatus() {
display: 'flex', alignItems: 'center', gap: 8, background: '#faf9f3',
}}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413' }}>AI </span>
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413' }}>{t('aiModelStatus')}</span>
</div>
<div style={{ padding: 14, display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 6 }}>
{models.map(m => (

View File

@@ -68,7 +68,7 @@ export function DispositionMini() {
}}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>{t('dispositionBreakdown')}</span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}> </span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}>{t('viewAllReport')} </span>
</div>
<div style={{ padding: 14, display: 'flex', gap: 14, alignItems: 'center' }}>
{/* 環形圖 */}

View File

@@ -8,6 +8,8 @@
import { useState, useEffect } from 'react'
import { useTranslations } from 'next-intl'
import { useRouter } from 'next/navigation'
import { useLocale } from 'next-intl'
const API_BASE = process.env.NEXT_PUBLIC_API_URL ?? ''
@@ -29,7 +31,10 @@ const RISK_STYLE: Record<string, { bg: string; color: string }> = {
export function PendingApprovalsCard() {
const t = useTranslations('dashboard')
const router = useRouter()
const locale = useLocale()
const [approvals, setApprovals] = useState<Approval[]>([])
const [actionError, setActionError] = useState<string | null>(null)
useEffect(() => {
fetch(`${API_BASE}/api/v1/approvals/pending`)
@@ -52,7 +57,7 @@ export function PendingApprovalsCard() {
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#F59E0B' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413' }}>{t('pendingApprovalsTitle')}</span>
<span style={{ fontSize: 11, background: 'rgba(249,115,22,0.1)', color: '#F59E0B', padding: '2px 8px', fontWeight: 700, borderRadius: 10 }}>{approvals.length}</span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}> </span>
<span onClick={() => router.push(`/${locale}/authorizations`)} style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}>{t('viewAllAuth')} </span>
</div>
<div style={{ padding: 14, display: 'flex', flexDirection: 'column', gap: 8 }}>
{approvals.slice(0, 3).map((ap, i) => {
@@ -74,17 +79,21 @@ export function PendingApprovalsCard() {
<div style={{ display: 'flex', gap: 4, marginTop: 5 }}>
<button
onClick={() => {
setActionError(null)
fetch(`${API_BASE}/api/v1/approvals/${ap.id}/sign`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ signer: 'web-ui' }) })
.then(r => { if (!r.ok) throw new Error(`${r.status}`); return r })
.then(() => setApprovals(prev => prev.filter(x => x.id !== ap.id)))
.catch(() => {})
.catch(e => setActionError(`approve failed: ${e.message}`))
}}
style={{ flex: 1, padding: 6, border: 'none', borderRadius: 5, fontSize: 11, fontWeight: 600, cursor: 'pointer', background: '#22C55E', color: '#fff' }}
>{t('approve')}</button>
<button
onClick={() => {
setActionError(null)
fetch(`${API_BASE}/api/v1/approvals/${ap.id}/reject`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ reason: 'rejected-from-web' }) })
.then(r => { if (!r.ok) throw new Error(`${r.status}`); return r })
.then(() => setApprovals(prev => prev.filter(x => x.id !== ap.id)))
.catch(() => {})
.catch(e => setActionError(`reject failed: ${e.message}`))
}}
style={{ flex: 1, padding: 6, border: '0.5px solid #e0ddd4', borderRadius: 5, fontSize: 11, cursor: 'pointer', background: '#fff', color: '#87867f' }}
>{t('reject')}</button>
@@ -93,6 +102,9 @@ export function PendingApprovalsCard() {
)
})}
</div>
{actionError && (
<div style={{ padding: '4px 14px 8px', fontSize: 11, color: '#cc2200' }}>{actionError}</div>
)}
</div>
)
}

View File

@@ -52,7 +52,7 @@ export function RecentActivity() {
}}>
<div style={{ width: 6, height: 6, borderRadius: '50%', background: '#d97757' }} />
<span style={{ fontSize: 14, fontWeight: 700, color: '#141413', letterSpacing: '0.5px' }}>{t('activityStream')}</span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}> </span>
<span style={{ marginLeft: 'auto', fontSize: 11, color: '#4A90D9', cursor: 'pointer', fontWeight: 500 }}>{t('viewAllAlerts')} </span>
</div>
<div style={{ padding: '10px 14px' }}>
{logs.map((log, i) => {