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
68
index.html
68
index.html
|
|
@ -1371,6 +1371,9 @@ let isReadOnly = false;
|
||||||
let currentSimNodes = [];
|
let currentSimNodes = [];
|
||||||
let currentSimEdges = [];
|
let currentSimEdges = [];
|
||||||
|
|
||||||
|
// ── Health metrics cache: nodeId → { cpu_pct, mem_pct, status } ──
|
||||||
|
const nodeHealthCache = new Map();
|
||||||
|
|
||||||
async function loadData() {
|
async function loadData() {
|
||||||
const saved = localStorage.getItem(STORAGE_KEY);
|
const saved = localStorage.getItem(STORAGE_KEY);
|
||||||
if (saved) {
|
if (saved) {
|
||||||
|
|
@ -1970,7 +1973,7 @@ function setMonitorDot(state) { // 'off' | 'active' | 'checking'
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runHealthCheckAll() {
|
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;
|
if (targets.length === 0) return;
|
||||||
|
|
||||||
setMonitorDot('checking');
|
setMonitorDot('checking');
|
||||||
|
|
@ -1978,22 +1981,50 @@ async function runHealthCheckAll() {
|
||||||
|
|
||||||
for (const node of targets) {
|
for (const node of targets) {
|
||||||
const prev = node.status;
|
const prev = node.status;
|
||||||
const result = await checkUrlHealth(node.url);
|
let next = prev;
|
||||||
const next = result === 'offline' ? 'inactive' : 'active';
|
|
||||||
|
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) {
|
if (next !== prev) {
|
||||||
node.status = next;
|
node.status = next;
|
||||||
changed = true;
|
changed = true;
|
||||||
if (next === 'inactive') {
|
if (next === 'inactive') showToast(`${node.label} が応答していません`);
|
||||||
showToast(`${node.label} が応答していません`);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always refresh visuals to reflect updated health cache colors
|
||||||
|
updateNodeStatusVisuals();
|
||||||
|
|
||||||
if (changed) {
|
if (changed) {
|
||||||
saveData();
|
saveData();
|
||||||
// Refresh node visuals (update opacity for inactive nodes)
|
|
||||||
updateNodeStatusVisuals();
|
|
||||||
// Refresh detail panel if open
|
|
||||||
if (selectedNodeId) showDetail(selectedNodeId, currentSimNodes, currentSimEdges);
|
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() {
|
function updateNodeStatusVisuals() {
|
||||||
if (!nodeSel) return;
|
if (!nodeSel) return;
|
||||||
nodeSel
|
nodeSel
|
||||||
.attr('fill', d => {
|
.attr('fill', d => {
|
||||||
const node = atlasData.nodes.find(n => n.id === d.id);
|
const node = atlasData.nodes.find(n => n.id === d.id);
|
||||||
const alpha = node?.status === 'inactive' ? '0A' : '18';
|
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 => {
|
.style('filter', d => {
|
||||||
const node = atlasData.nodes.find(n => n.id === d.id);
|
const node = atlasData.nodes.find(n => n.id === d.id);
|
||||||
if (node?.status === 'inactive') return 'none';
|
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 => {
|
.style('opacity', d => {
|
||||||
const node = atlasData.nodes.find(n => n.id === d.id);
|
const node = atlasData.nodes.find(n => n.id === d.id);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue