cosmostat python comment enrichment

This commit is contained in:
2026-03-29 10:38:07 -07:00
parent 9646ee92fd
commit 6453a839a9
14 changed files with 95 additions and 791 deletions

View File

@ -1,10 +1,16 @@
# this class file is for the cosmostat service #################################################################
#################################################################
### Cosmostat Component and System Class
#################################################################
#################################################################
import subprocess import subprocess
import json import json
import time import time
import weakref import weakref
import base64, hashlib import base64, hashlib
from typing import Dict, Any, List from typing import Dict, Any, List
# Import Cosmos Settings
from Cosmos_Settings import * from Cosmos_Settings import *
# Global Class Vars # Global Class Vars
@ -33,7 +39,13 @@ for entry in component_class_tree:
################################################################# #################################################################
################################################################# #################################################################
# Component Class ### Component Class
### Each Component type is defined by the descriptor and built
### as part of the System Class Instantiation
### Each Component Object contains static and dynamic data
### The static data is declared once at instantiation
### THe dynamic data is periodically updated by the application
### where the System Class Object is instantiated
################################################################# #################################################################
################################################################# #################################################################
@ -44,7 +56,6 @@ class Component:
# this_device is set when the component has multiple instances # this_device is set when the component has multiple instances
############################################################ ############################################################
def __init__(self, name: str, comp_type: str, parent_system, this_device=None): def __init__(self, name: str, comp_type: str, parent_system, this_device=None):
# begin init # begin init
self.name = name self.name = name
@ -246,7 +257,7 @@ class Component:
return result return result
######################################################## ########################################################
# random data functions # various data functions
######################################################## ########################################################
# complex data type return # complex data type return
@ -301,7 +312,15 @@ class Component:
############################################################ ############################################################
############################################################ ############################################################
# System Class ### System Class
### The System Class uses the Descriptor to build a List
### of Components and interact with the data in a
### useful manner. The System Object is similar to a
### Component Object in that it has Static and Dynamic
### properties, which are populated in a similar manner
### to the Components. In fact, this is designed for the
### System object to update Component Dymanic Metrics
### as part of the same subroutine that updates its own
############################################################ ############################################################
############################################################ ############################################################

View File

@ -1,9 +1,16 @@
# This will be a class definitation for the cosmostat server
# On the server, there will be a Cosmostat Class Object #################################################################
# This will have an array of System Class Objects #################################################################
# These will be created based on API input from remote systems ### Cosmostat Classes
# The remote systems will submit a json of their state to a private API ### The Cosmostat Server is a Class for the API running on the
# this will define the System Class ### dashboard. This keeps track of all active systems that are
### actively reporting back to the Cosmostat Server Dashboard
### The static and active data is maintained in a single
### Cosmostat Server Object, and this Object contains a List
### of Cosmostat Client Objects. This is where the data actually
### lives
#################################################################
#################################################################
import subprocess import subprocess
import json import json
@ -11,14 +18,15 @@ import time
import weakref import weakref
import base64, hashlib import base64, hashlib
from typing import Dict, Any, List from typing import Dict, Any, List
# Import Cosmos Settings
from Cosmos_Settings import * from Cosmos_Settings import *
#################################################################
#################################################################
# Cosmostat Class
################################################################# #################################################################
### Cosmostat Server Class
### This Class is for maintaining a list of Client Objects.
### Each Object is a remote System reporting back
### The Class Functions are for the main application to interact
### with client Object data.
################################################################# #################################################################
class CosmostatServer: class CosmostatServer:
@ -106,22 +114,30 @@ class CosmostatServer:
return result return result
def get_client_hostnames(self, send_age = False): def get_client_hostnames(self, send_age = False):
now = time.time() self.purge_stale_hostnames()
fresh_systems = []
result = [] result = []
for system in self.systems: for system in self.systems:
age = now - system.data_timestamp
if age <= 60: # keep only fresh servers
fresh_systems.append(system)
if send_age: if send_age:
result.append({"hostname": system.hostname, "data_age": age}) result.append({"hostname": system.hostname, "data_age": age})
else: else:
result.append(system.hostname) result.append(system.hostname)
self.systems = fresh_systems # replace the old list
return result return result
def purge_stale_hostnames(self):
now = time.time()
fresh_systems = []
for system in self.systems:
age = now - system.data_timestamp
if age <= 60: # keep only fresh servers
fresh_systems.append(system)
self.systems = fresh_systems # replace the old list
#################################################################
### Cosmostat Client Class
### Each Class Object contains static and active data as well as
### the hostname and uuid/short_id.
### The timestamp is for removing stale Clients
#################################################################
class CosmostatClient: class CosmostatClient:

