Files
cosmoserver/files/server/system_metrics.js
2026-03-29 09:39:43 -07:00

309 lines
10 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* ==============================================================
system_metrics.js
==============================================================
Updated to use the unique `short_id` (the systems key) rather
than the hostname. Hostnames are still displayed to the user
but every internal mapping and URL uses the short_id so duplicate
hostnames no longer collide.
============================================================== */
(() => {
/* ==========================================================
Socket.IO setup unchanged
========================================================== */
const socket = io({
transports: ['websocket', 'polling'],
reconnection: true,
reconnectionAttempts: Infinity,
reconnectionDelay: 3000,
reconnectionDelayMax: 60000,
timeout: 60000,
pingTimeout: 5000,
pingInterval: 25000,
});
/* ==========================================================
Color constants unchanged
========================================================== */
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; // nothing changed
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 colours 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 (unchanged)
========================================================== */
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;
}
// 1) Build the list first (so <span> elements exist)
buildList(payload);
// 2) 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
}
});
// 3) Immediately update colours 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 unchanged
========================================================== */
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 unchanged
========================================================== */
function requestSummary() {
if (!socket.connected) return; // guard against stale emits
socket.emit('get_client_summary'); // server will reply via client_summary
}
/* ==========================================================
Recursive polling unchanged
========================================================== */
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 15s
========================================================== */
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)))
);
});
})();