full server dashboard working

This commit is contained in:
2026-03-22 18:44:07 -07:00
parent 324eaff135
commit 97fdb3d5d8
19 changed files with 1441 additions and 169 deletions

View File

@ -0,0 +1,218 @@
/* ------------------------------------------------------------------ */
/* 1. SocketIO connection & helpers unchanged */
/* ------------------------------------------------------------------ */
const socket = io();
socket.on('connect_error', err => {
safeSetText('status', `Could not connect to server - ${err.message}`);
});
socket.on('reconnect', attempt => {
safeSetText('status', `Re-connected (attempt ${attempt})`);
});
function safeSetText(id, txt) {
const el = document.getElementById(id);
if (el) el.textContent = txt;
}
/* ------------------------------------------------------------------ */
/* 2. Global state */
/* ------------------------------------------------------------------ */
let selectedHost = null; // hostname that is currently displayed
const hostDataMap = {}; // hostname → client object (from CLIENT_LIST)
/* ------------------------------------------------------------------ */
/* 3. Build the host list once the page is ready */
/* ------------------------------------------------------------------ */
function initHostList() {
const listEl = document.getElementById('host-list');
listEl.innerHTML = ''; // clear any stray markup
CLIENT_LIST.forEach(host => {
hostDataMap[host.hostname] = host; // cache for quick lookup
const item = document.createElement('div');
item.textContent = host.hostname;
item.className = 'host-item';
item.dataset.hostname = host.hostname;
item.addEventListener('click', () => selectHost(host.hostname));
listEl.appendChild(item);
});
// autoselect the first host (you could also stay on "Loading…" until the user clicks)
if (CLIENT_LIST.length) selectHost(CLIENT_LIST[0].hostname);
}
/* ------------------------------------------------------------------ */
/* 4. Handle host click update UI and request live metrics */
/* ------------------------------------------------------------------ */
function selectHost(hostname) {
if (selectedHost === hostname) return; // already selected
selectedHost = hostname;
// Update active styling in the list
document.querySelectorAll('.host-item').forEach(el => {
el.classList.toggle('active', el.dataset.hostname === hostname);
});
// Render the static part of the page for this host
renderHostContent(hostDataMap[hostname]);
// Now request the live metrics for this host
// The server sends an array of all hosts well filter below
// (If you have a dedicated endpoint you could request only the chosen host here)
}
/* ------------------------------------------------------------------ */
/* 5. Render the static content (system properties + components) */
/* ------------------------------------------------------------------ */
function renderHostContent(host) {
const main = document.getElementById('main-content');
main.innerHTML = ''; // clear
// 5a. System Properties
if (host.client_properties?.[0]?.system_properties?.length) {
const propSection = document.createElement('div');
propSection.innerHTML = '<h2>System Properties</h2>';
const ul = document.createElement('ul');
ul.className = 'system-list';
host.client_properties[0].system_properties.forEach(p => {
const li = document.createElement('li');
li.textContent = p.Property;
ul.appendChild(li);
});
propSection.appendChild(ul);
main.appendChild(propSection);
}
// 5b. Components
if (host.client_properties?.[0]?.system_components?.length) {
const compSection = document.createElement('div');
compSection.innerHTML = '<h2>Components</h2>';
const compGrid = document.createElement('div');
compGrid.className = 'components';
host.client_properties[0].system_components.forEach(c => {
const compDiv = document.createElement('div');
compDiv.className = 'component';
compDiv.innerHTML = `<h3>${c.component_name}</h3>`;
const ul = document.createElement('ul');
ul.className = 'info-list';
c.info_strings.forEach(str => {
const li = document.createElement('li');
li.textContent = str;
ul.appendChild(li);
});
compDiv.appendChild(ul);
compGrid.appendChild(compDiv);
});
compSection.appendChild(compGrid);
main.appendChild(compSection);
}
// 5c. Placeholder for live metrics will be filled by Socket.IO
const metricsDiv = document.createElement('div');
metricsDiv.id = 'client_summary';
metricsDiv.textContent = 'Connecting…';
main.appendChild(metricsDiv);
}
/* ------------------------------------------------------------------ */
/* 6. Render metrics called when a client_summary event arrives */
/* ------------------------------------------------------------------ */
socket.on('client_summary', data => {
// `data` is an array of host objects (the same structure as CLIENT_LIST)
// Find the one that matches the currently selected host
const host = data.find(h => h.hostname === selectedHost);
if (!host) return; // no data for this host yet
const metrics = host.redis_data;
renderStatsTable('client_summary', metrics, 'No Stats available');
});
/* 7. Table rendering unchanged except we now target a specific
container (e.g. id = 'client_summary') */
function renderStatsTable(containerId, data, emptyMsg) {
socket.emit('tableRendered');
renderGenericTable(containerId, data, emptyMsg);
}
function renderGenericTable(containerId, data, emptyMsg) {
const container = document.getElementById(containerId);
if (!Array.isArray(data) || !data.length) {
container.textContent = emptyMsg;
return;
}
const mergedData = mergeRowsByName(data);
const orderedData = orderRows(mergedData);
const table = buildTable(orderedData);
table.id = `${containerId}_table`;
container.innerHTML = '';
container.appendChild(table);
}
function mergeRowsByName(data) {
const groups = {}; // { source: { Metric: [], Data: [] } }
data.forEach(row => {
const source = row.Source;
if (!source) return;
if (!groups[source]) groups[source] = { Metric: [], Data: [] };
if ('Metric' in row && 'Data' in row) {
groups[source].Metric.push(row.Metric);
groups[source].Data.push(row.Data);
}
});
const merged = [];
Object.entries(groups).forEach(([source, grp]) => {
merged.push({
Source: source,
Metric: grp.Metric,
Data: grp.Data
});
});
return merged;
}
function orderRows(rows) {
const priority = ['System', 'CPU', 'RAM'];
const priorityMap = {};
priority.forEach((src, idx) => priorityMap[src] = idx);
return [...rows].sort((a, b) => {
const aIdx = priorityMap[a.Source] ?? Infinity;
const bIdx = priorityMap[b.Source] ?? Infinity;
return aIdx - bIdx;
});
}
function buildTable(data) {
const cols = ['Source', 'Metric', 'Data'];
const table = document.createElement('table');
const thead = table.createTHead();
const headerRow = thead.insertRow();
cols.forEach(col => {
const th = document.createElement('th');
th.textContent = col;
headerRow.appendChild(th);
});
const tbody = table.createTBody();
data.forEach(item => {
const tr = tbody.insertRow();
cols.forEach(col => {
const td = tr.insertCell();
const val = item[col];
if (Array.isArray(val)) {
val.forEach((v, idx) => {
const span = document.createElement('span');
span.textContent = v;
td.appendChild(span);
if (idx < val.length - 1) td.appendChild(document.createElement('br'));
});
} else {
td.textContent = val ?? '';
}
});
});
return table;
}
/* ------------------------------------------------------------------ */
/* 8. Kick things off when the DOM is ready */
/* ------------------------------------------------------------------ */
document.addEventListener('DOMContentLoaded', initHostList);