View File

@ -1,16 +1,21 @@
#######################################################################
### app.py
### cosmostat service handler
#######################################################################
from flask import Flask, jsonify, request, Response from flask import Flask, jsonify, request, Response
from flask_apscheduler import APScheduler from flask_apscheduler import APScheduler
from typing import Dict, Union from typing import Dict, Union
import json, time, redis, yaml import json, time, redis, yaml
import base64, hashlib
import secrets, string import secrets, string
import requests import requests
from requests import RequestException, Response from requests import RequestException, Response
from Components import * # Import Cosmos Settings
from Cosmos_Settings import * from Cosmos_Settings import *
# System and Component Classes
from Components import *
# Cosmostat server Classes
from Cosmostat import * from Cosmostat import *
# declare flask apps # declare flask apps
@ -373,11 +378,11 @@ def client_update():
log_data(log_output = payload, log_level = "noisy_test") log_data(log_output = payload, log_level = "noisy_test")
# execute API call # execute API call
result = client_submission_handler(api_url, payload) result = client_submission_handler(api_url, payload)
client_initialize() client_api_initialize()
return result return result
# Cosmostat Client Initializer # Cosmostat Client Initializer
def client_initialize(): def client_api_initialize():
api_url = f"{cosmostat_server_api()}create_client" api_url = f"{cosmostat_server_api()}create_client"
# generate payload # generate payload
payload = get_client_payload(get_php_summary(), "client_properties") payload = get_client_payload(get_php_summary(), "client_properties")
@ -446,34 +451,39 @@ if __name__ == '__main__':
# Background Loop Function # Background Loop Function
def background_loop(): def background_loop():
# Update all data on the System object unless this is the server # Update all data on the local System object
if cosmostat_client.check_system_timer() and not run_cosmostat_server(): if cosmostat_client.check_system_timer() or run_cosmostat_server():
cosmostat_client.update_system_state() cosmostat_client.update_system_state()
# publish to redis if the web dashboard is active locally
if app_settings["push_redis"] and not app_settings["disable_local_api"]: if app_settings["push_redis"] and not app_settings["disable_local_api"]:
update_redis_server() update_redis_server()
# report data to the server if configured
if run_cosmostat_reporter(): if run_cosmostat_reporter():
if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer(): if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer():
cosmostat_client.update_system_state() cosmostat_client.update_system_state()
client_update() client_update()
# if this is the server, do this stuff
if run_cosmostat_server(): if run_cosmostat_server():
# update the client state since that was skipped # purge stale client systems
cosmostat_client.update_system_state() cosmostat_server.purge_stale_hostnames()
this_client = get_client_payload(get_client_redis_data(human_readable = False), "redis_data") # report the server's own client object to itself
if app_settings["noisy_test"]: run_update_client(get_client_payload(get_client_redis_data(human_readable = False), "redis_data"))
print(this_client) log_data(log_output = f"{this_client}", log_level = "noisy_test")
run_update_client(this_client)
time.sleep(0.5) time.sleep(0.5)
###################################### ######################################
# instantiate client # instantiate client
###################################### ######################################
# local client System Class Object
cosmostat_client = new_cosmos_client() cosmostat_client = new_cosmos_client()
# remote client reporter
if app_settings["cosmostat_server_reporter"] and not app_settings["cosmostat_server"]: if app_settings["cosmostat_server_reporter"] and not app_settings["cosmostat_server"]:
client_initialize() client_api_initialize()
###################################### ######################################
# instantiate server # instantiate server
@ -517,6 +527,7 @@ if __name__ == '__main__':
if not app_settings["disable_local_api"]: if not app_settings["disable_local_api"]:
app.run(debug=False, host=service_gateway_ip(), port=service_api_port()) app.run(debug=False, host=service_gateway_ip(), port=service_api_port())
else: else:
# if local API disabled, phone home if configured
print("Internal API Disabled.") print("Internal API Disabled.")
while True: while True:
if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer(): if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer():

