cosmostat working
This commit is contained in:
@ -14,6 +14,7 @@ cosmostat_packages:
|
|||||||
- jc
|
- jc
|
||||||
- smartmontools
|
- smartmontools
|
||||||
- inxi
|
- inxi
|
||||||
|
- easy-rsa
|
||||||
|
|
||||||
# python venv packages
|
# python venv packages
|
||||||
cosmostat_venv_packages: |
|
cosmostat_venv_packages: |
|
||||||
@ -38,7 +39,6 @@ docker_gateway: "192.168.37.1"
|
|||||||
cosmostat_server_ip: "10.200.27.20"
|
cosmostat_server_ip: "10.200.27.20"
|
||||||
api_bind_ip: "{{ docker_gateway }}"
|
api_bind_ip: "{{ docker_gateway }}"
|
||||||
|
|
||||||
|
|
||||||
# cosmostat service folder root
|
# cosmostat service folder root
|
||||||
service_folder: "/opt/cosmostat"
|
service_folder: "/opt/cosmostat"
|
||||||
|
|
||||||
@ -52,6 +52,7 @@ api_service_folder: "{{ service_folder }}/api"
|
|||||||
venv_folder: "{{ service_folder }}/venv"
|
venv_folder: "{{ service_folder }}/venv"
|
||||||
api_service_exe: "{{ venv_folder }}/bin/python -u {{ api_service_folder }}/app.py"
|
api_service_exe: "{{ venv_folder }}/bin/python -u {{ api_service_folder }}/app.py"
|
||||||
custom_api_port: "5000"
|
custom_api_port: "5000"
|
||||||
|
REAL_API_KEY: "DEADBEEF"
|
||||||
|
|
||||||
# dashboard vars
|
# dashboard vars
|
||||||
service_control_web_folder: "{{ service_folder }}/web"
|
service_control_web_folder: "{{ service_folder }}/web"
|
||||||
@ -67,11 +68,14 @@ x64_arch: true
|
|||||||
noisy_test: false
|
noisy_test: false
|
||||||
debug_output: true
|
debug_output: true
|
||||||
secure_api: true
|
secure_api: true
|
||||||
push_redis: true
|
push_redis: false
|
||||||
run_background : true
|
run_background : true
|
||||||
log_output: true
|
log_output: true
|
||||||
update_frequency: "1"
|
update_frequency: "1"
|
||||||
cosmostat_server: true
|
cosmostat_server: true
|
||||||
cosmostat_server_api: "http://{{ cosmostat_server_ip }}:{{ custom_api_port }}/"
|
cosmostat_server_api: "https://cosmostat.testy-cal.com/"
|
||||||
|
local_api_address: "http://{{ cosmostat_server_ip }}:{{ custom_api_port }}/"
|
||||||
cosmostat_server_reporter: false
|
cosmostat_server_reporter: false
|
||||||
|
# setting this to true for default install
|
||||||
|
disable_local_api: true
|
||||||
...
|
...
|
||||||
@ -69,6 +69,7 @@ class Component:
|
|||||||
self.virt_ignore = self._descriptor.get('virt_ignore', [])
|
self.virt_ignore = self._descriptor.get('virt_ignore', [])
|
||||||
self.multi_metrics = self._descriptor.get('multi_metrics', [])
|
self.multi_metrics = self._descriptor.get('multi_metrics', [])
|
||||||
self.arch_check = self._descriptor.get('arch_check', [])
|
self.arch_check = self._descriptor.get('arch_check', [])
|
||||||
|
self.php_extra_list = self._descriptor.get('php_extra', [])
|
||||||
if self.is_virtual:
|
if self.is_virtual:
|
||||||
self.virt_ignore = []
|
self.virt_ignore = []
|
||||||
|
|
||||||
@ -173,6 +174,13 @@ class Component:
|
|||||||
log_data(log_output = f"result - {result_command}", log_level = "debug_output")
|
log_data(log_output = f"result - {result_command}", log_level = "debug_output")
|
||||||
return result_command
|
return result_command
|
||||||
|
|
||||||
|
# check if this property should show in the System Properties box
|
||||||
|
def check_php_extra(self, property_name):
|
||||||
|
result = False
|
||||||
|
if property_name in self.php_extra_list:
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
# keyed data functions
|
# keyed data functions
|
||||||
########################################################
|
########################################################
|
||||||
@ -407,7 +415,6 @@ class System:
|
|||||||
if multi_check:
|
if multi_check:
|
||||||
log_data(log_output = f"Creating one component of type {component_name} for each one found", log_level = "log_output")
|
log_data(log_output = f"Creating one component of type {component_name} for each one found", log_level = "log_output")
|
||||||
component_type_device_list = get_device_list(component_name)
|
component_type_device_list = get_device_list(component_name)
|
||||||
component_id = 0
|
|
||||||
for this_device in component_type_device_list:
|
for this_device in component_type_device_list:
|
||||||
this_component_ID = component_type_device_list.index(this_device)
|
this_component_ID = component_type_device_list.index(this_device)
|
||||||
this_component_name = f"{component_name} {this_component_ID}"
|
this_component_name = f"{component_name} {this_component_ID}"
|
||||||
@ -486,7 +493,7 @@ class System:
|
|||||||
result.append(metric)
|
result.append(metric)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def get_system_properties(self, human_readable = False):
|
def get_system_properties(self, human_readable = False, php_extra = False):
|
||||||
result = []
|
result = []
|
||||||
for name, value in self._properties.items():
|
for name, value in self._properties.items():
|
||||||
if human_readable:
|
if human_readable:
|
||||||
@ -500,6 +507,22 @@ class System:
|
|||||||
"Property": name,
|
"Property": name,
|
||||||
"Value": value
|
"Value": value
|
||||||
})
|
})
|
||||||
|
if php_extra and human_readable:
|
||||||
|
for component_result in self.php_component_data():
|
||||||
|
result.append(component_result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
def php_component_data(self):
|
||||||
|
result = []
|
||||||
|
for component in self.components:
|
||||||
|
for this_property in component._properties:
|
||||||
|
if component.check_php_extra(this_property):
|
||||||
|
result_string = f"{this_property}: {component._properties[this_property]}"
|
||||||
|
result.append({
|
||||||
|
"Source": "System",
|
||||||
|
"Property": result_string
|
||||||
|
})
|
||||||
return result
|
return result
|
||||||
|
|
||||||
########################################################
|
########################################################
|
||||||
@ -584,7 +607,12 @@ def run_command(cmd, zero_only=False, use_shell=True, req_check = True):
|
|||||||
except:
|
except:
|
||||||
return output_lines
|
return output_lines
|
||||||
|
|
||||||
|
# need to add a archticture checker for this
|
||||||
|
# i also want to make the loop cleaner
|
||||||
|
# i don't need to iterate over the component class tree
|
||||||
|
# to get what I want, i think
|
||||||
def get_device_list(device_type_name: str):
|
def get_device_list(device_type_name: str):
|
||||||
|
|
||||||
result = []
|
result = []
|
||||||
for component in component_class_tree:
|
for component in component_class_tree:
|
||||||
precheck_value = 1
|
precheck_value = 1
|
||||||
@ -594,6 +622,7 @@ def get_device_list(device_type_name: str):
|
|||||||
precheck_value = int(precheck_value_output)
|
precheck_value = int(precheck_value_output)
|
||||||
log_data(log_output = f"Precheck found - {precheck_command} - {precheck_value}", log_level = "log_output")
|
log_data(log_output = f"Precheck found - {precheck_command} - {precheck_value}", log_level = "log_output")
|
||||||
if component["name"] == device_type_name and precheck_value != 0:
|
if component["name"] == device_type_name and precheck_value != 0:
|
||||||
|
|
||||||
device_list_command = component["device_list"]
|
device_list_command = component["device_list"]
|
||||||
device_list_result = run_command(device_list_command)
|
device_list_result = run_command(device_list_command)
|
||||||
result = device_list_result
|
result = device_list_result
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
import yaml
|
import yaml
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
import secrets, string
|
||||||
#######################################################################
|
#######################################################################
|
||||||
### Settings Handler Functions
|
### Settings Handler Functions
|
||||||
#######################################################################
|
#######################################################################
|
||||||
@ -16,7 +17,11 @@ app_settings = {
|
|||||||
"cosmostat_server_reporter": False,
|
"cosmostat_server_reporter": False,
|
||||||
"update_frequency": 1,
|
"update_frequency": 1,
|
||||||
"custom_api_port": "5000",
|
"custom_api_port": "5000",
|
||||||
"cosmostat_server_api": "http://10.200.27.20:5000/"
|
"cosmostat_server_api": "http://10.200.27.20:5000/",
|
||||||
|
"REAL_API_KEY": ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(256)),
|
||||||
|
"disable_local_api": False,
|
||||||
|
"local_api_address": "http://10.200.27.20:5000/",
|
||||||
|
"cosmostat_server_ip": "10.200.27.20"
|
||||||
}
|
}
|
||||||
|
|
||||||
with open('cosmostat_settings.yaml', 'r') as f:
|
with open('cosmostat_settings.yaml', 'r') as f:
|
||||||
@ -61,9 +66,7 @@ def run_cosmostat_reporter():
|
|||||||
|
|
||||||
def service_gateway_ip():
|
def service_gateway_ip():
|
||||||
result = "0.0.0.0"
|
result = "0.0.0.0"
|
||||||
if cosmostat_settings["cosmostat_server"]:
|
if cosmostat_settings["secure_api"] and not cosmostat_settings["cosmostat_server"]:
|
||||||
result = urlparse(cosmostat_settings["cosmostat_server_api"]).hostname
|
|
||||||
elif cosmostat_settings["secure_api"]:
|
|
||||||
result = cosmostat_bind_ip()
|
result = cosmostat_bind_ip()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
|||||||
@ -94,14 +94,32 @@ class CosmostatServer:
|
|||||||
client = self.get_system(system_uuid)
|
client = self.get_system(system_uuid)
|
||||||
return client.hostname
|
return client.hostname
|
||||||
|
|
||||||
def get_client_hostnames(self, send_age = False):
|
def get_client_timestamp(self, system_hostname: str):
|
||||||
result = []
|
client = self.get_system(get_uuid_from_hostname(system_hostname))
|
||||||
|
return client.data_timestamp
|
||||||
|
|
||||||
|
def get_uuid_from_hostname(self, system_hostname):
|
||||||
|
result = ""
|
||||||
for system in self.systems:
|
for system in self.systems:
|
||||||
data_age = time.time() - system.data_timestamp
|
if system.hostname == system_hostname:
|
||||||
if int(data_age) > 60:
|
result = system.uuid
|
||||||
self.systems.remove(system)
|
return result
|
||||||
else:
|
|
||||||
result.append(system.hostname)
|
def get_client_hostnames(self, send_age = False):
|
||||||
|
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
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
124
files/api/app.py
124
files/api/app.py
@ -4,6 +4,7 @@ from typing import Dict, Union
|
|||||||
|
|
||||||
import json, time, redis, yaml
|
import json, time, redis, yaml
|
||||||
import base64, hashlib
|
import base64, hashlib
|
||||||
|
import secrets, string
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
from requests import RequestException, Response
|
from requests import RequestException, Response
|
||||||
@ -35,7 +36,7 @@ def update_redis_server():
|
|||||||
|
|
||||||
if run_cosmostat_server():
|
if run_cosmostat_server():
|
||||||
update_redis_channel("client_summary", get_server_redis_data())
|
update_redis_channel("client_summary", get_server_redis_data())
|
||||||
update_redis_channel("client_hostnames", get_server_hostnames())
|
#update_redis_channel("client_hostnames", get_server_hostnames())
|
||||||
|
|
||||||
# History Redis Tree
|
# History Redis Tree
|
||||||
# Update history_stats Redis Channel
|
# Update history_stats Redis Channel
|
||||||
@ -54,6 +55,7 @@ def get_server_redis_data():
|
|||||||
for client in cosmostat_server.systems:
|
for client in cosmostat_server.systems:
|
||||||
this_client_key = {
|
this_client_key = {
|
||||||
"hostname": client.hostname,
|
"hostname": client.hostname,
|
||||||
|
"data_timestamp": client.data_timestamp,
|
||||||
"uuid": client.uuid,
|
"uuid": client.uuid,
|
||||||
"short_id": client.name,
|
"short_id": client.name,
|
||||||
"redis_data": client.redis_data
|
"redis_data": client.redis_data
|
||||||
@ -64,6 +66,7 @@ def get_server_redis_data():
|
|||||||
def get_server_hostnames():
|
def get_server_hostnames():
|
||||||
return cosmostat_server.get_client_hostnames()
|
return cosmostat_server.get_client_hostnames()
|
||||||
|
|
||||||
|
|
||||||
#######################################################################
|
#######################################################################
|
||||||
### Client Flask Routes
|
### Client Flask Routes
|
||||||
#######################################################################
|
#######################################################################
|
||||||
@ -152,7 +155,7 @@ def get_static_data(human_readable = False):
|
|||||||
return cosmostat_client.get_static_metrics(human_readable)
|
return cosmostat_client.get_static_metrics(human_readable)
|
||||||
|
|
||||||
def get_php_summary():
|
def get_php_summary():
|
||||||
system_properties = cosmostat_client.get_system_properties(human_readable = True)
|
system_properties = cosmostat_client.get_system_properties(human_readable = True, php_extra = True)
|
||||||
system_components = []
|
system_components = []
|
||||||
for component in cosmostat_client.get_components():
|
for component in cosmostat_client.get_components():
|
||||||
this_component = {
|
this_component = {
|
||||||
@ -161,6 +164,18 @@ def get_php_summary():
|
|||||||
}
|
}
|
||||||
system_components.append(this_component)
|
system_components.append(this_component)
|
||||||
|
|
||||||
|
if run_cosmostat_server():
|
||||||
|
print(cosmostat_client.name)
|
||||||
|
client_uuid = cosmostat_server.get_uuid_from_hostname(cosmostat_client.name)
|
||||||
|
print(client_uuid)
|
||||||
|
data_timestamp = cosmostat_server.get_system(client_uuid)
|
||||||
|
print(data_timestamp)
|
||||||
|
component_age = {
|
||||||
|
"component_name": "Data Timestamp",
|
||||||
|
"info_strings": f"Data is {data_timestamp} seconds old"
|
||||||
|
}
|
||||||
|
system_components.append(component_age)
|
||||||
|
|
||||||
result = [{
|
result = [{
|
||||||
"system_properties": system_properties,
|
"system_properties": system_properties,
|
||||||
"system_components": system_components
|
"system_components": system_components
|
||||||
@ -229,7 +244,17 @@ def client_details():
|
|||||||
def client_hostnames():
|
def client_hostnames():
|
||||||
result = []
|
result = []
|
||||||
if run_cosmostat_server():
|
if run_cosmostat_server():
|
||||||
result = cosmostat_server.get_client_hostnames()
|
result = cosmostat_server.get_client_hostnames(send_age = True)
|
||||||
|
else:
|
||||||
|
result = {"message": "server not running on this endpoint"}
|
||||||
|
return jsonify(result)
|
||||||
|
|
||||||
|
# api to get server redis data
|
||||||
|
@app.route('/get_server_redis', methods=['GET'])
|
||||||
|
def get_server_redis():
|
||||||
|
result = []
|
||||||
|
if run_cosmostat_server():
|
||||||
|
result = get_server_redis_data()
|
||||||
else:
|
else:
|
||||||
result = {"message": "server not running on this endpoint"}
|
result = {"message": "server not running on this endpoint"}
|
||||||
return jsonify(result)
|
return jsonify(result)
|
||||||
@ -241,30 +266,48 @@ def client_hostnames():
|
|||||||
|
|
||||||
# update client on server
|
# update client on server
|
||||||
def run_update_client(this_client):
|
def run_update_client(this_client):
|
||||||
if not cosmostat_server.check_uuid(this_client["uuid"]):
|
if public_api_check(this_client):
|
||||||
return { "message": "client not found" }
|
if not cosmostat_server.check_uuid(this_client["uuid"]):
|
||||||
else:
|
return { "message": "client not found" }
|
||||||
timestamp_update = cosmostat_server.update_system(system_dictionary = this_client["redis_data"], system_uuid = this_client["uuid"])
|
else:
|
||||||
update_status = f'updated client {this_client["short_id"]}'
|
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 {
|
return {
|
||||||
"status": update_status,
|
"status": update_status,
|
||||||
"uuid": this_client["uuid"],
|
"uuid": this_client["uuid"],
|
||||||
"redis_data": this_client,
|
"redis_data": this_client,
|
||||||
"timestamp_update": timestamp_update
|
"timestamp_update": timestamp_update
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
return{
|
||||||
|
"status": "api failure"
|
||||||
|
}
|
||||||
|
|
||||||
# create client on server
|
# create client on server
|
||||||
def run_create_client(this_client):
|
def run_create_client(this_client):
|
||||||
timestamp_update = cosmostat_server.add_system(system_dictionary = this_client)
|
if public_api_check(this_client):
|
||||||
update_status = f'created client {this_client["short_id"]}'
|
timestamp_update = cosmostat_server.add_system(system_dictionary = this_client)
|
||||||
return {
|
update_status = f'created client {this_client["short_id"]}'
|
||||||
"status": update_status,
|
return {
|
||||||
"uuid": this_client["uuid"],
|
"status": update_status,
|
||||||
"client_properties": this_client,
|
"uuid": this_client["uuid"],
|
||||||
"timestamp_update": timestamp_update
|
"client_properties": this_client,
|
||||||
|
"timestamp_update": timestamp_update
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return{
|
||||||
|
"status": "api failure"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def public_api_check(this_client):
|
||||||
|
result = False
|
||||||
|
default_key = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(256))
|
||||||
|
api_key = this_client.get('API_KEY', default_key)
|
||||||
|
if api_key == app_settings["REAL_API_KEY"]:
|
||||||
|
result = True
|
||||||
|
return result
|
||||||
|
|
||||||
# flask submission check function
|
# flask submission check function
|
||||||
def client_submit_check(request, dict_name: str):
|
def client_submit_check(request, dict_name: str):
|
||||||
payload = {}
|
payload = {}
|
||||||
@ -324,6 +367,7 @@ def get_client_details():
|
|||||||
# Cosmostat Client Reporter
|
# Cosmostat Client Reporter
|
||||||
def client_update():
|
def client_update():
|
||||||
api_url = f"{cosmostat_server_api()}update_client"
|
api_url = f"{cosmostat_server_api()}update_client"
|
||||||
|
print(api_url)
|
||||||
payload = get_client_payload(get_client_redis_data(human_readable = False), "redis_data")
|
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 = "client_update - redis data from local client:", log_level = "noisy_test")
|
||||||
log_data(log_output = payload, log_level = "noisy_test")
|
log_data(log_output = payload, log_level = "noisy_test")
|
||||||
@ -366,7 +410,8 @@ def get_client_payload(system_dictionary: dict, dictionary_name: str):
|
|||||||
"uuid": this_uuid,
|
"uuid": this_uuid,
|
||||||
"short_id": this_short_id,
|
"short_id": this_short_id,
|
||||||
"hostname": this_hostname,
|
"hostname": this_hostname,
|
||||||
dictionary_name: system_dictionary
|
dictionary_name: system_dictionary,
|
||||||
|
"API_KEY": app_settings["REAL_API_KEY"]
|
||||||
|
|
||||||
}
|
}
|
||||||
return payload
|
return payload
|
||||||
@ -401,25 +446,26 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
# Background Loop Function
|
# Background Loop Function
|
||||||
def background_loop():
|
def background_loop():
|
||||||
# Update all data on the System object
|
# Update all data on the System object unless this is the server
|
||||||
if cosmostat_client.check_system_timer():
|
if cosmostat_client.check_system_timer() and not run_cosmostat_server():
|
||||||
cosmostat_client.update_system_state()
|
cosmostat_client.update_system_state()
|
||||||
|
|
||||||
if app_settings["push_redis"]:
|
if app_settings["push_redis"] and not app_settings["disable_local_api"]:
|
||||||
update_redis_server()
|
update_redis_server()
|
||||||
|
|
||||||
if run_cosmostat_reporter():
|
if run_cosmostat_reporter():
|
||||||
if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer():
|
if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer():
|
||||||
cosmostat_client.update_system_state()
|
cosmostat_client.update_system_state()
|
||||||
client_update()
|
client_update()
|
||||||
|
|
||||||
if run_cosmostat_server():
|
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")
|
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)
|
run_update_client(this_client)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
time.sleep(0.5)
|
time.sleep(0.5)
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
@ -443,14 +489,14 @@ if __name__ == '__main__':
|
|||||||
# send initial stats update to redis
|
# send initial stats update to redis
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
if app_settings["push_redis"]:
|
if app_settings["push_redis"] and not app_settings["disable_local_api"]:
|
||||||
update_redis_server()
|
update_redis_server()
|
||||||
|
|
||||||
######################################
|
######################################
|
||||||
# Flask scheduler for scanner
|
# Flask scheduler for scanner
|
||||||
######################################
|
######################################
|
||||||
|
|
||||||
if app_settings["run_background"]:
|
if app_settings["run_background"] and not app_settings["disable_local_api"]:
|
||||||
log_data(log_output = "Loading flask background subroutine...", log_level = "log_output")
|
log_data(log_output = "Loading flask background subroutine...", log_level = "log_output")
|
||||||
|
|
||||||
scheduler.add_job(id='background_loop',
|
scheduler.add_job(id='background_loop',
|
||||||
@ -467,11 +513,15 @@ if __name__ == '__main__':
|
|||||||
######################################
|
######################################
|
||||||
# Flask API
|
# Flask API
|
||||||
######################################
|
######################################
|
||||||
|
print(f"gateway: {service_gateway_ip()} - port: {service_api_port()}")
|
||||||
app.run(debug=False, host=service_gateway_ip(), port=service_api_port())
|
if not app_settings["disable_local_api"]:
|
||||||
|
app.run(debug=False, host=service_gateway_ip(), port=service_api_port())
|
||||||
|
else:
|
||||||
|
print("Internal API Disabled.")
|
||||||
|
while True:
|
||||||
|
if int(time.time()) % 5 == 0 and not cosmostat_client.check_system_timer():
|
||||||
|
cosmostat_client.update_system_state()
|
||||||
|
client_update()
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
|
||||||
@ -89,6 +89,9 @@
|
|||||||
"arch_variance": [
|
"arch_variance": [
|
||||||
"current_mhz",
|
"current_mhz",
|
||||||
"Clock Speed"
|
"Clock Speed"
|
||||||
|
],
|
||||||
|
"php_extra" :[
|
||||||
|
"CPU Model"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -132,20 +135,23 @@
|
|||||||
"RAM Type",
|
"RAM Type",
|
||||||
"RAM Speed",
|
"RAM Speed",
|
||||||
"RAM Voltage"
|
"RAM Voltage"
|
||||||
|
],
|
||||||
|
"php_extra" :[
|
||||||
|
"Total GB"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "LAN",
|
"name": "LAN",
|
||||||
"description": "{Device Name} - {Device ID} - {MAC Address}",
|
"description": "{Device Name} - {Device ID}",
|
||||||
"multi_check": "True",
|
"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}}' ",
|
"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": {
|
"properties": {
|
||||||
"MAC Address": "ip link | grep -A1 ' {this_device}' | grep ether | awk '{{print $2}}'",
|
"MAC Address": "ip link | grep -A1 ' {this_device}' | grep ether | awk '{{print $2}}' || echo MAC missing",
|
||||||
"Device Name": "echo {this_device}",
|
"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 || echo 'ID_MODEL_FROM_DATABASE=missing' ) | 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": {
|
"metrics": {
|
||||||
"IP Address": "ip -o -4 ad | grep -v -e docker -e 127.0.0.1 -e br- | grep {this_device} | awk '{{print $4}}'",
|
"IP Address": "ip -o -4 ad | grep -v -e docker -e 127.0.0.1 -e br- -e tun | grep {this_device} | awk '{{print $4}}'",
|
||||||
"Data Transmitted": "ifconfig {this_device} | grep RX | grep bytes | cut -d '(' -f2 | tr -d ')'",
|
"Data Transmitted": "ifconfig {this_device} | grep RX | grep bytes | cut -d '(' -f2 | tr -d ')'",
|
||||||
"Data Received": "ifconfig {this_device} | grep TX | grep bytes | cut -d '(' -f2 | tr -d ')'",
|
"Data Received": "ifconfig {this_device} | grep TX | grep bytes | cut -d '(' -f2 | tr -d ')'",
|
||||||
"Link State": "cat /sys/class/net/{this_device}/operstate",
|
"Link State": "cat /sys/class/net/{this_device}/operstate",
|
||||||
@ -155,13 +161,28 @@
|
|||||||
"IP Address"
|
"IP Address"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "VPN",
|
||||||
|
"description": "{Device Name} - VPN Tunnel",
|
||||||
|
"multi_check": "True",
|
||||||
|
"precheck": "ip link | grep tun | wc -l",
|
||||||
|
"device_list": "ip link | grep default | grep tun | cut -d ':' -f 2 | awk '{{print $1}}' ",
|
||||||
|
"properties": {
|
||||||
|
"Device Name": "echo {this_device}"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"IP Address": "ip -o -4 ad | grep -v -e docker -e 127.0.0.1 -e br- | grep {this_device} | awk '{{print $4}}'",
|
||||||
|
"Data Transmitted": "ifconfig {this_device} | grep RX | grep bytes | cut -d '(' -f2 | tr -d ')'",
|
||||||
|
"Data Received": "ifconfig {this_device} | grep TX | grep bytes | cut -d '(' -f2 | tr -d ')'"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "NVGPU",
|
"name": "NVGPU",
|
||||||
"description": "NVGPU{Device ID} - {Device Model} with {Memory Size}, Max Power {Maximum Power}",
|
"description": "NVGPU{Device ID} - {GPU Model} with {Memory Size}, Max Power {Maximum Power}",
|
||||||
"multi_check": "True",
|
"multi_check": "True",
|
||||||
"device_list": "nvidia-smi --query-gpu=index --format=csv,noheader,nounits",
|
"device_list": "nvidia-smi --query-gpu=index --format=csv,noheader,nounits",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Device Model": "nvidia-smi --id={this_device} --query-gpu=name --format=csv,noheader,nounits",
|
"GPU Model": "nvidia-smi --id={this_device} --query-gpu=name --format=csv,noheader,nounits",
|
||||||
"Device ID": "echo NVGPU{this_device}",
|
"Device ID": "echo NVGPU{this_device}",
|
||||||
"Driver Version": "nvidia-smi --id={this_device} --query-gpu=driver_version --format=csv,noheader,nounits",
|
"Driver Version": "nvidia-smi --id={this_device} --query-gpu=driver_version --format=csv,noheader,nounits",
|
||||||
"Maximum Power": "nvidia-smi --id={this_device} --query-gpu=power.limit --format=csv,noheader,nounits",
|
"Maximum Power": "nvidia-smi --id={this_device} --query-gpu=power.limit --format=csv,noheader,nounits",
|
||||||
@ -175,16 +196,19 @@
|
|||||||
"GPU Load": "nvidia-smi --id={this_device} --query-gpu=utilization.gpu --format=csv,noheader,nounits"
|
"GPU Load": "nvidia-smi --id={this_device} --query-gpu=utilization.gpu --format=csv,noheader,nounits"
|
||||||
|
|
||||||
},
|
},
|
||||||
"precheck": "lspci | grep NVIDIA | wc -l"
|
"precheck": "lspci | grep NVIDIA | wc -l",
|
||||||
|
"php_extra" :[
|
||||||
|
"GPU Model"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "STOR",
|
"name": "STOR",
|
||||||
"description": "{Device Path} is of type {Drive Type} with capacity of {Total Capacity}.",
|
"description": "{Device Path} is of type {Drive Type} with capacity of {Total Capacity}.",
|
||||||
"multi_check": "True",
|
"multi_check": "True",
|
||||||
"device_list": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{print $1}'",
|
"device_list": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME -e sr0| awk '{print $1}'",
|
||||||
"properties": {
|
"properties": {
|
||||||
"Device Name": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print $1}}' | grep {this_device}",
|
"Device Name": "echo {this_device}",
|
||||||
"Device Path": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print \"/dev/\"$1}}' | grep {this_device}",
|
"Device Path": "echo /dev/{this_device}",
|
||||||
"Drive Type": "lsblk -d -o NAME,TRAN | grep {this_device} | awk '{{print ($2 != \"\" ? $2 : \"missing\")}}'",
|
"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}}'",
|
"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"
|
"SMART Check": "sudo /usr/sbin/smartctl -x --json /dev/{this_device} | jq -r .smart_status.passed"
|
||||||
@ -192,5 +216,34 @@
|
|||||||
"metrics": {
|
"metrics": {
|
||||||
"placeholder": ""
|
"placeholder": ""
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "MOUNT",
|
||||||
|
"description": "Storage device {Device Location} mounted at {Storage Path} with {Total Space} total space",
|
||||||
|
"multi_check": "True",
|
||||||
|
"device_list": "df -h | grep -v -e 'Use%' -e tmpfs -e overlay -e efi -e udev | awk '{{print $1}}'",
|
||||||
|
"properties": {
|
||||||
|
"Device Location": "echo {this_device}",
|
||||||
|
"Storage Path": "df -h | grep '{this_device} ' | awk '{{print $6}}'",
|
||||||
|
"Total Space": "df -h | grep '{this_device} ' | awk '{{print $2}}'"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"Free Space": "df -h | grep '{this_device} ' | awk '{{print $4}}' ",
|
||||||
|
"Used Space": "df -h | grep '{this_device} ' | awk '{{print $3}}' "
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "DVD",
|
||||||
|
"description": "{Device Path} is a DVD or Virtual DVD drive.",
|
||||||
|
"multi_check": "True",
|
||||||
|
"device_list": "lsblk -d -o NAME,SIZE | grep sr0| awk '{print $1}'",
|
||||||
|
"properties": {
|
||||||
|
"Device Name": "echo {this_device}",
|
||||||
|
"Device Path": "lsblk -d -o NAME,SIZE | grep -v -e 0B -e NAME | awk '{{print \"/dev/\"$1}}' | grep {this_device}",
|
||||||
|
"Total Capacity": "lsblk -d -o NAME,SIZE | grep {this_device} | awk '{{print $2}}'"
|
||||||
|
},
|
||||||
|
"metrics": {
|
||||||
|
"placeholder": ""
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@ -1,14 +1,11 @@
|
|||||||
<?php
|
<?php
|
||||||
/* -------------------------------------------------------------
|
|
||||||
* Cosmostat Dashboard - updated to support host‑specific view
|
|
||||||
* -------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
function h(string $s): string
|
function h(string $s): string
|
||||||
{
|
{
|
||||||
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
return htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------- 1. Load API data --------------------- */
|
// Load API data
|
||||||
$raw_api_settings = file('/opt/api_settings/cosmostat_settings.yaml',
|
$raw_api_settings = file('/opt/api_settings/cosmostat_settings.yaml',
|
||||||
FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||||
|
|
||||||
@ -42,24 +39,26 @@ if ($clients === null || !is_array($clients)) {
|
|||||||
die('<p style="color:red;">Malformed JSON returned from the API.</p>');
|
die('<p style="color:red;">Malformed JSON returned from the API.</p>');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --------------------- 2. Resolve selected host ------------- */
|
// hostname get handler
|
||||||
$selectedHost = $_GET['host'] ?? '';
|
$selectedId = $_GET['host'] ?? ''; // the value passed in ?host=
|
||||||
$selectedIdx = null;
|
$selectedIdx = null;
|
||||||
foreach ($clients as $idx => $client) {
|
foreach ($clients as $idx => $client) {
|
||||||
if (strtolower($client['hostname']) === strtolower($selectedHost)) {
|
if (isset($client['short_id']) && $client['short_id'] === $selectedId) {
|
||||||
$selectedIdx = $idx;
|
$selectedIdx = $idx;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ($selectedIdx === null) {
|
if ($selectedIdx === null) {
|
||||||
// no match - default to the first host (or none)
|
// No match – fall back to the first client (or none)
|
||||||
$selectedIdx = 0;
|
$selectedIdx = 0;
|
||||||
$selectedHost = $clients[$selectedIdx]['hostname'] ?? '';
|
$selectedId = $clients[$selectedIdx]['short_id'] ?? '';
|
||||||
}
|
}
|
||||||
$client = $clients[$selectedIdx] ?? null;
|
$client = $clients[$selectedIdx] ?? null;
|
||||||
$properties = $client['client_properties'][0] ?? [];
|
$properties = $client['client_properties'][0] ?? [];
|
||||||
$systemProperties = $properties['system_properties'] ?? [];
|
$systemProperties = $properties['system_properties'] ?? [];
|
||||||
$systemComponents = $properties['system_components'] ?? [];
|
$systemComponents = $properties['system_components'] ?? [];
|
||||||
|
$selectedHost = $clients[$selectedIdx]['hostname'];
|
||||||
|
|
||||||
|
|
||||||
?>
|
?>
|
||||||
|
|
||||||
@ -76,86 +75,82 @@ $systemComponents = $properties['system_components'] ?? [];
|
|||||||
|
|
||||||
<!-- Sidebar -->
|
<!-- Sidebar -->
|
||||||
<nav class="sidebar">
|
<nav class="sidebar">
|
||||||
<div class="sidebar">
|
<h3>Endpoints</h3>
|
||||||
<h3>Endpoints</h3>
|
<!-- The list will be populated by JavaScript -->
|
||||||
<!-- The list will be populated by the JavaScript below -->
|
<ol id="endpointList"></ol>
|
||||||
<ol id="endpointList"></ol>
|
|
||||||
</div>
|
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<div class="main">
|
<div class="main">
|
||||||
|
|
||||||
|
<!-- Header Card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h2>Matt-Cloud Cosmostat Dashboard</h2>
|
<h2>Matt-Cloud Cosmostat Dashboard</h2>
|
||||||
<p>This dashboard shows the local Matt-Cloud system stats.</p>
|
<p>This dashboard shows the local Matt-Cloud system stats.</p>
|
||||||
<div class="help-link" id="helpToggle">API</div>
|
<div class="help-link" id="helpToggle">API</div>
|
||||||
</div>
|
</div> <!-- / Header Card -->
|
||||||
|
|
||||||
|
<!-- Hidden API Card -->
|
||||||
<div id="helpText" class="card">
|
<div id="helpText" class="card">
|
||||||
<strong>Component Desriptor</strong>
|
<strong>Component Desriptor</strong>
|
||||||
<p>To view the component descriptor, you may <br>
|
<p>To view the component descriptor, you may <br>
|
||||||
<code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p>
|
<code>curl -s https://<?= h($_SERVER['SERVER_NAME']) ?>/descriptor</code></p>
|
||||||
<p>This will return the entire JSON descriptor variable</p>
|
<p>This will return the entire JSON descriptor variable</p>
|
||||||
</div>
|
</div> <!-- / Header Card -->
|
||||||
|
|
||||||
|
<!-- summary card -->
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div id="host_components" class="column">
|
<?php if (!empty($systemProperties)): ?><h2>System Properties</h2>
|
||||||
|
<table><tr>
|
||||||
|
<td>
|
||||||
|
<ul class="system-list">
|
||||||
|
<?php foreach ($systemProperties as $prop): ?><li><?= h($prop['Property']) ?></li>
|
||||||
|
<?php endforeach; ?></ul>
|
||||||
|
</td><td>
|
||||||
|
<h2>Live System Metrics</h2>
|
||||||
|
<!-- Live content, javascript rendered -->
|
||||||
|
<div id="host_metrics" class="column">
|
||||||
|
Connecting...
|
||||||
|
</div> <!--/live content -->
|
||||||
|
</td>
|
||||||
|
</tr></table>
|
||||||
|
<?php endif; ?><br>
|
||||||
|
<!-- api help toggle -->
|
||||||
|
<div class="componentDetail-link" id="componentDetailToggle">Toggle Component Details</div>
|
||||||
|
</div> <!--/summary card -->
|
||||||
|
<!-- hidden detail card -->
|
||||||
|
<div id="componentDetailText" class="card">
|
||||||
|
<?php if (!empty($systemComponents)): ?><h2>Components</h2>
|
||||||
|
<!-- component bucket -->
|
||||||
|
<div class="components">
|
||||||
|
<?php foreach ($systemComponents as $comp): ?>
|
||||||
|
|
||||||
<?php if (!empty($systemProperties)): ?>
|
<!-- individual component -->
|
||||||
<h2>System Properties</h2>
|
<div class="component">
|
||||||
<div class="system">
|
<h3><?= h($comp['component_name']) ?></h3>
|
||||||
<table>
|
<ul class="info-list">
|
||||||
<tr>
|
<?php foreach ($comp['info_strings'] as $info): ?><li><?= h($info) ?></li>
|
||||||
<td>
|
<?php endforeach; ?></ul>
|
||||||
<ul class="system-list">
|
</div> <!--/individual component -->
|
||||||
<?php foreach ($systemProperties as $prop): ?>
|
<?php endforeach; ?></div> <!--/component bucket -->
|
||||||
<li><?= h($prop['Property']) ?></li>
|
<?php endif; ?></div> <!--/hidden detail card -->
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<h2>Live System Metrics</h2>
|
|
||||||
<div id="host_metrics" class="column">Connecting...</div>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if (!empty($systemComponents)): ?>
|
|
||||||
<h2>Components</h2>
|
|
||||||
<div class="components">
|
|
||||||
<?php foreach ($systemComponents as $comp): ?>
|
|
||||||
<div class="component">
|
|
||||||
<h3><?= h($comp['component_name']) ?></h3>
|
|
||||||
<ul class="info-list">
|
|
||||||
<?php foreach ($comp['info_strings'] as $info): ?>
|
|
||||||
<li><?= h($info) ?></li>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<?php endforeach; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div> <!-- /main -->
|
</div> <!-- /main -->
|
||||||
|
|
||||||
</div> <!-- /wrapper -->
|
</div> <!-- /wrapper -->
|
||||||
|
|
||||||
<!-- Socket.IO client library -->
|
<!-- Socket.IO client library -->
|
||||||
<script src="socket.io/socket.io.js"></script>
|
<script src="socket.io/socket.io.js"></script>
|
||||||
<!-- system metrics script -->
|
<!-- system metrics script -->
|
||||||
<script src="src/system_metrics.js"></script>
|
<script src="src/system_metrics.js"></script>
|
||||||
<!-- sidebar script -->
|
<!-- Panel Toggles -->
|
||||||
<script src="src/sidebar.js"></script>
|
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('helpToggle').addEventListener('click', function () {
|
document.getElementById('helpToggle').addEventListener('click', function () {
|
||||||
const help = document.getElementById('helpText');
|
const help = document.getElementById('helpText');
|
||||||
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
||||||
});
|
});
|
||||||
|
document.getElementById('componentDetailToggle').addEventListener('click', function () {
|
||||||
|
const help = document.getElementById('componentDetailText');
|
||||||
|
help.style.display = help.style.display === 'none' || help.style.display === '' ? 'block' : 'none';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@ -1,134 +0,0 @@
|
|||||||
|
|
||||||
// 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 <ul id="endpointList"> with <li><a> 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);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
@ -1,179 +1,309 @@
|
|||||||
/* ------------------------------------------------------------
|
/* ==============================================================
|
||||||
1. Socket-IO connection & helper functions (unchanged)
|
system_metrics.js
|
||||||
------------------------------------------------------------ */
|
==============================================================
|
||||||
const socket = io();
|
Updated to use the unique `short_id` (the system’s key) rather
|
||||||
|
than the hostname. Hostnames are still displayed to the user
|
||||||
|
but every internal mapping and URL uses the short_id so duplicate
|
||||||
|
hostnames no longer collide.
|
||||||
|
============================================================== */
|
||||||
|
(() => {
|
||||||
|
/* ==========================================================
|
||||||
|
Socket.IO setup – unchanged
|
||||||
|
========================================================== */
|
||||||
|
const socket = io({
|
||||||
|
transports: ['websocket', 'polling'],
|
||||||
|
reconnection: true,
|
||||||
|
reconnectionAttempts: Infinity,
|
||||||
|
reconnectionDelay: 3000,
|
||||||
|
reconnectionDelayMax: 60000,
|
||||||
|
timeout: 60000,
|
||||||
|
pingTimeout: 5000,
|
||||||
|
pingInterval: 25000,
|
||||||
|
});
|
||||||
|
|
||||||
socket.on('client_summary', renderStatsTable);
|
/* ==========================================================
|
||||||
|
Color constants – unchanged
|
||||||
|
========================================================== */
|
||||||
|
const GREEN = [ 39, 174, 96]; // #27ae60
|
||||||
|
const YELLOW = [243, 156, 18]; // #f39c12
|
||||||
|
const RED = [192, 57, 43]; // #c0392b
|
||||||
|
|
||||||
socket.on('connect_error', err => {
|
/* ==========================================================
|
||||||
safeSetText('client_summary', `Could not connect to server - ${err.message}`);
|
Helpers
|
||||||
});
|
========================================================== */
|
||||||
|
const hostTimestamps = {}; // keyed by short_id
|
||||||
|
|
||||||
socket.on('reconnect', attempt => {
|
const toRgb = (r, g, b) => `rgb(${r},${g},${b})`;
|
||||||
safeSetText('client_summary', `Re-connected (attempt ${attempt})`);
|
|
||||||
});
|
|
||||||
|
|
||||||
function safeSetText(id, txt) {
|
const T20 = 20 * 1000;
|
||||||
const el = document.getElementById(id);
|
const T40 = 40 * 1000;
|
||||||
if (el) el.textContent = txt;
|
const T60 = 60 * 1000;
|
||||||
}
|
|
||||||
|
|
||||||
/* ------------------------------------------------------------
|
function getFreshnessColor(ageMs) {
|
||||||
2. Render the table for the *selected* host
|
if (ageMs <= T20) {
|
||||||
------------------------------------------------------------ */
|
return toRgb(...GREEN);
|
||||||
function renderStatsTable(raw) {
|
}
|
||||||
// Raw may be a string (from Redis) or already parsed by socket.io
|
if (ageMs <= T40) {
|
||||||
let payload;
|
const t = (ageMs - T20) / (T40 - T20);
|
||||||
if (typeof raw === 'string') {
|
const r = Math.round(GREEN[0] + t * (YELLOW[0] - GREEN[0]));
|
||||||
try {
|
const g = Math.round(GREEN[1] + t * (YELLOW[1] - GREEN[1]));
|
||||||
payload = JSON.parse(raw);
|
const b = Math.round(GREEN[2] + t * (YELLOW[2] - GREEN[2]));
|
||||||
} catch (e) {
|
return toRgb(r, g, b);
|
||||||
safeSetText('client_summary', 'Invalid data received');
|
}
|
||||||
|
if (ageMs <= T60) {
|
||||||
|
const t = (ageMs - T40) / (T60 - T40);
|
||||||
|
const r = Math.round(YELLOW[0] + t * (RED[0] - YELLOW[0]));
|
||||||
|
const g = Math.round(YELLOW[1] + t * (RED[1] - YELLOW[1]));
|
||||||
|
const b = Math.round(YELLOW[2] + t * (RED[2] - YELLOW[2]));
|
||||||
|
return toRgb(r, g, b);
|
||||||
|
}
|
||||||
|
return toRgb(...RED);
|
||||||
|
}
|
||||||
|
|
||||||
|
function safeSetText(id, txt) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) el.textContent = txt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Get the *short_id* from the query string
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
function getSelectedId() {
|
||||||
|
return new URLSearchParams(window.location.search).get('host') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Sidebar building – uses short_id for status key
|
||||||
|
========================================================== */
|
||||||
|
function buildList(systemList) {
|
||||||
|
const ul = document.getElementById('endpointList');
|
||||||
|
const current = Array.from(ul.children).map(li => li.dataset.id);
|
||||||
|
const newIds = systemList.map(s => s.short_id);
|
||||||
|
if (arraysEqual(current, newIds)) return; // nothing changed
|
||||||
|
|
||||||
|
const selected = getSelectedId().toLowerCase();
|
||||||
|
ul.innerHTML = ''; // reset list
|
||||||
|
|
||||||
|
systemList.forEach(item => {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
|
||||||
|
// status dot – keyed by short_id
|
||||||
|
const status = document.createElement('span');
|
||||||
|
status.className = 'host-status';
|
||||||
|
status.dataset.id = item.short_id;
|
||||||
|
|
||||||
|
// link – display hostname, encode short_id in URL
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = '?host=' + encodeURIComponent(item.short_id);
|
||||||
|
a.textContent = item.hostname;
|
||||||
|
if (item.short_id.toLowerCase() === selected) a.classList.add('active');
|
||||||
|
|
||||||
|
li.appendChild(status);
|
||||||
|
li.appendChild(a);
|
||||||
|
ul.appendChild(li);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Update status colours every second
|
||||||
|
========================================================== */
|
||||||
|
function updateStatusColors() {
|
||||||
|
const nowSec = Date.now() / 1000;
|
||||||
|
Object.entries(hostTimestamps).forEach(([id, ts]) => {
|
||||||
|
const ageMs = (nowSec - ts) * 1000;
|
||||||
|
const color = getFreshnessColor(ageMs);
|
||||||
|
const span = document.querySelector(
|
||||||
|
`.host-status[data-id="${id}"]`
|
||||||
|
);
|
||||||
|
if (span) span.style.backgroundColor = color;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateStatusColors, 1000);
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Utility helpers (unchanged)
|
||||||
|
========================================================== */
|
||||||
|
function arraysEqual(a, b) {
|
||||||
|
if (a.length !== b.length) return false;
|
||||||
|
for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGenericTable(containerId, data, emptyMsg) {
|
||||||
|
const container = document.getElementById(containerId);
|
||||||
|
if (!Array.isArray(data) || !data.length) {
|
||||||
|
container.textContent = emptyMsg;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} else {
|
const merged = mergeRowsByName(data);
|
||||||
payload = raw;
|
const ordered = orderRows(merged);
|
||||||
|
const table = buildTable(ordered);
|
||||||
|
table.id = 'host_metrics_table';
|
||||||
|
container.innerHTML = '';
|
||||||
|
container.appendChild(table);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(payload) || !payload.length) {
|
function mergeRowsByName(rows) {
|
||||||
safeSetText('client_summary', 'No data available');
|
const groups = {}; // { Source: { Metric: [], Data: [] } }
|
||||||
return;
|
rows.forEach(r => {
|
||||||
}
|
const src = r.Source;
|
||||||
|
if (!src) return;
|
||||||
/* ---------------------------------------------
|
if (!groups[src]) groups[src] = { Metric: [], Data: [] };
|
||||||
2a. Determine the hostname to display
|
if ('Metric' in r && 'Data' in r) {
|
||||||
--------------------------------------------- */
|
groups[src].Metric.push(r.Metric);
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
groups[src].Data.push(r.Data);
|
||||||
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 Object.entries(groups).map(([src, g]) => ({
|
||||||
|
Source: src,
|
||||||
|
Metric: g.Metric,
|
||||||
|
Data: g.Data,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function orderRows(rows) {
|
||||||
|
const priority = ['System', 'CPU', 'RAM'];
|
||||||
|
const map = {};
|
||||||
|
priority.forEach((s, i) => map[s] = i);
|
||||||
|
return [...rows].sort((a, b) => {
|
||||||
|
const ai = map.hasOwnProperty(a.Source) ? map[a.Source] : Infinity;
|
||||||
|
const bi = map.hasOwnProperty(b.Source) ? map[b.Source] : Infinity;
|
||||||
|
return ai - bi;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildTable(rows) {
|
||||||
|
const cols = ['Source', 'Metric', 'Data'];
|
||||||
|
const table = document.createElement('table');
|
||||||
|
// Header
|
||||||
|
const thead = table.createTHead();
|
||||||
|
const headerRow = thead.insertRow();
|
||||||
|
cols.forEach(col => {
|
||||||
|
const th = document.createElement('th');
|
||||||
|
th.textContent = col;
|
||||||
|
headerRow.appendChild(th);
|
||||||
|
});
|
||||||
|
// Body
|
||||||
|
const tbody = table.createTBody();
|
||||||
|
rows.forEach(item => {
|
||||||
|
const tr = tbody.insertRow();
|
||||||
|
cols.forEach(col => {
|
||||||
|
const td = tr.insertCell();
|
||||||
|
const val = item[col];
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
val.forEach((v, i) => {
|
||||||
|
const span = document.createElement('span');
|
||||||
|
span.textContent = v;
|
||||||
|
td.appendChild(span);
|
||||||
|
if (i < val.length - 1) td.appendChild(document.createElement('br'));
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
td.textContent = val !== undefined ? val : '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Handle incoming data
|
||||||
|
========================================================== */
|
||||||
|
let lastUpdate = Date.now();
|
||||||
|
|
||||||
|
function handleSummary(raw) {
|
||||||
|
lastUpdate = Date.now(); // reset watchdog
|
||||||
|
let payload;
|
||||||
|
if (typeof raw === 'string') {
|
||||||
|
try { payload = JSON.parse(raw); } catch (e) {
|
||||||
|
safeSetText('client_summary', 'Invalid data received');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} else payload = raw;
|
||||||
|
|
||||||
|
if (!Array.isArray(payload) || !payload.length) {
|
||||||
|
safeSetText('client_summary', 'No data available');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) Build the list first (so <span> elements exist)
|
||||||
|
buildList(payload);
|
||||||
|
|
||||||
|
// 2) Store the timestamp for every short_id
|
||||||
|
payload.forEach(hostObj => {
|
||||||
|
if (hostObj.short_id && hostObj.data_timestamp) {
|
||||||
|
hostTimestamps[hostObj.short_id] = hostObj.data_timestamp; // seconds
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Immediately update colours for the current view
|
||||||
|
updateStatusColors();
|
||||||
|
|
||||||
|
// Metric table for selected host
|
||||||
|
const selectedId = getSelectedId();
|
||||||
|
const hostObj = payload.find(h => h.short_id === selectedId) || payload[0];
|
||||||
|
const hostData = hostObj && Array.isArray(hostObj.redis_data)
|
||||||
|
? hostObj.redis_data
|
||||||
|
: [];
|
||||||
|
renderGenericTable('host_metrics', hostData, 'No Stats available');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Socket event wiring – unchanged
|
||||||
|
========================================================== */
|
||||||
|
socket.on('client_summary', handleSummary);
|
||||||
|
socket.on('connect', () => {
|
||||||
|
safeSetText('client_summary', 'Connected');
|
||||||
|
requestSummary();
|
||||||
|
});
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
safeSetText('client_summary', 'Disconnected - retrying...');
|
||||||
|
});
|
||||||
|
socket.on('reconnect', attempt => {
|
||||||
|
safeSetText('client_summary', `Re-connected (attempt ${attempt})`);
|
||||||
|
requestSummary();
|
||||||
});
|
});
|
||||||
|
|
||||||
return table;
|
/* ==========================================================
|
||||||
}
|
Request logic – unchanged
|
||||||
|
========================================================== */
|
||||||
|
function requestSummary() {
|
||||||
|
if (!socket.connected) return; // guard against stale emits
|
||||||
|
socket.emit('get_client_summary'); // server will reply via client_summary
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Recursive polling – unchanged
|
||||||
|
========================================================== */
|
||||||
|
let pollTimer = null;
|
||||||
|
function pollLoop() {
|
||||||
|
if (!socket.connected) return;
|
||||||
|
requestSummary();
|
||||||
|
pollTimer = setTimeout(pollLoop, 5000);
|
||||||
|
}
|
||||||
|
socket.on('connect', () => {
|
||||||
|
if (!pollTimer) pollLoop();
|
||||||
|
});
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Watchdog – force reconnect if no data for 15 s
|
||||||
|
========================================================== */
|
||||||
|
function watchdog() {
|
||||||
|
if (Date.now() - lastUpdate > 15000 && socket.connected) {
|
||||||
|
safeSetText('client_summary', 'No updates - reconnecting...');
|
||||||
|
socket.disconnect(); // forces a reconnect cycle
|
||||||
|
}
|
||||||
|
setTimeout(watchdog, 5000);
|
||||||
|
}
|
||||||
|
watchdog();
|
||||||
|
|
||||||
|
/* ==========================================================
|
||||||
|
Keep the 'active' link in sync when the URL changes
|
||||||
|
========================================================== */
|
||||||
|
window.addEventListener('popstate', () => {
|
||||||
|
const selected = getSelectedId().toLowerCase();
|
||||||
|
document.querySelectorAll('#endpointList a').forEach(a =>
|
||||||
|
a.classList.toggle('active', a.href.includes('host=' + encodeURIComponent(selected)))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})();
|
||||||
@ -2,7 +2,7 @@
|
|||||||
1. Global settings & color palette
|
1. Global settings & color palette
|
||||||
------------------------------------------------- */
|
------------------------------------------------- */
|
||||||
:root {
|
:root {
|
||||||
/* Dark theme – body & card backgrounds */
|
/* Dark theme - body & card backgrounds */
|
||||||
--bg-body: #2c3e50; /* main page background */
|
--bg-body: #2c3e50; /* main page background */
|
||||||
--bg-card: #34495e; /* card / panel background */
|
--bg-card: #34495e; /* card / panel background */
|
||||||
--bg-sidebar: #3d566e; /* sidebar background (slightly lighter) */
|
--bg-sidebar: #3d566e; /* sidebar background (slightly lighter) */
|
||||||
@ -30,14 +30,23 @@ a { color: var(--clr-accent); text-decoration: none; }
|
|||||||
a:hover { text-decoration: underline; }
|
a:hover { text-decoration: underline; }
|
||||||
|
|
||||||
/* -------------------------------------------------
|
/* -------------------------------------------------
|
||||||
2. Layout – wrapper, sidebar, main
|
2. Layout - wrapper, sidebar, main
|
||||||
------------------------------------------------- */
|
------------------------------------------------- */
|
||||||
.wrapper { display: flex; min-height: 100vh; }
|
.wrapper { display: flex; min-height: 100vh; }
|
||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 200px;
|
position: fixed; /* keep sidebar visible during scroll */
|
||||||
background: var(--bg-sidebar);
|
top: 0; /* stick to the top of the viewport */
|
||||||
|
left: 0; /* align to the left edge */
|
||||||
|
height: 100vh; /* full viewport height */
|
||||||
|
/* ---- size & spacing ------------------------------------------- */
|
||||||
|
width: 200px; /* same as before */
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
|
overflow-y: auto; /* allow sidebar content to scroll if needed */
|
||||||
|
/* ---- look ------------------------------------------------------- */
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
/* optional: keep it above other content */
|
||||||
|
z-index: 1000;
|
||||||
}
|
}
|
||||||
.sidebar h3 { margin: 0 0 .4rem 0; font-size: 1.1rem; }
|
.sidebar h3 { margin: 0 0 .4rem 0; font-size: 1.1rem; }
|
||||||
.sidebar ul { list-style: none; padding: 0; margin: 0; }
|
.sidebar ul { list-style: none; padding: 0; margin: 0; }
|
||||||
@ -46,8 +55,13 @@ a:hover { text-decoration: underline; }
|
|||||||
.sidebar a { color: var(--clr-accent); }
|
.sidebar a { color: var(--clr-accent); }
|
||||||
.sidebar a.active { font-weight: bold; }
|
.sidebar a.active { font-weight: bold; }
|
||||||
|
|
||||||
.main { flex: 1; padding: 1rem; }
|
.main{
|
||||||
|
flex: 1;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-left: 200px; /* space for the fixed sidebar */
|
||||||
|
/* optional: avoid accidental horizontal overflow */
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
/* -------------------------------------------------
|
/* -------------------------------------------------
|
||||||
3. Card component
|
3. Card component
|
||||||
------------------------------------------------- */
|
------------------------------------------------- */
|
||||||
@ -105,7 +119,7 @@ li { margin-bottom: 10px; color: var(--clr-text); }
|
|||||||
.component h3 { margin: 0 0 5px; }
|
.component h3 { margin: 0 0 5px; }
|
||||||
|
|
||||||
/* -------------------------------------------------
|
/* -------------------------------------------------
|
||||||
7. Help toggle / modal
|
7. Panel toggles / modal
|
||||||
------------------------------------------------- */
|
------------------------------------------------- */
|
||||||
.help-link {
|
.help-link {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
@ -116,8 +130,117 @@ li { margin-bottom: 10px; color: var(--clr-text); }
|
|||||||
.help-link:hover { text-decoration: underline; }
|
.help-link:hover { text-decoration: underline; }
|
||||||
#helpText { display: none; }
|
#helpText { display: none; }
|
||||||
|
|
||||||
|
.componentDetail-link {
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
color: var(--clr-accent);
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
.componentDetail-link:hover { text-decoration: underline; }
|
||||||
|
#componentDetailText { display: none; }
|
||||||
|
|
||||||
|
|
||||||
/* -------------------------------------------------
|
/* -------------------------------------------------
|
||||||
8. Misc helpers
|
8. Misc helpers
|
||||||
------------------------------------------------- */
|
------------------------------------------------- */
|
||||||
/* Hide numeric markers in metric columns (if any) */
|
/* Hide numeric markers in metric columns (if any) */
|
||||||
#host_metrics_column td { list-style: none; padding-left: 0; margin-left: 0; }
|
#host_metrics_column td { list-style: none; padding-left: 0; margin-left: 0; }
|
||||||
|
|
||||||
|
.host-status {
|
||||||
|
display: inline-block;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-left: 6px;
|
||||||
|
margin-right: 8px;
|
||||||
|
vertical-align: middle;
|
||||||
|
background: #808080; /* default – unknown / no timestamp */
|
||||||
|
transition: background-color 1s linear; /* smooth fade */
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* -------------------------------------------------
|
||||||
|
9. Mobile adjustments
|
||||||
|
------------------------------------------------- */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
/* 1. Make the whole page a column */
|
||||||
|
.wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. Hide the sidebar initially */
|
||||||
|
.sidebar {
|
||||||
|
position: relative; /* take it out of the flow */
|
||||||
|
width: 100%;
|
||||||
|
max-height: 0; /* collapsed */
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease-out;
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
padding: 0; /* remove padding */
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar.show {
|
||||||
|
max-height: 500px; /* enough for all items */
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. Move the toggle button into the header */
|
||||||
|
.mobile-toggle {
|
||||||
|
display: block;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--clr-accent);
|
||||||
|
padding: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 4. Main content no left padding */
|
||||||
|
.main {
|
||||||
|
padding-left: 0;
|
||||||
|
padding-right: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 5. Table scroll on small screens */
|
||||||
|
.card table {
|
||||||
|
width: 100%;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card table thead,
|
||||||
|
.card table tbody,
|
||||||
|
.card table tr,
|
||||||
|
.card table td,
|
||||||
|
.card table th {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card table tbody {
|
||||||
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card table td,
|
||||||
|
.card table th {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 6. Adjust the host status dot positioning */
|
||||||
|
.host-status {
|
||||||
|
margin-left: 2px;
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Optional: style the drawer indicator */
|
||||||
|
.sidebar.show::before {
|
||||||
|
content: "✕ Close";
|
||||||
|
display: block;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
background: var(--bg-sidebar);
|
||||||
|
color: var(--clr-accent);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@ -61,65 +61,66 @@ io.on('connection', async socket => {
|
|||||||
/* ---------- 3. Serve static files ----------------------------------- */
|
/* ---------- 3. Serve static files ----------------------------------- */
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
app.use(express.static('public'));
|
app.use(express.static('public'));
|
||||||
|
/* --- 4. Redis subscriber (patched) --------------------------------- */
|
||||||
|
const redisClient = createClient({
|
||||||
|
url: 'redis://192.168.37.1:6379',
|
||||||
|
socket: { keepAlive: 60000, // 60 s TCP keep-alive
|
||||||
|
reconnectStrategy: attempts => Math.min(attempts * 100, 3000) } // back-off
|
||||||
|
});
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
/* ---------- 4. Redis subscriber ------------------------------------- */
|
|
||||||
/* --------------------------------------------------------------------- */
|
|
||||||
const redisClient = createClient({ url: 'redis://192.168.37.1:6379' });
|
|
||||||
redisClient.on('error', err => console.error('Redis error', err));
|
redisClient.on('error', err => console.error('Redis error', err));
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
await redisClient.connect();
|
await redisClient.connect();
|
||||||
const sub = redisClient.duplicate(); // duplicate to keep separate pub/sub
|
|
||||||
|
const sub = redisClient.duplicate();
|
||||||
await sub.connect();
|
await sub.connect();
|
||||||
|
|
||||||
// Subscribe to the channel that sends host stats
|
// --------------------------------------------------------------------
|
||||||
await sub.subscribe(
|
// Helper that re-subscribes to a channel (and re-sends the handler)
|
||||||
['host_metrics'],
|
// --------------------------------------------------------------------
|
||||||
(message, channel) => {
|
async function safeSubscribe(channel, handler) {
|
||||||
let payload;
|
try {
|
||||||
try {
|
await sub.subscribe(channel, handler);
|
||||||
payload = JSON.parse(message); // message is a JSON string
|
console.log(`Subscribed to ${channel}`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`Failed to parse ${channel}`, e);
|
console.error(`Failed to subscribe to ${channel}`, e);
|
||||||
return;
|
|
||||||
}
|
|
||||||
io.emit(channel, payload);
|
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
// Subscribe to the channel that sends host stats
|
// ---------------------------------------------------------------
|
||||||
await sub.subscribe(
|
// Subscribe to all required channels
|
||||||
['client_summary'],
|
// ---------------------------------------------------------------
|
||||||
(message, channel) => {
|
await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg));
|
||||||
let payload;
|
await safeSubscribe('client_summary', (msg) => forward('client_summary', msg));
|
||||||
try {
|
|
||||||
payload = JSON.parse(message); // message is a JSON string
|
// ---------------------------------------------------------------
|
||||||
} catch (e) {
|
// Forward messages to Socket.io
|
||||||
console.error(`Failed to parse ${channel}`, e);
|
// ---------------------------------------------------------------
|
||||||
return;
|
function forward(channel, message) {
|
||||||
}
|
try {
|
||||||
|
const payload = JSON.parse(message);
|
||||||
io.emit(channel, payload);
|
io.emit(channel, payload);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`Failed to parse message from ${channel}`, e);
|
||||||
}
|
}
|
||||||
);
|
}
|
||||||
|
|
||||||
// Subscribe to the channel that sends host stats
|
// ----------------------------------------------------------------
|
||||||
await sub.subscribe(
|
// Re-subscribe automatically when the Redis connection reconnects
|
||||||
['client_hostnames'],
|
// ----------------------------------------------------------------
|
||||||
(message, channel) => {
|
sub.on('reconnecting', () => console.log('Redis reconnecting…'));
|
||||||
let payload;
|
sub.on('ready', async () => {
|
||||||
try {
|
console.log('Redis ready - re-subscribing to all channels');
|
||||||
payload = JSON.parse(message); // message is a JSON string
|
await safeSubscribe('host_metrics', (msg) => forward('host_metrics', msg));
|
||||||
} catch (e) {
|
await safeSubscribe('client_summary', (msg) => forward('client_summary', msg));
|
||||||
console.error(`Failed to parse ${channel}`, e);
|
});
|
||||||
return;
|
|
||||||
}
|
|
||||||
io.emit(channel, payload);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
// Optional: if the connection ends for any reason, close the process
|
||||||
sub.on('error', err => console.error('Subscriber error', err));
|
sub.on('end', () => {
|
||||||
|
console.error('Redis connection closed - exiting');
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/* --------------------------------------------------------------------- */
|
/* --------------------------------------------------------------------- */
|
||||||
|
|||||||
@ -11,7 +11,7 @@ listen 80;
|
|||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
# ---------------------------------------
|
# ---------------------------------------
|
||||||
# The API – only /descriptor
|
# API Endpoints
|
||||||
# ---------------------------------------
|
# ---------------------------------------
|
||||||
location = /descriptor {
|
location = /descriptor {
|
||||||
proxy_pass http://192.168.37.1:5000/descriptor;
|
proxy_pass http://192.168.37.1:5000/descriptor;
|
||||||
@ -20,6 +20,20 @@ server_name localhost;
|
|||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
}
|
}
|
||||||
|
location = /update_client {
|
||||||
|
proxy_pass http://192.168.37.1:5000/update_client;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
location = /create_client {
|
||||||
|
proxy_pass http://192.168.37.1:5000/create_client;
|
||||||
|
proxy_set_header Host $host;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
}
|
||||||
|
|
||||||
# ---------------------------------------
|
# ---------------------------------------
|
||||||
# WebSocket endpoint
|
# WebSocket endpoint
|
||||||
|
|||||||
@ -132,5 +132,4 @@
|
|||||||
state: present
|
state: present
|
||||||
force_source: true
|
force_source: true
|
||||||
|
|
||||||
|
|
||||||
...
|
...
|
||||||
@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
# set up web stack
|
# set up web stack
|
||||||
- name: Build Web Dashboard
|
- name: Build Web Dashboard
|
||||||
|
when: not disable_local_api
|
||||||
include_tasks: web.yaml
|
include_tasks: web.yaml
|
||||||
|
|
||||||
#- name: Purge Old Containers
|
#- name: Purge Old Containers
|
||||||
|
|||||||
@ -4,15 +4,7 @@
|
|||||||
- name: Cosmostat - Server Dashboard - replace index.php
|
- name: Cosmostat - Server Dashboard - replace index.php
|
||||||
copy:
|
copy:
|
||||||
src: server/server.php
|
src: server/server.php
|
||||||
dest: "{{ service_control_web_folder }}/index.php"
|
dest: "{{ service_control_web_folder }}/html/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
|
mode: 0755
|
||||||
owner: "{{ service_user }}"
|
owner: "{{ service_user }}"
|
||||||
group: "{{ service_user }}"
|
group: "{{ service_user }}"
|
||||||
@ -20,14 +12,14 @@
|
|||||||
- name: Cosmostat - Server Dashboard - copy system_metrics.js
|
- name: Cosmostat - Server Dashboard - copy system_metrics.js
|
||||||
copy:
|
copy:
|
||||||
src: server/system_metrics.js
|
src: server/system_metrics.js
|
||||||
dest: "{{ service_control_web_folder }}/src/system_metrics.js"
|
dest: "{{ service_control_web_folder }}/html/src/system_metrics.js"
|
||||||
mode: 0755
|
mode: 0755
|
||||||
owner: "{{ service_user }}"
|
owner: "{{ service_user }}"
|
||||||
group: "{{ service_user }}"
|
group: "{{ service_user }}"
|
||||||
|
|
||||||
- name: Cosmostat - Server Dashboard - delete redis.js
|
- name: Cosmostat - Server Dashboard - delete redis.js
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
path: "{{ service_control_web_folder }}/src/redis.js"
|
path: "{{ service_control_web_folder }}/html/src/redis.js"
|
||||||
state: absent
|
state: absent
|
||||||
|
|
||||||
...
|
...
|
||||||
@ -25,6 +25,12 @@
|
|||||||
owner: "{{ service_user }}"
|
owner: "{{ service_user }}"
|
||||||
group: "{{ service_user }}"
|
group: "{{ service_user }}"
|
||||||
|
|
||||||
|
#######################
|
||||||
|
# configure as server
|
||||||
|
- name: Cosmostat - Web - Configure Server Dashboard
|
||||||
|
when: cosmostat_server | bool
|
||||||
|
include_tasks: server.yaml
|
||||||
|
|
||||||
- name: Cosmostat - Web - template docker-compose.yaml
|
- name: Cosmostat - Web - template docker-compose.yaml
|
||||||
template:
|
template:
|
||||||
src: docker-compose-php.yaml
|
src: docker-compose-php.yaml
|
||||||
|
|||||||
@ -20,6 +20,7 @@ ansible_hostname: "{{ ansible_hostname }}"
|
|||||||
# docker subnet, will use to bind the IP in default secure mode
|
# docker subnet, will use to bind the IP in default secure mode
|
||||||
api_bind_ip: {{ api_bind_ip }}
|
api_bind_ip: {{ api_bind_ip }}
|
||||||
docker_gateway: {{ docker_gateway }}
|
docker_gateway: {{ docker_gateway }}
|
||||||
|
local_api_address: {{ local_api_address }}
|
||||||
|
|
||||||
# python system variables, no quotes for bool or int
|
# python system variables, no quotes for bool or int
|
||||||
secure_api: {{ secure_api }}
|
secure_api: {{ secure_api }}
|
||||||
@ -33,4 +34,7 @@ custom_api_port: {{ custom_api_port }}
|
|||||||
cosmostat_server: {{ cosmostat_server }}
|
cosmostat_server: {{ cosmostat_server }}
|
||||||
cosmostat_server_api: "{{ cosmostat_server_api }}"
|
cosmostat_server_api: "{{ cosmostat_server_api }}"
|
||||||
cosmostat_server_reporter: {{ cosmostat_server_reporter }}
|
cosmostat_server_reporter: {{ cosmostat_server_reporter }}
|
||||||
|
disable_local_api: {{ disable_local_api }}
|
||||||
|
REAL_API_KEY: "{{ REAL_API_KEY }}"
|
||||||
|
cosmostat_server_ip: "{{ cosmostat_server_ip }}"
|
||||||
...
|
...
|
||||||
15
templates/vpn_client.conf
Normal file
15
templates/vpn_client.conf
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
client
|
||||||
|
dev tun
|
||||||
|
proto {{ vpn_proto }}
|
||||||
|
remote {{ vpn_server_ip }} {{ vpn_port }}
|
||||||
|
resolv-retry infinite
|
||||||
|
nobind
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
user nobody
|
||||||
|
group nogroup
|
||||||
|
|
||||||
|
|
||||||
|
tls-auth {{ psk_key_path }} 1
|
||||||
|
cipher AES-256-GCM
|
||||||
|
auth SHA256
|
||||||
19
templates/vpn_server.conf
Normal file
19
templates/vpn_server.conf
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
|
||||||
|
ifconfig-pool-persist ipp.txt
|
||||||
|
port {{ vpn_port }}
|
||||||
|
proto {{ vpn_proto }}
|
||||||
|
dev tun
|
||||||
|
tls-crypt {{ psk_key_path }}
|
||||||
|
server {{ vpn_network }} {{ vpn_netmask }}
|
||||||
|
topology subnet
|
||||||
|
push "route {{ private_server_address }} 255.255.255.255"
|
||||||
|
keepalive 10 120
|
||||||
|
cipher AES-256-GCM
|
||||||
|
auth SHA256
|
||||||
|
user nobody
|
||||||
|
group nogroup
|
||||||
|
persist-key
|
||||||
|
persist-tun
|
||||||
|
status openvpn-status.log
|
||||||
|
verb 3
|
||||||
|
explicit-exit-notify 1
|
||||||
Reference in New Issue
Block a user