From 6453a839a977011c50ab13a297394a5d689ec4e4 Mon Sep 17 00:00:00 2001 From: Matt Date: Sun, 29 Mar 2026 10:38:07 -0700 Subject: [PATCH] cosmostat python comment enrichment --- files/api/Components.py | 31 +++- files/api/Cosmostat.py | 56 ++++--- files/api/app.py | 43 +++-- files/api/new_descriptors.json | 85 +--------- files/api/shrink.py | 46 ------ files/server/server.php | 5 +- files/web/archive/redis-server.js | 218 ------------------------- files/web/archive/server.php | 107 ------------ files/web/archive/sidebar.js | 36 ---- files/web/archive/system_metric_old.js | 177 -------------------- files/web/node_server/Dockerfile | 18 -- tasks/init.yaml | 25 --- tasks/main.yaml | 5 - tasks/purge.yaml | 34 ---- 14 files changed, 95 insertions(+), 791 deletions(-) delete mode 100644 files/api/shrink.py delete mode 100644 files/web/archive/redis-server.js delete mode 100644 files/web/archive/server.php delete mode 100644 files/web/archive/sidebar.js delete mode 100644 files/web/archive/system_metric_old.js delete mode 100644 files/web/node_server/Dockerfile delete mode 100644 tasks/purge.yaml diff --git a/files/api/Components.py b/files/api/Components.py index 81120cb..511a9a8 100644 --- a/files/api/Components.py +++ b/files/api/Components.py @@ -1,10 +1,16 @@ -# this class file is for the cosmostat service +################################################################# +################################################################# +### Cosmostat Component and System Class +################################################################# +################################################################# + import subprocess import json import time import weakref import base64, hashlib from typing import Dict, Any, List +# Import Cosmos Settings from Cosmos_Settings import * # 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 ################################################################# ################################################################# @@ -42,8 +54,7 @@ class Component: ############################################################ # instantiate new component # this_device is set when the component has multiple instances - ############################################################ - + ############################################################ def __init__(self, name: str, comp_type: str, parent_system, this_device=None): # begin init @@ -246,7 +257,7 @@ class Component: return result ######################################################## - # random data functions + # various data functions ######################################################## # 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 ############################################################ ############################################################ diff --git a/files/api/Cosmostat.py b/files/api/Cosmostat.py index 1744dcf..5e5e5bd 100644 --- a/files/api/Cosmostat.py +++ b/files/api/Cosmostat.py @@ -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 -# The remote systems will submit a json of their state to a private API -# this will define the System Class + +################################################################# +################################################################# +### Cosmostat Classes +### The Cosmostat Server is a Class for the API running on the +### 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 json @@ -11,14 +18,15 @@ import time import weakref import base64, hashlib from typing import Dict, Any, List +# Import Cosmos Settings 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: @@ -106,22 +114,30 @@ class CosmostatServer: return result def get_client_hostnames(self, send_age = False): + self.purge_stale_hostnames() + result = [] + for system in self.systems: + if send_age: + result.append({"hostname": system.hostname, "data_age": age}) + else: + result.append(system.hostname) + return result + + def purge_stale_hostnames(self): now = time.time() fresh_systems = [] - result = [] - for system in self.systems: age = now - system.data_timestamp if age <= 60: # keep only fresh servers fresh_systems.append(system) - if send_age: - result.append({"hostname": system.hostname, "data_age": age}) - else: - result.append(system.hostname) - - self.systems = fresh_systems # replace the old list - return result + 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: diff --git a/files/api/app.py b/files/api/app.py index cad0222..cb72b04 100644 --- a/files/api/app.py +++ b/files/api/app.py @@ -1,16 +1,21 @@ +####################################################################### +### app.py +### cosmostat service handler +####################################################################### + from flask import Flask, jsonify, request, Response from flask_apscheduler import APScheduler from typing import Dict, Union - import json, time, redis, yaml -import base64, hashlib import secrets, string - import requests from requests import RequestException, Response -from Components import * +# Import Cosmos Settings from Cosmos_Settings import * +# System and Component Classes +from Components import * +# Cosmostat server Classes from Cosmostat import * # declare flask apps @@ -373,11 +378,11 @@ def client_update(): log_data(log_output = payload, log_level = "noisy_test") # execute API call result = client_submission_handler(api_url, payload) - client_initialize() + client_api_initialize() return result # Cosmostat Client Initializer -def client_initialize(): +def client_api_initialize(): api_url = f"{cosmostat_server_api()}create_client" # generate payload payload = get_client_payload(get_php_summary(), "client_properties") @@ -446,34 +451,39 @@ if __name__ == '__main__': # Background Loop Function def background_loop(): - # Update all data on the System object unless this is the server - if cosmostat_client.check_system_timer() and not run_cosmostat_server(): + # Update all data on the local System object + if cosmostat_client.check_system_timer() or run_cosmostat_server(): 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"]: update_redis_server() + # report data to the server if configured if run_cosmostat_reporter(): if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer(): cosmostat_client.update_system_state() client_update() + # if this is the server, do this stuff if run_cosmostat_server(): - # update the client state since that was skipped - cosmostat_client.update_system_state() - this_client = get_client_payload(get_client_redis_data(human_readable = False), "redis_data") - if app_settings["noisy_test"]: - print(this_client) - run_update_client(this_client) + # purge stale client systems + cosmostat_server.purge_stale_hostnames() + # report the server's own client object to itself + run_update_client(get_client_payload(get_client_redis_data(human_readable = False), "redis_data")) + log_data(log_output = f"{this_client}", log_level = "noisy_test") time.sleep(0.5) ###################################### - # instantiate client + # instantiate client ###################################### + + # local client System Class Object cosmostat_client = new_cosmos_client() + # remote client reporter if app_settings["cosmostat_server_reporter"] and not app_settings["cosmostat_server"]: - client_initialize() + client_api_initialize() ###################################### # instantiate server @@ -517,6 +527,7 @@ if __name__ == '__main__': if not app_settings["disable_local_api"]: app.run(debug=False, host=service_gateway_ip(), port=service_api_port()) else: + # if local API disabled, phone home if configured print("Internal API Disabled.") while True: if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer(): diff --git a/files/api/new_descriptors.json b/files/api/new_descriptors.json index 4bbcd7d..42d5543 100644 --- a/files/api/new_descriptors.json +++ b/files/api/new_descriptors.json @@ -1,4 +1,7 @@ [ + { + "notes:": "this is both a scratch file and a reference for new component descriptors" + }, { "name": "", "description": "", @@ -37,88 +40,6 @@ "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", diff --git a/files/api/shrink.py b/files/api/shrink.py deleted file mode 100644 index 4f7313e..0000000 --- a/files/api/shrink.py +++ /dev/null @@ -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 \ No newline at end of file diff --git a/files/server/server.php b/files/server/server.php index adb2f4e..5a27b81 100644 --- a/files/server/server.php +++ b/files/server/server.php @@ -95,7 +95,10 @@ $selectedHost = $clients[$selectedIdx]['hostname']; Component Desriptor