View File

@ -1,4 +1,7 @@
[ [
{
"notes:": "this is both a scratch file and a reference for new component descriptors"
},
{ {
"name": "", "name": "",
"description": "", "description": "",
@ -37,88 +40,6 @@
"which have variance" "which have variance"
] ]
}, },
{
"static_key_variables": [
{"name": "Hostname", "command": "hostname"},
{"name": "Virtual Machine", "command": "echo $( [ \"$(systemd-detect-virt)\" = none ] && echo False || echo True )", "req_check": "False"},
{"name": "CPU Architecture", "command": "lscpu --json | jq -r '.lscpu[] | select(.field==\"Architecture:\") | .data'"},
{"name": "OS Kernel", "command": "uname -r"},
{"name": "OS Name", "command": "cat /etc/os-release | grep PRETTY | cut -d\\\" -f2"},
{"name": "Manufacturer", "command": "sudo dmidecode --type 1 | grep Manufacturer: | cut -d: -f2 | sed -e 's/^[ \\t]*//'"},
{"name": "Product Name", "command": "sudo dmidecode --type 2 | grep 'Product Name:' | cut -d: -f2 | sed -e 's/^[ \\t]*//'"},
{"name": "Serial Number", "command": "sudo dmidecode --type 2 | grep 'Serial Number: '| cut -d: -f2 | sed -e 's/^[ \\t]*//'"}
],
"dynamic_key_variables": [
{"name": "System Uptime", "command": "uptime -p"},
{"name": "Current Date", "command": "date '+%D %r'"}
],
"virt_ignore": [
"Product Name",
"Serial Number"
]
},
{
"name:": "System",
"static_key_variables": [
{
"name": "Hostname",
"command": "hostname"
},
{
"name": "Virtual Machine",
"command": "echo $( [ \"$(systemd-detect-virt)\" = none ] && echo False || echo True )",
"req_check": "False"
},
{
"name": "CPU Architecture",
"command": "lscpu --json | jq -r '.lscpu[] | select(.field==\"Architecture:\") | .data'"
},
{
"name": "OS Kernel",
"command": "uname -r"
},
{
"name": "OS Name",
"command": "cat /etc/os-release | grep PRETTY | cut -d\\\" -f2"
},
{
"name": "Manufacturer",
"command":{
"x86_64": "sudo dmidecode --type 1 | grep Manufacturer: | cut -d: -f2 | sed -e 's/^[ \\t]*//'"
},
"arch_check": "true"
},
{
"name": "Product Name",
"command": {
"x86_64": "sudo dmidecode --type 2 | grep 'Product Name:' | cut -d: -f2 | sed -e 's/^[ \\t]*//'"
},
"arch_check": "true"
},
{
"name": "Serial Number",
"command": {
"x86_64": "sudo dmidecode --type 2 | grep 'Serial Number: '| cut -d: -f2 | sed -e 's/^[ \\t]*//'"
},
"arch_check": "true"
}
],
"dynamic_key_variables": [
{
"name": "System Uptime",
"command": "uptime -p"
},
{
"name": "Current Date",
"command": "date '+%D %r'"
}
],
"virt_ignore": [
"Product Name",
"Serial Number"
]
},
{ {
"SATA GBW": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r '.physical_block_size as $block |.ata_device_statistics.pages[] | select(.name == \"General Statistics\") | .table[] | select(.name == \"Logical Sectors Written\") | .value as $sectors | ($sectors * $block) / 1073741824 ' | awk '{{printf \"%.2f GiB Written\\n\", $0}}' || true", "SATA GBW": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r '.physical_block_size as $block |.ata_device_statistics.pages[] | select(.name == \"General Statistics\") | .table[] | select(.name == \"Logical Sectors Written\") | .value as $sectors | ($sectors * $block) / 1073741824 ' | awk '{{printf \"%.2f GiB Written\\n\", $0}}' || true",

View File

@ -1,46 +0,0 @@
def get_properties_keys(self, component = None):
component_properties = []
if component == None:
component_properties = self._properties.items()
else:
component_properties = self.get_property(component)
result = self.process_key_list(key_items = component_properties, key_name = "Property", return_type = "key" key_value = "Value")
return result
def get_metrics_keys(self):
result = self.process_key_list(key_items = self._metrics.items(), key_name = "Metric", key_value = "Data", return_type = "key")
return result
def get_properties_strings(self, return_simple = True):
result = self.process_key_list(key_items = self._properties.items(), key_name = "Property", return_type = "string", return_simple = return_simple)
return result
def get_metrics_strings(self, return_simple = True):
result = self.process_key_list(key_items = self._metrics.items(), key_name = "Metric", return_type = "string", return_simple = return_simple)
return result
def process_key_list(self, key_items: str, key_name: str, return_type: str, key_value = "none"):
result = []
empty_value = ["", "null", None, []]
for name, values in key_items:
for value in (values if isinstance(values, list) else [values]):
if value not in empty_value and name not in self.virt_ignore:
this_key_string = f"{name}: {value}"
if return_simple:
result.append(this_key_string)
elif return_keys:
this_key_value = {
"Source": self.name,
key_name: name,
key_value: value
}
result.append(this_key_value)
else:
complex_key_string = {
"Source": self.name,
key_name: this_key_string
}
result.append(complex_key_string)
return result

View File

@ -95,7 +95,10 @@ $selectedHost = $clients[$selectedIdx]['hostname'];
<strong>Component Desriptor</strong> <strong>Component Desriptor</strong>
<p>To view the component descriptor, you may <br> <p>To view the component descriptor, you may <br>
<code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p> <code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p>
<p>This will return the entire JSON descriptor variable</p> This will return the entire JSON descriptor variable.<br>
The endpoint agent uses this descriptor to build out its local System Object.<br>
The agent then reports back to the Cosmostat Server with all the data found in the descriptor.<br>
Full Source Code can be found at its <a href=https://gitea.matt-cloud.com/matt/cosmoserver>Gitea</a> page.
</div> <!-- / Header Card --> </div> <!-- / Header Card -->
<!-- summary card --> <!-- summary card -->

View File

@ -1,218 +0,0 @@
/* ------------------------------------------------------------------ */
/* 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);

View File

@ -1,107 +0,0 @@
<?php
/* ------------------------------------------------------------------ */
/* Load the API that returns the list of all clients */
/* ------------------------------------------------------------------ */
# load API settings, this requires a simple yaml file
$raw_api_settings = file('/opt/api_settings/cosmostat_settings.yaml', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
$api_settings = [];
foreach ($raw_api_settings as $line) {
if ($line[0] === '#') {
continue;
}
$pos = strpos($line, ':');
if ($pos === false) {
continue;
}
$key = trim(substr($line, 0, $pos));
$value = trim(substr($line, $pos + 1));
if ($value === '') {
$value = null;
}
$api_settings[$key] = $value;
}
$api_bind_ip = trim($api_settings['api_bind_ip'], "\"'") ?? null;
$customApiPort = trim($api_settings['custom_api_port'], "\"'") ?? null;
$apiUrl = 'http://'.$api_bind_ip.':'.$customApiPort.'/client_details';
$apiCtx = stream_context_create([
'http' => [
'timeout' => 5,
'header' => "User-Agent: PHP/".PHP_VERSION."\r\n"
]
]);
$apiJson = @file_get_contents($apiUrl, false, $apiCtx);
$clients = json_decode($apiJson, true) ?: [];
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Cosmostat Server Dashboard</title>
<link rel="stylesheet" href="src/styles.css">
<style>
/* ------------------------------------------------------------------ */
/* Layout tweaks 2 column grid, left column 200px wide */
/* ------------------------------------------------------------------ */
.layout{display:grid;grid-template-columns:200px 1fr;gap:1rem;}
.sidebar{background:#34495e;padding:10px;border-radius:6px;}
.main{background:#34495e;padding:20px;border-radius:6px;}
.host-item{cursor:pointer;color:#bdc3c7;padding:4px 8px;border-radius:4px;}
.host-item:hover{background:#2c3e50;}
.host-item.active{background:#1abc9c;color:#fff;}
/* Preserve existing table styling --------------------------------- */
</style>
</head>
<body>
<div class="card">
<h2>Matt-Cloud Cosmostat Dashboard</h2>
<p>This dashboard shows MattCloud system stats.</p>
<div class="help-link" id="helpToggle">API</div>
</div>
<div id="helpText" class="card">
<strong>Component Desriptor</strong>
<p>To view the component descriptor, you may <br>
<code>curl -s https://<?php echo $_SERVER['SERVER_NAME']; ?>/descriptor</code>
<p>This will return the entire JSON descriptor variable
</div>
<!-- --------------------------------------------------- -->
<!-- Page layout sidebar + main content -->
<!-- --------------------------------------------------- -->
<div class="layout">
<!-- Left side host list -->
<div class="sidebar">
<h3>Hosts</h3>
<div id="host-list"></div>
</div>
<!-- Right side content -->
<div class="main" id="main-content">
<h2>Loading…</h2>
</div>
</div>
<!-- --------------------------------------------------- -->
<!-- The client list is embedded so JS can build UI -->
<!-- --------------------------------------------------- -->
<script>
/* Expose the whole client list to JS this is the data that
the old PHP template used to render one host. */
const CLIENT_LIST = <?php echo json_encode($clients, JSON_UNESCAPED_SLASHES); ?>;
</script>
<!-- Socket.IO client -->
<script src="socket.io/socket.io.js"></script>
<!-- Custom Redis logic (see below) -->
<script src="src/redis-server.js"></script>
<script>
/* Toggle help panel unchanged from the original */
document.getElementById('helpToggle').addEventListener('click', function () {
const help = document.getElementById('helpText');
help.style.display = (help.style.display === 'none' || help.style.display === '') ? 'block' : 'none';
});
</script>
</body>
</html>

View File

@ -1,36 +0,0 @@
/* --------------------------------------------------
1. Expose the API URL (identical to PHPs $apiUrl)
-------------------------------------------------- */
const API_URL = 'http://<?= h($api_bind_ip) ?>:<?= h($customApiPort) ?>/php_summary';
/* --------------------------------------------------
2. Build the endpoint list
-------------------------------------------------- */
document.addEventListener('DOMContentLoaded', () => {
const listEl = document.getElementById('endpointList');
const urlParam = new URLSearchParams(window.location.search);
const current = urlParam.get('host'); // e.g. "MC-CM3588"
fetch(API_URL)
.then(r => r.json())
.then(hosts => {
hosts.forEach(hostObj => {
const li = document.createElement('li');
const a = document.createElement('a');
a.href = '?host=' + encodeURIComponent(hostObj.hostname);
a.textContent = hostObj.hostname;
if (hostObj.hostname === current) a.classList.add('active');
li.appendChild(a);
listEl.appendChild(li);
});
})
.catch(err => {
console.error('Failed to load endpoint list:', err);
const li = document.createElement('li');
li.textContent = 'Unable to load endpoints';
listEl.appendChild(li);
});
});

View File

@ -1,177 +0,0 @@
/* ------------------------------------------------------------
1. Socket-IO connection & helper functions (unchanged)
------------------------------------------------------------ */
const socket = io();
socket.on('client_summary', renderStatsTable);
socket.on('connect_error', err => {
safeSetText('client_summary', `Could not connect to server - ${err.message}`);
});
socket.on('reconnect', attempt => {
safeSetText('client_summary', `Re-connected (attempt ${attempt})`);
});
function safeSetText(id, txt) {
const el = document.getElementById(id);
if (el) el.textContent = txt;
}
/* ------------------------------------------------------------
2. Render the table for the *selected* host
------------------------------------------------------------ */
function renderStatsTable(raw) {
// Raw may be a string (from Redis) or already parsed by socket.io
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;
}
/* ---------------------------------------------
2a. Determine the hostname to display
--------------------------------------------- */
const urlParams = new URLSearchParams(window.location.search);
const selectedHost = urlParams.get('host');
/* ---------------------------------------------
2b. Find the host object in the payload
--------------------------------------------- */
const hostObj =
payload.find(item => item.hostname === selectedHost) || payload[0];
/* ---------------------------------------------
2c. Extract the Redis data for that host
--------------------------------------------- */
const hostData = hostObj && Array.isArray(hostObj.redis_data)
? hostObj.redis_data
: [];
/* ---------------------------------------------
2d. Pass the host-specific data to the generic renderer
--------------------------------------------- */
renderGenericTable('host_metrics', hostData, 'No Stats available');
}
/* ------------------------------------------------------------
3. Table rendering - unchanged from original
------------------------------------------------------------ */
function renderGenericTable(containerId, data, emptyMsg) {
const container = document.getElementById(containerId);
if (!Array.isArray(data) || !data.length) {
container.textContent = emptyMsg;
return;
}
// Merge rows by source name
const mergedData = mergeRowsByName(data);
// Order the merged rows - priority first
const orderedData = orderRows(mergedData);
// Build the table from the ordered data
const table = buildTable(orderedData);
table.id = 'host_metrics_table';
container.innerHTML = '';
container.appendChild(table);
}
/* ------------------------------------------------------------
4. Merge rows by source name
------------------------------------------------------------ */
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;
}
/* ------------------------------------------------------------
5. Order rows - put “System”, “CPU”, “RAM” first
------------------------------------------------------------ */
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.hasOwnProperty(a.Source) ? priorityMap[a.Source] : Infinity;
const bIdx = priorityMap.hasOwnProperty(b.Source) ? priorityMap[b.Source] : Infinity;
return aIdx - bIdx;
});
}
/* ------------------------------------------------------------
6. Build an HTML table from an array of objects
------------------------------------------------------------ */
function buildTable(data) {
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();
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) => {
td.id = 'host_metrics_column';
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 !== undefined ? val : '';
}
});
});
return table;
}

