feat(homepage): Metrics Strip 7指標視覺強化 + 真實資料串接
- 新增 podHealth/allRunning i18n key (zh-TW + en) - Metrics Strip: 6個指標全部串接真實 API - 活躍事件: incidents count + P0 badge - 服務健康: dashboard services healthy/total + RPS sparkline - 待簽核: dashboard pendingApprovals + 橘色 badge - 自動處置率: incidents resolved rate + error rate sparkline - MTTR 均值: incidents resolved avg duration - POD 健康: dashboard services up/total + 顏色狀態 - Right panel 固定 530px 寬度 (55/45 比例) - 禁止假數據: 無 API 資料時顯示 "--" Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -140,7 +140,10 @@
|
||||
"stable": "Stable",
|
||||
"normal": "Normal",
|
||||
"openclawEngine": "OPENCLAW COGNITIVE ENGINE",
|
||||
"infrastructure": "INFRASTRUCTURE"
|
||||
"infrastructure": "INFRASTRUCTURE",
|
||||
"podHealth": "POD Health",
|
||||
"allRunning": "All Running",
|
||||
"servicesUp": "Services Up"
|
||||
},
|
||||
"openclaw": {
|
||||
"name": "OpenClaw",
|
||||
|
||||
@@ -141,7 +141,10 @@
|
||||
"stable": "穩定",
|
||||
"normal": "正常",
|
||||
"openclawEngine": "OPENCLAW 認知引擎",
|
||||
"infrastructure": "基礎架構"
|
||||
"infrastructure": "基礎架構",
|
||||
"podHealth": "POD 健康",
|
||||
"allRunning": "全部運行中",
|
||||
"servicesUp": "服務上線"
|
||||
},
|
||||
"openclaw": {
|
||||
"name": "OpenClaw",
|
||||
|
||||
@@ -1,45 +1,70 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* AWOOOI AI Center 主頁 (Task 10 重構)
|
||||
* AWOOOI AI Center 主頁
|
||||
* ====================================
|
||||
* 2欄佈局(Sidebar 由 AppLayout 提供): Feed + RightPanel
|
||||
*
|
||||
* 統帥鐵律: 使用真實數據 Hook,禁止假數據!
|
||||
*
|
||||
* @updated 2026-04-02 Claude Code — Metrics Strip 7指標視覺強化
|
||||
* 串接: incidents(count/P0/MTTR/autoRemediation) + dashboard(serviceHealth/pendingApprovals/podHealth)
|
||||
*/
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import { useGlobalPulseMetrics } from '@/hooks/useGlobalPulseMetrics'
|
||||
import { useIncidents } from '@/hooks/useIncidents'
|
||||
import { useHosts } from '@/stores/dashboard.store'
|
||||
import { useHosts, useDashboardStore } from '@/stores/dashboard.store'
|
||||
import { IncidentCard } from '@/components/incident'
|
||||
import { OpenClawPanel } from '@/components/ai/openclaw-panel'
|
||||
import { HostGrid, type HostInfo } from '@/components/infra/host-grid'
|
||||
import { AppLayout } from '@/components/layout'
|
||||
|
||||
// =============================================================================
|
||||
// Mini Sparkline (SVG inline)
|
||||
// =============================================================================
|
||||
|
||||
function MiniSparkline({ values, color }: { values: number[]; color: string }) {
|
||||
if (!values || values.length < 2) return null
|
||||
const min = Math.min(...values)
|
||||
const max = Math.max(...values)
|
||||
const range = max - min || 1
|
||||
const w = 48, h = 16
|
||||
const pts = values.map((v, i) => {
|
||||
const x = (i / (values.length - 1)) * w
|
||||
const y = h - ((v - min) / range) * h
|
||||
return `${x},${y}`
|
||||
}).join(' ')
|
||||
return (
|
||||
<svg width={w} height={h} style={{ display: 'block', flexShrink: 0 }}>
|
||||
<polyline points={pts} fill="none" stroke={color} strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Main Page
|
||||
// =============================================================================
|
||||
|
||||
export default function Home({ params }: { params: { locale: string } }) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const t = useTranslations()
|
||||
const tDashboard = useTranslations('dashboard')
|
||||
const tCommon = useTranslations('common')
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const locale = params.locale
|
||||
const hosts = useHosts()
|
||||
|
||||
// 統帥鐵律: 使用真實數據 Hook,禁止假數據!
|
||||
// dashboard store — pending approvals + overall pod health
|
||||
const pendingApprovals = useDashboardStore(s => s.pendingApprovals)
|
||||
const dashboardHosts = useDashboardStore(s => s.hosts)
|
||||
|
||||
// Gold metrics (RPS / Error Rate / P99 / AI Success + sparklines)
|
||||
const { metrics: pulseMetrics } = useGlobalPulseMetrics({
|
||||
pollInterval: 30000,
|
||||
enablePolling: true,
|
||||
})
|
||||
|
||||
// Phase 7: 真實 Incident 數據
|
||||
// Real incidents
|
||||
const {
|
||||
incidents,
|
||||
pendingApprovals,
|
||||
isLoading: isIncidentsLoading,
|
||||
error: incidentsError,
|
||||
} = useIncidents({
|
||||
@@ -47,189 +72,281 @@ export default function Home({ params }: { params: { locale: string } }) {
|
||||
enablePolling: true,
|
||||
})
|
||||
|
||||
// Metrics Strip 計算
|
||||
// ── Metrics 計算 ────────────────────────────────────────────────────────────
|
||||
|
||||
// 服務健康: dashboard hosts healthy services count
|
||||
const { healthyServices, totalServices } = (() => {
|
||||
let healthy = 0, total = 0
|
||||
for (const h of dashboardHosts) {
|
||||
for (const s of h.services) {
|
||||
total++
|
||||
if (s.status === 'up' || s.status === 'healthy') healthy++
|
||||
}
|
||||
}
|
||||
return { healthyServices: healthy, totalServices: total }
|
||||
})()
|
||||
|
||||
// P0 count
|
||||
const p0Count = incidents?.filter(i => i.severity === 'P0').length ?? 0
|
||||
|
||||
// 自動處置率
|
||||
const autoRemediationRate = (() => {
|
||||
if (!incidents || incidents.length === 0) return '--'
|
||||
if (!incidents?.length) return '--'
|
||||
const resolved = incidents.filter(i => i.status === 'resolved' || i.status === 'closed').length
|
||||
if (resolved === 0) return '0%'
|
||||
return `${((resolved / incidents.length) * 100).toFixed(0)}%`
|
||||
})()
|
||||
|
||||
// MTTR 均值
|
||||
const mttrAvg = (() => {
|
||||
if (!incidents || incidents.length === 0) return '--'
|
||||
if (!incidents?.length) return '--'
|
||||
const resolved = incidents.filter(i => i.updated_at && (i.status === 'resolved' || i.status === 'closed'))
|
||||
if (resolved.length === 0) return '--'
|
||||
const avgMs = resolved.reduce((sum, i) => {
|
||||
return sum + (new Date(i.updated_at).getTime() - new Date(i.created_at).getTime())
|
||||
}, 0) / resolved.length
|
||||
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
|
||||
const mins = Math.round(avgMs / 60000)
|
||||
if (mins < 60) return `${mins}m`
|
||||
return `${(mins / 60).toFixed(1)}h`
|
||||
return mins < 60 ? `${mins}m` : `${(mins / 60).toFixed(1)}h`
|
||||
})()
|
||||
|
||||
// Gold metric sparklines
|
||||
const errorRateMetric = pulseMetrics?.find(m => m.label === 'Error Rate')
|
||||
const rpsMetric = pulseMetrics?.find(m => m.label === 'RPS')
|
||||
|
||||
// POD health: healthy services / total
|
||||
const podHealthStr = totalServices > 0 ? `${healthyServices}/${totalServices}` : '--'
|
||||
const podAllRunning = totalServices > 0 && healthyServices === totalServices
|
||||
|
||||
// ── 7 Metrics Strip ─────────────────────────────────────────────────────────
|
||||
|
||||
type MetricItem = {
|
||||
label: string
|
||||
value: string | number
|
||||
sub?: string
|
||||
badge?: { text: string; color: string; bg: string }
|
||||
sparkline?: { values: number[]; color: string }
|
||||
valueColor?: string
|
||||
}
|
||||
|
||||
const metrics: MetricItem[] = [
|
||||
{
|
||||
label: tDashboard('activeIncidents'),
|
||||
value: incidents?.length ?? '--',
|
||||
sub: p0Count > 0 ? undefined : tDashboard('stable'),
|
||||
badge: p0Count > 0 ? { text: `P0 ×${p0Count}`, color: '#cc2200', bg: 'rgba(204,34,0,0.08)' } : undefined,
|
||||
valueColor: p0Count > 0 ? '#cc2200' : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('serviceHealth'),
|
||||
value: totalServices > 0 ? `${healthyServices}/${totalServices}` : '--',
|
||||
sub: tDashboard('normal'),
|
||||
sparkline: rpsMetric?.trend ? { values: rpsMetric.trend, color: '#22C55E' } : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('pendingApprovals'),
|
||||
value: pendingApprovals ?? '--',
|
||||
sub: pendingApprovals > 0 ? undefined : tDashboard('stable'),
|
||||
badge: pendingApprovals > 0 ? { text: tDashboard('pendingApprovals'), color: '#F59E0B', bg: 'rgba(245,158,11,0.08)' } : undefined,
|
||||
valueColor: pendingApprovals > 0 ? '#F59E0B' : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('autoRemediationRate'),
|
||||
value: autoRemediationRate,
|
||||
sparkline: errorRateMetric?.trend ? { values: errorRateMetric.trend.map(v => 100 - v), color: '#4A90D9' } : undefined,
|
||||
},
|
||||
{
|
||||
label: tDashboard('mttrAvg'),
|
||||
value: mttrAvg,
|
||||
},
|
||||
{
|
||||
label: tDashboard('podHealth'),
|
||||
value: podHealthStr,
|
||||
sub: podAllRunning ? tDashboard('allRunning') : undefined,
|
||||
badge: !podAllRunning && totalServices > 0
|
||||
? { text: `${totalServices - healthyServices} down`, color: '#cc2200', bg: 'rgba(204,34,0,0.08)' }
|
||||
: undefined,
|
||||
valueColor: podAllRunning ? '#22C55E' : totalServices > 0 ? '#cc2200' : undefined,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<AppLayout locale={locale} showBackground={false}>
|
||||
<div style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: 'calc(100vh - 64px)',
|
||||
background: '#f5f4ed',
|
||||
fontFamily: 'var(--font-body), monospace',
|
||||
overflow: 'hidden',
|
||||
margin: '-24px',
|
||||
}}>
|
||||
{/* ── Metrics Strip (50px) ────────────────────────────────────── */}
|
||||
<div style={{
|
||||
background: '#faf9f3',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
height: 60,
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
flexShrink: 0,
|
||||
gap: 0,
|
||||
flexDirection: 'column',
|
||||
height: 'calc(100vh - 64px)',
|
||||
background: '#f5f4ed',
|
||||
fontFamily: 'var(--font-body), monospace',
|
||||
overflow: 'hidden',
|
||||
margin: '-24px',
|
||||
}}>
|
||||
{[
|
||||
{ label: tDashboard('activeIncidents'), value: incidents?.length ?? '--', sub: incidents?.filter((i) => i.severity === 'P0').length ? `+${incidents.filter((i) => i.severity === 'P0').length} P0` : tDashboard('stable') },
|
||||
{ label: tDashboard('serviceHealth'), value: `${pulseMetrics?.length ?? '--'}/${pulseMetrics?.length ?? '--'}`, sub: tDashboard('normal') },
|
||||
{ label: tDashboard('todayIncidents'), value: incidents?.length ?? '--', sub: '' },
|
||||
{ label: tDashboard('autoRemediationRate'), value: autoRemediationRate, sub: '' },
|
||||
{ label: tDashboard('mttrAvg'), value: mttrAvg, sub: '' },
|
||||
].map((m, i, arr) => (
|
||||
<div key={i} style={{
|
||||
|
||||
{/* ── Metrics Strip ─────────────────────────────────────────────────── */}
|
||||
<div style={{
|
||||
background: '#faf9f3',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
height: 68,
|
||||
padding: '0 20px',
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
flexShrink: 0,
|
||||
}}>
|
||||
{metrics.map((m, i) => (
|
||||
<div key={i} style={{
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
gap: 2,
|
||||
paddingRight: 14,
|
||||
borderRight: i < metrics.length - 1 ? '0.5px solid #e8e5dc' : 'none',
|
||||
marginRight: i < metrics.length - 1 ? 14 : 0,
|
||||
}}>
|
||||
{/* Label */}
|
||||
<span style={{ fontSize: 11, color: '#b0ad9f', letterSpacing: '1.5px', textTransform: 'uppercase' }}>
|
||||
{m.label}
|
||||
</span>
|
||||
{/* Value row */}
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
||||
<span style={{
|
||||
fontSize: 22,
|
||||
fontWeight: 700,
|
||||
color: m.valueColor ?? '#141413',
|
||||
lineHeight: 1,
|
||||
fontFamily: 'var(--font-body), monospace',
|
||||
}}>
|
||||
{String(m.value)}
|
||||
</span>
|
||||
{m.sparkline && (
|
||||
<MiniSparkline values={m.sparkline.values} color={m.sparkline.color} />
|
||||
)}
|
||||
</div>
|
||||
{/* Sub row */}
|
||||
<div style={{ height: 16, display: 'flex', alignItems: 'center', gap: 4 }}>
|
||||
{m.badge ? (
|
||||
<span style={{
|
||||
fontSize: 10, padding: '1px 6px',
|
||||
background: m.badge.bg, color: m.badge.color,
|
||||
border: `0.5px solid ${m.badge.color}40`,
|
||||
borderRadius: 8, fontWeight: 600, whiteSpace: 'nowrap',
|
||||
}}>
|
||||
{m.badge.text}
|
||||
</span>
|
||||
) : m.sub ? (
|
||||
<span style={{ fontSize: 11, color: '#87867f' }}>{m.sub}</span>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── 主體 2 欄 ─────────────────────────────────────────────────────── */}
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||
|
||||
{/* ── Feed:活躍事件 (flex:1) ───────────────────────────────────── */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
borderRight: '0.5px solid #e0ddd4',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 1,
|
||||
paddingRight: 14,
|
||||
borderRight: i < arr.length - 1 ? '0.5px solid #e8e5dc' : 'none',
|
||||
marginRight: i < arr.length - 1 ? 14 : 0,
|
||||
}}>
|
||||
<span style={{ fontSize: 12, color: '#b0ad9f', letterSpacing: '2px', textTransform: 'uppercase' }}>
|
||||
{m.label}
|
||||
</span>
|
||||
<span style={{ fontSize: 20, fontWeight: 700, color: '#141413', lineHeight: 1.1, fontFamily: 'var(--font-body), monospace' }}>
|
||||
{String(m.value)}
|
||||
</span>
|
||||
{m.sub && <span style={{ fontSize: 12, color: '#87867f' }}>{m.sub}</span>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* ── 主體 2 欄(Sidebar 外部)────────────────────────────────── */}
|
||||
<div style={{ display: 'flex', flex: 1, overflow: 'hidden' }}>
|
||||
|
||||
{/* ── Feed:活躍事件(flex:1)──────────────────────────────── */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
borderRight: '0.5px solid #e0ddd4',
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
{/* Feed 標題列 */}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 8,
|
||||
flexShrink: 0,
|
||||
background: '#faf9f3',
|
||||
}}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', letterSpacing: '1px', textTransform: 'uppercase' }}>
|
||||
{tDashboard('activeIncidents')}
|
||||
</span>
|
||||
{(incidents?.length ?? 0) > 0 && (
|
||||
<span style={{
|
||||
fontSize: 12, background: 'rgba(217,119,87,0.1)', color: '#a04010',
|
||||
padding: '2px 8px', fontWeight: 700,
|
||||
border: '0.5px solid rgba(217,119,87,0.25)', borderRadius: 10,
|
||||
}}>
|
||||
{incidents?.length}
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
flexShrink: 0, background: '#faf9f3',
|
||||
}}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: '#141413', letterSpacing: '1px', textTransform: 'uppercase' }}>
|
||||
{tDashboard('activeIncidents')}
|
||||
</span>
|
||||
)}
|
||||
{(incidents?.length ?? 0) > 0 && (
|
||||
<span style={{
|
||||
fontSize: 12, background: 'rgba(217,119,87,0.1)', color: '#a04010',
|
||||
padding: '2px 8px', fontWeight: 700,
|
||||
border: '0.5px solid rgba(217,119,87,0.25)', borderRadius: 10,
|
||||
}}>
|
||||
{incidents?.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 10px' }}>
|
||||
{isIncidentsLoading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 32 }}>
|
||||
<span style={{ fontSize: 13, color: '#b0ad9f' }}>{tCommon('loading')}</span>
|
||||
</div>
|
||||
) : incidentsError ? (
|
||||
<div style={{ padding: 16, fontSize: 13, color: '#cc2200' }}>{incidentsError}</div>
|
||||
) : (incidents?.length ?? 0) === 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22C55E' }} />
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>{tDashboard('stable')} · 0 {tDashboard('activeIncidents')}</span>
|
||||
</div>
|
||||
) : (
|
||||
incidents?.map((incident) => (
|
||||
<IncidentCard
|
||||
key={incident.incident_id}
|
||||
incident={incident}
|
||||
decision={incident.decision}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feed 內容 */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '8px 10px' }}>
|
||||
{isIncidentsLoading ? (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', padding: 32 }}>
|
||||
<span style={{ fontSize: 13, color: '#b0ad9f' }}>{tCommon('loading')}</span>
|
||||
</div>
|
||||
) : incidentsError ? (
|
||||
<div style={{ padding: 16, fontSize: 13, color: '#cc2200' }}>{incidentsError}</div>
|
||||
) : (incidents?.length ?? 0) === 0 ? (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', padding: 48, gap: 8 }}>
|
||||
<div style={{ width: 8, height: 8, borderRadius: '50%', background: '#22C55E' }} />
|
||||
<span style={{ fontSize: 13, color: '#87867f' }}>{tDashboard('stable')} · 0 {tDashboard('activeIncidents')}</span>
|
||||
</div>
|
||||
) : (
|
||||
incidents?.map((incident) => (
|
||||
<IncidentCard
|
||||
key={incident.incident_id}
|
||||
incident={incident}
|
||||
decision={incident.decision}
|
||||
{/* ── Right Panel:AI + Infra (width:530px) ────────────────────── */}
|
||||
<div style={{
|
||||
width: 530,
|
||||
flexShrink: 0,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
|
||||
{/* WoooClaw + Reasoning Stream */}
|
||||
<div style={{ borderBottom: '0.5px solid #e0ddd4' }}>
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 13, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '1px', textTransform: 'uppercase',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
}}>
|
||||
{tDashboard('openclawEngine')}
|
||||
</div>
|
||||
<OpenClawPanel
|
||||
status={(incidents?.length ?? 0) > 0 ? 'analyzing' : 'patrolling'}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ── Right Panel:AI + Infra(flex:1)─────────────────────── */}
|
||||
<div style={{
|
||||
flex: 1,
|
||||
overflow: 'hidden',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
}}>
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
|
||||
{/* 7.1 NemoClaw + Reasoning Stream */}
|
||||
<div style={{ borderBottom: '0.5px solid #e0ddd4' }}>
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 13, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '1px', textTransform: 'uppercase',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
}}>
|
||||
{tDashboard('openclawEngine')}
|
||||
</div>
|
||||
<OpenClawPanel
|
||||
status={
|
||||
(incidents?.length ?? 0) > 0 ? 'analyzing' : 'patrolling'
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 7.3 基礎架構 2×2 Grid */}
|
||||
<div style={{ borderBottom: '0.5px solid #e0ddd4' }}>
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 13, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '1px', textTransform: 'uppercase',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
}}>
|
||||
{tDashboard('infrastructure')}
|
||||
{/* 基礎架構 Grid */}
|
||||
<div>
|
||||
<div style={{
|
||||
padding: '8px 12px',
|
||||
borderBottom: '0.5px solid #e0ddd4',
|
||||
fontSize: 13, fontWeight: 700, color: '#141413',
|
||||
letterSpacing: '1px', textTransform: 'uppercase',
|
||||
fontFamily: 'var(--font-body), monospace', background: '#faf9f3',
|
||||
}}>
|
||||
{tDashboard('infrastructure')}
|
||||
</div>
|
||||
<HostGrid hosts={hosts.map((h): HostInfo => ({
|
||||
hostname: h.name,
|
||||
ip: h.ip,
|
||||
cpuPct: h.metrics?.cpu_percent ?? null,
|
||||
ramPct: h.metrics?.memory_percent ?? null,
|
||||
services: h.services.map(s => ({
|
||||
name: s.name,
|
||||
healthy: s.status === 'up' || s.status === 'healthy',
|
||||
})),
|
||||
}))} />
|
||||
</div>
|
||||
<HostGrid hosts={hosts.map((h): HostInfo => ({
|
||||
hostname: h.name,
|
||||
ip: h.ip,
|
||||
cpuPct: h.metrics?.cpu_percent ?? null,
|
||||
ramPct: h.metrics?.memory_percent ?? null,
|
||||
services: h.services.map(s => ({
|
||||
name: s.name,
|
||||
healthy: s.status === 'up' || s.status === 'healthy',
|
||||
})),
|
||||
}))} />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user