Using websockets for resource monitor instead of rest api

This commit is contained in:
ChrisColeTech 2024-09-01 08:17:08 -04:00
parent ce74b6b733
commit eb934c9db1
15 changed files with 1373 additions and 431 deletions

View File

@ -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('/<int:job_id>')
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

View File

@ -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('/<string:key>')
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'}

View File

@ -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()

View File

@ -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('/<path:filename>')
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()

View File

@ -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 */
}

View File

@ -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)

View File

@ -11694,6 +11694,7 @@
);
});
//# sourceMappingURL=chart.umd.js.map
/*!
* chartjs-plugin-datalabels v2.2.0
* https://chartjs-plugin-datalabels.netlify.app

View File

@ -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)

View File

@ -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)

View File

@ -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 = `
<div id="chart-button-container" draggable="true">
<div id="chart-button">
<div class="chart-row">
<div class="left-col">
<i class="material-icons" id="popupTrigger">settings</i>
<div id="settingsMenu" class="settings-menu">
<div class="settings-row"><div class="settings-col">Settings</div></div>
<hr id="settings-hr" class="settings-hr" />
<div class="settings-row">
<div class="settings-row">
<div class="settings-col">Layout:</div>
<div class="settings-col">
<a href="#" onclick="barChart()">1</a> |
<a href="#" onclick="lineChart()">2</a>
</div>
</div>
<div class="settings-row">
<div class="settings-col">Size:</div>
<div class="settings-col">
<a href="#" onclick="smallChart()">S</a> |
<a href="#" onclick="mediumChart()">M</a>
</div>
</div>
</div>
<div class="settings-row">
<div class="settings-col">Position</div>
<div id="positionMenu" class="position-menu">
<button class="position-btn position-clickable" id="top-left"><i class="material-icons">north_west</i></button>
<button class="position-btn position-clickable" id="top-center"><i class="material-icons">north</i></button>
<button class="position-btn position-clickable" id="top-right"><i class="material-icons">north_east</i></button>
<button class="position-btn position-clickable" id="left-center"><i class="material-icons">west</i></button>
<button class="position-btn position-clickable" id="center" onclick="largeChart()"><i class="material-icons">radio_button_checked</i></button>
<button class="position-btn position-clickable" id="right-center"><i class="material-icons">east</i></button>
<button class="position-btn position-clickable" id="bottom-left"><i class="material-icons">south_west</i></button>
<button class="position-btn position-clickable" id="bottom-center"><i class="material-icons">south</i></button>
<button class="position-btn position-clickable" id="bottom-right"><i class="material-icons">south_east</i></button>
</div>
</div>
</div>
</div>
<div class="chart-col">
<i class="material-icons" id="close-button">close</i>
</div>
</div>
<div id="chart-wrapper">
<div id="progress-bar-container">
<div id="progress-bar"></div>
</div>
<div id="chart-container">
<canvas id="usage-chart" style="width: 100%; height: 100%"></canvas>
</div>
<div id="custom-legend"></div>
<div id="table-view">
<table id="item-table">
<tbody id="item-body">
<!-- Table rows will be dynamically added here -->
</tbody>
</table>
</div>
</div>
</div>
</div>
`
}
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)

View File

@ -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)
})

View File

@ -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)
})
}

7
web/assets/js/socket.io.min.js vendored Normal file

File diff suppressed because one or more lines are too long

61
web/templates/index.html Normal file
View File

@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>System Monitor</title>
<script src="https://cdn.socket.io/4.0.0/socket.io.min.js"></script>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
}
.data-container {
margin-top: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.data-item {
padding: 10px;
border: 1px solid #ccc;
border-radius: 5px;
background-color: #f9f9f9;
}
</style>
</head>
<body>
<h1>Real-Time System Monitor</h1>
<div class="data-container" id="data-container">
<div class="data-item" id="cpu">CPU: Loading...</div>
<div class="data-item" id="ram">RAM: Loading...</div>
<div class="data-item" id="gpu">GPU: Loading...</div>
<div class="data-item" id="vram">VRAM: Loading...</div>
<div class="data-item" id="hdd">HDD: Loading...</div>
<div class="data-item" id="temp">Temperature: Loading...</div>
</div>
<script>
const socket = io('http://localhost:5000')
// Listen for 'data_update' event and update the data on the page
socket.on('data_update', (data) => {
document.getElementById('cpu').textContent = `CPU: ${data.cpu.toFixed(2)}%`
document.getElementById('ram').textContent = `RAM: ${data.ram.toFixed(2)}%`
document.getElementById('gpu').textContent = `GPU: ${data.gpu.toFixed(2)}%`
document.getElementById('vram').textContent = `VRAM: ${data.vram.toFixed(2)}%`
document.getElementById('hdd').textContent = `HDD: ${data.hdd.toFixed(2)}%`
document.getElementById('temp').textContent = `Temperature: ${data.temp.toFixed(2)}°C`
})
socket.on('connect', () => {
console.log('Connected to the server')
})
socket.on('disconnect', () => {
console.log('Disconnected from the server')
})
</script>
</body>
</html>

View File

@ -1,75 +1,6 @@
<div id="chart-button-container" draggable="true" ondragstart="onDragStart(event);">
<div id="chart-button" class="">
<div class="chart-row">
<div class="left-col">
<i class="material-icons" id="popupTrigger">settings</i>
<div id="settingsMenu" class="settings-menu">
<div class="settings-row">
<div class="settings-col">Settings</div>
</div>
<hr id="settings-hr" class="settings-hr" />
<div class="settings-row">
<div class="settings-row">
<div class="settings-col">Layout:</div>
<div class="settings-col">
<a onclick="barChart()"> 1 </a> |
<a onclick="lineChart()"> 2 </a>
</div>
</div>
<div class="settings-row">
<div class="settings-col">Size:</div>
<div class="settings-col">
<a onclick="smallChart()"> S </a> |
<a onclick="mediumChart()"> M </a>
</div>
</div>
</div>
<div class="settings-row">
<div class="settings-col">Position</div>
<div id="positionMenu" class="position-menu">
<button class="position-btn position-clickable" id="top-left">
<i class="material-icons">north_west</i>
</button>
<button class="position-btn position-clickable" id="top-center">
<i class="material-icons">north</i>
</button>
<button class="position-btn position-clickable" id="top-right">
<i class="material-icons">north_east</i>
</button>
<button class="position-btn position-clickable" id="left-center">
<i class="material-icons">west</i>
</button>
<button class="position-btn" id="center" onclick="largeChart()">
<i class="material-icons">radio_button_checked</i>
</button>
<button class="position-btn position-clickable" id="right-center">
<i class="material-icons">east</i>
</button>
<button class="position-btn position-clickable" id="bottom-left">
<i class="material-icons">south_west</i>
</button>
<button class="position-btn position-clickable" id="bottom-center">
<i class="material-icons">south</i>
</button>
<button class="position-btn position-clickable" id="bottom-right">
<i class="material-icons">south_east</i>
</button>
</div>
</div>
</div>
</div>
<div class="chart-col">
<i class="material-icons" id="close-button">close</i>
</div>
</div>
<div id="chart-wrapper">
<div id="chart-container">
<canvas id="usage-chart" style="width: 100%; height: 100%"></canvas>
</div>
<div id="custom-legend"></div>
</div>
</div>
</div>
<!DOCTYPE html>
<html lang="en">
<body>
<elegant-resource-monitor></elegant-resource-monitor>
</body>
</html>