feat(atlas): dynamic node colors from live health metrics (warn/crit thresholds)
Auto-monitor now fetches /api/health for nodes with health_url, caching CPU/mem metrics. Node stroke and glow shift to orange (warn) or red (crit) when CPU >60/80% or memory >65/85%. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
538cbc582c
commit
dc602f7ea7
66
index.html
66
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;
|
||||
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);
|
||||
const next = result === 'offline' ? 'inactive' : 'active';
|
||||
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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue