fix(web): QA 掃描 — alert-operation-logs i18n + classic emoji→icon + knowledge 載入中
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 12m28s
All checks were successful
CD Pipeline / build-and-deploy (push) Successful in 12m28s
- alert-operation-logs: 30+ 處硬編碼中文改 useTranslations (18 event types + UI) - classic: 告警 badge + 等待確認 + TOOL_EMOJI → Lucide icon - knowledge: 載入中 → common.loading - 新增 alertOpLogs i18n section (zh-TW + en) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1137,5 +1137,44 @@
|
||||
"dispositionManual": "Manual Resolved",
|
||||
"dispositionCold": "Cold Start Trust",
|
||||
"autoRateLabel": "Automation Rate"
|
||||
},
|
||||
"alertOpLogs": {
|
||||
"title": "Alert Operation Logs",
|
||||
"subtitle": "alert_operation_log · Full event stream",
|
||||
"refresh": "Refresh",
|
||||
"totalEvents24h": "24h Total Events",
|
||||
"allEventTypes": "All Event Types",
|
||||
"incidentIdFilter": "Filter by Incident ID...",
|
||||
"totalCount": "{count} total",
|
||||
"colTime": "Time",
|
||||
"colEventType": "Event Type",
|
||||
"colIncident": "Incident",
|
||||
"colActor": "Actor",
|
||||
"colDetail": "Detail",
|
||||
"colResult": "Result",
|
||||
"loading": "Loading...",
|
||||
"noRecords": "No records",
|
||||
"loadError": "Failed to load, please retry",
|
||||
"pageInfo": "Page {page} / {total}",
|
||||
"prevPage": "Previous",
|
||||
"nextPage": "Next",
|
||||
"eventAlertReceived": "Alert Received",
|
||||
"eventTelegramSent": "TG Notified",
|
||||
"eventUserAction": "User Action",
|
||||
"eventAutoRepairTriggered": "Auto Repair",
|
||||
"eventExecutionStarted": "Execution Started",
|
||||
"eventExecutionCompleted": "Execution Completed",
|
||||
"eventTelegramResultSent": "TG Result",
|
||||
"eventResolved": "Resolved",
|
||||
"eventSilenced": "Silenced",
|
||||
"eventEscalated": "Escalated",
|
||||
"eventGuardrailBlocked": "Guardrail Blocked",
|
||||
"eventPreFlightPassed": "Pre-flight Passed",
|
||||
"eventPreFlightFailed": "Pre-flight Failed",
|
||||
"eventBackupTriggered": "Backup Triggered",
|
||||
"eventBackupCompleted": "Backup Completed",
|
||||
"eventBackupFailed": "Backup Failed",
|
||||
"eventApprovalEscalated": "Approval Escalated",
|
||||
"eventChangeApplied": "Change Applied"
|
||||
}
|
||||
}
|
||||
@@ -1138,5 +1138,44 @@
|
||||
"dispositionManual": "手動處理",
|
||||
"dispositionCold": "冷啟動信任",
|
||||
"autoRateLabel": "自動化率"
|
||||
},
|
||||
"alertOpLogs": {
|
||||
"title": "告警操作日誌",
|
||||
"subtitle": "alert_operation_log · 全事件流追蹤",
|
||||
"refresh": "重新整理",
|
||||
"totalEvents24h": "24h 總事件",
|
||||
"allEventTypes": "全部事件類型",
|
||||
"incidentIdFilter": "Incident ID 篩選...",
|
||||
"totalCount": "共 {count} 筆",
|
||||
"colTime": "時間",
|
||||
"colEventType": "事件類型",
|
||||
"colIncident": "Incident",
|
||||
"colActor": "操作者",
|
||||
"colDetail": "說明",
|
||||
"colResult": "結果",
|
||||
"loading": "載入中...",
|
||||
"noRecords": "無記錄",
|
||||
"loadError": "載入失敗,請重試",
|
||||
"pageInfo": "第 {page} / {total} 頁",
|
||||
"prevPage": "上一頁",
|
||||
"nextPage": "下一頁",
|
||||
"eventAlertReceived": "告警收到",
|
||||
"eventTelegramSent": "TG 通知",
|
||||
"eventUserAction": "用戶操作",
|
||||
"eventAutoRepairTriggered": "自動修復",
|
||||
"eventExecutionStarted": "執行開始",
|
||||
"eventExecutionCompleted": "執行完成",
|
||||
"eventTelegramResultSent": "TG 結果",
|
||||
"eventResolved": "已解決",
|
||||
"eventSilenced": "已靜音",
|
||||
"eventEscalated": "已升級",
|
||||
"eventGuardrailBlocked": "護欄攔截",
|
||||
"eventPreFlightPassed": "預檢通過",
|
||||
"eventPreFlightFailed": "預檢失敗",
|
||||
"eventBackupTriggered": "備份觸發",
|
||||
"eventBackupCompleted": "備份完成",
|
||||
"eventBackupFailed": "備份失敗",
|
||||
"eventApprovalEscalated": "審批升級",
|
||||
"eventChangeApplied": "變更套用"
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,11 @@
|
||||
* - event_type 篩選、incident_id 篩選
|
||||
* - 18 種事件類型顏色標記
|
||||
*
|
||||
* i18n: 介面文字直接使用中文(此頁為內部工具,非公開 i18n)
|
||||
* 變更: 2026-04-09 Claude Sonnet 4.6 Asia/Taipei
|
||||
* @updated 2026-04-09 Claude Opus 4.6 — i18n 全面改用 useTranslations
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
@@ -68,35 +68,36 @@ interface StatsResponse {
|
||||
// Constants
|
||||
// =============================================================================
|
||||
|
||||
const EVENT_TYPE_CONFIG: Record<string, { label: string; color: string; icon: React.ReactNode }> = {
|
||||
ALERT_RECEIVED: { label: '告警收到', color: 'text-blue-400 bg-blue-900/30', icon: <Activity className="h-3 w-3" /> },
|
||||
TELEGRAM_SENT: { label: 'TG 通知', color: 'text-sky-400 bg-sky-900/30', icon: <Zap className="h-3 w-3" /> },
|
||||
USER_ACTION: { label: '用戶操作', color: 'text-purple-400 bg-purple-900/30', icon: <Shield className="h-3 w-3" /> },
|
||||
AUTO_REPAIR_TRIGGERED: { label: '自動修復', color: 'text-yellow-400 bg-yellow-900/30', icon: <Zap className="h-3 w-3" /> },
|
||||
EXECUTION_STARTED: { label: '執行開始', color: 'text-orange-400 bg-orange-900/30', icon: <Clock className="h-3 w-3" /> },
|
||||
EXECUTION_COMPLETED: { label: '執行完成', color: 'text-green-400 bg-green-900/30', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
TELEGRAM_RESULT_SENT: { label: 'TG 結果', color: 'text-sky-300 bg-sky-900/20', icon: <Zap className="h-3 w-3" /> },
|
||||
RESOLVED: { label: '已解決', color: 'text-green-500 bg-green-900/40', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
SILENCED: { label: '已靜音', color: 'text-gray-400 bg-gray-900/30', icon: <Shield className="h-3 w-3" /> },
|
||||
ESCALATED: { label: '已升級', color: 'text-red-400 bg-red-900/30', icon: <AlertTriangle className="h-3 w-3" /> },
|
||||
GUARDRAIL_BLOCKED: { label: '護欄攔截', color: 'text-red-500 bg-red-900/40', icon: <Shield className="h-3 w-3" /> },
|
||||
PRE_FLIGHT_PASSED: { label: '預檢通過', color: 'text-green-400 bg-green-900/30', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
PRE_FLIGHT_FAILED: { label: '預檢失敗', color: 'text-red-400 bg-red-900/30', icon: <XCircle className="h-3 w-3" /> },
|
||||
BACKUP_TRIGGERED: { label: '備份觸發', color: 'text-blue-300 bg-blue-900/20', icon: <Database className="h-3 w-3" /> },
|
||||
BACKUP_COMPLETED: { label: '備份完成', color: 'text-green-400 bg-green-900/30', icon: <Database className="h-3 w-3" /> },
|
||||
BACKUP_FAILED: { label: '備份失敗', color: 'text-red-400 bg-red-900/30', icon: <Database className="h-3 w-3" /> },
|
||||
APPROVAL_ESCALATED: { label: '審批升級', color: 'text-orange-400 bg-orange-900/30', icon: <AlertTriangle className="h-3 w-3" /> },
|
||||
CHANGE_APPLIED: { label: '變更套用', color: 'text-teal-400 bg-teal-900/30', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
const EVENT_TYPE_STYLE: Record<string, { i18nKey: string; color: string; icon: React.ReactNode }> = {
|
||||
ALERT_RECEIVED: { i18nKey: 'eventAlertReceived', color: 'text-blue-400 bg-blue-900/30', icon: <Activity className="h-3 w-3" /> },
|
||||
TELEGRAM_SENT: { i18nKey: 'eventTelegramSent', color: 'text-sky-400 bg-sky-900/30', icon: <Zap className="h-3 w-3" /> },
|
||||
USER_ACTION: { i18nKey: 'eventUserAction', color: 'text-purple-400 bg-purple-900/30', icon: <Shield className="h-3 w-3" /> },
|
||||
AUTO_REPAIR_TRIGGERED: { i18nKey: 'eventAutoRepairTriggered', color: 'text-yellow-400 bg-yellow-900/30', icon: <Zap className="h-3 w-3" /> },
|
||||
EXECUTION_STARTED: { i18nKey: 'eventExecutionStarted', color: 'text-orange-400 bg-orange-900/30', icon: <Clock className="h-3 w-3" /> },
|
||||
EXECUTION_COMPLETED: { i18nKey: 'eventExecutionCompleted', color: 'text-green-400 bg-green-900/30', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
TELEGRAM_RESULT_SENT: { i18nKey: 'eventTelegramResultSent', color: 'text-sky-300 bg-sky-900/20', icon: <Zap className="h-3 w-3" /> },
|
||||
RESOLVED: { i18nKey: 'eventResolved', color: 'text-green-500 bg-green-900/40', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
SILENCED: { i18nKey: 'eventSilenced', color: 'text-gray-400 bg-gray-900/30', icon: <Shield className="h-3 w-3" /> },
|
||||
ESCALATED: { i18nKey: 'eventEscalated', color: 'text-red-400 bg-red-900/30', icon: <AlertTriangle className="h-3 w-3" /> },
|
||||
GUARDRAIL_BLOCKED: { i18nKey: 'eventGuardrailBlocked', color: 'text-red-500 bg-red-900/40', icon: <Shield className="h-3 w-3" /> },
|
||||
PRE_FLIGHT_PASSED: { i18nKey: 'eventPreFlightPassed', color: 'text-green-400 bg-green-900/30', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
PRE_FLIGHT_FAILED: { i18nKey: 'eventPreFlightFailed', color: 'text-red-400 bg-red-900/30', icon: <XCircle className="h-3 w-3" /> },
|
||||
BACKUP_TRIGGERED: { i18nKey: 'eventBackupTriggered', color: 'text-blue-300 bg-blue-900/20', icon: <Database className="h-3 w-3" /> },
|
||||
BACKUP_COMPLETED: { i18nKey: 'eventBackupCompleted', color: 'text-green-400 bg-green-900/30', icon: <Database className="h-3 w-3" /> },
|
||||
BACKUP_FAILED: { i18nKey: 'eventBackupFailed', color: 'text-red-400 bg-red-900/30', icon: <Database className="h-3 w-3" /> },
|
||||
APPROVAL_ESCALATED: { i18nKey: 'eventApprovalEscalated', color: 'text-orange-400 bg-orange-900/30', icon: <AlertTriangle className="h-3 w-3" /> },
|
||||
CHANGE_APPLIED: { i18nKey: 'eventChangeApplied', color: 'text-teal-400 bg-teal-900/30', icon: <CheckCircle2 className="h-3 w-3" /> },
|
||||
}
|
||||
|
||||
const PAGE_SIZE = 50
|
||||
const ALL_EVENT_TYPES = Object.keys(EVENT_TYPE_CONFIG)
|
||||
const ALL_EVENT_TYPES = Object.keys(EVENT_TYPE_STYLE)
|
||||
|
||||
// =============================================================================
|
||||
// Component
|
||||
// =============================================================================
|
||||
|
||||
export default function AlertOperationLogsPage() {
|
||||
export default function AlertOperationLogsPage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('alertOpLogs')
|
||||
const [logs, setLogs] = useState<AlertOpLog[]>([])
|
||||
const [total, setTotal] = useState(0)
|
||||
const [page, setPage] = useState(1)
|
||||
@@ -109,6 +110,20 @@ export default function AlertOperationLogsPage() {
|
||||
|
||||
const apiBase = process.env.NEXT_PUBLIC_API_URL ?? ''
|
||||
|
||||
const getEventLabel = (et: string) => {
|
||||
const style = EVENT_TYPE_STYLE[et]
|
||||
return style ? t(style.i18nKey as any) : et
|
||||
}
|
||||
|
||||
const getEventConfig = (et: string) => {
|
||||
const style = EVENT_TYPE_STYLE[et]
|
||||
return {
|
||||
label: style ? t(style.i18nKey as any) : et,
|
||||
color: style?.color ?? 'text-gray-400 bg-gray-900/30',
|
||||
icon: style?.icon ?? <Activity className="h-3 w-3" />,
|
||||
}
|
||||
}
|
||||
|
||||
const fetchLogs = useCallback(async (pageNum: number, evType: string, incId: string) => {
|
||||
abortRef.current?.abort()
|
||||
abortRef.current = new AbortController()
|
||||
@@ -132,12 +147,12 @@ export default function AlertOperationLogsPage() {
|
||||
setTotal(data.total)
|
||||
} catch (err) {
|
||||
if ((err as Error).name !== 'AbortError') {
|
||||
setError('載入失敗,請重試')
|
||||
setError(t('loadError'))
|
||||
}
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [apiBase])
|
||||
}, [apiBase, t])
|
||||
|
||||
const fetchStats = useCallback(async () => {
|
||||
try {
|
||||
@@ -162,25 +177,22 @@ export default function AlertOperationLogsPage() {
|
||||
return d.toLocaleString('zh-TW', { timeZone: 'Asia/Taipei', hour12: false })
|
||||
}
|
||||
|
||||
const getEventConfig = (et: string) =>
|
||||
EVENT_TYPE_CONFIG[et] ?? { label: et, color: 'text-gray-400 bg-gray-900/30', icon: <Activity className="h-3 w-3" /> }
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<AppLayout locale={params.locale}>
|
||||
<div className="p-6 space-y-6 max-w-7xl mx-auto">
|
||||
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-xl font-mono font-semibold text-white">告警操作日誌</h1>
|
||||
<p className="text-xs text-gray-500 mt-0.5 font-mono">alert_operation_log · 全事件流追蹤</p>
|
||||
<h1 className="text-xl font-mono font-semibold text-white">{t('title')}</h1>
|
||||
<p className="text-xs text-gray-500 mt-0.5 font-mono">{t('subtitle')}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => { fetchLogs(page, filterEventType, filterIncidentId); fetchStats() }}
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-mono text-gray-400 hover:text-white border border-gray-700 hover:border-gray-500 rounded transition-colors"
|
||||
>
|
||||
<RefreshCw className={cn('h-3 w-3', loading && 'animate-spin')} />
|
||||
重新整理
|
||||
{t('refresh')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -188,12 +200,12 @@ export default function AlertOperationLogsPage() {
|
||||
{stats && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||
<div className="bg-gray-900 border border-gray-800 rounded p-3">
|
||||
<div className="text-xs text-gray-500 font-mono">24h 總事件</div>
|
||||
<div className="text-xs text-gray-500 font-mono">{t('totalEvents24h')}</div>
|
||||
<div className="text-2xl font-mono font-semibold text-white mt-1">{stats.total.toLocaleString()}</div>
|
||||
</div>
|
||||
{['ALERT_RECEIVED', 'GUARDRAIL_BLOCKED', 'AUTO_REPAIR_TRIGGERED', 'RESOLVED'].map(et => (
|
||||
<div key={et} className="bg-gray-900 border border-gray-800 rounded p-3">
|
||||
<div className="text-xs text-gray-500 font-mono">{getEventConfig(et).label}</div>
|
||||
<div className="text-xs text-gray-500 font-mono">{getEventLabel(et)}</div>
|
||||
<div className="text-2xl font-mono font-semibold text-white mt-1">
|
||||
{(stats.by_event_type[et] ?? 0).toLocaleString()}
|
||||
</div>
|
||||
@@ -209,20 +221,20 @@ export default function AlertOperationLogsPage() {
|
||||
onChange={e => { setFilterEventType(e.target.value); setPage(1) }}
|
||||
className="px-3 py-1.5 text-xs font-mono bg-gray-900 border border-gray-700 text-gray-300 rounded focus:border-gray-500 outline-none"
|
||||
>
|
||||
<option value="">全部事件類型</option>
|
||||
<option value="">{t('allEventTypes')}</option>
|
||||
{ALL_EVENT_TYPES.map(et => (
|
||||
<option key={et} value={et}>{EVENT_TYPE_CONFIG[et]?.label ?? et}</option>
|
||||
<option key={et} value={et}>{getEventLabel(et)}</option>
|
||||
))}
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Incident ID 篩選..."
|
||||
placeholder={t('incidentIdFilter')}
|
||||
value={filterIncidentId}
|
||||
onChange={e => { setFilterIncidentId(e.target.value); setPage(1) }}
|
||||
className="px-3 py-1.5 text-xs font-mono bg-gray-900 border border-gray-700 text-gray-300 rounded focus:border-gray-500 outline-none w-52"
|
||||
/>
|
||||
<span className="text-xs font-mono text-gray-500 self-center">
|
||||
共 {total.toLocaleString()} 筆
|
||||
{t('totalCount', { count: total.toLocaleString() })}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -239,20 +251,20 @@ export default function AlertOperationLogsPage() {
|
||||
<table className="w-full text-xs font-mono">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-800 text-gray-500">
|
||||
<th className="text-left px-4 py-2.5">時間</th>
|
||||
<th className="text-left px-4 py-2.5">事件類型</th>
|
||||
<th className="text-left px-4 py-2.5">Incident</th>
|
||||
<th className="text-left px-4 py-2.5">操作者</th>
|
||||
<th className="text-left px-4 py-2.5">說明</th>
|
||||
<th className="text-left px-4 py-2.5">結果</th>
|
||||
<th className="text-left px-4 py-2.5">{t('colTime')}</th>
|
||||
<th className="text-left px-4 py-2.5">{t('colEventType')}</th>
|
||||
<th className="text-left px-4 py-2.5">{t('colIncident')}</th>
|
||||
<th className="text-left px-4 py-2.5">{t('colActor')}</th>
|
||||
<th className="text-left px-4 py-2.5">{t('colDetail')}</th>
|
||||
<th className="text-left px-4 py-2.5">{t('colResult')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading && (
|
||||
<tr><td colSpan={6} className="text-center py-8 text-gray-600">載入中...</td></tr>
|
||||
<tr><td colSpan={6} className="text-center py-8 text-gray-600">{t('loading')}</td></tr>
|
||||
)}
|
||||
{!loading && logs.length === 0 && (
|
||||
<tr><td colSpan={6} className="text-center py-8 text-gray-600">無記錄</td></tr>
|
||||
<tr><td colSpan={6} className="text-center py-8 text-gray-600">{t('noRecords')}</td></tr>
|
||||
)}
|
||||
{!loading && logs.map(log => {
|
||||
const cfg = getEventConfig(log.event_type)
|
||||
@@ -294,21 +306,21 @@ export default function AlertOperationLogsPage() {
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between text-xs font-mono text-gray-500">
|
||||
<span>第 {page} / {totalPages} 頁</span>
|
||||
<span>{t('pageInfo', { page, total: totalPages })}</span>
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage(p => p - 1)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 border border-gray-700 rounded disabled:opacity-30 hover:border-gray-500 transition-colors"
|
||||
>
|
||||
<ChevronLeft className="h-3 w-3" />上一頁
|
||||
<ChevronLeft className="h-3 w-3" />{t('prevPage')}
|
||||
</button>
|
||||
<button
|
||||
disabled={page >= totalPages}
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
className="flex items-center gap-1 px-3 py-1.5 border border-gray-700 rounded disabled:opacity-30 hover:border-gray-500 transition-colors"
|
||||
>
|
||||
下一頁<ChevronRight className="h-3 w-3" />
|
||||
{t('nextPage')}<ChevronRight className="h-3 w-3" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
import React from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { BarChart3, Flame, Telescope, FlaskConical, Activity, GitBranch } from 'lucide-react'
|
||||
import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics'
|
||||
import { useIncidents } from '@/hooks/useIncidents'
|
||||
import { useHosts, useDashboardStore } from '@/stores/dashboard.store'
|
||||
@@ -93,14 +94,14 @@ const TOOL_ACCENT_COLOR: Record<string, string> = {
|
||||
Gitea: '#22C55E',
|
||||
}
|
||||
|
||||
// 圖示 emoji
|
||||
const TOOL_EMOJI: Record<string, string> = {
|
||||
Grafana: '📊',
|
||||
Prometheus: '🔥',
|
||||
Sentry: '🔭',
|
||||
Langfuse: '🧪',
|
||||
SigNoz: '🔭',
|
||||
Gitea: '🐙',
|
||||
// 圖示 Lucide icon (feedback_no_emoji_use_icons.md)
|
||||
const TOOL_ICON: Record<string, React.ReactNode> = {
|
||||
Grafana: <BarChart3 size={16} />,
|
||||
Prometheus: <Flame size={16} />,
|
||||
Sentry: <Telescope size={16} />,
|
||||
Langfuse: <FlaskConical size={16} />,
|
||||
SigNoz: <Activity size={16} />,
|
||||
Gitea: <GitBranch size={16} />,
|
||||
}
|
||||
|
||||
function MonitoringTools() {
|
||||
@@ -136,7 +137,7 @@ function MonitoringTools() {
|
||||
const statusColor = isUp ? (hasFiring ? '#F59E0B' : '#22C55E') : '#cc2200'
|
||||
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 icon = TOOL_ICON[tool.name] ?? <Activity size={16} />
|
||||
const link = tool.url ?? '#'
|
||||
const timeStr = (() => {
|
||||
try { return new Date(tool.checked_at).toLocaleTimeString('zh-TW', { timeZone: 'Asia/Taipei', hour: '2-digit', minute: '2-digit' }) }
|
||||
@@ -174,7 +175,7 @@ function MonitoringTools() {
|
||||
|
||||
{/* 主行 */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 12, paddingLeft: 8 }}>
|
||||
<span style={{ fontSize: 18 }}>{emoji}</span>
|
||||
<span style={{ display: 'inline-flex', color: accentColor }}>{icon}</span>
|
||||
<div style={{ flex: 1 }}>
|
||||
<div style={{ fontSize: 12, fontWeight: 600, color: '#141413', marginBottom: 2 }}>
|
||||
{tool.name}
|
||||
@@ -192,7 +193,7 @@ function MonitoringTools() {
|
||||
fontSize: 10, padding: '1px 7px', borderRadius: 8, fontWeight: 600,
|
||||
background: 'rgba(245,158,11,0.12)', color: '#F59E0B',
|
||||
}}>
|
||||
{tool.firing_count} 告警
|
||||
{tDash('alertBadge', { count: tool.firing_count })}
|
||||
</span>
|
||||
) : (
|
||||
<span style={{
|
||||
@@ -200,7 +201,7 @@ function MonitoringTools() {
|
||||
fontSize: 10, padding: '1px 7px', borderRadius: 8, fontWeight: 600,
|
||||
background: 'rgba(34,197,94,0.1)', color: '#22C55E',
|
||||
}}>
|
||||
0 告警
|
||||
{tDash('alertBadgeZero')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
@@ -502,7 +503,7 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
label: tDashboard('pendingApprovals'),
|
||||
value: pendingApprovals ?? '--',
|
||||
sub: hasPendingApprovals ? undefined : tDashboard('stable'),
|
||||
badge: hasPendingApprovals ? { text: `⏳ 等待確認`, color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
badge: hasPendingApprovals ? { text: tDashboard('awaitingConfirm'), color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
valueColor: hasPendingApprovals ? '#F59E0B' : undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,14 +8,16 @@
|
||||
*/
|
||||
|
||||
import { lazy, Suspense } from 'react'
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
|
||||
const KnowledgeBaseContent = lazy(() => import('@/app/[locale]/knowledge-base/page'))
|
||||
|
||||
export default function KnowledgePage({ params }: { params: { locale: string } }) {
|
||||
const t = useTranslations('common')
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
<Suspense fallback={<div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>載入中...</div>}>
|
||||
<Suspense fallback={<div style={{ padding: 32, textAlign: 'center', color: '#87867f' }}>{t('loading')}</div>}>
|
||||
<KnowledgeBaseContent params={params} />
|
||||
</Suspense>
|
||||
</AppLayout>
|
||||
|
||||
Reference in New Issue
Block a user