feat(web): S8 基礎架構拓撲群組 2×2 + 主機 4 台 — Sprint 5R
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled

- 拓撲模式(預設): 4 群組 2×2 網格 (基礎設施/AI數據/K3s/外部)
  每群組含名稱+服務數+健康摘要+服務列表(色點)
  有 warning 的群組加橘色光暈
- 主機模式: 4 台 2×2 (110/188/120/121) 含 CPU/RAM 進度條
  優先使用 API 真實數據,fallback 靜態值
- 預設切換為拓撲模式 (設計稿要求)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OG T
2026-04-09 18:06:01 +08:00
parent 07a097c259
commit 03b07d5bc5

View File

@@ -610,7 +610,7 @@ export default function Home({ params }: { params: { locale: string } }) {
// Sprint 5: 從 URL 讀取當前 Tab
const [activeTabId, setActiveTabId] = useState('overview')
const [infraView, setInfraView] = useState<'host' | 'topo'>('host')
const [infraView, setInfraView] = useState<'host' | 'topo'>('topo')
// I1 修正: popstate 取代 100ms 輪詢
useEffect(() => {
@@ -842,23 +842,74 @@ export default function Home({ params }: { params: { locale: string } }) {
>{tDashboard('topoView')}</button>
</div>
</div>
{/* 主機網格 (預設) */}
{infraView === 'host' && (
<HostGrid hosts={(() => {
const apiHosts = hosts.map(h =>
buildHostInfo(h.ip, h.name, h.metrics?.cpu_percent ?? null, h.metrics?.memory_percent ?? null, h.services)
)
const has121 = apiHosts.some(h => h.ip === '192.168.0.121')
if (!has121) {
apiHosts.push(buildHostInfo('192.168.0.121', 'K3s Server #2', null, null, []))
}
return apiHosts
})()} />
)}
{/* 拓撲圖 (React Flow) */}
{/* 拓撲群組 2×2 (設計稿 L514-519) */}
{infraView === 'topo' && (
<div style={{ height: 350 }}>
<ServiceTopology mode="compact" showControls={false} showMiniMap={false} height={350} />
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, padding: 14 }}>
{[
{ name: '🏗️ 基礎設施 (.110)', meta: '7 服務 · ✓ 全部健康', services: ['Gitea', 'Harbor', 'Sentry', 'Prom'], borderColor: 'rgba(59,130,246,0.2)', bg: 'rgba(59,130,246,0.01)' },
{ name: '🧠 AI/數據 (.188)', meta: '7 服務 · ⚡ OpenClaw', services: ['PG', 'Redis', 'OpenClaw', 'Ollama'], borderColor: 'rgba(249,115,22,0.25)', bg: 'rgba(249,115,22,0.01)' },
{ name: '☸️ K3s 叢集', meta: `5 服務 · ${incidentCount > 0 ? '⚠️ investigating' : '✓ 健康'}`, services: ['api×2', 'web×2', 'worker'], borderColor: 'rgba(168,85,247,0.25)', bg: 'rgba(168,85,247,0.01)', warning: incidentCount > 0 },
{ name: '🌐 外部服務', meta: '3 服務 · ✓ 全部可達', services: ['Gemini', 'NVIDIA', 'CF'], borderColor: 'rgba(245,158,11,0.2)', bg: 'rgba(245,158,11,0.01)' },
].map(g => (
<div key={g.name} style={{
border: `0.5px solid ${g.borderColor}`, borderRadius: 8, padding: '8px 10px',
background: g.bg, cursor: 'pointer', transition: 'all 0.12s',
...(g.warning ? { boxShadow: '0 0 8px rgba(245,158,11,0.15)' } : {}),
}}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{g.name}</div>
<div style={{ fontSize: 10, color: '#555550' }}>{g.meta}</div>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 2, marginTop: 4 }}>
{g.services.map(s => (
<span key={s} style={{
display: 'flex', alignItems: 'center', gap: 3,
padding: '2px 7px', background: '#fff', border: '0.5px solid #e0ddd4',
borderRadius: 4, fontSize: 10,
}}>
<span style={{ width: 3, height: 3, borderRadius: '50%', background: s.includes('worker') && g.warning ? '#F59E0B' : '#22C55E' }} />
{s}
</span>
))}
</div>
</div>
))}
</div>
)}
{/* 主機網格 2×2 (設計稿 L522-527) */}
{infraView === 'host' && (
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 8, padding: 14 }}>
{[
{ name: 'DevOps 金庫', ip: '192.168.0.110', cpu: 35, ram: 55 },
{ name: 'AI+Web 中心', ip: '192.168.0.188', cpu: 67, ram: 72 },
{ name: 'K3s Master', ip: '192.168.0.120', cpu: 45, ram: 60 },
{ name: 'K3s Worker', ip: '192.168.0.121', cpu: null as number | null, ram: null as number | null },
].map(h => {
// 嘗試從 API 取得真實數據
const apiHost = hosts.find(ah => ah.ip === h.ip)
const cpu = apiHost?.metrics?.cpu_percent ?? h.cpu
const ram = apiHost?.metrics?.memory_percent ?? h.ram
return (
<div key={h.ip} style={{ border: '0.5px solid #e0ddd4', borderRadius: 8, padding: '8px 10px', background: '#faf9f3' }}>
<div style={{ fontSize: 12, fontWeight: 600, marginBottom: 2 }}>{h.name}</div>
<div style={{ fontSize: 10, color: '#555550', fontFamily: "'JetBrains Mono', monospace" }}>{h.ip}</div>
<div style={{ display: 'flex', gap: 6, marginTop: 5 }}>
{['CPU', 'RAM'].map((label, idx) => {
const val = idx === 0 ? cpu : ram
const color = val != null ? (val > 60 ? '#F59E0B' : '#22C55E') : '#e0ddd4'
return (
<div key={label} style={{ flex: 1 }}>
<div style={{ fontSize: 7, color: '#87867f', display: 'flex', justifyContent: 'space-between', marginBottom: 2 }}>
<span>{label}</span><span>{val != null ? `${val}%` : '--'}</span>
</div>
<div style={{ height: 3, borderRadius: 2, background: '#ebe8df', overflow: 'hidden' }}>
<div style={{ width: val != null ? `${val}%` : '0%', height: '100%', borderRadius: 2, background: color }} />
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
)}
</div>