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);