To view the component descriptor, you may
curl -s https:///descriptor

-

This will return the entire JSON descriptor variable

+ This will return the entire JSON descriptor variable.
+ The endpoint agent uses this descriptor to build out its local System Object.
+ The agent then reports back to the Cosmostat Server with all the data found in the descriptor.
+ Full Source Code can be found at its Gitea page. diff --git a/files/web/archive/redis-server.js b/files/web/archive/redis-server.js deleted file mode 100644 index 37d8924..0000000 --- a/files/web/archive/redis-server.js +++ /dev/null @@ -1,218 +0,0 @@ -/* ------------------------------------------------------------------ */ -/* 1. Socket‑IO 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); - }); - - // auto‑select 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 – we’ll 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 = '

System Properties

'; - 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 = '

Components

'; - 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 = `

${c.component_name}

`; - 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); \ No newline at end of file diff --git a/files/web/archive/server.php b/files/web/archive/server.php deleted file mode 100644 index a913f92..0000000 --- a/files/web/archive/server.php +++ /dev/null @@ -1,107 +0,0 @@ - [ - 'timeout' => 5, - 'header' => "User-Agent: PHP/".PHP_VERSION."\r\n" - ] -]); - -$apiJson = @file_get_contents($apiUrl, false, $apiCtx); -$clients = json_decode($apiJson, true) ?: []; -?> - - - - -Cosmostat Server Dashboard - - - - -
-

Matt-Cloud Cosmostat Dashboard

-

This dashboard shows Matt‑Cloud system stats.

- -
-
- Component Desriptor -

To view the component descriptor, you may
- curl -s https:///descriptor -

This will return the entire JSON descriptor variable -

- - - - -
- - - - -
-

Loading…

-
-
- - - - - - - - - - - - - \ No newline at end of file diff --git a/files/web/archive/sidebar.js b/files/web/archive/sidebar.js deleted file mode 100644 index 264d87b..0000000 --- a/files/web/archive/sidebar.js +++ /dev/null @@ -1,36 +0,0 @@ - -/* -------------------------------------------------- - 1. Expose the API URL (identical to PHP’s $apiUrl) - -------------------------------------------------- */ -const API_URL = 'http://:/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); - }); -}); \ No newline at end of file diff --git a/files/web/archive/system_metric_old.js b/files/web/archive/system_metric_old.js deleted file mode 100644 index e322f27..0000000 --- a/files/web/archive/system_metric_old.js +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/files/web/node_server/Dockerfile b/files/web/node_server/Dockerfile deleted file mode 100644 index f3aed61..0000000 --- a/files/web/node_server/Dockerfile +++ /dev/null @@ -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"] \ No newline at end of file diff --git a/tasks/init.yaml b/tasks/init.yaml index b3ff624..62653ba 100644 --- a/tasks/init.yaml +++ b/tasks/init.yaml @@ -107,29 +107,4 @@ virtualenv_command: python3 -m venv 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 - ... \ No newline at end of file diff --git a/tasks/main.yaml b/tasks/main.yaml index cf3a557..c2d807c 100644 --- a/tasks/main.yaml +++ b/tasks/main.yaml @@ -1,7 +1,5 @@ --- - - # initializa environment - name: Initialize Environment when: not quick_refresh | bool @@ -16,7 +14,4 @@ when: not disable_local_api include_tasks: web.yaml -#- name: Purge Old Containers -# when: not quick_refresh | bool -# include_tasks: purge.yaml ... \ No newline at end of file diff --git a/tasks/purge.yaml b/tasks/purge.yaml deleted file mode 100644 index 85df7f8..0000000 --- a/tasks/purge.yaml +++ /dev/null @@ -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 \ No newline at end of file