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:
@@ -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 => (
|
||||
|
||||
@@ -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' }}>
|
||||
{/* 環形圖 */}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user