fix(web): 首席架構師 P0 修正 — i18n 硬編碼 + 效能輪詢
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
C1: 首頁 4 Tab 30+ 處硬編碼中文改為 useTranslations - 新增 dashboard.tabs.* / alertEvents / approve / reject 等 30+ i18n key - zh-TW + en 雙語同步 C3: automation/operations Loading 改用 LobsterLoading (i18n) I1: 100ms setInterval 改為 popstate + 1s 低頻備援 (效能 10x 改善) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -163,7 +163,39 @@
|
||||
"connectionError": "Connection failed",
|
||||
"metaVersion": "Version",
|
||||
"metaStats": "Stats",
|
||||
"metaUpdatedAt": "Updated"
|
||||
"metaUpdatedAt": "Updated",
|
||||
"tabs": {
|
||||
"overview": "Overview",
|
||||
"alerts": "Alerts & Approvals",
|
||||
"stream": "Activity Stream",
|
||||
"disposition": "Disposition Stats"
|
||||
},
|
||||
"alertEvents": "Alert Events",
|
||||
"noActiveAlerts": "No active alerts",
|
||||
"pendingApprovalsTitle": "Pending Approvals",
|
||||
"noPendingApprovals": "No pending approvals",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"activityStream": "System Activity Stream",
|
||||
"sseConnected": "SSE Connected",
|
||||
"sseDisconnected": "Disconnected",
|
||||
"waitingEvents": "Waiting for events...",
|
||||
"statusLabel": "Status",
|
||||
"hostsLabel": "Hosts",
|
||||
"eventsCount": "{count} events",
|
||||
"noDispositionData": "No disposition data available",
|
||||
"totalDispositions": "Total Dispositions",
|
||||
"autoRate": "Automation Rate",
|
||||
"humanRate": "Human Intervention Rate",
|
||||
"autoRepairLabel": "Auto Repair",
|
||||
"humanApprovedLabel": "Human Approved",
|
||||
"manualResolvedLabel": "Manual Resolved",
|
||||
"coldStartLabel": "Cold Start",
|
||||
"dispositionBreakdown": "Disposition Breakdown",
|
||||
"hostView": "Hosts",
|
||||
"topoView": "Topology",
|
||||
"waitingHostData": "Waiting for host data...",
|
||||
"dashboardConnecting": "Dashboard API connecting..."
|
||||
},
|
||||
"openclaw": {
|
||||
"name": "OpenClaw",
|
||||
|
||||
@@ -164,7 +164,39 @@
|
||||
"connectionError": "無法連線",
|
||||
"metaVersion": "版本",
|
||||
"metaStats": "統計",
|
||||
"metaUpdatedAt": "更新"
|
||||
"metaUpdatedAt": "更新",
|
||||
"tabs": {
|
||||
"overview": "戰情總覽",
|
||||
"alerts": "告警 & 授權",
|
||||
"stream": "活動串流",
|
||||
"disposition": "處置統計"
|
||||
},
|
||||
"alertEvents": "告警事件",
|
||||
"noActiveAlerts": "目前無活躍告警",
|
||||
"pendingApprovalsTitle": "待批准授權",
|
||||
"noPendingApprovals": "無待批准項目",
|
||||
"approve": "批准",
|
||||
"reject": "拒絕",
|
||||
"activityStream": "系統活動串流",
|
||||
"sseConnected": "SSE 連線中",
|
||||
"sseDisconnected": "連線中斷",
|
||||
"waitingEvents": "等待即時事件...",
|
||||
"statusLabel": "狀態",
|
||||
"hostsLabel": "主機",
|
||||
"eventsCount": "{count} 筆",
|
||||
"noDispositionData": "目前無處置統計資料",
|
||||
"totalDispositions": "處置總次數",
|
||||
"autoRate": "自動化率",
|
||||
"humanRate": "人工介入率",
|
||||
"autoRepairLabel": "自動修復",
|
||||
"humanApprovedLabel": "人工審核",
|
||||
"manualResolvedLabel": "手動處理",
|
||||
"coldStartLabel": "冷啟動",
|
||||
"dispositionBreakdown": "處置方式分佈",
|
||||
"hostView": "主機",
|
||||
"topoView": "拓撲",
|
||||
"waitingHostData": "等待主機資料...",
|
||||
"dashboardConnecting": "Dashboard API 連線中"
|
||||
},
|
||||
"openclaw": {
|
||||
"name": "OpenClaw",
|
||||
|
||||
@@ -11,13 +11,15 @@ import { lazy, Suspense } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { PageTabs, type TabConfig } from '@/components/layout/page-tabs'
|
||||
import { LobsterLoading } from '@/components/shared/lobster-loading'
|
||||
|
||||
const AutoRepairContent = lazy(() => import('@/app/[locale]/auto-repair/page'))
|
||||
const NeuralCommandContent = lazy(() => import('@/app/[locale]/neural-command/page'))
|
||||
const DriftContent = lazy(() => import('@/app/[locale]/drift/page'))
|
||||
|
||||
// C3 修正: 用 LobsterLoading 取代硬編碼「載入中」
|
||||
function Loading() {
|
||||
return <div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>載入中...</div>
|
||||
return <LobsterLoading size="sm" />
|
||||
}
|
||||
|
||||
export default function AutomationPage({ params }: { params: { locale: string } }) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { lazy, Suspense } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { PageTabs, type TabConfig } from '@/components/layout/page-tabs'
|
||||
import { LobsterLoading } from '@/components/shared/lobster-loading'
|
||||
|
||||
const DeploymentsContent = lazy(() => import('@/app/[locale]/deployments/page'))
|
||||
const TicketsContent = lazy(() => import('@/app/[locale]/tickets/page'))
|
||||
@@ -19,7 +20,7 @@ const ActionLogsContent = lazy(() => import('@/app/[locale]/action-logs/page'))
|
||||
const BillingContent = lazy(() => import('@/app/[locale]/billing/page'))
|
||||
|
||||
function Loading() {
|
||||
return <div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>載入中...</div>
|
||||
return <LobsterLoading size="sm" />
|
||||
}
|
||||
|
||||
export default function OperationsPage({ params }: { params: { locale: string } }) {
|
||||
|
||||
@@ -57,9 +57,9 @@ function AlertsAndApprovalsTab() {
|
||||
<div style={{ display: 'flex', gap: 20, padding: '16px 20px', flex: 1, overflow: 'hidden' }}>
|
||||
{/* 左: 告警列表 */}
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12, color: '#141413' }}>告警事件 ({alerts.length})</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12, color: '#141413' }}>{t('alertEvents')} ({alerts.length})</div>
|
||||
{alerts.length === 0 ? (
|
||||
<div style={{ padding: 24, textAlign: 'center', color: '#87867f' }}>目前無活躍告警</div>
|
||||
<div style={{ padding: 24, textAlign: 'center', color: '#87867f' }}>{t('noActiveAlerts')}</div>
|
||||
) : alerts.map((a: any, i: number) => (
|
||||
<div key={a.incident_id || i} style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '10px 14px', marginBottom: 8 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
@@ -72,16 +72,16 @@ function AlertsAndApprovalsTab() {
|
||||
</div>
|
||||
{/* 右: 授權待批准 */}
|
||||
<div style={{ width: 400, flexShrink: 0, overflowY: 'auto' }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12, color: '#141413' }}>待批准授權 ({approvals.length})</div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, marginBottom: 12, color: '#141413' }}>{t('pendingApprovalsTitle')} ({approvals.length})</div>
|
||||
{approvals.length === 0 ? (
|
||||
<div style={{ padding: 24, textAlign: 'center', color: '#87867f' }}>無待批准項目</div>
|
||||
<div style={{ padding: 24, textAlign: 'center', color: '#87867f' }}>{t('noPendingApprovals')}</div>
|
||||
) : approvals.map((ap: any, i: number) => (
|
||||
<div key={ap.id || i} style={{ background: '#faf9f3', border: '0.5px solid #e0ddd4', borderRadius: 8, padding: '10px 12px', marginBottom: 8 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#cc2200' }}>{ap.action || ap.title || '--'}</div>
|
||||
<div style={{ fontSize: 10, color: '#555550', marginTop: 2, fontFamily: "'JetBrains Mono', monospace" }}>{ap.resource || '--'}</div>
|
||||
<div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
|
||||
<button style={{ flex: 1, padding: '5px 0', border: 'none', borderRadius: 5, fontSize: 11, fontWeight: 600, cursor: 'pointer', background: '#22C55E', color: '#fff' }}>批准</button>
|
||||
<button style={{ flex: 1, padding: '5px 0', border: '0.5px solid #e0ddd4', borderRadius: 5, fontSize: 11, cursor: 'pointer', background: '#fff', color: '#87867f' }}>拒絕</button>
|
||||
<button style={{ flex: 1, padding: '5px 0', border: 'none', borderRadius: 5, fontSize: 11, fontWeight: 600, cursor: 'pointer', background: '#22C55E', color: '#fff' }}>{t('approve')}</button>
|
||||
<button style={{ flex: 1, padding: '5px 0', border: '0.5px solid #e0ddd4', borderRadius: 5, fontSize: 11, cursor: 'pointer', background: '#fff', color: '#87867f' }}>{t('reject')}</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -95,7 +95,7 @@ function AlertsAndApprovalsTab() {
|
||||
// =============================================================================
|
||||
|
||||
function ActivityStreamTab() {
|
||||
const tc = useTranslations('common')
|
||||
const t = useTranslations('dashboard')
|
||||
const [events, setEvents] = useState<any[]>([])
|
||||
const [connected, setConnected] = useState(false)
|
||||
|
||||
@@ -116,19 +116,19 @@ function ActivityStreamTab() {
|
||||
<div style={{ padding: '16px 20px', flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
||||
<div style={{ width: 6, height: 6, borderRadius: '50%', background: connected ? '#22C55E' : '#cc2200' }} />
|
||||
<span style={{ fontSize: 14, fontWeight: 700 }}>系統活動串流</span>
|
||||
<span style={{ fontSize: 11, color: '#87867f' }}>{connected ? 'SSE 連線中' : '連線中斷'} · {events.length} 筆</span>
|
||||
<span style={{ fontSize: 14, fontWeight: 700 }}>{t('activityStream')}</span>
|
||||
<span style={{ fontSize: 11, color: '#87867f' }}>{connected ? t('sseConnected') : t('sseDisconnected')} · {events.length}</span>
|
||||
</div>
|
||||
{events.length === 0 ? (
|
||||
<div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>等待即時事件...</div>
|
||||
<div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>{t('waitingEvents')}</div>
|
||||
) : events.map((ev, i) => (
|
||||
<div key={i} style={{ display: 'flex', gap: 8, padding: '6px 0', borderBottom: '0.5px solid #f0ede5', fontSize: 12 }}>
|
||||
<span style={{ fontSize: 10, color: '#87867f', fontFamily: "'JetBrains Mono', monospace", width: 60, flexShrink: 0 }}>{ev._time}</span>
|
||||
<span style={{ width: 5, height: 5, borderRadius: '50%', background: ev.type === 'HOST_UPDATE' ? '#22C55E' : '#4A90D9', marginTop: 5, flexShrink: 0 }} />
|
||||
<span style={{ flex: 1, lineHeight: 1.4 }}>
|
||||
<b>{ev.type || 'EVENT'}</b>
|
||||
{ev.data?.overall_status && ` · 狀態: ${ev.data.overall_status}`}
|
||||
{ev.data?.hosts && ` · ${ev.data.hosts.length} 主機`}
|
||||
{ev.data?.overall_status && ` · ${t('statusLabel')}: ${ev.data.overall_status}`}
|
||||
{ev.data?.hosts && ` · ${ev.data.hosts.length} ${t('hostsLabel')}`}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -141,7 +141,7 @@ function ActivityStreamTab() {
|
||||
// =============================================================================
|
||||
|
||||
function DispositionTab() {
|
||||
const tc = useTranslations('common')
|
||||
const t = useTranslations('dashboard')
|
||||
const [data, setData] = useState<any>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
@@ -158,8 +158,8 @@ function DispositionTab() {
|
||||
const s = data?.summary
|
||||
if (!s || s.total === 0) return (
|
||||
<div style={{ padding: 48, textAlign: 'center', color: '#87867f' }}>
|
||||
<div style={{ fontSize: 32, marginBottom: 8, opacity: 0.3 }}>📊</div>
|
||||
<div>目前無處置統計資料</div>
|
||||
<div style={{ fontSize: 32, marginBottom: 8, opacity: 0.3 }}>--</div>
|
||||
<div>{t('noDispositionData')}</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -170,25 +170,25 @@ function DispositionTab() {
|
||||
{/* KPI 3 卡 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, marginBottom: 16 }}>
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', fontWeight: 500 }}>處置總次數</div>
|
||||
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', fontWeight: 500 }}>{t('totalDispositions')}</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#4A90D9', marginTop: 4 }}>{s.total}</div>
|
||||
</div>
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', fontWeight: 500 }}>自動化率</div>
|
||||
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', fontWeight: 500 }}>{t('autoRate')}</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#22C55E', marginTop: 4 }}>{autoRate}%</div>
|
||||
</div>
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', fontWeight: 500 }}>人工介入率</div>
|
||||
<div style={{ fontSize: 10, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', fontWeight: 500 }}>{t('humanRate')}</div>
|
||||
<div style={{ fontSize: 28, fontWeight: 700, color: '#F59E0B', marginTop: 4 }}>{Math.round(s.human_rate * 100)}%</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* 四大計數 */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 10, marginBottom: 16 }}>
|
||||
{[
|
||||
{ label: '自動修復', count: s.auto_repair, color: '#22C55E', bg: 'rgba(34,197,94,0.06)', border: 'rgba(34,197,94,0.25)' },
|
||||
{ label: '人工審核', count: s.human_approved, color: '#F59E0B', bg: 'rgba(249,115,22,0.06)', border: 'rgba(249,115,22,0.25)' },
|
||||
{ label: '手動處理', count: s.manual_resolved, color: '#A855F7', bg: 'rgba(168,85,247,0.06)', border: 'rgba(168,85,247,0.25)' },
|
||||
{ label: '冷啟動', count: s.cold_start_trust, color: '#4A90D9', bg: 'rgba(59,130,246,0.06)', border: 'rgba(59,130,246,0.25)' },
|
||||
{ label: t('autoRepairLabel'), count: s.auto_repair, color: '#22C55E', bg: 'rgba(34,197,94,0.06)', border: 'rgba(34,197,94,0.25)' },
|
||||
{ label: t('humanApprovedLabel'), count: s.human_approved, color: '#F59E0B', bg: 'rgba(249,115,22,0.06)', border: 'rgba(249,115,22,0.25)' },
|
||||
{ label: t('manualResolvedLabel'), count: s.manual_resolved, color: '#A855F7', bg: 'rgba(168,85,247,0.06)', border: 'rgba(168,85,247,0.25)' },
|
||||
{ label: t('coldStartLabel'), count: s.cold_start_trust, color: '#4A90D9', bg: 'rgba(59,130,246,0.06)', border: 'rgba(59,130,246,0.25)' },
|
||||
].map(item => (
|
||||
<div key={item.label} style={{ background: item.bg, border: `0.5px solid ${item.border}`, borderRadius: 10, padding: 14, textAlign: 'center' }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: item.color, marginBottom: 6 }}>{item.label}</div>
|
||||
@@ -198,7 +198,7 @@ function DispositionTab() {
|
||||
</div>
|
||||
{/* 堆疊分佈條 */}
|
||||
<div style={{ background: '#fff', border: '0.5px solid #e0ddd4', borderRadius: 10, padding: '14px 16px' }}>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', marginBottom: 8 }}>處置方式分佈</div>
|
||||
<div style={{ fontSize: 10, fontWeight: 700, textTransform: 'uppercase', letterSpacing: 0.5, color: '#87867f', marginBottom: 8 }}>{t('dispositionBreakdown')}</div>
|
||||
<div style={{ display: 'flex', height: 12, borderRadius: 6, overflow: 'hidden' }}>
|
||||
{s.total > 0 && <>
|
||||
<div style={{ width: `${(s.auto_repair/s.total)*100}%`, background: '#22C55E' }} />
|
||||
@@ -729,26 +729,27 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
// Sprint 5: 4 Tab 配置 (統帥批准 2026-04-08)
|
||||
const alertsCount = incidents?.length ?? 0
|
||||
const tabs: TabConfig[] = [
|
||||
{ id: 'overview', label: '戰情總覽', content: null }, // Tab 1 用現有內容,下方直接渲染
|
||||
{ id: 'alerts', label: '告警 & 授權', badge: alertsCount > 0 ? alertsCount : undefined, content: <AlertsAndApprovalsTab /> },
|
||||
{ id: 'stream', label: '活動串流', content: <ActivityStreamTab /> },
|
||||
{ id: 'disposition', label: '處置統計', content: <DispositionTab /> },
|
||||
{ id: 'overview', label: tDashboard('tabs.overview'), content: null },
|
||||
{ id: 'alerts', label: tDashboard('tabs.alerts'), badge: alertsCount > 0 ? alertsCount : undefined, content: <AlertsAndApprovalsTab /> },
|
||||
{ id: 'stream', label: tDashboard('tabs.stream'), content: <ActivityStreamTab /> },
|
||||
{ id: 'disposition', label: tDashboard('tabs.disposition'), content: <DispositionTab /> },
|
||||
]
|
||||
|
||||
// Sprint 5: 從 URL 讀取當前 Tab
|
||||
const [activeTabId, setActiveTabId] = useState('overview')
|
||||
const [infraView, setInfraView] = useState<'host' | 'topo'>('host')
|
||||
|
||||
// 每 100ms 檢查 URL query 變化(PageTabs 用 router.push 更新)
|
||||
// I1 修正: popstate 取代 100ms 輪詢
|
||||
useEffect(() => {
|
||||
const check = () => {
|
||||
const params = new URLSearchParams(window.location.search)
|
||||
const tab = params.get('tab') || 'overview'
|
||||
const syncTab = () => {
|
||||
const tab = new URLSearchParams(window.location.search).get('tab') || 'overview'
|
||||
setActiveTabId(prev => prev !== tab ? tab : prev)
|
||||
}
|
||||
check()
|
||||
const interval = setInterval(check, 100)
|
||||
return () => clearInterval(interval)
|
||||
syncTab()
|
||||
window.addEventListener('popstate', syncTab)
|
||||
// PageTabs router.push 後 URL 改變,用 hashchange + 定期低頻同步(1s)作為備援
|
||||
const fallback = setInterval(syncTab, 1000)
|
||||
return () => { window.removeEventListener('popstate', syncTab); clearInterval(fallback) }
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user