/* ============================================================== system_metrics.js ============================================================== */ (() => { /* ========================================================== Socket.IO setup ========================================================== */ const socket = io({ transports: ['websocket', 'polling'], reconnection: true, reconnectionAttempts: Infinity, reconnectionDelay: 3000, reconnectionDelayMax: 60000, timeout: 60000, pingTimeout: 5000, pingInterval: 25000, }); /* ========================================================== Color constants ========================================================== */ const GREEN = [ 39, 174, 96]; // #27ae60 const YELLOW = [243, 156, 18]; // #f39c12 const RED = [192, 57, 43]; // #c0392b /* ========================================================== Helpers ========================================================== */ const hostTimestamps = {}; // keyed by short_id const toRgb = (r, g, b) => `rgb(${r},${g},${b})`; const T20 = 20 * 1000; const T40 = 40 * 1000; const T60 = 60 * 1000; function getFreshnessColor(ageMs) { if (ageMs <= T20) { return toRgb(...GREEN); } if (ageMs <= T40) { const t = (ageMs - T20) / (T40 - T20); const r = Math.round(GREEN[0] + t * (YELLOW[0] - GREEN[0])); const g = Math.round(GREEN[1] + t * (YELLOW[1] - GREEN[1])); const b = Math.round(GREEN[2] + t * (YELLOW[2] - GREEN[2])); return toRgb(r, g, b); } if (ageMs <= T60) { const t = (ageMs - T40) / (T60 - T40); const r = Math.round(YELLOW[0] + t * (RED[0] - YELLOW[0])); const g = Math.round(YELLOW[1] + t * (RED[1] - YELLOW[1])); const b = Math.round(YELLOW[2] + t * (RED[2] - YELLOW[2])); return toRgb(r, g, b); } return toRgb(...RED); } function safeSetText(id, txt) { const el = document.getElementById(id); if (el) el.textContent = txt; } /* ========================================================== Get the short_id from the query string ========================================================== */ function getSelectedId() { return new URLSearchParams(window.location.search).get('host') || ''; } /* ========================================================== Sidebar building - uses short_id for status key ========================================================== */ function buildList(systemList) { const ul = document.getElementById('endpointList'); const current = Array.from(ul.children).map(li => li.dataset.id); const newIds = systemList.map(s => s.short_id); if (arraysEqual(current, newIds)) return; const selected = getSelectedId().toLowerCase(); ul.innerHTML = ''; // reset list systemList.forEach(item => { const li = document.createElement('li'); // status dot - keyed by short_id const status = document.createElement('span'); status.className = 'host-status'; status.dataset.id = item.short_id; // link - display hostname, encode short_id in URL const a = document.createElement('a'); a.href = '?host=' + encodeURIComponent(item.short_id); a.textContent = item.hostname; if (item.short_id.toLowerCase() === selected) a.classList.add('active'); li.appendChild(status); li.appendChild(a); ul.appendChild(li); }); } /* ========================================================== Update status colors every second ========================================================== */ function updateStatusColors() { const nowSec = Date.now() / 1000; Object.entries(hostTimestamps).forEach(([id, ts]) => { const ageMs = (nowSec - ts) * 1000; const color = getFreshnessColor(ageMs); const span = document.querySelector( `.host-status[data-id="${id}"]` ); if (span) span.style.backgroundColor = color; }); } setInterval(updateStatusColors, 1000); /* ========================================================== Utility helpers ========================================================== */ function arraysEqual(a, b) { if (a.length !== b.length) return false; for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false; return true; } function renderGenericTable(containerId, data, emptyMsg) { const container = document.getElementById(containerId); if (!Array.isArray(data) || !data.length) { container.textContent = emptyMsg; return; } const merged = mergeRowsByName(data); const ordered = orderRows(merged); const table = buildTable(ordered); table.id = 'host_metrics_table'; container.innerHTML = ''; container.appendChild(table); } function mergeRowsByName(rows) { const groups = {}; // { Source: { Metric: [], Data: [] } } rows.forEach(r => { const src = r.Source; if (!src) return; if (!groups[src]) groups[src] = { Metric: [], Data: [] }; if ('Metric' in r && 'Data' in r) { groups[src].Metric.push(r.Metric); groups[src].Data.push(r.Data); } }); return Object.entries(groups).map(([src, g]) => ({ Source: src, Metric: g.Metric, Data: g.Data, })); } function orderRows(rows) { const priority = ['System', 'CPU', 'RAM']; const map = {}; priority.forEach((s, i) => map[s] = i); return [...rows].sort((a, b) => { const ai = map.hasOwnProperty(a.Source) ? map[a.Source] : Infinity; const bi = map.hasOwnProperty(b.Source) ? map[b.Source] : Infinity; return ai - bi; }); } function buildTable(rows) { const cols = ['Source', 'Metric', 'Data']; const table = document.createElement('table'); // Header const thead = table.createTHead(); const headerRow = thead.insertRow(); cols.forEach(col => { const th = document.createElement('th'); th.textContent = col; headerRow.appendChild(th); }); // Body const tbody = table.createTBody(); rows.forEach(item => { const tr = tbody.insertRow(); cols.forEach(col => { const td = tr.insertCell(); const val = item[col]; if (Array.isArray(val)) { val.forEach((v, i) => { const span = document.createElement('span'); span.textContent = v; td.appendChild(span); if (i < val.length - 1) td.appendChild(document.createElement('br')); }); } else { td.textContent = val !== undefined ? val : ''; } }); }); return table; } /* ========================================================== Handle incoming data ========================================================== */ let lastUpdate = Date.now(); function handleSummary(raw) { lastUpdate = Date.now(); // reset watchdog let payload; if (typeof raw === 'string') { try { payload = JSON.parse(raw); } catch (e) { safeSetText('client_summary', 'Invalid data received'); return; } } else payload = raw; if (!Array.isArray(payload) || !payload.length) { safeSetText('client_summary', 'No data available'); return; } // Build the list first (so elements exist) buildList(payload); // Store the timestamp for every short_id payload.forEach(hostObj => { if (hostObj.short_id && hostObj.data_timestamp) { hostTimestamps[hostObj.short_id] = hostObj.data_timestamp; // seconds } }); // Immediately update colors for the current view updateStatusColors(); // Metric table for selected host const selectedId = getSelectedId(); const hostObj = payload.find(h => h.short_id === selectedId) || payload[0]; const hostData = hostObj && Array.isArray(hostObj.redis_data) ? hostObj.redis_data : []; renderGenericTable('host_metrics', hostData, 'No Stats available'); } /* ========================================================== Socket event wiring ========================================================== */ socket.on('client_summary', handleSummary); socket.on('connect', () => { safeSetText('client_summary', 'Connected'); requestSummary(); }); socket.on('disconnect', () => { safeSetText('client_summary', 'Disconnected - retrying...'); }); socket.on('reconnect', attempt => { safeSetText('client_summary', `Re-connected (attempt ${attempt})`); requestSummary(); }); /* ========================================================== Request logic ========================================================== */ function requestSummary() { if (!socket.connected) return; // guard against stale emits socket.emit('get_client_summary'); // server will reply via client_summary } /* ========================================================== Recursive polling ========================================================== */ let pollTimer = null; function pollLoop() { if (!socket.connected) return; requestSummary(); pollTimer = setTimeout(pollLoop, 5000); } socket.on('connect', () => { if (!pollTimer) pollLoop(); }); /* ========================================================== Watchdog - force reconnect if no data for 15 s ========================================================== */ function watchdog() { if (Date.now() - lastUpdate > 15000 && socket.connected) { safeSetText('client_summary', 'No updates - reconnecting...'); socket.disconnect(); // forces a reconnect cycle } setTimeout(watchdog, 5000); } watchdog(); /* ========================================================== Keep the 'active' link in sync when the URL changes ========================================================== */ window.addEventListener('popstate', () => { const selected = getSelectedId().toLowerCase(); document.querySelectorAll('#endpointList a').forEach(a => a.classList.toggle('active', a.href.includes('host=' + encodeURIComponent(selected))) ); }); })();