diff --git a/index.html b/index.html index 029de2f..5e02dfa 100644 --- a/index.html +++ b/index.html @@ -1371,6 +1371,9 @@ let isReadOnly = false; let currentSimNodes = []; let currentSimEdges = []; +// ── Health metrics cache: nodeId → { cpu_pct, mem_pct, status } ── +const nodeHealthCache = new Map(); + async function loadData() { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { @@ -1970,7 +1973,7 @@ function setMonitorDot(state) { // 'off' | 'active' | 'checking' } async function runHealthCheckAll() { - const targets = atlasData.nodes.filter(n => n.url); + const targets = atlasData.nodes.filter(n => n.url || n.health_url); if (targets.length === 0) return; setMonitorDot('checking'); @@ -1978,22 +1981,50 @@ async function runHealthCheckAll() { for (const node of targets) { const prev = node.status; - const result = await checkUrlHealth(node.url); - const next = result === 'offline' ? 'inactive' : 'active'; + let next = prev; + + if (node.health_url) { + // Rich health check — get metrics too + try { + const ctrl = new AbortController(); + const timer = setTimeout(() => ctrl.abort(), 7000); + const res = await fetch(node.health_url, { signal: ctrl.signal }); + clearTimeout(timer); + if (res.ok) { + const data = await res.json(); + next = data.ok ? 'active' : 'inactive'; + const cpuPct = data.cpu_pct || 0; + const memPct = data.mem_total_mb + ? Math.round((data.mem_used_mb / data.mem_total_mb) * 100) : 0; + const health = cpuPct > 80 || memPct > 85 ? 'crit' + : cpuPct > 60 || memPct > 65 ? 'warn' : 'ok'; + nodeHealthCache.set(node.id, { cpu_pct: cpuPct, mem_pct: memPct, health }); + } else { + next = 'inactive'; + nodeHealthCache.delete(node.id); + } + } catch (e) { + next = 'inactive'; + nodeHealthCache.delete(node.id); + } + } else { + const result = await checkUrlHealth(node.url); + next = result === 'offline' ? 'inactive' : 'active'; + nodeHealthCache.delete(node.id); + } + if (next !== prev) { node.status = next; changed = true; - if (next === 'inactive') { - showToast(`${node.label} が応答していません`); - } + if (next === 'inactive') showToast(`${node.label} が応答していません`); } } + // Always refresh visuals to reflect updated health cache colors + updateNodeStatusVisuals(); + if (changed) { saveData(); - // Refresh node visuals (update opacity for inactive nodes) - updateNodeStatusVisuals(); - // Refresh detail panel if open if (selectedNodeId) showDetail(selectedNodeId, currentSimNodes, currentSimEdges); } @@ -2016,18 +2047,33 @@ async function checkUrlHealth(url) { } } +function nodeHealthColor(nodeId) { + const h = nodeHealthCache.get(nodeId); + if (!h) return null; + if (h.health === 'crit') return '#F87171'; + if (h.health === 'warn') return '#FB923C'; + return null; // ok — use type color +} + function updateNodeStatusVisuals() { if (!nodeSel) return; nodeSel .attr('fill', d => { const node = atlasData.nodes.find(n => n.id === d.id); const alpha = node?.status === 'inactive' ? '0A' : '18'; - return TYPE_COLORS[d.type] + alpha; + const baseColor = nodeHealthColor(d.id) || TYPE_COLORS[d.type]; + return baseColor + alpha; + }) + .attr('stroke', d => { + const node = atlasData.nodes.find(n => n.id === d.id); + if (node?.status === 'inactive') return TYPE_COLORS[d.type]; + return nodeHealthColor(d.id) || TYPE_COLORS[d.type]; }) .style('filter', d => { const node = atlasData.nodes.find(n => n.id === d.id); if (node?.status === 'inactive') return 'none'; - return `drop-shadow(0 0 8px ${TYPE_COLORS[d.type]}55)`; + const glowColor = nodeHealthColor(d.id) || TYPE_COLORS[d.type]; + return `drop-shadow(0 0 8px ${glowColor}88)`; }) .style('opacity', d => { const node = atlasData.nodes.find(n => n.id === d.id);