From eb934c9db19433afdcf7b4bc6833db84b622e272 Mon Sep 17 00:00:00 2001 From: ChrisColeTech Date: Sun, 1 Sep 2024 08:17:08 -0400 Subject: [PATCH] Using websockets for resource monitor instead of rest api --- api/controllers/jobs_controller.py | 110 ++++ api/controllers/settings_controller.py | 71 +++ api/dependency_installer.py | 12 +- api/http_server.py | 114 +++- web/assets/css/styles.css | 78 ++- web/assets/js/chart-settings.js | 509 +++++++++--------- web/assets/js/chart.js | 1 + web/assets/js/dependencies.js | 8 + .../js/elegant-resource-monitor-service.js | 211 ++++++++ web/assets/js/elegant-resource-monitor.js | 288 ++++++++++ web/assets/js/script.js | 138 +++-- web/assets/js/shared_utils.js | 115 ++++ web/assets/js/socket.io.min.js | 7 + web/templates/index.html | 61 +++ web/templates/perf-monitor/perf-monitor.html | 81 +-- 15 files changed, 1373 insertions(+), 431 deletions(-) create mode 100644 api/controllers/jobs_controller.py create mode 100644 api/controllers/settings_controller.py create mode 100644 web/assets/js/elegant-resource-monitor-service.js create mode 100644 web/assets/js/elegant-resource-monitor.js create mode 100644 web/assets/js/shared_utils.js create mode 100644 web/assets/js/socket.io.min.js create mode 100644 web/templates/index.html diff --git a/api/controllers/jobs_controller.py b/api/controllers/jobs_controller.py new file mode 100644 index 00000000..c8313855 --- /dev/null +++ b/api/controllers/jobs_controller.py @@ -0,0 +1,110 @@ +import pickle +import os +from flask import Blueprint, request, jsonify, make_response +from flask_restx import Api, Resource, fields, Namespace + +# Create a Blueprint for the jobs controller +jobs_bp = Blueprint('jobs', __name__) +jobs_api = Api(jobs_bp, version='1.0', title='Jobs API', + description='API for managing jobs') + +# Define a namespace for jobs +jobs_ns = Namespace('jobs', description='Job operations') + +# Define the model for a job +job_model = jobs_ns.model('Job', { + 'id': fields.Integer(required=True, description='The unique identifier of the job'), + 'description': fields.String(required=True, description='Description of the job'), + 'status': fields.String(description='Status of the job') +}) + + +# File to persist data +DATA_FILE = 'jobs.pkl' + + +def load_jobs(): + if os.path.exists(DATA_FILE): + with open(DATA_FILE, 'rb') as file: + return pickle.load(file) + else: + # Create an empty file if it doesn't exist + with open(DATA_FILE, 'wb') as file: + pickle.dump({}, file) + return {} + + +def save_jobs(jobs): + with open(DATA_FILE, 'wb') as file: + pickle.dump(jobs, file) + + +# Load initial data +jobs_store = load_jobs() + + +@jobs_ns.route('/') +class JobList(Resource): + def get(self): + """List all jobs""" + jobs_store = load_jobs() + return _corsify_actual_response(jsonify(list(jobs_store.values()))) + + @jobs_ns.expect(job_model) + def post(self): + """Create a new job""" + if request.method == "OPTIONS": # CORS preflight + return _build_cors_preflight_response() + + job = request.json + job_id = job['id'] + if job_id in jobs_store: + return {'message': 'Job already exists'}, 400 + jobs_store[job_id] = job + save_jobs(jobs_store) # Save to file + return _corsify_actual_response(jsonify(job)) + + +@jobs_ns.route('/') +class JobItem(Resource): + def get(self, job_id): + """Get a job by ID""" + job = jobs_store.get(job_id) + if job is None: + return {'message': 'Job not found'}, 404 + return _corsify_actual_response(jsonify(job)) + + @jobs_ns.expect(job_model) + def put(self, job_id): + """Update a job by ID""" + if request.method == "OPTIONS": # CORS preflight + return _build_cors_preflight_response() + job = request.json + if job_id not in jobs_store: + return {'message': 'Job not found'}, 404 + jobs_store[job_id] = job + save_jobs(jobs_store) # Save to file + return _corsify_actual_response(jsonify(job)) + + def delete(self, job_id): + """Delete a job by ID""" + if request.method == "OPTIONS": # CORS preflight + return _build_cors_preflight_response() + if job_id not in jobs_store: + return {'message': 'Job not found'}, 404 + del jobs_store[job_id] + save_jobs(jobs_store) # Save to file + return _corsify_actual_response(jsonify({'message': 'Job deleted'})) + + +def _build_cors_preflight_response(): + response = make_response() + response.headers.add("Access-Control-Allow-Origin", "*") + response.headers.add("Access-Control-Allow-Headers", "*") + response.headers.add("Access-Control-Allow-Methods", "*") + return response + + +def _corsify_actual_response(response): + response.headers.add("Access-Control-Allow-Origin", "*") + return response diff --git a/api/controllers/settings_controller.py b/api/controllers/settings_controller.py new file mode 100644 index 00000000..575843ce --- /dev/null +++ b/api/controllers/settings_controller.py @@ -0,0 +1,71 @@ +import json +import os +from flask import Blueprint, jsonify, request +from flask_restx import Api, Resource, fields, Namespace + +# Create a Blueprint for the settings controller +settings_bp = Blueprint('settings', __name__) +settings_api = Api(settings_bp, version='1.0', title='Settings API', + description='API for managing settings') + +# Define a namespace for settings +settings_ns = Namespace('settings', description='Settings operations') + +# Define the model for settings +settings_model = settings_ns.model('Setting', { + 'key': fields.String(required=True, description='The key of the setting'), + 'value': fields.String(required=True, description='The value of the setting') +}) + +# File to persist settings data +SETTINGS_FILE = 'settings.json' + + +def load_settings(): + if os.path.exists(SETTINGS_FILE): + with open(SETTINGS_FILE, 'r') as file: + return json.load(file) + return {} + + +def save_settings(settings): + with open(SETTINGS_FILE, 'w') as file: + json.dump(settings, file, indent=4) + + +# Load initial data +settings_store = load_settings() + + +@settings_ns.route('/') +class SettingsList(Resource): + def get(self): + """List all settings""" + return jsonify({'settings': list(settings_store.values())}) + + @settings_ns.expect(settings_model) + def post(self): + """Create or update a setting""" + setting = request.json + key = setting['key'] + settings_store[key] = setting + save_settings(settings_store) # Save to file + return jsonify(setting) + + +@settings_ns.route('/') +class SettingItem(Resource): + def get(self, key): + """Get a setting by key""" + setting = settings_store.get(key) + if setting is None: + return {'message': 'Setting not found'}, 404 + return jsonify(setting) + + def delete(self, key): + """Delete a setting by key""" + if key not in settings_store: + return {'message': 'Setting not found'}, 404 + del settings_store[key] + save_settings(settings_store) # Save to file + return {'message': 'Setting deleted'} diff --git a/api/dependency_installer.py b/api/dependency_installer.py index d629209c..1637d13e 100644 --- a/api/dependency_installer.py +++ b/api/dependency_installer.py @@ -43,6 +43,7 @@ def check_GPUtil_installed(): try: import GPUtil + import psutil return True except ImportError: import_GPUtil() @@ -55,6 +56,8 @@ def check_flask_installed(): import flask import flask_restx import flask_cors + + import flask_socketio return True except ImportError: import_flask() @@ -137,11 +140,13 @@ def import_tkinter(): def import_GPUtil(): - run_pip(f"install GPUtil", desc="GPU Utility for NVIDIA GPUs") + run_pip(f"install GPUtil psutil", desc="GPU Utility for NVIDIA GPUs") try: GPUtil = importlib.import_module( "GPUtil") + psutil = importlib.import_module( + "psutil") return GPUtil except ImportError: print("Failed to import GPUtil after installation.") @@ -149,11 +154,12 @@ def import_GPUtil(): def import_flask(): - run_pip(f"install flask flask-restx flask-cors", desc="Flask Rest API") + run_pip(f"install flask flask-restx flask-cors flask_socketio", desc="Flask Rest API") try: flask = importlib.import_module("flask") restx = importlib.import_module("flask-restx") + flask_socketio = importlib.import_module("flask_socketio") return restx except ImportError: print("Failed to import flask after installation.") @@ -203,6 +209,6 @@ def run_pip(command, desc=None, live=default_command_live): return None -# check_tkinter_installed() +check_tkinter_installed() check_GPUtil_installed() check_flask_installed() diff --git a/api/http_server.py b/api/http_server.py index 66fb7e39..c6829880 100644 --- a/api/http_server.py +++ b/api/http_server.py @@ -1,48 +1,116 @@ - from .dependency_installer import * -from flask import Flask, send_from_directory, jsonify, render_template +from flask import Flask, send_from_directory, render_template +from flask_socketio import SocketIO, emit from flask_restx import Api -import threading import logging -from flask_cors import CORS +import time from .controllers import register_blueprints -import os +import psutil +import GPUtil +import threading +# Initialize Flask app +title = f"Resource Monitor" +app = Flask(title, static_folder='web/assets', template_folder='web/templates') +app.config['CORS_HEADERS'] = 'Content-Type' -def load_page(filename): - """Load an HTML file as a string and return it""" - file_path = os.path.join("web", filename) - with open(file_path, 'r') as file: - content = file.read() - return content +# Initialize Flask-RESTx API +api = Api(app, version='1.0', title=title, description='API for system resource monitoring') +# Register blueprints (API endpoints) +register_blueprints(app, api) + +# Initialize SocketIO with the Flask app +socketio = SocketIO(app, cors_allowed_origins="*") + +# Cache for system usage data +cache = { + 'timestamp': 0, + 'data': { + 'cpu': 0, + 'ram': 0, + 'gpu': 0, + 'vram': 0, + 'hdd': 0, + 'temp': 0 + } +} +CACHE_DURATION = 1 # Cache duration in seconds # Suppress the Flask development server warning log = logging.getLogger('werkzeug') log.setLevel(logging.ERROR) # Set level to ERROR to suppress warnings -title = f"Elegant Resource Monitor" -app = Flask(title, static_folder='web/assets', template_folder='web/templates') -app.config['CORS_HEADERS'] = 'Content-Type' -api = Api(app, version='1.0', title=title, - description='Elegant Resource Monitor REST API') +def get_cache(current_time): + # Get CPU utilization + cpu_percent = psutil.cpu_percent(interval=0) -# Register blueprints (API endpoints) -register_blueprints(app, api) + # Get Memory utilization + mem = psutil.virtual_memory() + mem_percent = mem.percent -# Enable CORS for all origins -CORS(app, resources={r"/*": {"origins": "*"}}) + # Get GPU utilization (considering only the first GPU) + gpus = GPUtil.getGPUs() + gpu_percent = gpus[0].load * 100 if gpus else 0 + # Get VRAM usage (considering only the first GPU) + vram_usage = 0 + if gpus: + used = gpus[0].memoryUsed + total = gpus[0].memoryTotal + vram_usage = (used / total) * 100 + + # Get HDD usage (assuming usage of the primary disk) + hdd = psutil.disk_usage('/') + hdd_percent = hdd.percent + + # Get temperature (if available) + temperature = gpus[0].temperature if gpus else 0 + + # Update the cache + cache['data'] = { + 'cpu': cpu_percent, + 'ram': mem_percent, + 'gpu': gpu_percent, + 'vram': vram_usage, # Convert bytes to MB + 'hdd': hdd_percent, + 'temp': temperature # Add temperature + } + cache['timestamp'] = current_time + +@app.route('/') +def home(): + return render_template('index.html') @app.route('/') def serve_static(filename): return send_from_directory('web', filename) +@socketio.on('connect') +def handle_connect(): + # Emit initial data + current_time = time.time() + get_cache(current_time) + emit('data_update', cache['data']) + +@socketio.on('disconnect') +def handle_disconnect(): + pass + +def background_thread(): + while True: + current_time = time.time() + get_cache(current_time) + socketio.emit('data_update', cache['data']) + time.sleep(.5) def run_app(): - app.run(port=5000) - + time.sleep(1) # Sleep for a short while to let the server start + # Start the background thread for emitting data + socketio.start_background_task(target=background_thread) + # Run the Flask app with SocketIO + socketio.run(app, port=5000) # Start Flask app in a separate thread thread = threading.Thread(target=run_app) -thread.start() +thread.start() \ No newline at end of file diff --git a/web/assets/css/styles.css b/web/assets/css/styles.css index 2ebb10d6..6e9afcc5 100644 --- a/web/assets/css/styles.css +++ b/web/assets/css/styles.css @@ -21,16 +21,16 @@ left: 0px; height: 0% !important; width: 0% !important; + transform: scale(0); position: fixed; - z-index: 9; + z-index: 9991; will-change: transform; text-align: center; transition: width 0.8s ease, height 0.8s ease, transform 0.3s ease; - display: none; } #chart-button-container.active { - display: block; + transform: scale(1); } #mydivheader { @@ -214,6 +214,7 @@ button { } #settingsMenu.show { + min-width: 110px; opacity: 1 !important; z-index: 1000 !important; transform: scale(0.8) translateY(-30px) translateX(-10px) !important; /* Animate to visible state */ @@ -324,3 +325,74 @@ button { .window { transition: width 0.3s ease, height 0.3s ease; } + +#show_resource_monitor_link { + cursor: pointer !important; + position: fixed; + top: 20px; + left: 10px; + z-index: 9991; + margin: 5px; + display: flex; + font-size: 12px; + align-items: center; +} + +.resource-monitor-icon { + margin: 5px; + height: 14px; +} +#item-table { + width: -webkit-fill-available; + margin-left: 5px; + margin-right: 5px; + margin-bottom: 5px; + border-collapse: separate; /* Ensures that border-radius works */ + border-spacing: 0; /* Removes spacing between cells */ + border: 0px solid #ddd; /* Adds border to the table */ + border-radius: 8px; /* Adjust the value for desired corner radius */ + overflow: hidden; /* Ensures rounded corners are visible */ +} + +#item-table th, +#item-table td { + padding: 10px; + border: 1px solid #ddd; + text-align: left; + line-height: 10px; +} + +#item-table thead { + background-color: #333; + color: #fff; +} + +#item-table tbody tr:nth-child(even) { + background-color: rgba(255, 255, 255, 0.1); +} + +#item-table tbody tr:nth-child(odd) { + background-color: transparent; +} + +#item-table tbody td { + color: #fff; +} + +#progress-bar-container { + width: -webkit-fill-available; + + height: 20px; /* Height of the progress bar */ + background-color: #e0e0e0; /* Light gray background */ + border-radius: 10px; /* Rounded corners for the progress bar container */ + margin: 0px 5px; /* Space between the progress bar and the table */ + display: none; +} + +#progress-bar { + height: 100%; + width: 0%; /* Start with 0% width */ + background-color: #76c7c0; /* Fill color of the progress bar */ + border-radius: 10px; /* Rounded corners for the progress bar */ + transition: width 0.5s ease; /* Smooth transition effect */ +} diff --git a/web/assets/js/chart-settings.js b/web/assets/js/chart-settings.js index 163e6443..79c12d4b 100644 --- a/web/assets/js/chart-settings.js +++ b/web/assets/js/chart-settings.js @@ -1,137 +1,139 @@ // VARS AND CONST -const hostname = "localhost"; // Gets the host without port -const baseUrl = `http://${hostname}:5000`; // Append the port 5000 -const apiUrl = `${baseUrl}/gpu_usage/`; +const hostname = 'localhost' // Gets the host without port +const baseUrl = `http://${hostname}:5000` // Append the port 5000 +const apiUrl = `${baseUrl}/gpu_usage/` const colorPalette = [ - "rgb(240, 193, 90, 0.2)", - "rgb(240, 142, 219, 0.2)", - "rgb(24, 90, 219, 0.2)", - "rgb(127, 161, 195, 0.2)", - "rgb(128, 239, 145, 0.2)", - "rgb(245, 245, 245, 0.2)", - "rgb(240, 142, 219, 0.2)", - "rgb(159, 238, 209, 0.2)", -]; + 'rgb(240, 193, 90, 0.2)', + 'rgb(240, 142, 219, 0.2)', + 'rgb(24, 90, 219, 0.2)', + 'rgb(127, 161, 195, 0.2)', + 'rgb(128, 239, 145, 0.2)', + 'rgb(245, 245, 245, 0.2)', + 'rgb(240, 142, 219, 0.2)', + 'rgb(159, 238, 209, 0.2)', +] const borderColors = [ - "rgb(240, 193, 90)", - "rgb(240, 142, 219)", - "rgb(24, 90, 219)", - "rgb(127, 161, 195)", - "rgb(128, 239, 145)", - "rgb(245, 245, 245)", - "rgb(240, 142, 219)", - "rgb(159, 238, 209)", -]; + 'rgb(240, 193, 90)', + 'rgb(240, 142, 219)', + 'rgb(24, 90, 219)', + 'rgb(127, 161, 195)', + 'rgb(128, 239, 145)', + 'rgb(245, 245, 245)', + 'rgb(240, 142, 219)', + 'rgb(159, 238, 209)', +] -let currentChart = null; // Track the current chart instance -const MAX_DATA_POINTS = 50; // Number of data points to keep +let currentChart = null // Track the current chart instance +const MAX_DATA_POINTS = 50 // Number of data points to keep // Custom plugin to draw fixed labels in the middle of the chart area const fixedLabelPlugin = { - id: "fixedLabelPlugin", + id: 'fixedLabelPlugin', afterDatasetsDraw(chart) { - const { ctx, scales, data } = chart; - ctx.save(); + const { ctx, scales, data } = chart + ctx.save() - const centerX = scales.x.left + scales.x.width / 2; - const labelPositions = []; + const centerX = scales.x.left + scales.x.width / 2 + const labelPositions = [] data.datasets[0].data.forEach((value, index) => { - const yPos = chart.getDatasetMeta(0).data[index].y; + const yPos = chart.getDatasetMeta(0).data[index].y // Store yPos for positioning labels labelPositions.push({ x: centerX, y: yPos, - value: `${+value.toFixed(2)}` + `${index == 5 ? "\u00B0" : "%"}`, - }); - }); - const size = localStorage.getItem("chart-size") ?? "small"; - let fontSize = 10; // Default font size + value: `${+value.toFixed(2)}` + `${index == 5 ? '\u00B0' : '%'}`, + }) + }) + const size = localStorage.getItem('chart-size') ?? 'small' + let fontSize = 10 // Default font size switch (size) { - case "small": - fontSize = "10px"; - break; - case "medium": - fontSize = "16px"; - break; + case 'small': + fontSize = '7px' + break + case 'medium': + fontSize = '16px' + break default: - fontSize = "18px"; - break; + fontSize = '18px' + break } - ctx.font = fontSize; - ctx.fillStyle = "#FFFFFF"; - ctx.textAlign = "center"; - ctx.textBaseline = "middle"; + ctx.font = fontSize + ctx.fillStyle = '#FFFFFF' + ctx.textAlign = 'center' + ctx.textBaseline = 'middle' labelPositions.forEach((label) => { - ctx.fillText(label.value, label.x, label.y); - }); + ctx.fillText(label.value, label.x, label.y) + }) - ctx.restore(); + ctx.restore() }, -}; +} // Initialize the bar chart -function initializeBarChart() { - localStorage.setItem("active-chart", "bar"); - const chartContainer = document.getElementById("chart-container"); - const existingCanvas = document.getElementById("usage-chart"); - const chartWrapper = document.getElementById("chart-wrapper"); +window.initializeBarChart = function () { + localStorage.setItem('active-chart', 'bar') + var table = document.getElementById('item-table') + table.style.display = 'none' + const chartContainer = document.getElementById('chart-container') + const existingCanvas = document.getElementById('usage-chart') + const chartWrapper = document.getElementById('chart-wrapper') if (existingCanvas) { - chartContainer.removeChild(existingCanvas); + chartContainer.removeChild(existingCanvas) } - const size = localStorage.getItem("chart-size") ?? "small"; - let fontSize = 10; // Default font size + const size = localStorage.getItem('chart-size') ?? 'small' + let fontSize = 10 // Default font size switch (size) { - case "small": - fontSize = "10px"; - break; - case "medium": - fontSize = "16px"; - break; + case 'small': + fontSize = '7px' + break + case 'medium': + fontSize = '16px' + break default: - fontSize = "18px"; - break; + fontSize = '18px' + break } // Create a new canvas element - const newCanvas = document.createElement("canvas"); - newCanvas.id = "usage-chart"; - newCanvas.classList.add("bar"); // Add the class directly to the canvas element - chartContainer.appendChild(newCanvas); + const newCanvas = document.createElement('canvas') + newCanvas.id = 'usage-chart' + newCanvas.classList.add('bar') // Add the class directly to the canvas element + chartContainer.appendChild(newCanvas) - const ctx = newCanvas.getContext("2d"); - $(chartWrapper).hide(); + const ctx = newCanvas.getContext('2d') + $(chartWrapper).hide() currentChart = new Chart(ctx, { - type: "bar", + type: 'bar', data: { - labels: ["CPU", "RAM", "GPU", "VRAM", "HDD", "TEMP"], + labels: ['CPU', 'RAM', 'GPU', 'VRAM', 'HDD', 'TEMP'], datasets: [ { - label: "Usage", + label: 'Usage', data: [0, 0, 0, 0, 0], barPercentage: 0.8, // Adjust space occupied by bars categoryPercentage: 1, // Adjust space between bars backgroundColor: function (context) { - const value = context.dataset.data[context.dataIndex]; - return value > 90 ? "#D9534F" : colorPalette[context.dataIndex]; + const value = context.dataset.data[context.dataIndex] + return value > 90 ? '#D9534F' : colorPalette[context.dataIndex] }, borderColor: function (context) { - const value = context.dataset.data[context.dataIndex]; - return value > 90 ? "#D9534F" : borderColors[context.dataIndex]; + const value = context.dataset.data[context.dataIndex] + return value > 90 ? '#D9534F' : borderColors[context.dataIndex] }, borderWidth: 1.5, }, ], }, options: { - indexAxis: "y", // Horizontal bars + indexAxis: 'y', // Horizontal bars scales: { x: { grid: { @@ -143,14 +145,14 @@ function initializeBarChart() { beginAtZero: true, max: 100, ticks: { - color: "#ffffff", + color: '#ffffff', font: { size: fontSize, weight: 600, }, - align: "center", + align: 'center', callback: function (value, index, ticks) { - return value + "%"; + return value + '%' }, }, }, @@ -159,12 +161,12 @@ function initializeBarChart() { display: false, }, border: { - color: "#ffffff30", + color: '#ffffff30', width: 1, // Width of the axis border }, ticks: { - color: "#FFFFFF", - crossAlign: "far", + color: '#FFFFFF', + crossAlign: 'far', font: { weight: 600, size: fontSize, @@ -189,207 +191,213 @@ function initializeBarChart() { maintainAspectRatio: false, }, plugins: [fixedLabelPlugin], // Register the custom plugins - }); + }) - currentChart.options.animation = true; - const legendContainer = document.getElementById("custom-legend"); - legendContainer.innerHTML = ""; + currentChart.options.animation = true + const legendContainer = document.getElementById('custom-legend') + legendContainer.innerHTML = '' - document.getElementById("settingsMenu").classList.remove("show"); // Hide the menu + document.getElementById('settingsMenu').classList.remove('show') // Hide the menu - document.querySelectorAll("canvas").forEach((row) => { - row.classList.remove("no-drag"); - row.classList.add("drag"); - }); + document.querySelectorAll('canvas').forEach((row) => { + row.classList.remove('no-drag') + row.classList.add('drag') + }) - window.addEventListener("resize", () => { - currentChart.resize(); - }); - $(chartWrapper).fadeIn(300); + window.addEventListener('resize', () => { + currentChart.resize() + }) + $(chartWrapper).fadeIn(300) + if (size == 'large') { + $(table).fadeIn(800) + } } // Initialize the line chart -function initializeLineChart() { - localStorage.setItem("active-chart", "line"); - const existingCanvas = document.getElementById("usage-chart"); - const chartContainer = document.getElementById("chart-container"); - const chartWrapper = document.getElementById("chart-wrapper"); +window.initializeLineChart = function () { + localStorage.setItem('active-chart', 'line') + var table = document.getElementById('item-table') + table.style.display = 'none' + const size = localStorage.getItem('chart-size') ?? 'medium' + const existingCanvas = document.getElementById('usage-chart') + const chartContainer = document.getElementById('chart-container') + const chartWrapper = document.getElementById('chart-wrapper') if (existingCanvas) { - chartContainer.removeChild(existingCanvas); + chartContainer.removeChild(existingCanvas) } // Create a new canvas element - const newCanvas = document.createElement("canvas"); - newCanvas.id = "usage-chart"; - newCanvas.classList.add("line"); // Add the class directly to the canvas element - chartContainer.appendChild(newCanvas); - $(chartWrapper).hide(); + const newCanvas = document.createElement('canvas') + newCanvas.id = 'usage-chart' + newCanvas.classList.add('line') // Add the class directly to the canvas element + chartContainer.appendChild(newCanvas) + $(chartWrapper).hide() - const ctx = newCanvas.getContext("2d"); + const ctx = newCanvas.getContext('2d') currentChart = new Chart(ctx, { - type: "line", + type: 'line', data: { labels: [], datasets: [ { - label: "CPU", + label: 'CPU', data: [], borderColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return borderColors[datasetIndex % borderColors.length]; + return borderColors[datasetIndex % borderColors.length] }, borderWidth: 1.5, backgroundColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return colorPalette[datasetIndex % borderColors.length]; + return colorPalette[datasetIndex % borderColors.length] }, fill: false, tension: 0.1, }, { - label: "RAM", + label: 'RAM', data: [], borderColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return borderColors[datasetIndex % borderColors.length]; + return borderColors[datasetIndex % borderColors.length] }, borderWidth: 1.5, backgroundColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return colorPalette[datasetIndex % borderColors.length]; + return colorPalette[datasetIndex % borderColors.length] }, fill: false, tension: 0.1, }, { - label: "GPU", + label: 'GPU', data: [], borderColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return borderColors[datasetIndex % borderColors.length]; + return borderColors[datasetIndex % borderColors.length] }, borderWidth: 1.5, backgroundColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return colorPalette[datasetIndex % borderColors.length]; + return colorPalette[datasetIndex % borderColors.length] }, fill: false, tension: 0.1, }, { - label: "VRAM", + label: 'VRAM', data: [], borderColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return borderColors[datasetIndex % borderColors.length]; + return borderColors[datasetIndex % borderColors.length] }, borderWidth: 1.5, backgroundColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return colorPalette[datasetIndex % borderColors.length]; + return colorPalette[datasetIndex % borderColors.length] }, fill: false, tension: 0.1, }, { - label: "HDD", + label: 'HDD', data: [], borderColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return borderColors[datasetIndex % borderColors.length]; + return borderColors[datasetIndex % borderColors.length] }, borderWidth: 1.5, backgroundColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return colorPalette[datasetIndex % borderColors.length]; + return colorPalette[datasetIndex % borderColors.length] }, fill: false, tension: 0.1, }, { - label: "TEMP", + label: 'TEMP', data: [], borderColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return borderColors[datasetIndex % borderColors.length]; + return borderColors[datasetIndex % borderColors.length] }, borderWidth: 1.5, backgroundColor: function (context) { - const dataset = context.dataset; - const datasetIndex = context.datasetIndex; - const shouldUseRed = dataset.data.some((value) => value > 90); + const dataset = context.dataset + const datasetIndex = context.datasetIndex + const shouldUseRed = dataset.data.some((value) => value > 90) if (shouldUseRed) { - return "#D9534F"; // Return red color if any value exceeds 90 + return '#D9534F' // Return red color if any value exceeds 90 } - return colorPalette[datasetIndex % borderColors.length]; + return colorPalette[datasetIndex % borderColors.length] }, fill: false, tension: 0.1, @@ -401,7 +409,7 @@ function initializeLineChart() { enabled: false, tension: { duration: 1000, - easing: "linear", + easing: 'linear', from: 1, to: 0, loop: true, @@ -422,15 +430,15 @@ function initializeLineChart() { beginAtZero: true, max: 100, ticks: { - color: "#FFFFFF", - crossAlign: "far", + color: '#FFFFFF', + crossAlign: 'far', padding: 0, font: { weight: 600, size: 7, }, callback: function (value, index, ticks) { - return value + "%"; + return value + '%' }, }, }, @@ -448,61 +456,64 @@ function initializeLineChart() { }, }, }, - }); + }) - currentChart.options.animation = false; - generateCustomLegend(); - document.getElementById("settingsMenu").classList.remove("show"); // Hide the menu + currentChart.options.animation = false + generateCustomLegend() + document.getElementById('settingsMenu').classList.remove('show') // Hide the menu - window.addEventListener("resize", () => { - currentChart.resize(); - }); - $(chartWrapper).fadeIn(300); + window.addEventListener('resize', () => { + currentChart.resize() + }) + $(chartWrapper).fadeIn(300) + if (size == 'large') { + $(table).fadeIn(800) + } } function generateCustomLegend() { - const legendContainer = document.getElementById("custom-legend"); - legendContainer.innerHTML = ""; + const legendContainer = document.getElementById('custom-legend') + legendContainer.innerHTML = '' currentChart.data.datasets.forEach((dataset, index) => { - const legendItem = document.createElement("div"); - legendItem.className = "custom-legend-item"; + const legendItem = document.createElement('div') + legendItem.className = 'custom-legend-item' // Create text element - const legendText = document.createElement("span"); - legendText.className = "custom-legend-text"; - legendText.textContent = dataset.label; - const shouldUseRed = dataset.data.some((value) => value > 90); - legendText.style.color = shouldUseRed - ? "#D9534F" - : `${borderColors[index]}`; + const legendText = document.createElement('span') + legendText.className = 'custom-legend-text' + legendText.textContent = dataset.label + const shouldUseRed = dataset.data.some((value) => value > 90) + legendText.style.color = shouldUseRed ? '#D9534F' : `${borderColors[index]}` - legendText.style.fontWeight = shouldUseRed ? "800" : `600`; - const size = localStorage.getItem("chart-size") ?? "small"; + legendText.style.fontWeight = shouldUseRed ? '800' : `600` + const size = localStorage.getItem('chart-size') ?? 'small' switch (size) { - case "small": - legendText.style.fontSize = "10px"; - break; - case "medium": - legendText.style.fontSize = "16px"; - break; + case 'small': + legendText.style.fontSize = '7px' + break + case 'medium': + legendText.style.fontSize = '16px' + break default: - legendText.style.fontSize = "18px"; - break; + legendText.style.fontSize = '18px' + break } - legendItem.appendChild(legendText); - legendContainer.appendChild(legendItem); - }); + legendItem.appendChild(legendText) + legendContainer.appendChild(legendItem) + }) } -async function updateUsage() { - try { - const response = await fetch(apiUrl); - const data = await response.json(); - const timestamp = new Date(); + +setTimeout(() => { + const socket = io('http://localhost:5000') + + // Function to update the chart with new data + function updateChart(data) { + const timestamp = new Date().toLocaleTimeString() if (currentChart) { - if (currentChart.config.type === "bar") { + if (currentChart.config.type === 'bar') { // Update data for bar chart currentChart.data.datasets[0].data = [ data.cpu, @@ -511,42 +522,42 @@ async function updateUsage() { data.vram, data.hdd, data.temp, - ]; - } else if (currentChart.config.type === "line") { + ] + } else if (currentChart.config.type === 'line') { // Update data for line chart - currentChart.data.labels.push(timestamp); - currentChart.data.datasets[0].data.push(data.cpu); - currentChart.data.datasets[1].data.push(data.ram); - currentChart.data.datasets[2].data.push(data.gpu); - currentChart.data.datasets[3].data.push(data.vram); - currentChart.data.datasets[4].data.push(data.hdd); - currentChart.data.datasets[5].data.push(data.temp); + currentChart.data.labels.push(timestamp) + currentChart.data.datasets[0].data.push(data.cpu) + currentChart.data.datasets[1].data.push(data.ram) + currentChart.data.datasets[2].data.push(data.gpu) + currentChart.data.datasets[3].data.push(data.vram) + currentChart.data.datasets[4].data.push(data.hdd) + currentChart.data.datasets[5].data.push(data.temp) // Prune old data if the number of points exceeds the limit + const MAX_DATA_POINTS = 50 // Adjust as needed if (currentChart.data.labels.length > MAX_DATA_POINTS) { - currentChart.data.labels.shift(); // Remove the oldest label - currentChart.data.datasets.forEach((dataset) => dataset.data.shift()); // Remove the oldest data points + currentChart.data.labels.shift() // Remove the oldest label + currentChart.data.datasets.forEach((dataset) => dataset.data.shift()) // Remove the oldest data points } - generateCustomLegend(); + generateCustomLegend() } - // Update the chart with new data - currentChart.update(); + currentChart.update() } - } catch (error) { - console.error("Failed to fetch usage data.", error); } -} -let intervalId; // Variable to store the interval ID -// Function to start the interval -function startInterval() { - intervalId = setInterval(updateUsage, 500); -} -// Function to stop the interval -function stopInterval() { - if (intervalId) { - clearInterval(intervalId); - intervalId = null; // Optional: Reset intervalId to indicate no active interval - } -} + // Handle data updates from the WebSocket server + socket.on('data_update', function (data) { + updateChart(data) + }) + + // Optional: Handle WebSocket connection errors + socket.on('connect_error', function (error) { + console.error('Connection error:', error) + }) + + // Optional: Handle WebSocket disconnection + socket.on('disconnect', function () { + console.log('WebSocket disconnected') + }) +}, 4000) diff --git a/web/assets/js/chart.js b/web/assets/js/chart.js index 2463ad56..6c0082bb 100644 --- a/web/assets/js/chart.js +++ b/web/assets/js/chart.js @@ -11694,6 +11694,7 @@ ); }); //# sourceMappingURL=chart.umd.js.map + /*! * chartjs-plugin-datalabels v2.2.0 * https://chartjs-plugin-datalabels.netlify.app diff --git a/web/assets/js/dependencies.js b/web/assets/js/dependencies.js index 1200b147..a0d60394 100644 --- a/web/assets/js/dependencies.js +++ b/web/assets/js/dependencies.js @@ -19,6 +19,14 @@ var script = document.createElement('script') script.src = '/file=web/assets/js/jquery-3.7.1.min.js' document.body.appendChild(script) +var script = document.createElement('script') +script.src = '/file=web/assets/js/elegant-resource-monitor.js' +document.body.appendChild(script) + +var script = document.createElement('script') +script.src = '/file=web/assets/js/socket.io.min.js' +document.body.appendChild(script) + var script = document.createElement('script') script.src = '/file=web/assets/js/chart.js' document.body.appendChild(script) diff --git a/web/assets/js/elegant-resource-monitor-service.js b/web/assets/js/elegant-resource-monitor-service.js new file mode 100644 index 00000000..798d4ec8 --- /dev/null +++ b/web/assets/js/elegant-resource-monitor-service.js @@ -0,0 +1,211 @@ +import { api } from '/scripts/api.js' +import { getResolver } from './shared_utils.js' +export class ElegantResourceMonitorExecution { + constructor(id) { + this.promptApi = null + this.executedNodeIds = [] + this.totalNodes = 0 + this.currentlyExecuting = null + this.errorDetails = null + this.apiPrompt = getResolver() + this.id = id + } + setPrompt(prompt) { + this.promptApi = prompt.output + this.totalNodes = Object.keys(this.promptApi).length + this.apiPrompt.resolve(null) + } + getApiNode(nodeId) { + var _a + return ((_a = this.promptApi) === null || _a === void 0 ? void 0 : _a[String(nodeId)]) || null + } + getNodeLabel(nodeId) { + var _a, _b + const apiNode = this.getApiNode(nodeId) + let label = + ((_a = apiNode === null || apiNode === void 0 ? void 0 : apiNode._meta) === null || + _a === void 0 + ? void 0 + : _a.title) || + (apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) || + undefined + if (!label) { + const graphNode = + (_b = this.maybeGetComfyGraph()) === null || _b === void 0 + ? void 0 + : _b.getNodeById(Number(nodeId)) + label = + (graphNode === null || graphNode === void 0 ? void 0 : graphNode.title) || + (graphNode === null || graphNode === void 0 ? void 0 : graphNode.type) || + undefined + } + return label + } + executing(nodeId, step, maxSteps) { + var _a + if (nodeId == null) { + this.currentlyExecuting = null + return + } + if ( + ((_a = this.currentlyExecuting) === null || _a === void 0 ? void 0 : _a.nodeId) !== nodeId + ) { + if (this.currentlyExecuting != null) { + this.executedNodeIds.push(nodeId) + } + this.currentlyExecuting = { nodeId, nodeLabel: this.getNodeLabel(nodeId), pass: 0 } + this.apiPrompt.promise.then(() => { + var _a + if (this.currentlyExecuting == null) { + return + } + const apiNode = this.getApiNode(nodeId) + if (!this.currentlyExecuting.nodeLabel) { + this.currentlyExecuting.nodeLabel = this.getNodeLabel(nodeId) + } + if ( + (apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) === + 'UltimateSDUpscale' + ) { + this.currentlyExecuting.pass-- + this.currentlyExecuting.maxPasses = -1 + } else if ( + (apiNode === null || apiNode === void 0 ? void 0 : apiNode.class_type) === + 'IterativeImageUpscale' + ) { + this.currentlyExecuting.maxPasses = + (_a = apiNode === null || apiNode === void 0 ? void 0 : apiNode.inputs['steps']) !== + null && _a !== void 0 + ? _a + : -1 + } + }) + } + if (step != null) { + if (!this.currentlyExecuting.step || step < this.currentlyExecuting.step) { + this.currentlyExecuting.pass++ + } + this.currentlyExecuting.step = step + this.currentlyExecuting.maxSteps = maxSteps + } + } + error(details) { + this.errorDetails = details + } + maybeGetComfyGraph() { + var _a + return ( + ((_a = window === null || window === void 0 ? void 0 : window.app) === null || _a === void 0 + ? void 0 + : _a.graph) || null + ) + } +} +class ElegantResourceMonitorService extends EventTarget { + constructor(api) { + super() + this.promptsMap = new Map() + this.currentExecution = null + this.lastQueueRemaining = 0 + const that = this + const queuePrompt = api.queuePrompt + api.queuePrompt = async function (num, prompt) { + let response + try { + response = await queuePrompt.apply(api, [...arguments]) + } catch (e) { + const promptExecution = that.getOrMakePrompt('error') + promptExecution.error({ exception_type: 'Unknown.' }) + throw e + } + const promptExecution = that.getOrMakePrompt(response.prompt_id) + promptExecution.setPrompt(prompt) + if (!that.currentExecution) { + that.currentExecution = promptExecution + } + that.promptsMap.set(response.prompt_id, promptExecution) + + return response + } + api.addEventListener('status', (e) => { + var _a + if (!((_a = e.detail) === null || _a === void 0 ? void 0 : _a.exec_info)) return + this.lastQueueRemaining = e.detail.exec_info.queue_remaining + this.dispatchProgressUpdate() + }) + api.addEventListener('execution_start', (e) => { + if (!this.promptsMap.has(e.detail.prompt_id)) { + console.warn("'execution_start' fired before prompt was made.") + } + const prompt = this.getOrMakePrompt(e.detail.prompt_id) + this.currentExecution = prompt + this.dispatchProgressUpdate() + }) + api.addEventListener('executing', (e) => { + if (!this.currentExecution) { + this.currentExecution = this.getOrMakePrompt('unknown') + console.warn("'executing' fired before prompt was made.") + } + this.currentExecution.executing(e.detail) + this.dispatchProgressUpdate() + if (e.detail == null) { + this.currentExecution = null + } + }) + api.addEventListener('progress', (e) => { + if (!this.currentExecution) { + this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id) + console.warn("'progress' fired before prompt was made.") + } + this.currentExecution.executing(e.detail.node, e.detail.value, e.detail.max) + this.dispatchProgressUpdate() + }) + api.addEventListener('execution_cached', (e) => { + if (!this.currentExecution) { + this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id) + console.warn("'execution_cached' fired before prompt was made.") + } + for (const cached of e.detail.nodes) { + this.currentExecution.executing(cached) + } + this.dispatchProgressUpdate() + }) + api.addEventListener('executed', (e) => { + if (!this.currentExecution) { + this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id) + console.warn("'executed' fired before prompt was made.") + } + }) + api.addEventListener('execution_error', (e) => { + var _a + if (!this.currentExecution) { + this.currentExecution = this.getOrMakePrompt(e.detail.prompt_id) + console.warn("'execution_error' fired before prompt was made.") + } + ;(_a = this.currentExecution) === null || _a === void 0 ? void 0 : _a.error(e.detail) + this.dispatchProgressUpdate() + }) + } + async queuePrompt(prompt) { + return await api.queuePrompt(-1, prompt) + } + dispatchProgressUpdate() { + this.dispatchEvent( + new CustomEvent('elegant-resource-monitor-update', { + detail: { + queue: this.lastQueueRemaining, + prompt: this.currentExecution, + }, + }), + ) + } + getOrMakePrompt(id) { + let prompt = this.promptsMap.get(id) + if (!prompt) { + prompt = new ElegantResourceMonitorExecution(id) + this.promptsMap.set(id, prompt) + } + return prompt + } +} +export const MONITOR_SERVICE = new ElegantResourceMonitorService(api) diff --git a/web/assets/js/elegant-resource-monitor.js b/web/assets/js/elegant-resource-monitor.js new file mode 100644 index 00000000..9359bea8 --- /dev/null +++ b/web/assets/js/elegant-resource-monitor.js @@ -0,0 +1,288 @@ +class ElegantResourceMonitor extends HTMLElement { + constructor() { + super() + this.shadow = null + this.currentPromptExecution = null + this.onProgressUpdateBound = this.onProgressUpdate.bind(this) + this.connected = false + } + + render() { + this.innerHTML = ` +
+
+
+
+ settings +
+
Settings
+
+
+
+
Layout:
+
+ 1 | + 2 +
+
+
+
Size:
+
+ S | + M +
+
+
+
+
Position
+
+ + + + + + + + + +
+
+
+
+
+ close +
+
+
+
+
+
+
+ +
+
+
+ + + + +
+
+
+
+
+ ` + } + + addEventListeners() { + this.querySelector('#popupTrigger').addEventListener('click', () => { + const settingsMenu = this.querySelector('#settingsMenu') + settingsMenu.style.display = settingsMenu.style.display === 'block' ? 'none' : 'block' + }) + + this.querySelector('#close-button').addEventListener('click', () => { + this.dispatchEvent(new CustomEvent('close', { bubbles: true, composed: true })) + }) + } + + getSizes() { + const savedChart = localStorage.getItem('active-chart') ?? 'bar' + var sizes = {} + if (savedChart == 'bar') { + sizes = { + small: { height: '120', width: '150' }, + medium: { height: '300', width: '410' }, + large: { height: '450', width: '700' }, + } + } else { + sizes = { + small: { height: '110', width: '160' }, + medium: { height: '245', width: '425' }, + large: { height: '380', width: '700' }, + } + } + return sizes + } + getButtonSize() { + const size = localStorage.getItem('chart-size') ?? 'medium' + const sizes = this.getSizes() + const sizeStyles = sizes[size] + var buttonHeight = +sizeStyles.height + 25 + const buttonWidth = +sizeStyles.width + 25 + var table = this.querySelector('#item-table') + var totalRowCount = table.rows.length // 5 + var tableHeight = totalRowCount * 30 + if (size == 'large') { + buttonHeight = +buttonHeight + tableHeight + } + + return { buttonHeight, buttonWidth } + } + + addListItem(itemContent) { + const itemBody = this.querySelector('#item-body') + const row = document.createElement('tr') + const cell = document.createElement('td') + cell.innerText = itemContent + row.appendChild(cell) + itemBody.appendChild(row) + } + + get currentNodeId() { + var _a, _b + const prompt = this.currentPromptExecution + const nodeId = + ((_a = prompt === null || prompt === void 0 ? void 0 : prompt.errorDetails) === null || + _a === void 0 + ? void 0 + : _a.node_id) || + ((_b = prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting) === null || + _b === void 0 + ? void 0 + : _b.nodeId) + return nodeId || null + } + + adjustButtonHeight(e) { + const chartButtonContainer = this.querySelector('#chart-button-container') + + const shouldShowPerfMonitor = JSON.parse(localStorage.getItem('shouldShowPerfMonitor')) ?? false + if (!shouldShowPerfMonitor || chartButtonContainer.clientHeight == 0) return + + const { buttonHeight, buttonWidth } = this.getButtonSize() + const chartContainer = this.querySelector('#chart-container') + const savedChart = localStorage.getItem('active-chart') ?? 'bar' + const size = localStorage.getItem('chart-size') ?? 'medium' + + const height = this.querySelector('#chart-container').style.height.replace('px', '') + let bar = this.querySelector('#progress-bar-container') + + var actulaButtonHeight = 0 + var actualButtonWidth = 0 + + var viewportHeight = +buttonHeight + var viewportWidth = +buttonWidth + + $(chartButtonContainer).each(function () { + this.style.setProperty('height', `${viewportHeight}px`, 'important') + this.style.setProperty('width', `${viewportWidth}px`, 'important') + }) + var table = this.querySelector('#item-table') + var totalRowCount = table.rows.length // 5 + var tableHeight = totalRowCount * 35 + var workArea = actulaButtonHeight + if (size == 'large') { + workArea = viewportHeight - tableHeight + } + + const prompt = e.detail.prompt + if (prompt && prompt.currentlyExecuting) { + bar.style.display = 'block' + workArea = workArea - 30 + } else { + bar.style.display = 'none' + } + + if (savedChart == 'bar') { + $(chartContainer).each(function () { + this.style.setProperty('height', `${workArea * 0.95}px`, 'important') + }) + } else { + $(chartContainer).each(function () { + this.style.setProperty('height', `${workArea * 0.87}px`, 'important') + }) + } + } + + onProgressUpdate(e) { + // this.adjustButtonHeight(e) + var _a, _b, _c, _d + if (!this.connected) return + const prompt = e.detail.prompt + + this.currentPromptExecution = prompt + + // Default progress to 0 if no totalNodes + let progressPercentage = 0 + + if (prompt === null || prompt === void 0 ? void 0 : prompt.errorDetails) { + let progressText = `${ + (_a = prompt.errorDetails) === null || _a === void 0 ? void 0 : _a.exception_type + } ${((_b = prompt.errorDetails) === null || _b === void 0 ? void 0 : _b.node_id) || ''} ${ + ((_c = prompt.errorDetails) === null || _c === void 0 ? void 0 : _c.node_type) || '' + }` + console.log(progressText) + // Set the progress bar to 0% or some error state if needed + this.querySelector('#progress-bar').style.width = '0%' + return + } + if (prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting) { + const current = prompt === null || prompt === void 0 ? void 0 : prompt.currentlyExecuting + + let progressText = `(${e.detail.queue}) ` + if (!prompt.totalNodes) { + progressText += `??%` + } else { + progressPercentage = (prompt.executedNodeIds.length / prompt.totalNodes) * 100 + progressText += `${Math.round(progressPercentage)}%` + } + + let nodeLabel = (_d = current.nodeLabel) === null || _d === void 0 ? void 0 : _d.trim() + const monitor = document.querySelector('elegant-resource-monitor') + // monitor.addListItem(nodeLabel) + + let stepsLabel = '' + if (current.step != null && current.maxSteps) { + const percent = (current.step / current.maxSteps) * 100 + if (current.pass > 1 || current.maxPasses != null) { + stepsLabel += `#${current.pass}` + if (current.maxPasses && current.maxPasses > 0) { + stepsLabel += `/${current.maxPasses}` + } + stepsLabel += ` - ` + } + stepsLabel += `${Math.round(percent)}%` + } + + if (nodeLabel || stepsLabel) { + progressText += ` - ${nodeLabel || '???'}${stepsLabel ? ` (${stepsLabel})` : ''}` + } + console.log(progressText) + } else { + if (e === null || e === void 0 ? void 0 : e.detail.queue) { + console.log(`(${e.detail.queue}) Running... in another tab`) + } else { + console.log('Idle') + } + } + + // Update the progress bar width + this.querySelector('#progress-bar').style.width = `${Math.round(progressPercentage)}%` + } + + connectedCallback() { + if (!this.connected) { + console.log('Adding event listener to MONITOR_SERVICE') + // MONITOR_SERVICE.addEventListener( + // 'elegant-resource-monitor-update', + // this.onProgressUpdateBound, + // ) + + this.render() + this.addEventListeners() + + this.connected = true + } + } + + disconnectedCallback() { + this.connected = false + // MONITOR_SERVICE.removeEventListener( + // 'elegant-resource-monitor-update', + // this.onProgressUpdateBound, + // ) + } +} + +// Register the custom element +customElements.define('elegant-resource-monitor', ElegantResourceMonitor) diff --git a/web/assets/js/script.js b/web/assets/js/script.js index 0ed20643..6afdf205 100644 --- a/web/assets/js/script.js +++ b/web/assets/js/script.js @@ -1,4 +1,5 @@ dragElement(document.getElementById('chart-button-container')) + var wasDragged = false function dragElement(elmnt) { var isDragging = false @@ -52,13 +53,7 @@ function dragElement(elmnt) { } function moveButtonToCenter(duration = 300) { - const widget = document.getElementById('chart-button-container') - const size = localStorage.getItem('chart-size') ?? 'medium' - const sizes = getSizes() - const sizeStyles = sizes[size] - const buttonHeight = +sizeStyles.height + 25 - const buttonWidth = +sizeStyles.width + 25 - + const { buttonHeight, buttonWidth } = getButtonSize() // Get button dimensions and viewport dimensions const widgetWidth = buttonWidth const widgetHeight = buttonHeight @@ -84,13 +79,6 @@ function moveButtonToCenter(duration = 300) { // HELPER FUNCTIONS // -if (localStorage.getItem('lastClass') && localStorage.getItem('lastInactiveClass')) { - var lastClass = JSON.parse(localStorage.getItem('lastClass')) - var lastInactiveClass = JSON.parse(localStorage.getItem('lastInactiveClass')) - addCSS(lastInactiveClass.key, lastInactiveClass.values[0]) - addCSS(lastClass.key, lastClass.values[0]) -} - function getCSSRule(ruleName) { ruleName = ruleName.toLowerCase() var result = null @@ -112,22 +100,6 @@ function getCSSRule(ruleName) { return result } -function addCSS(selector, styles) { - var rule = getCSSRule(selector) - - for (var property in styles) { - if (styles.hasOwnProperty(property)) { - rule.style.setProperty(property, styles[property], 'important') - } - } -} - -function updateCSS(selector, styles) { - var button = getCSSRule(selector) - button.style.setProperty('bottom', styles.bottom, 'important') - button.style.setProperty('right', styles.right, 'important') -} - window.barChart = async function () { checkForUpdates('active-chart', 'bar') await updateChartSize() @@ -151,9 +123,9 @@ window.mediumChart = async function () { window.largeChart = async function () { setTimeout(async () => { checkForUpdates('perf-monitor-position', 'center') + checkForUpdates('chart-size', 'large') await updateChartSize() }, 50) - checkForUpdates('chart-size', 'large') } function moveToCenter() { @@ -169,14 +141,10 @@ function checkForUpdates(key, value) { } function isWindowOutsideWorkingArea() { - const size = localStorage.getItem('chart-size') ?? 'medium' - const sizes = getSizes() - const sizeStyles = sizes[size] - const buttonHeight = +sizeStyles.height + 25 - const buttonWidth = +sizeStyles.width + 25 + const { buttonHeight, buttonWidth } = getButtonSize() // Get display bounds - const { displayBounds, windowBounds } = getDisplayAndWindowBounds() + const { displayBounds } = getDisplayAndWindowBounds() const widget = document.getElementById('chart-button-container') const rect = widget.getBoundingClientRect() @@ -327,8 +295,6 @@ function goToPosition(pos) { const currentLeft = +widget.style.left.replace('px', '') // Target position - const targetTop = pos.y - const targetLeft = pos.x const offsetX = pos.x - currentLeft const offsetY = pos.y - currentTop @@ -342,8 +308,6 @@ function goToPosition(pos) { // MAIN METHODS // function getDisplayAndWindowBounds() { - const screenWidth = window.screen.width - const screenHeight = window.screen.height const availWidth = window.screen.availWidth const availHeight = window.screen.availHeight @@ -373,11 +337,7 @@ function getDisplayAndWindowBounds() { } function getPositions() { - const size = localStorage.getItem('chart-size') ?? 'medium' - const sizes = getSizes() - const sizeStyles = sizes[size] - const buttonHeight = +sizeStyles.height + 25 - const buttonWidth = +sizeStyles.width + 25 + const { buttonHeight, buttonWidth } = getButtonSize() // Get button dimensions and viewport dimensions const widgetWidth = buttonWidth @@ -394,8 +354,6 @@ function getPositions() { const buttonCenterY = widgetHeight / 2 // Calculate the translation offsets needed to center the button - const offsetX = windowCenterX - buttonCenterX - const offsetY = windowCenterY - buttonCenterY // Define positions based on work area const positions = { @@ -437,7 +395,7 @@ function getPositions() { } function getCoordinates(isOutside) { - var position = localStorage.getItem('perf-monitor-position') ?? 'bottom-right' + var position = localStorage.getItem('perf-monitor-position') if (isOutside) { var outsidePosition = getNearestPosition() @@ -450,24 +408,18 @@ function getCoordinates(isOutside) { } function getNearestPosition() { - const size = localStorage.getItem('chart-size') ?? 'medium' - const sizes = getSizes() - const sizeStyles = sizes[size] - const buttonHeight = +sizeStyles.height + 25 - const buttonWidth = +sizeStyles.width + 25 + const { buttonHeight, buttonWidth } = getButtonSize() const widget = document.getElementById('chart-button-container') const viewportWidth = window.innerWidth const viewportHeight = window.innerHeight // Get display bounds - const { displayBounds, windowBounds } = getDisplayAndWindowBounds() + const { displayBounds } = getDisplayAndWindowBounds() // Define positions based on work area const positions = getPositions() // Get current window position const currentX = $(widget).offset().left let currentY = $(widget).offset().top - const buttonCenterX = buttonWidth / 2 - const buttonCenterY = buttonHeight / 2 const windowCenter = { x: $(widget).offset().left, y: $(widget).offset().top, @@ -481,6 +433,7 @@ function getNearestPosition() { y: Math.abs(workAreaCenter.y - windowCenter.y), } var threshold = 100 // Define a threshold to determine proximity + const size = localStorage.getItem('chart-size') ?? 'medium' switch (size) { case 'small': threshold = 250 @@ -494,6 +447,8 @@ function getNearestPosition() { break } + var nearestPosition = '' + if (distanceToCenter.x < threshold && distanceToCenter.y < threshold) { nearestPosition = 'center' } else { @@ -532,8 +487,25 @@ function getNearestPosition() { return pos } +function getButtonSize() { + const size = localStorage.getItem('chart-size') ?? 'medium' + const sizes = getSizes() + const sizeStyles = sizes[size] + var buttonHeight = +sizeStyles.height + 25 + const buttonWidth = +sizeStyles.width + 25 + var table = document.getElementById('item-table') + var totalRowCount = table.rows.length // 5 + var tableHeight = totalRowCount * 30 + if (size == 'large') { + buttonHeight = +buttonHeight + tableHeight + } + return { buttonHeight, buttonWidth } +} async function updateChartSize() { + var table = document.getElementById('item-table') + table.style.display = 'none' + const settingsMenu = document.getElementById('settingsMenu') settingsMenu.classList.remove('show') // Hide the menu if visible $('#chart-wrapper').fadeOut() @@ -555,15 +527,18 @@ async function updateChartSize() { chartContainer.classList.add(size) chartContainer.classList.add(savedChart) - const sizes = getSizes() - const sizeStyles = sizes[size] - const buttonHeight = +sizeStyles.height - const buttonWidth = +sizeStyles.width + const { buttonHeight, buttonWidth } = getButtonSize() const chartButtonContainer = document.getElementById('chart-button-container') + var actulaButtonHeight = 0 + var actualButtonWidth = 0 + + var viewportHeight = +buttonHeight + var viewportWidth = +buttonWidth + $(chartButtonContainer).each(function () { - this.style.setProperty('height', `${+buttonHeight + 25}px`, 'important') - this.style.setProperty('width', `${+buttonWidth + 25}px`, 'important') + this.style.setProperty('height', `${viewportHeight}px`, 'important') + this.style.setProperty('width', `${viewportWidth}px`, 'important') }) var position = localStorage.getItem('perf-monitor-position') @@ -593,11 +568,6 @@ async function updateChartSize() { } }) - var actulaButtonHeight = 0 - - viewportHeight = +buttonHeight + 25 - viewportWidth = +buttonWidth + 25 - switch (size) { case 'small': actulaButtonHeight = viewportHeight * 0.83 @@ -626,18 +596,24 @@ async function updateChartSize() { this.style.setProperty('background-color', ` #00000096`, 'important') } }) + var totalRowCount = table.rows.length // 5 + var tableHeight = totalRowCount * 30 + var workArea = actulaButtonHeight + if (size == 'large') { + workArea = viewportHeight - tableHeight + } const hasUpdates = localStorage.getItem('hasUpdates') ?? 'false' if (hasUpdates === 'true') { if (savedChart == 'bar') { $(chartContainer).each(function () { - this.style.setProperty('height', `${actulaButtonHeight * 0.95}px`, 'important') + this.style.setProperty('height', `${workArea * 0.95}px`, 'important') }) initializeBarChart() } else { $(chartContainer).each(function () { - this.style.setProperty('height', `${actulaButtonHeight * 0.87}px`, 'important') + this.style.setProperty('height', `${workArea * 0.87}px`, 'important') }) setTimeout(() => { initializeLineChart() @@ -646,10 +622,11 @@ async function updateChartSize() { } else { $('#chart-wrapper').fadeIn() } + localStorage.setItem('hasUpdates', 'false') const active = `#chart-button.top-left.active` - positionStyles = { + var positionStyles = { bottom: bottom, right: right, } @@ -663,9 +640,13 @@ async function updateChartSize() { const pos = getCoordinates(false) goToPosition(pos) - -var shouldShowPerfMonitor = false var appIsLoaded = false +var shouldShowPerfMonitor = false +if (JSON.parse(localStorage.getItem('shouldShowPerfMonitor')) ?? false) { + setTimeout(() => { + // showPerfMonitor() + }, 1500) +} window.showPerfMonitor = async function () { shouldShowPerfMonitor = !shouldShowPerfMonitor @@ -673,21 +654,25 @@ window.showPerfMonitor = async function () { const chartButton = document.getElementById('chart-button') const chartWrapper = document.getElementById('chart-wrapper') const chartButtonContainer = document.getElementById('chart-button-container') + const resourceMonitorLink = document.getElementById('show_resource_monitor') if (shouldShowPerfMonitor === true) { $(chartButtonContainer).toggleClass('active') localStorage.setItem('hasUpdates', 'true') - startInterval() await updateChartSize() + $(resourceMonitorLink).fadeOut(500) appIsLoaded = true } else { setTimeout(() => { - stopInterval() $(chartButtonContainer).toggleClass('active') + $(chartButtonContainer).each(function () { + this.style.setProperty('height', `0px`, 'important') + }) }, 500) chartButton.classList.remove('small', 'medium', 'large') $(chartWrapper).fadeOut() + $(resourceMonitorLink).fadeIn(500) } $(chartButton).toggleClass('active') @@ -701,7 +686,4 @@ document.getElementById('close-button').addEventListener('click', function () { row.classList.add('drag') }) showPerfMonitor() - setTimeout(() => { - window.electron.ipcRenderer.send('close-window') - }, 500) }) diff --git a/web/assets/js/shared_utils.js b/web/assets/js/shared_utils.js new file mode 100644 index 00000000..dad89f6c --- /dev/null +++ b/web/assets/js/shared_utils.js @@ -0,0 +1,115 @@ +export function getResolver(timeout = 5000) { + const resolver = {} + resolver.id = generateId(8) + resolver.completed = false + resolver.resolved = false + resolver.rejected = false + resolver.promise = new Promise((resolve, reject) => { + resolver.reject = () => { + resolver.completed = true + resolver.rejected = true + reject() + } + resolver.resolve = (data) => { + resolver.completed = true + resolver.resolved = true + resolve(data) + } + }) + resolver.timeout = setTimeout(() => { + if (!resolver.completed) { + resolver.reject() + } + }, timeout) + return resolver +} +const DEBOUNCE_FN_TO_PROMISE = new WeakMap() +export function debounce(fn, ms = 64) { + if (!DEBOUNCE_FN_TO_PROMISE.get(fn)) { + DEBOUNCE_FN_TO_PROMISE.set( + fn, + wait(ms).then(() => { + DEBOUNCE_FN_TO_PROMISE.delete(fn) + fn() + }), + ) + } + return DEBOUNCE_FN_TO_PROMISE.get(fn) +} +export function wait(ms = 16) { + if (ms === 16) { + return new Promise((resolve) => { + requestAnimationFrame(() => { + resolve() + }) + }) + } + return new Promise((resolve) => { + setTimeout(() => { + resolve() + }, ms) + }) +} +function dec2hex(dec) { + return dec.toString(16).padStart(2, '0') +} +export function generateId(length) { + const arr = new Uint8Array(length / 2) + crypto.getRandomValues(arr) + return Array.from(arr, dec2hex).join('') +} +export function getObjectValue(obj, objKey, def) { + if (!obj || !objKey) return def + const keys = objKey.split('.') + const key = keys.shift() + const found = obj[key] + if (keys.length) { + return getObjectValue(found, keys.join('.'), def) + } + return found +} +export function setObjectValue(obj, objKey, value, createMissingObjects = true) { + if (!obj || !objKey) return obj + const keys = objKey.split('.') + const key = keys.shift() + if (obj[key] === undefined) { + if (!createMissingObjects) { + return + } + obj[key] = {} + } + if (!keys.length) { + obj[key] = value + } else { + if (typeof obj[key] != 'object') { + obj[key] = {} + } + setObjectValue(obj[key], keys.join('.'), value, createMissingObjects) + } + return obj +} +export function moveArrayItem(arr, itemOrFrom, to) { + const from = typeof itemOrFrom === 'number' ? itemOrFrom : arr.indexOf(itemOrFrom) + arr.splice(to, 0, arr.splice(from, 1)[0]) +} +export function removeArrayItem(arr, itemOrIndex) { + const index = typeof itemOrIndex === 'number' ? itemOrIndex : arr.indexOf(itemOrIndex) + arr.splice(index, 1) +} +export function injectCss(href) { + if (document.querySelector(`link[href^="${href}"]`)) { + return Promise.resolve() + } + return new Promise((resolve) => { + const link = document.createElement('link') + link.setAttribute('rel', 'stylesheet') + link.setAttribute('type', 'text/css') + const timeout = setTimeout(resolve, 1000) + link.addEventListener('load', (e) => { + clearInterval(timeout) + resolve() + }) + link.href = href + document.head.appendChild(link) + }) +} diff --git a/web/assets/js/socket.io.min.js b/web/assets/js/socket.io.min.js new file mode 100644 index 00000000..0ae3cf09 --- /dev/null +++ b/web/assets/js/socket.io.min.js @@ -0,0 +1,7 @@ +/*! + * Socket.IO v4.0.1 + * (c) 2014-2021 Guillermo Rauch + * Released under the MIT License. + */ +!function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.io=e():t.io=e()}(self,(function(){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var o in t)n.d(r,o,function(e){return t[e]}.bind(null,o));return r},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},n.p="",n(n.s=18)}([function(t,e,n){var r=n(24),o=n(25),i=String.fromCharCode(30);t.exports={protocol:4,encodePacket:r,encodePayload:function(t,e){var n=t.length,o=new Array(n),s=0;t.forEach((function(t,c){r(t,!1,(function(t){o[c]=t,++s===n&&e(o.join(i))}))}))},decodePacket:o,decodePayload:function(t,e){for(var n=t.split(i),r=[],s=0;s0;case l.ACK:case l.BINARY_ACK:return Array.isArray(e)}}}]),n}(h);e.Decoder=b;var m=function(){function t(e){u(this,t),this.packet=e,this.buffers=[],this.reconPack=e}return p(t,[{key:"takeBinaryData",value:function(t){if(this.buffers.push(t),this.buffers.length===this.reconPack.attachments){var e=y.reconstructPacket(this.reconPack,this.buffers);return this.finishedReconstruction(),e}return null}},{key:"finishedReconstruction",value:function(){this.reconPack=null,this.buffers=[]}}]),t}()},function(t,e){var n=/^(?:(?![^:@]+:[^:@\/]*@)(http|https|ws|wss):\/\/)?((?:(([^:@]*)(?::([^:@]*))?)?@)?((?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}|[^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/,r=["source","protocol","authority","userInfo","user","password","host","port","relative","path","directory","file","query","anchor"];t.exports=function(t){var e=t,o=t.indexOf("["),i=t.indexOf("]");-1!=o&&-1!=i&&(t=t.substring(0,o)+t.substring(o,i).replace(/:/g,";")+t.substring(i,t.length));for(var s,c,a=n.exec(t||""),u={},f=14;f--;)u[r[f]]=a[f]||"";return-1!=o&&-1!=i&&(u.source=e,u.host=u.host.substring(1,u.host.length-1).replace(/;/g,":"),u.authority=u.authority.replace("[","").replace("]","").replace(/;/g,":"),u.ipv6uri=!0),u.pathNames=function(t,e){var n=e.replace(/\/{2,9}/g,"/").split("/");"/"!=e.substr(0,1)&&0!==e.length||n.splice(0,1);"/"==e.substr(e.length-1,1)&&n.splice(n.length-1,1);return n}(0,u.path),u.queryKey=(s=u.query,c={},s.replace(/(?:^|&)([^&=]*)=?([^&]*)/g,(function(t,e,n){e&&(c[e]=n)})),c),u}},function(t,e,n){"use strict";function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n=this._reconnectionAttempts)this.backoff.reset(),this.emitReserved("reconnect_failed"),this._reconnecting=!1;else{var n=this.backoff.duration();this._reconnecting=!0;var r=setTimeout((function(){e.skipReconnect||(t.emitReserved("reconnect_attempt",e.backoff.attempts),e.skipReconnect||e.open((function(n){n?(e._reconnecting=!1,e.reconnect(),t.emitReserved("reconnect_error",n)):e.onreconnect()})))}),n);this.opts.autoUnref&&r.unref(),this.subs.push((function(){clearTimeout(r)}))}}},{key:"onreconnect",value:function(){var t=this.backoff.attempts;this._reconnecting=!1,this.backoff.reset(),this.emitReserved("reconnect",t)}}])&&o(e.prototype,n),c&&o(e,c),y}(n(17).StrictEventEmitter);e.Manager=y},function(t,e,n){var r=n(9),o=n(23),i=n(27),s=n(28);e.polling=function(t){var e=!1,n=!1,s=!1!==t.jsonp;if("undefined"!=typeof location){var c="https:"===location.protocol,a=location.port;a||(a=c?443:80),e=t.hostname!==location.hostname||a!==t.port,n=t.secure!==c}if(t.xdomain=e,t.xscheme=n,"open"in new r(t)&&!t.forceJSONP)return new o(t);if(!s)throw new Error("JSONP disabled");return new i(t)},e.websocket=s},function(t,e,n){var r=n(22),o=n(2);t.exports=function(t){var e=t.xdomain,n=t.xscheme,i=t.enablesXDR;try{if("undefined"!=typeof XMLHttpRequest&&(!e||r))return new XMLHttpRequest}catch(t){}try{if("undefined"!=typeof XDomainRequest&&!n&&i)return new XDomainRequest}catch(t){}if(!e)try{return new(o[["Active"].concat("Object").join("X")])("Microsoft.XMLHTTP")}catch(t){}}},function(t,e,n){function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function i(t,e){for(var n=0;n0);return e}function u(){var t=a(+new Date);return t!==r?(s=0,r=t):t+"."+a(s++)}for(;c<64;c++)i[o[c]]=c;u.encode=a,u.decode=function(t){var e=0;for(c=0;c1?e-1:0),r=1;r=t.length?{done:!0}:{done:!1,value:t[r++]}},e:function(t){throw t},f:o}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var s,c=!0,a=!1;return{s:function(){n=t[Symbol.iterator]()},n:function(){var t=n.next();return c=t.done,t},e:function(t){a=!0,s=t},f:function(){try{c||null==n.return||n.return()}finally{if(a)throw s}}}}function i(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,r=new Array(e);n1?e-1:0),r=1;r1?n-1:0),o=1;o1?n-1:0),o=1;o1&&void 0!==arguments[1]?arguments[1]:"",n=arguments.length>2?arguments[2]:void 0,o=t;n=n||"undefined"!=typeof location&&location,null==t&&(t=n.protocol+"//"+n.host),"string"==typeof t&&("/"===t.charAt(0)&&(t="/"===t.charAt(1)?n.protocol+t:n.host+t),/^(https?|wss?):\/\//.test(t)||(t=void 0!==n?n.protocol+"//"+t:"https://"+t),o=r(t)),o.port||(/^(http|ws)$/.test(o.protocol)?o.port="80":/^(http|ws)s$/.test(o.protocol)&&(o.port="443")),o.path=o.path||"/";var i=-1!==o.host.indexOf(":"),s=i?"["+o.host+"]":o.host;return o.id=o.protocol+"://"+s+":"+o.port+e,o.href=o.protocol+"://"+s+(n&&n.port===o.port?"":":"+o.port),o}},function(t,e,n){var r=n(21);t.exports=function(t,e){return new r(t,e)},t.exports.Socket=r,t.exports.protocol=r.protocol,t.exports.Transport=n(3),t.exports.transports=n(8),t.exports.parser=n(0)},function(t,e,n){function r(){return(r=Object.assign||function(t){for(var e=1;e1&&void 0!==arguments[1]?arguments[1]:{};return i(this,l),e=f.call(this),t&&"object"===o(t)&&(n=t,t=null),t?(t=y(t),n.hostname=t.host,n.secure="https"===t.protocol||"wss"===t.protocol,n.port=t.port,t.query&&(n.query=t.query)):n.host&&(n.hostname=y(n.host).host),e.secure=null!=n.secure?n.secure:"undefined"!=typeof location&&"https:"===location.protocol,n.hostname&&!n.port&&(n.port=e.secure?"443":"80"),e.hostname=n.hostname||("undefined"!=typeof location?location.hostname:"localhost"),e.port=n.port||("undefined"!=typeof location&&location.port?location.port:e.secure?443:80),e.transports=n.transports||["polling","websocket"],e.readyState="",e.writeBuffer=[],e.prevBufferLen=0,e.opts=r({path:"/engine.io",agent:!1,withCredentials:!1,upgrade:!0,jsonp:!0,timestampParam:"t",rememberUpgrade:!1,rejectUnauthorized:!0,perMessageDeflate:{threshold:1024},transportOptions:{}},n),e.opts.path=e.opts.path.replace(/\/$/,"")+"/","string"==typeof e.opts.query&&(e.opts.query=d.decode(e.opts.query)),e.id=null,e.upgrades=null,e.pingInterval=null,e.pingTimeout=null,e.pingTimeoutTimer=null,"function"==typeof addEventListener&&(addEventListener("beforeunload",(function(){e.transport&&(e.transport.removeAllListeners(),e.transport.close())}),!1),"localhost"!==e.hostname&&(e.offlineEventListener=function(){e.onClose("transport close")},addEventListener("offline",e.offlineEventListener,!1))),e.open(),e}return e=l,(n=[{key:"createTransport",value:function(t){var e=function(t){var e={};for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n]);return e}(this.opts.query);e.EIO=h.protocol,e.transport=t,this.id&&(e.sid=this.id);var n=r({},this.opts.transportOptions[t],this.opts,{query:e,socket:this,hostname:this.hostname,secure:this.secure,port:this.port});return new p[t](n)}},{key:"open",value:function(){var t;if(this.opts.rememberUpgrade&&l.priorWebsocketSuccess&&-1!==this.transports.indexOf("websocket"))t="websocket";else{if(0===this.transports.length){var e=this;return void setTimeout((function(){e.emit("error","No transports available")}),0)}t=this.transports[0]}this.readyState="opening";try{t=this.createTransport(t)}catch(t){return this.transports.shift(),void this.open()}t.open(),this.setTransport(t)}},{key:"setTransport",value:function(t){var e=this;this.transport&&this.transport.removeAllListeners(),this.transport=t,t.on("drain",(function(){e.onDrain()})).on("packet",(function(t){e.onPacket(t)})).on("error",(function(t){e.onError(t)})).on("close",(function(){e.onClose("transport close")}))}},{key:"probe",value:function(t){var e=this.createTransport(t,{probe:1}),n=!1,r=this;function o(){if(r.onlyBinaryUpgrades){var t=!this.supportsBinary&&r.transport.supportsBinary;n=n||t}n||(e.send([{type:"ping",data:"probe"}]),e.once("packet",(function(t){if(!n)if("pong"===t.type&&"probe"===t.data){if(r.upgrading=!0,r.emit("upgrading",e),!e)return;l.priorWebsocketSuccess="websocket"===e.name,r.transport.pause((function(){n||"closed"!==r.readyState&&(f(),r.setTransport(e),e.send([{type:"upgrade"}]),r.emit("upgrade",e),e=null,r.upgrading=!1,r.flush())}))}else{var o=new Error("probe error");o.transport=e.name,r.emit("upgradeError",o)}})))}function i(){n||(n=!0,f(),e.close(),e=null)}function s(t){var n=new Error("probe error: "+t);n.transport=e.name,i(),r.emit("upgradeError",n)}function c(){s("transport closed")}function a(){s("socket closed")}function u(t){e&&t.name!==e.name&&i()}function f(){e.removeListener("open",o),e.removeListener("error",s),e.removeListener("close",c),r.removeListener("close",a),r.removeListener("upgrading",u)}l.priorWebsocketSuccess=!1,e.once("open",o),e.once("error",s),e.once("close",c),this.once("close",a),this.once("upgrading",u),e.open()}},{key:"onOpen",value:function(){if(this.readyState="open",l.priorWebsocketSuccess="websocket"===this.transport.name,this.emit("open"),this.flush(),"open"===this.readyState&&this.opts.upgrade&&this.transport.pause)for(var t=0,e=this.upgrades.length;t0&&void 0!==arguments[0]?arguments[0]:{};return o(t,{xd:this.xd,xs:this.xs},this.opts),new w(this.uri(),t)}},{key:"doWrite",value:function(t,e){var n=this.request({method:"POST",data:t}),r=this;n.on("success",e),n.on("error",(function(t){r.onError("xhr post error",t)}))}},{key:"doPoll",value:function(){var t=this.request(),e=this;t.on("data",(function(t){e.onData(t)})),t.on("error",(function(t){e.onError("xhr poll error",t)})),this.pollXhr=t}}]),n}(y),w=function(t){a(n,t);var e=f(n);function n(t,r){var o;return i(this,n),(o=e.call(this)).opts=r,o.method=r.method||"GET",o.uri=t,o.async=!1!==r.async,o.data=void 0!==r.data?r.data:null,o.create(),o}return c(n,[{key:"create",value:function(){var t=v(this.opts,"agent","enablesXDR","pfx","key","passphrase","cert","ca","ciphers","rejectUnauthorized","autoUnref");t.xdomain=!!this.opts.xd,t.xscheme=!!this.opts.xs;var e=this.xhr=new h(t),r=this;try{e.open(this.method,this.uri,this.async);try{if(this.opts.extraHeaders)for(var o in e.setDisableHeaderCheck&&e.setDisableHeaderCheck(!0),this.opts.extraHeaders)this.opts.extraHeaders.hasOwnProperty(o)&&e.setRequestHeader(o,this.opts.extraHeaders[o])}catch(t){}if("POST"===this.method)try{e.setRequestHeader("Content-type","text/plain;charset=UTF-8")}catch(t){}try{e.setRequestHeader("Accept","*/*")}catch(t){}"withCredentials"in e&&(e.withCredentials=this.opts.withCredentials),this.opts.requestTimeout&&(e.timeout=this.opts.requestTimeout),this.hasXDR()?(e.onload=function(){r.onLoad()},e.onerror=function(){r.onError(e.responseText)}):e.onreadystatechange=function(){4===e.readyState&&(200===e.status||1223===e.status?r.onLoad():setTimeout((function(){r.onError("number"==typeof e.status?e.status:0)}),0))},e.send(this.data)}catch(t){return void setTimeout((function(){r.onError(t)}),0)}"undefined"!=typeof document&&(this.index=n.requestsCount++,n.requests[this.index]=this)}},{key:"onSuccess",value:function(){this.emit("success"),this.cleanup()}},{key:"onData",value:function(t){this.emit("data",t),this.onSuccess()}},{key:"onError",value:function(t){this.emit("error",t),this.cleanup(!0)}},{key:"cleanup",value:function(t){if(void 0!==this.xhr&&null!==this.xhr){if(this.hasXDR()?this.xhr.onload=this.xhr.onerror=m:this.xhr.onreadystatechange=m,t)try{this.xhr.abort()}catch(t){}"undefined"!=typeof document&&delete n.requests[this.index],this.xhr=null}}},{key:"onLoad",value:function(){var t=this.xhr.responseText;null!==t&&this.onData(t)}},{key:"hasXDR",value:function(){return"undefined"!=typeof XDomainRequest&&!this.xs&&this.enablesXDR}},{key:"abort",value:function(){this.cleanup()}}]),n}(d);if(w.requestsCount=0,w.requests={},"undefined"!=typeof document)if("function"==typeof attachEvent)attachEvent("onunload",_);else if("function"==typeof addEventListener){addEventListener("onpagehide"in b?"pagehide":"unload",_,!1)}function _(){for(var t in w.requests)w.requests.hasOwnProperty(t)&&w.requests[t].abort()}t.exports=k,t.exports.Request=w},function(t,e,n){var r=n(11).PACKET_TYPES,o="function"==typeof Blob||"undefined"!=typeof Blob&&"[object BlobConstructor]"===Object.prototype.toString.call(Blob),i="function"==typeof ArrayBuffer,s=function(t,e){var n=new FileReader;return n.onload=function(){var t=n.result.split(",")[1];e("b"+t)},n.readAsDataURL(t)};t.exports=function(t,e,n){var c,a=t.type,u=t.data;return o&&u instanceof Blob?e?n(u):s(u,n):i&&(u instanceof ArrayBuffer||(c=u,"function"==typeof ArrayBuffer.isView?ArrayBuffer.isView(c):c&&c.buffer instanceof ArrayBuffer))?e?n(u instanceof ArrayBuffer?u:u.buffer):s(new Blob([u]),n):n(r[a]+(u||""))}},function(t,e,n){var r,o=n(11),i=o.PACKET_TYPES_REVERSE,s=o.ERROR_PACKET;"function"==typeof ArrayBuffer&&(r=n(26));var c=function(t,e){if(r){var n=r.decode(t);return a(n,e)}return{base64:!0,data:t}},a=function(t,e){switch(e){case"blob":return t instanceof ArrayBuffer?new Blob([t]):t;case"arraybuffer":default:return t}};t.exports=function(t,e){if("string"!=typeof t)return{type:"message",data:a(t,e)};var n=t.charAt(0);return"b"===n?{type:"message",data:c(t.substring(1),e)}:i[n]?t.length>1?{type:i[n],data:t.substring(1)}:{type:i[n]}:s}},function(t,e){!function(t){"use strict";e.encode=function(e){var n,r=new Uint8Array(e),o=r.length,i="";for(n=0;n>2],i+=t[(3&r[n])<<4|r[n+1]>>4],i+=t[(15&r[n+1])<<2|r[n+2]>>6],i+=t[63&r[n+2]];return o%3==2?i=i.substring(0,i.length-1)+"=":o%3==1&&(i=i.substring(0,i.length-2)+"=="),i},e.decode=function(e){var n,r,o,i,s,c=.75*e.length,a=e.length,u=0;"="===e[e.length-1]&&(c--,"="===e[e.length-2]&&c--);var f=new ArrayBuffer(c),p=new Uint8Array(f);for(n=0;n>4,p[u++]=(15&o)<<4|i>>2,p[u++]=(3&i)<<6|63&s;return f}}("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")},function(t,e,n){function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n';n=document.createElement(t)}catch(t){(n=document.createElement("iframe")).name=r.iframeId,n.src="javascript:0"}n.id=r.iframeId,r.form.appendChild(n),r.iframe=n}this.form.action=this.uri(),a(),t=t.replace(d,"\\\n"),this.area.value=t.replace(y,"\\n");try{this.form.submit()}catch(t){}this.iframe.attachEvent?this.iframe.onreadystatechange=function(){"complete"===r.iframe.readyState&&c()}:this.iframe.onload=c}},{key:"supportsBinary",get:function(){return!1}}])&&o(e.prototype,n),r&&o(e,r),l}(l);t.exports=v},function(t,e,n){function r(t){return(r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t})(t)}function o(t,e){for(var n=0;n0&&t.jitter<=1?t.jitter:0,this.attempts=0}t.exports=n,n.prototype.duration=function(){var t=this.ms*Math.pow(this.factor,this.attempts++);if(this.jitter){var e=Math.random(),n=Math.floor(e*this.jitter*t);t=0==(1&Math.floor(10*e))?t-n:t+n}return 0|Math.min(t,this.max)},n.prototype.reset=function(){this.attempts=0},n.prototype.setMin=function(t){this.ms=t},n.prototype.setMax=function(t){this.max=t},n.prototype.setJitter=function(t){this.jitter=t}}])})); +//# sourceMappingURL=socket.io.min.js.map \ No newline at end of file diff --git a/web/templates/index.html b/web/templates/index.html new file mode 100644 index 00000000..eda8d51d --- /dev/null +++ b/web/templates/index.html @@ -0,0 +1,61 @@ + + + + + + + System Monitor + + + + +

Real-Time System Monitor

+
+
CPU: Loading...
+
RAM: Loading...
+
GPU: Loading...
+
VRAM: Loading...
+
HDD: Loading...
+
Temperature: Loading...
+
+ + + + diff --git a/web/templates/perf-monitor/perf-monitor.html b/web/templates/perf-monitor/perf-monitor.html index b9deed36..adc62801 100644 --- a/web/templates/perf-monitor/perf-monitor.html +++ b/web/templates/perf-monitor/perf-monitor.html @@ -1,75 +1,6 @@ -
-
-
-
- settings -
-
-
Settings
-
-
- -
-
-
Layout:
-
- 1 | - 2 -
-
-
-
Size:
-
- S | - M -
-
-
- -
-
Position
-
- - - - - - - - - -
-
-
-
- -
- close -
-
-
-
- -
-
-
-
-
+ + + + + +