View File

@ -1,18 +0,0 @@
# Use an official Node runtime
FROM node:20-alpine
# Create app directory
WORKDIR /usr/src/app
# Install dependencies
COPY package.json .
RUN npm install --only=production
# Copy app source
COPY . .
# Expose the port that the app listens on
EXPOSE 3000
# Start the server
CMD ["node", "server.js"]

View File

@ -107,29 +107,4 @@
virtualenv_command: python3 -m venv virtualenv_command: python3 -m venv
state: present state: present
# create node.js docker container for web dashboard
- name: node.js server container handler
when: false
block:
- name: Cosmostat - Init - node.js - copy server files
copy:
src: "web/node_server"
dest: "{{ service_control_web_folder }}/"
mode: 0755
owner: "{{ service_user }}"
group: "{{ service_user }}"
- name: Cosmostat - Init - node.js - build docker container
community.docker.docker_image:
name: ws_node
tag: latest
source: local
build:
path: "{{ service_control_web_folder }}/node_server"
dockerfile: Dockerfile
force_tag: true
state: present
force_source: true
... ...

View File

@ -1,7 +1,5 @@
--- ---
# initializa environment # initializa environment
- name: Initialize Environment - name: Initialize Environment
when: not quick_refresh | bool when: not quick_refresh | bool
@ -16,7 +14,4 @@
when: not disable_local_api when: not disable_local_api
include_tasks: web.yaml include_tasks: web.yaml
#- name: Purge Old Containers
# when: not quick_refresh | bool
# include_tasks: purge.yaml
... ...

View File

@ -1,34 +0,0 @@
- name: Cosmostat - Clean up old ws_node image tags
block:
# Grab a list of all tags the image has
- name: Get all ws_node image tags
command: |
docker images --format "{{.Repository}}:{{.Tag}}" \
--filter=reference="ws_node:*"
register: all_tags_raw
changed_when: false
# Turn that raw string into a list of just the tag names
- name: Parse tag names out of the list
set_fact:
all_tags: >-
{{ all_tags_raw.stdout_lines |
map('regex_replace', '^ws_node:', '') |
list }}
# Keep everything *except* the one that ends with “:latest”
- name: Build list of tags that should be removed
set_fact:
tags_to_remove: "{{ all_tags | difference(['latest']) }}"
# Remove each old tag
- name: Delete old ws_node image tags
community.docker.docker_image:
name: ws_node
tag: "{{ item }}"
state: absent
loop: "{{ tags_to_remove }}"
when: tags_to_remove | length > 0
when: tags_to_remove | length > 0
tags:
- cleanup