diff --git a/defaults/main.yaml b/defaults/main.yaml
index 56114ad..52c5640 100644
--- a/defaults/main.yaml
+++ b/defaults/main.yaml
@@ -2,7 +2,7 @@
# required system packages
cosmostat_packages:
- - "{{ 'docker' if x64_arch else 'wmdocker' }}"
+ - "{{ '' if x64_arch else 'wmdocker' }}"
- docker.io
- docker-compose
- python3
@@ -35,6 +35,9 @@ cosmostat_sudoers_content: |
# subnet for service
docker_subnet: "192.168.37.0/24"
docker_gateway: "192.168.37.1"
+cosmostat_server_ip: "10.200.27.20"
+api_bind_ip: "{{ docker_gateway }}"
+
# cosmostat service folder root
service_folder: "/opt/cosmostat"
@@ -54,12 +57,13 @@ custom_api_port: "5000"
service_control_web_folder: "{{ service_folder }}/web"
public_dashboard: true
custom_port: "80"
+web_src: "/web"
# other vars
quick_refresh: false
x64_arch: true
-# cosmostat_settings
+# cosmostat_settings, will be for special_server defaults
noisy_test: false
debug_output: true
secure_api: true
@@ -67,7 +71,7 @@ push_redis: true
run_background : true
log_output: true
update_frequency: "1"
-cosmostat_server: false
-cosmostat_server_api: "http://10.200.27.20/"
+cosmostat_server: true
+cosmostat_server_api: "http://{{ cosmostat_server_ip }}:{{ custom_api_port }}/"
cosmostat_server_reporter: false
...
\ No newline at end of file
diff --git a/files/api/Cosmos_Settings.py b/files/api/Cosmos_Settings.py
index 785c50c..bab2c72 100644
--- a/files/api/Cosmos_Settings.py
+++ b/files/api/Cosmos_Settings.py
@@ -1,4 +1,5 @@
import yaml
+from urllib.parse import urlparse
#######################################################################
### Settings Handler Functions
#######################################################################
@@ -32,8 +33,9 @@ with open('cosmostat_settings.yaml', 'r') as f:
app_settings[setting] = cosmos_setting
print("...Done")
+
# this returns the docker gateway from the settings
-def docker_gateway_settings() -> str:
+def cosmostat_bind_ip() -> str:
return cosmostat_settings["docker_gateway"]
# this returns the jenkins user that ran the pipeline
@@ -51,9 +53,23 @@ def jenkins_inventory_generation_timestamp_settings() -> str:
def run_cosmostat_server():
return cosmostat_settings["cosmostat_server"]
+def run_cosmostat_reporter():
+ result = False
+ if not cosmostat_settings["cosmostat_server"] and cosmostat_settings["cosmostat_server_reporter"]:
+ result = True
+ return result
+
def service_gateway_ip():
+ result = "0.0.0.0"
+ if cosmostat_settings["cosmostat_server"]:
+ result = urlparse(cosmostat_settings["cosmostat_server_api"]).hostname
+ elif cosmostat_settings["secure_api"]:
+ result = cosmostat_bind_ip()
+ return result
+
+def redis_gateway_ip():
if cosmostat_settings["secure_api"]:
- return docker_gateway_settings()
+ return cosmostat_bind_ip()
else:
return "0.0.0.0"
@@ -63,7 +79,7 @@ def cosmostat_server_api():
def service_api_port():
return cosmostat_settings["custom_api_port"]
-def log_data(log_output:str, log_level = cosmostat_settings["noisy_test"]):
+def log_data(log_output:str, log_level = "noisy_test"):
log_levels = [
"noisy_test",
"debug_output",
@@ -76,6 +92,9 @@ def log_data(log_output:str, log_level = cosmostat_settings["noisy_test"]):
else:
print(f"Warning - {log_level} not valid log level")
+if app_settings["cosmostat_server"]:
+ app_settings["cosmostat_server_reporter"] = False
+ log_data(log_output = "Warning - server and reporter cannot run concurrently, server is prioritized.", log_level = cosmostat_settings["log_output"])
diff --git a/files/api/Cosmostat.py b/files/api/Cosmostat.py
index 5b7771a..8ddcb26 100644
--- a/files/api/Cosmostat.py
+++ b/files/api/Cosmostat.py
@@ -21,7 +21,7 @@ from Cosmos_Settings import *
#################################################################
#################################################################
-class Cosmostat:
+class CosmostatServer:
############################################################
# instantiate new Cosmostat server
@@ -32,7 +32,7 @@ class Cosmostat:
self.name = name
self.short_id = self.short_uuid(self.name)
log_data(log_output = f"Cosmostat Server {self.short_id} initializing", log_level = "log_output")
- # system contains an array of keys with component objects
+ # system contains an array of CosmostatClient Objects
self.systems = []
def __str__(self):
@@ -43,29 +43,38 @@ class Cosmostat:
self_string = f"Cosmostat Server {self.short_id}"
def add_system(self, system_dictionary: dict):
- new_system_key = {
- "data_timestamp": time.time(),
- "uuid": system_dictionary["uuid"],
- "short_id": system_dictionary["short_id"],
- "client_properties": system_dictionary["client_properties"],
- "redis_data": {}
- }
- log_data(log_output = f"Client system {system_dictionary["short_id"]} added", log_level = "log_output")
- self.systems.append(new_system_key)
+ if not self.check_uuid(system_dictionary["uuid"]):
+ new_cosmostat_clilent = CosmostatClient(
+ name = system_dictionary["short_id"],
+ uuid = system_dictionary["uuid"],
+ hostname = system_dictionary["hostname"],
+ data_timestamp = time.time(),
+ client_properties = system_dictionary["client_properties"],
+ redis_data = {}
+ )
+ self.systems.append(new_cosmostat_clilent)
+ log_data(log_output = f'Client system {system_dictionary["short_id"]} added', log_level = "log_output")
+ return new_cosmostat_clilent.data_timestamp
- def update_system(self, system_state: {}, system_uuid: str):
+
+ def update_system(self, system_dictionary: {}, system_uuid: str):
this_system = self.get_system(system_uuid)
- this_system["redis_data"] = system_state
- this_system["data_timestamp"] = time.time()
- log_data(log_output = f"Client system {this_system["short_id"]} addupdateded", log_level = "log_output")
- return this_system["data_timestamp"]
+ this_system.redis_data = system_dictionary
+ this_system.data_timestamp = time.time()
+ log_data(log_output = f"Client system {this_system.name} update requested, {this_system.uuid}", log_level = "log_output")
+ data_age = time.time() - this_system.data_timestamp
+ if int(data_age) > 60:
+ self.systems.remove(this_system)
+ return this_system.data_timestamp
- def get_system(self, system_uuid: str) -> dict:
- result = {}
+ def get_system(self, system_uuid: str):
+ log_data(log_output = f'Cosmostat - get_system - {system_uuid}', log_level = "debug_output")
+ result = None
for system in self.systems:
- if system["uuid"] == system_uuid:
- return system
+ if system.uuid == system_uuid:
+ result = system
+ break
return result
def short_uuid(self, value: str, length=8):
@@ -73,3 +82,53 @@ class Cosmostat:
hasher.update(value.encode('utf-8'))
full_hex = hasher.hexdigest()
return full_hex[:length]
+
+ def check_uuid(self, uuid: str):
+ uuid_exists = False
+ for system in self.systems:
+ if system.uuid == uuid:
+ uuid_exists = True
+ return uuid_exists
+
+ def get_client_hostname(self, system_uuid: str):
+ client = self.get_system(system_uuid)
+ return client.hostname
+
+ def get_client_hostnames(self, send_age = False):
+ result = []
+ for system in self.systems:
+ data_age = time.time() - system.data_timestamp
+ if int(data_age) > 60:
+ self.systems.remove(system)
+ else:
+ result.append(system.hostname)
+ return result
+
+
+class CosmostatClient:
+
+ ############################################################
+ # instantiate new Cosmostat server
+ ############################################################
+
+ def __init__(self, name: str, uuid: str, hostname: str, data_timestamp: float, client_properties: dict, redis_data: dict):
+ self.name = name
+ self.uuid = uuid
+ self.hostname = hostname
+ self.data_timestamp = data_timestamp
+ self.client_properties = client_properties
+ self.redis_data = redis_data
+
+ def __str__(self):
+ self_string = f'Cosmostat Client {self.name} - Hostname {self.hostname}'
+ return self_string
+
+ def __repr__(self):
+ self_string = f'Cosmostat Client {self.name} - Hostname {self.hostname}'
+ return self_string
+
+ def get_properties(self):
+ return self.client.properties
+
+ def get_redis(self):
+ return self.redis_data
\ No newline at end of file
diff --git a/files/api/app.py b/files/api/app.py
index d10bb9d..6ea1886 100644
--- a/files/api/app.py
+++ b/files/api/app.py
@@ -1,12 +1,17 @@
-from flask import Flask, jsonify, request
+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 requests
+from requests import RequestException, Response
+
from Components import *
from Cosmos_Settings import *
+from Cosmostat import *
+
# declare flask apps
app = Flask(__name__)
scheduler = APScheduler()
@@ -16,7 +21,7 @@ scheduler = APScheduler()
#######################################################################
# Redis client - will publish updates
-r = redis.Redis(host=service_gateway_ip(), port=6379)
+r = redis.Redis(host=redis_gateway_ip(), port=6379)
def update_redis_channel(redis_channel, data):
# Publish to the specified Redis channel
@@ -25,14 +30,14 @@ def update_redis_channel(redis_channel, data):
def update_redis_server():
# Client Redis Tree
- if not run_cosmostat_server():
- if cosmostat_client.check_system_timer():
+ if cosmostat_client.check_system_timer():
update_redis_channel("host_metrics", get_client_redis_data(human_readable = False))
if run_cosmostat_server():
update_redis_channel("client_summary", get_server_redis_data())
+ update_redis_channel("client_hostnames", get_server_hostnames())
- # Server Redis Tree
+ # History Redis Tree
# Update history_stats Redis Channel
# update_redis_channel("history_stats", get_component_list())
@@ -48,13 +53,17 @@ def get_server_redis_data():
result = []
for client in cosmostat_server.systems:
this_client_key = {
- "uuid": client["uuid"],
- "short_id": client["short_id"],
- "redis_data": client["redis_data"]
+ "hostname": client.hostname,
+ "uuid": client.uuid,
+ "short_id": client.name,
+ "redis_data": client.redis_data
}
result.append(this_client_key)
return result
+def get_server_hostnames():
+ return cosmostat_server.get_client_hostnames()
+
#######################################################################
### Client Flask Routes
#######################################################################
@@ -175,30 +184,56 @@ def generate_state_definition():
#######################################################################
# update client on server
-@app.route('/update_client', methods=['GET'])
+@app.route('/update_client', methods=['POST'])
def update_client():
result = {}
- # check the request and return payload if all good
+ # check the request and return payload dict {} if all good
payload = client_submit_check(request = request, dict_name = "redis_data")
- this_client = cosmostat_server.get_system(uuid = payload["uuid"])
- result = run_update_client(this_client)
+ result = run_update_client(payload)
return jsonify(result), 200
# create client on server
-@app.route('/create_client', methods=['GET'])
+@app.route('/create_client', methods=['POST'])
def create_client():
result = {}
- # check the request and return payload if all good
+ # check the request and return payload dict {} if all good
payload = client_submit_check(request = request, dict_name = "client_properties")
- this_client = cosmostat_server.get_system(uuid = payload["uuid"])
- result = run_create_client(this_client)
+ if not cosmostat_server.check_uuid(payload["uuid"]):
+ result = run_create_client(payload)
+ else:
+ result = {"message": "object already exists, skipping creation"}
return jsonify(result), 200
# api to validate Cosmostat Class
@app.route('/client_summary', methods=['GET'])
def client_summary():
- client_summary = get_client_summary()
- return jsonify()
+ result = []
+ if run_cosmostat_server():
+ result = get_client_summary()
+ else:
+ result = {"message": "server not running on this endpoint"}
+ return jsonify(result)
+
+# api to pull all data
+@app.route('/client_details', methods=['GET'])
+def client_details():
+ result = []
+ if run_cosmostat_server():
+ result = get_client_details()
+ else:
+ result = {"message": "server not running on this endpoint"}
+ return jsonify(result)
+
+# api to get all hostnames
+@app.route('/client_hostnames', methods=['GET'])
+def client_hostnames():
+ result = []
+ if run_cosmostat_server():
+ result = cosmostat_server.get_client_hostnames()
+ else:
+ result = {"message": "server not running on this endpoint"}
+ return jsonify(result)
+
#######################################################################
### Server Flask Helpers
@@ -206,29 +241,34 @@ def client_summary():
# update client on server
def run_update_client(this_client):
- if this_client == {}:
+ if not cosmostat_server.check_uuid(this_client["uuid"]):
return { "message": "client not found" }
- update_status = f"updated client {this_client.short_id}"
- timestamp_update = cosmostat_server.update_system(system_state = payload, system_uuid = payload["uuid"])
- return {
- "status": update_status,
- "uuid": payload["uuid"],
- "timestamp": timestamp_update
- }
+ else:
+ timestamp_update = cosmostat_server.update_system(system_dictionary = this_client["redis_data"], system_uuid = this_client["uuid"])
+ update_status = f'updated client {this_client["short_id"]}'
+
+ return {
+ "status": update_status,
+ "uuid": this_client["uuid"],
+ "redis_data": this_client,
+ "timestamp_update": timestamp_update
+ }
# create client on server
def run_create_client(this_client):
- update_status = f"created client {this_client.short_id}"
- timestamp_update = cosmostat_server.create_system(system_state = payload, system_uuid = payload["uuid"])
+ timestamp_update = cosmostat_server.add_system(system_dictionary = this_client)
+ update_status = f'created client {this_client["short_id"]}'
return {
"status": update_status,
- "uuid": payload["uuid"],
- "timestamp": timestamp_update
+ "uuid": this_client["uuid"],
+ "client_properties": this_client,
+ "timestamp_update": timestamp_update
}
-# flask submission check fucntion
+# flask submission check function
def client_submit_check(request, dict_name: str):
- required_keys = {"uuid", "short_id", "data_timestamp", dict_name}
+ payload = {}
+ required_keys = {"uuid", "short_id", "hostname", dict_name}
if not request.is_json:
logging.warning("Received non-JSON request")
return jsonify({"error": "Content-type must be application/json"}), 400
@@ -239,97 +279,98 @@ def client_submit_check(request, dict_name: str):
missing = required_keys - payload.keys()
if missing:
raise ValueError(f"Missing required keys: {', '.join(sorted(missing))}")
-
return payload
# generate cosmostat server summary
def get_client_summary():
result = []
for client in cosmostat_server.systems:
- this_client_properties = client.get_system_properties(human_readable = True)
- this_client_components = []
- for component in client.get_components():
- this_component = {
- "component_name": component.name,
- "info_strings": component.get_properties_strings(return_simple = True)
- }
- this_client_components.append(this_component)
+ data_age = time.time() - client.data_timestamp
this_client = {
- "client_properties": this_client_properties,
- "client_components": this_client_components
+ "uuid": client.uuid,
+ "short_id": client.name,
+ "data_age": data_age,
+ "hostname": client.hostname
}
result.append(this_client)
+ if result == []:
+ result = {"message": "no clients reporting"}
+ return result
+
+# no redis data needed here
+def get_client_details():
+ result = []
+ for client in cosmostat_server.systems:
+ data_age = time.time() - client.data_timestamp
+ this_client = {
+ "uuid": client.uuid,
+ "short_id": client.name,
+ "client_properties": client.client_properties,
+ # "redis_data": client.redis_data,
+ "hostname": client.hostname
+ }
+ result.append(this_client)
+ if result == []:
+ result = {"message": "no clients reporting"}
return result
#######################################################################
### Cosmostat Client Subroutines
#######################################################################
+# since the API isn't running
+# def local_client_update():
+
# Cosmostat Client Reporter
-def client_update(this_client: dict, api_endpoint = "update_client"):
- # set variables for API call
- this_uuid = cosmostat_client.uuid
- this_short_id = cosmostat_client.short_id
- this_timestamp = time.time()
- api_url = f"{cosmostat_server_api()}{api_endpoint}"
- # generate payload
- payload = {
- "uuid": this_uuid,
- "short_id": this_short_id,
- "data_timestamp": this_timestamp, # Unix epoch float
- "redis_data": get_client_redis_data(human_readable = False),
- }
+def client_update():
+ api_url = f"{cosmostat_server_api()}update_client"
+ payload = get_client_payload(get_client_redis_data(human_readable = False), "redis_data")
+ log_data(log_output = "client_update - redis data from local client:", log_level = "noisy_test")
+ log_data(log_output = payload, log_level = "noisy_test")
# execute API call
- result = client_submission_handler()
- if (
- isinstance(result, dict)
- and result.get("message", "").lower() == "client not found"
- ):
- # if client not found, create client
- if api_endpoint == "update_client":
- client_initialize()
- raise RuntimeError("Client not found - initializing")
+ result = client_submission_handler(api_url, payload)
+ client_initialize()
return result
# Cosmostat Client Initializer
def client_initialize():
- # set variables for API call
- this_uuid = cosmostat_client.uuid
- this_short_id = cosmostat_client.short_id
- this_timestamp = time.time()
api_url = f"{cosmostat_server_api()}create_client"
# generate payload
- payload = {
- "uuid": this_uuid,
- "short_id": this_short_id,
- "data_timestamp": this_timestamp, # Unix epoch float
- "client_properties": get_php_summary(),
- }
+ payload = get_client_payload(get_php_summary(), "client_properties")
# execute API call
- result = client_submission_handler()
+ result = client_submission_handler(api_url, payload)
return result
# Cosmostat Client API Reporting Handler
-def client_submission_handler():
+def client_submission_handler(api_url: str, payload: dict):
result = None
try:
# `json=` automatically sets Content-Type to application/json
- response: Response = requests.post(api_url, json=payload, timeout=timeout)
+ response: Response = requests.post(api_url, json=payload, timeout=4)
response.raise_for_status() # raise HTTPError for 4xx/5xx
except RequestException as exc:
# Wrap the low-level exception in a more descriptive one
- raise RuntimeError(
- f"Failed to POST to {url!r}: {exc}"
- ) from exc
+ log_data(log_output = f"Failed to POST to {api_url!r}: {exc}", log_level = "log_output")
# process reply from API
try:
result = response.json()
except ValueError as exc:
- raise RuntimeError(
- f"Server responded with non-JSON payload: {response.text!r}"
- ) from exc
+ log_data(log_output = "Server responded with non-JSON payload: {response.text!r}", log_level = "log_output")
return result
+def get_client_payload(system_dictionary: dict, dictionary_name: str):
+ this_uuid = cosmostat_client.uuid
+ this_short_id = cosmostat_client.short_id
+ this_hostname = cosmostat_client.name
+ payload = {
+ "uuid": this_uuid,
+ "short_id": this_short_id,
+ "hostname": this_hostname,
+ dictionary_name: system_dictionary
+
+ }
+ return payload
+
#######################################################################
#######################################################################
@@ -353,9 +394,9 @@ if __name__ == '__main__':
return new_client
# instantiate and return the Cosmoserver System object
- def new_cosmos_server():
- new_server = Cosmoserver(cosmostat_client.uuid)
- log_data(log_output = f"New Cosmostat object name: {new_server.name}", log_level = "log_output")
+ def new_cosmostat_server():
+ new_server = CosmostatServer(cosmostat_client.uuid)
+ log_data(log_output = f"New Cosmostat serverobject name: {new_server.name}", log_level = "log_output")
return new_server
# Background Loop Function
@@ -366,15 +407,26 @@ if __name__ == '__main__':
if app_settings["push_redis"]:
update_redis_server()
-
- if app_settings["cosmostat_server_reporter"]:
+
+ 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 run_cosmostat_server():
+ this_client = get_client_payload(get_client_redis_data(human_readable = False), "redis_data")
+ run_update_client(this_client)
+
+
+
+
+ time.sleep(0.5)
+
######################################
# instantiate client
######################################
cosmostat_client = new_cosmos_client()
- if app_settings["cosmostat_server_reporter"]:
+ if app_settings["cosmostat_server_reporter"] and not app_settings["cosmostat_server"]:
client_initialize()
######################################
@@ -383,7 +435,9 @@ if __name__ == '__main__':
cosmostat_server = None
if run_cosmostat_server():
- cosmostat_server = new_cosmos_server()
+ cosmostat_server = new_cosmostat_server()
+ this_client = get_client_payload(get_php_summary(), "client_properties")
+ timestamp_update = cosmostat_server.add_system(system_dictionary = this_client)
######################################
# send initial stats update to redis
diff --git a/files/api/descriptors.json b/files/api/descriptors.json
index 0659abf..9dbd9ab 100644
--- a/files/api/descriptors.json
+++ b/files/api/descriptors.json
@@ -118,7 +118,7 @@
},
"metrics": {
"MB Used": "free -m | grep Mem | awk '{print $3}'",
- "MB Free": "free -m | grep Mem | awk '{print $4}'"
+ "MB Available": "free -m | grep Mem | awk '{print $7}'"
},
"virt_ignore": [
"RAM Type",
@@ -136,13 +136,13 @@
},
{
"name": "LAN",
- "description": "{Device ID} - {Device Name} - {MAC Address}",
+ "description": "{Device Name} - {Device ID} - {MAC Address}",
"multi_check": "True",
"device_list": "ip link | grep default | grep -v -e docker -e 127.0.0.1 -e br- -e veth -e lo -e tun | cut -d ':' -f 2 | awk '{{print $1}}' ",
"properties": {
"MAC Address": "ip link | grep -A1 ' {this_device}' | grep ether | awk '{{print $2}}'",
"Device Name": "echo {this_device}",
- "Device ID": "udevadm info -q property -p $(ls -l /sys/class/net/ | grep {this_device} | cut -d '>' -f2 | cut -b 8- ) | grep ID_MODEL_FROM_DATABASE | cut -d '=' -f2 "
+ "Device ID": "( udevadm info -q property -p $(ls -l /sys/class/net/ | grep {this_device} | cut -d '>' -f2 | cut -b 8-) | grep ID_MODEL_FROM_DATABASE || echo 'ID_MODEL_FROM_DATABASE=missing' ) | cut -d '=' -f2"
},
"metrics": {
"IP Address": "ip -o -4 ad | grep -v -e docker -e 127.0.0.1 -e br- | grep {this_device} | awk '{{print $4}}'",
@@ -185,7 +185,7 @@
"properties": {
"Device Name": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print $1}}' | grep {this_device}",
"Device Path": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print \"/dev/\"$1}}' | grep {this_device}",
- "Drive Type": "lsblk -d -o NAME,TRAN | grep {this_device} | awk '{{print $2}}'",
+ "Drive Type": "lsblk -d -o NAME,TRAN | grep {this_device} | awk '{{print ($2 != \"\" ? $2 : \"missing\")}}'",
"Total Capacity": "lsblk -d -o NAME,SIZE | grep {this_device} | awk '{{print $2}}'",
"SMART Check": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r .smart_status.passed"
},
diff --git a/files/server/server.php b/files/server/server.php
new file mode 100644
index 0000000..21125ff
--- /dev/null
+++ b/files/server/server.php
@@ -0,0 +1,161 @@
+";
+$context = stream_context_create([
+ 'http' => [
+ 'timeout' => 5,
+ 'header' => "User-Agent: PHP/" . PHP_VERSION . "\r\n"
+ ]
+]);
+
+$json = @file_get_contents($apiUrl, false, $context);
+if ($json === false) {
+ die('
Could not fetch data from the API.
');
+}
+$clients = json_decode($json, true);
+if ($clients === null || !is_array($clients)) {
+ die('Malformed JSON returned from the API.
');
+}
+
+/* --------------------- 2. Resolve selected host ------------- */
+$selectedHost = $_GET['host'] ?? '';
+$selectedIdx = null;
+foreach ($clients as $idx => $client) {
+ if (strtolower($client['hostname']) === strtolower($selectedHost)) {
+ $selectedIdx = $idx;
+ break;
+ }
+}
+if ($selectedIdx === null) {
+ // no match - default to the first host (or none)
+ $selectedIdx = 0;
+ $selectedHost = $clients[$selectedIdx]['hostname'] ?? '';
+}
+$client = $clients[$selectedIdx] ?? null;
+$properties = $client['client_properties'][0] ?? [];
+$systemProperties = $properties['system_properties'] ?? [];
+$systemComponents = $properties['system_components'] ?? [];
+
+?>
+
+
+
+
+
+Cosmostat - = h($selectedHost) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Matt-Cloud Cosmostat Dashboard
+
This dashboard shows the local Matt-Cloud system stats.
+
API
+
+
+
+
Component Desriptor
+
To view the component descriptor, you may
+ curl -s https://= h($_SERVER['SERVER_NAME']) ?>/descriptor
+
This will return the entire JSON descriptor variable
+
+
+
+
+
+
+
System Properties
+
+
+
+
+
+
+ - = h($prop['Property']) ?>
+
+
+ |
+
+ Live System Metrics
+ Connecting...
+ |
+
+
+
+
+
+
+
Components
+
+
+
+
= h($comp['component_name']) ?>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/files/server/sidebar.js b/files/server/sidebar.js
new file mode 100644
index 0000000..87a7f01
--- /dev/null
+++ b/files/server/sidebar.js
@@ -0,0 +1,134 @@
+
+// Helper - return the value of the ?host= query‑string
+function getSelectedHost() {
+ const params = new URLSearchParams(window.location.search);
+ return params.get('host') || '';
+}
+
+// Build the endpoints list when we receive data
+socket.on('client_hostnames', rawMsg => {
+ // rawMsg is the JSON string that redis-cli prints
+ let hosts;
+ try {
+ hosts = JSON.parse(rawMsg);
+ } catch (e) {
+ console.warn('Could not parse client_hostnames message', rawMsg);
+ return;
+ }
+
+ // Sanity‑check
+ if (!Array.isArray(hosts)) { return; }
+
+ const ol = document.getElementById('endpointList');
+ const selected = getSelectedHost().toLowerCase();
+
+ // Clear old list
+ ol.innerHTML = '';
+
+ hosts.forEach(host => {
+ const li = document.createElement('li');
+ const a = document.createElement('a');
+ a.href = '?host=' + encodeURIComponent(host);
+ a.textContent = host;
+ if (host.toLowerCase() === selected) {
+ a.classList.add('active');
+ }
+ li.appendChild(a);
+ ol.appendChild(li);
+ });
+});
+
+/* -----------------------------------------------
+ 2. (Optional) Re‑build the list if the URL changes
+ ----------------------------------------------- */
+window.addEventListener('popstate', () => {
+ // When the user navigates via back/forward the page
+ // still holds the old list, so we rebuild it.
+ const currentSelected = getSelectedHost().toLowerCase();
+ const anchors = document.querySelectorAll('#endpointList a');
+ anchors.forEach(a => {
+ a.classList.toggle('active', a.textContent.toLowerCase() === currentSelected);
+ });
+});
+
+(function () {
+ /* ----------------------------------------------------------
+ Use the socket that system_metrics.js already created.
+ If for some reason it isn’t defined, create a new one.
+ ---------------------------------------------------------- */
+ const sock = typeof socket !== 'undefined' ? socket : io();
+
+ /* ----------------------------------------------------------
+ Return the hostname that is currently selected in the URL
+ (the value of the “?host=…” query string).
+ ---------------------------------------------------------- */
+ function getSelectedHost() {
+ const params = new URLSearchParams(window.location.search);
+ return params.get('host') || '';
+ }
+
+ /* ----------------------------------------------------------
+ Populate with - items.
+ ---------------------------------------------------------- */
+ function buildList(hosts) {
+ const ol = document.getElementById('endpointList');
+ const selected = getSelectedHost().toLowerCase();
+
+ ol.innerHTML = ''; // clear old items
+
+ hosts.forEach(host => {
+ const li = document.createElement('li');
+ const a = document.createElement('a');
+ a.href = '?host=' + encodeURIComponent(host);
+ a.textContent = host;
+ if (host.toLowerCase() === selected) a.classList.add('active');
+ li.appendChild(a);
+ ol.appendChild(li);
+ });
+ }
+
+ /* ----------------------------------------------------------
+ Listen for the “client_hostnames” event from the server.
+ The payload can be:
+ – a JSON string → parse it
+ – a plain array → use it directly
+ – an object with a .data array (fallback)
+ ---------------------------------------------------------- */
+ sock.on('client_hostnames', payload => {
+ let hosts;
+
+ if (typeof payload === 'string') {
+ try {
+ hosts = JSON.parse(payload);
+ } catch (e) {
+ console.warn('client_hostnames message is not JSON:', payload);
+ return;
+ }
+ } else if (Array.isArray(payload)) {
+ hosts = payload;
+ } else if (payload && Array.isArray(payload.data)) {
+ hosts = payload.data;
+ } else {
+ console.warn('client_hostnames payload format unrecognised:', payload);
+ return;
+ }
+
+ if (!Array.isArray(hosts)) {
+ console.warn('client_hostnames payload did not resolve to an array:', hosts);
+ return;
+ }
+
+ buildList(hosts);
+ });
+
+ /* ----------------------------------------------------------
+ When the user navigates via the back/forward buttons,
+ re‑apply the “active” class to the correct link.
+ ---------------------------------------------------------- */
+ window.addEventListener('popstate', () => {
+ const selected = getSelectedHost().toLowerCase();
+ document.querySelectorAll('#endpointList a').forEach(a => {
+ a.classList.toggle('active', a.textContent.toLowerCase() === selected);
+ });
+ });
+})();
\ No newline at end of file
diff --git a/files/server/system_metrics.js b/files/server/system_metrics.js
new file mode 100644
index 0000000..a3429a9
--- /dev/null
+++ b/files/server/system_metrics.js
@@ -0,0 +1,179 @@
+/* ------------------------------------------------------------
+ 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;
+}
+
+
diff --git a/files/web/archive/redis-server.js b/files/web/archive/redis-server.js
new file mode 100644
index 0000000..37d8924
--- /dev/null
+++ b/files/web/archive/redis-server.js
@@ -0,0 +1,218 @@
+/* ------------------------------------------------------------------ */
+/* 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
new file mode 100644
index 0000000..a913f92
--- /dev/null
+++ b/files/web/archive/server.php
@@ -0,0 +1,107 @@
+ [
+ '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.
+
API
+
+
+
Component Desriptor
+
To view the component descriptor, you may
+ curl -s https:///descriptor
+
This will return the entire JSON descriptor variable
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/files/web/archive/sidebar.js b/files/web/archive/sidebar.js
new file mode 100644
index 0000000..264d87b
--- /dev/null
+++ b/files/web/archive/sidebar.js
@@ -0,0 +1,36 @@
+
+/* --------------------------------------------------
+ 1. Expose the API URL (identical to PHP’s $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);
+ });
+});
\ No newline at end of file
diff --git a/files/web/archive/system_metric_old.js b/files/web/archive/system_metric_old.js
new file mode 100644
index 0000000..e322f27
--- /dev/null
+++ b/files/web/archive/system_metric_old.js
@@ -0,0 +1,177 @@
+/* ------------------------------------------------------------
+ 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/html/index.php b/files/web/html/index.php
index 71e74e8..1fe5e56 100644
--- a/files/web/html/index.php
+++ b/files/web/html/index.php
@@ -56,10 +56,11 @@
}
$api_settings[$key] = $value;
}
- $dockerGateway = trim($api_settings['docker_gateway'], "\"'") ?? null;
+ $api_bind_ip = trim($api_settings['api_bind_ip'], "\"'") ?? null;
$customApiPort = trim($api_settings['custom_api_port'], "\"'") ?? null;
# load API data
- $apiUrl = 'http://'.$dockerGateway.':'.$customApiPort.'/php_summary';
+ $apiUrl = 'http://'.$api_bind_ip.':'.$customApiPort.'/php_summary';
+ echo "";
$context = stream_context_create([
'http' => [
'timeout' => 5, // seconds
diff --git a/files/web/html/src/styles.css b/files/web/html/src/styles.css
index 1a4d30f..3c0191e 100644
--- a/files/web/html/src/styles.css
+++ b/files/web/html/src/styles.css
@@ -1,63 +1,123 @@
-/* styles.css */
+/* -------------------------------------------------
+ 1. Global settings & color palette
+ ------------------------------------------------- */
+:root {
+ /* Dark theme – body & card backgrounds */
+ --bg-body: #2c3e50; /* main page background */
+ --bg-card: #34495e; /* card / panel background */
+ --bg-sidebar: #3d566e; /* sidebar background (slightly lighter) */
+ /* Accent / link colour */
+ --clr-accent: #3498db; /* blue accent for links */
+ /* Text colour */
+ --clr-text: #ecf0f1; /* light whiteish text */
+ /* Borders / accents */
+ --clr-border: #1f2b38; /* dark border colour */
+}
+* { box-sizing: border-box; }
+
+/* Body */
body {
- font-family: Arial, sans-serif;
margin: 0;
padding: 0;
- background-color: #2c3e50; /* Dark background color */
- color: #bdc3c7; /* Dimmer text color */
+ background: var(--bg-body);
+ color: var(--clr-text);
+ font-family: Arial, Helvetica, sans-serif;
}
-table, th, td {
- border: 2px solid #182939;
- border-collapse: collapse;
-}
-th, td {
- padding: 10px;
-}
+/* Links */
+a { color: var(--clr-accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+/* -------------------------------------------------
+ 2. Layout – wrapper, sidebar, main
+ ------------------------------------------------- */
+.wrapper { display: flex; min-height: 100vh; }
+
+.sidebar {
+ width: 200px;
+ background: var(--bg-sidebar);
+ padding: 1rem;
+}
+.sidebar h3 { margin: 0 0 .4rem 0; font-size: 1.1rem; }
+.sidebar ul { list-style: none; padding: 0; margin: 0; }
+.sidebar ol { list-style: none; padding: 0; margin: 0; }
+.sidebar li { margin-bottom: .4rem; }
+.sidebar a { color: var(--clr-accent); }
+.sidebar a.active { font-weight: bold; }
+
+.main { flex: 1; padding: 1rem; }
+
+/* -------------------------------------------------
+ 3. Card component
+ ------------------------------------------------- */
.card {
max-width: 950px;
- margin: 0 auto;
+ margin: 20px auto 1rem auto;
padding: 20px;
- background-color: #34495e; /* Darker background for container */
+ background: var(--bg-card);
+ border: 1px solid var(--clr-border);
border-radius: 8px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); /* Slightly darker shadow */
- margin-top: 20px;
+ box-shadow: 0 2px 4px rgba(0,0,0,.3);
}
-h1, h2, h3, h4 {
- color: #bdc3c7; /* Dimmer text color */
+/* -------------------------------------------------
+ 4. Tables
+ ------------------------------------------------- */
+table, th, td {
+ border: 2px solid var(--clr-border);
+ border-collapse: collapse;
+}
+th, td { padding: 10px; }
+
+/* Alternate row colour for metrics table */
+#host_metrics_table tbody tr td:nth-of-type(even) {
+ background: #3e5c78; /* slight contrast */
}
-ul {
- list-style-type: none;
- padding: 0;
-}
+/* -------------------------------------------------
+ 5. Lists & headings
+ ------------------------------------------------- */
+h1, h2, h3, h4 { color: var(--clr-text); margin: 0 0 .4rem 0; }
+ul { list-style: none; padding: 0; }
+ol { list-style: none; padding: 0; }
+li { margin-bottom: 10px; color: var(--clr-text); }
-li {
- margin-bottom: 10px;
- color: #bdc3c7; /* Dimmer text color */
+/* System & component lists */
+.system-list, .info-list {
+ list-style: none; padding: 0; margin: 0;
}
+.system-list li, .info-list li { margin-bottom: 5px; color: var(--clr-text); }
-#host_metrics_column td {
- list-style: none; /* removes the numeric markers */
- padding-left: 0; /* remove the default left indent */
- margin-left: 0; /* remove the default left margin */
+/* -------------------------------------------------
+ 6. Components grid
+ ------------------------------------------------- */
+.components {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
+ gap: 1rem;
}
-
-#host_metrics_table tbody tr td :nth-of-type(even) {
- background-color: #3e5c78;
+.component {
+ padding: 10px;
+ border: 1px solid var(--clr-border);
+ border-radius: 4px;
}
+.component h3 { margin: 0 0 5px; }
-.help-link{
- cursor:pointer;
- user-select:none;
- color: #2c3e50;
+/* -------------------------------------------------
+ 7. Help toggle / modal
+ ------------------------------------------------- */
+.help-link {
+ cursor: pointer;
+ user-select: none;
+ color: var(--clr-accent);
text-align: right;
}
-.help-link:hover{ text-decoration:underline; }
+.help-link:hover { text-decoration: underline; }
+#helpText { display: none; }
-#helpText{
- display:none; /* hidden by default */
-}
+/* -------------------------------------------------
+ 8. Misc helpers
+ ------------------------------------------------- */
+/* Hide numeric markers in metric columns (if any) */
+#host_metrics_column td { list-style: none; padding-left: 0; margin-left: 0; }
\ No newline at end of file
diff --git a/files/web/node_server/server.js b/files/web/node_server/server.js
index 9421b98..6ade805 100644
--- a/files/web/node_server/server.js
+++ b/files/web/node_server/server.js
@@ -25,8 +25,9 @@ try {
}
const API_PORT = config.custom_api_port || 5000; // fallback to 5000
-const API_HOST = config.docker_gateway || '192.168.37.1'; // fallback IP
+const API_HOST = config.api_bind_ip || '192.168.37.1'; // fallback IP
const API_BASE = `http://${API_HOST}:${API_PORT}`;
+console.log('API URL:', API_BASE);
// ---------------------------------------------------------------------
// ---------- 2. Socket.io ------------------------------------------------
@@ -87,6 +88,37 @@ redisClient.on('error', err => console.error('Redis error', err));
}
);
+ // Subscribe to the channel that sends host stats
+ await sub.subscribe(
+ ['client_summary'],
+ (message, channel) => {
+ let payload;
+ try {
+ payload = JSON.parse(message); // message is a JSON string
+ } catch (e) {
+ console.error(`Failed to parse ${channel}`, e);
+ return;
+ }
+ io.emit(channel, payload);
+ }
+ );
+
+ // Subscribe to the channel that sends host stats
+ await sub.subscribe(
+ ['client_hostnames'],
+ (message, channel) => {
+ let payload;
+ try {
+ payload = JSON.parse(message); // message is a JSON string
+ } catch (e) {
+ console.error(`Failed to parse ${channel}`, e);
+ return;
+ }
+ io.emit(channel, payload);
+ }
+ );
+
+
sub.on('error', err => console.error('Subscriber error', err));
})();
diff --git a/tasks/api.yaml b/tasks/api.yaml
index 65ef8a0..4d72b71 100644
--- a/tasks/api.yaml
+++ b/tasks/api.yaml
@@ -1,4 +1,8 @@
---
+- name: Cosmostat - API - Set api_bind_ip
+ when: cosmostat_server | bool
+ set_fact:
+ api_bind_ip: "{{ cosmostat_server_ip }}"
- name: Cosmostat - API - Stop Service
become: true
@@ -32,7 +36,7 @@
service_exe: "{{ api_service_exe }}"
service_group: "{{ service_user }}"
extra_options: ""
- extra_service_options: ""
+ extra_service_options: "RestartSec=5"
template:
src: "service_template.service"
dest: "{{ user_service_folder }}/{{ api_service_name }}.service"
diff --git a/tasks/init.yaml b/tasks/init.yaml
index 8c1130c..a589da6 100644
--- a/tasks/init.yaml
+++ b/tasks/init.yaml
@@ -15,7 +15,9 @@
register: dpkg_output
- name: Cosmostat - Init - Install Prereq Packages
- when: cosmostat_packages_item not in dpkg_output.stdout_lines
+ when:
+ - cosmostat_packages_item not in dpkg_output.stdout_lines
+ - cosmostat_packages_item | length > 0
apt:
name:
- "{{ cosmostat_packages_item }}"
diff --git a/tasks/server.yaml b/tasks/server.yaml
index 6f639b6..3ceacbb 100644
--- a/tasks/server.yaml
+++ b/tasks/server.yaml
@@ -1,8 +1,33 @@
---
-# this will be ran to install the full cosmostat server dashboard
+# this will be ran to install the server dashboard at root
+- name: Cosmostat - Server Dashboard - replace index.php
+ copy:
+ src: server/server.php
+ dest: "{{ service_control_web_folder }}/index.php"
+ mode: 0755
+ owner: "{{ service_user }}"
+ group: "{{ service_user }}"
+- name: Cosmostat - Server Dashboard - copy sidebar.js
+ copy:
+ src: server/sidebar.js
+ dest: "{{ service_control_web_folder }}/src/sidebar.js"
+ mode: 0755
+ owner: "{{ service_user }}"
+ group: "{{ service_user }}"
+- name: Cosmostat - Server Dashboard - copy system_metrics.js
+ copy:
+ src: server/system_metrics.js
+ dest: "{{ service_control_web_folder }}/src/system_metrics.js"
+ mode: 0755
+ owner: "{{ service_user }}"
+ group: "{{ service_user }}"
+- name: Cosmostat - Server Dashboard - delete redis.js
+ ansible.builtin.file:
+ path: "{{ service_control_web_folder }}/src/redis.js"
+ state: absent
...
\ No newline at end of file
diff --git a/templates/cosmostat_settings.yaml b/templates/cosmostat_settings.yaml
index 171a1b2..1ef4b86 100644
--- a/templates/cosmostat_settings.yaml
+++ b/templates/cosmostat_settings.yaml
@@ -18,8 +18,8 @@ ansible_hostname: "{{ ansible_hostname }}"
##################################################################
# docker subnet, will use to bind the IP in default secure mode
-docker_subnet: "{{ docker_subnet }}"
-docker_gateway: "{{ docker_gateway }}"
+api_bind_ip: {{ api_bind_ip }}
+docker_gateway: {{ docker_gateway }}
# python system variables, no quotes for bool or int
secure_api: {{ secure_api }}