feat(web): S8 基礎架構拓撲群組 2×2 + 主機 4 台 — Sprint 5R
Some checks failed
CD Pipeline / build-and-deploy (push) Has been cancelled
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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user