Files
Anonymzx-BangtrixToolkit/web/hw_monitor.js
T
2026-05-19 02:29:09 +07:00

533 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* BangtrixToolkit — Universal Hardware Monitor Overlay
* ComfyUI Extension
*
* Strategy: REST API polling via GET /bangtrix/hw/stats.
* Fallback: WebSocket /ws/hw_monitor.
* Toggle: Ctrl+Shift+M
*
* Settings (5 integrated via app.ui.settings.addSetting):
* 1. HW Monitor Theme — Combo (4 themes)
* 2. HW Monitor Refresh Rate — Combo (500/1000/2000 ms)
* 3. Show HW Monitor on Startup — Boolean
* 4. Background Opacity — Slider (0.1 .. 1.0)
* 5. Compact Mode — Boolean
*/
(function() {
"use strict";
console.log("🖥️ Bangtrix HW Monitor: loading...");
// ================================================================
// T H E M E S
// ================================================================
const THEMES = {
"Default (Dark Green)": {
accent: "#00e676", accentRgb: "0,230,118", accentWarm: "#ffaa00",
accentCrit: "#ff4444", gpuName: "#66aaff", fillBar: "#00e676",
tempGrad: "linear-gradient(90deg,#00e676,#ffaa00,#ff4444)",
sparklineLine: "#00e676", sparklineTop: "#00e67633", sparklineBot: "#00e67605",
liveColor: "#00e676", vendorColor: "#666", labelColor: "#666",
statValueColor: "#00e676", headerBg: "rgba(100,150,255,0.1)", headerBgHex: "rgba(100,150,255,0.1)"
},
"Neon Blue": {
accent: "#00bfff", accentRgb: "0,191,255", accentWarm: "#ff7700",
accentCrit: "#ff3355", gpuName: "#66ddff", fillBar: "#00bfff",
tempGrad: "linear-gradient(90deg,#00bfff,#ff7700,#ff3355)",
sparklineLine: "#00bfff", sparklineTop: "#00bfff33", sparklineBot: "#00bfff05",
liveColor: "#00bfff", vendorColor: "#6688aa", labelColor: "#6688aa",
statValueColor: "#00bfff", headerBg: "rgba(0,180,255,0.1)", headerBgHex: "rgba(0,180,255,0.1)"
},
"Crimson Red": {
accent: "#ff4444", accentRgb: "255,68,68", accentWarm: "#ff8844",
accentCrit: "#cc0000", gpuName: "#ff6666", fillBar: "#ff4444",
tempGrad: "linear-gradient(90deg,#ff4444,#ff8844,#cc0000)",
sparklineLine: "#ff4444", sparklineTop: "#ff444433", sparklineBot: "#ff444405",
liveColor: "#ff4444", vendorColor: "#996666", labelColor: "#996666",
statValueColor: "#ff4444", headerBg: "rgba(255,68,68,0.1)", headerBgHex: "rgba(255,68,68,0.1)"
},
"Hacker (Black & Bright Green)": {
accent: "#00ff00", accentRgb: "0,255,0", accentWarm: "#88ff00",
accentCrit: "#ff0000", gpuName: "#00ff00", fillBar: "#00ff00",
tempGrad: "linear-gradient(90deg,#00ff00,#88ff00,#ff0000)",
sparklineLine: "#00ff00", sparklineTop: "#00ff0033", sparklineBot: "#00ff0005",
liveColor: "#00ff00", vendorColor: "#338833", labelColor: "#338833",
statValueColor: "#00ff00", headerBg: "rgba(0,255,0,0.05)", headerBgHex: "rgba(0,255,0,0.05)"
}
};
// ================================================================
// S T A T E
// ================================================================
let widget = null, dynamicCss = null, isMinimized = false, isVisible = true;
let isDragging = false, dragStart = { x: 0, y: 0 };
let pollInterval = null, pollRetries = 0;
const MAX_RETRIES = 30;
let curTheme = "Default (Dark Green)", curRefreshMs = 1000;
let curShowOnStartup = true, curBgOpacity = 0.92, curCompactMode = false;
// ================================================================
// U T I L S
// ================================================================
function $id(id) {
const el = document.getElementById(id);
if (!el) console.error("🖥️ DOM Element:", id);
return el;
}
function _saveSetting(key, value) {
try {
const raw = localStorage.getItem('Comfy.Settings');
const s = raw ? JSON.parse(raw) : {};
s[key] = value;
localStorage.setItem('Comfy.Settings', JSON.stringify(s));
} catch(e) {}
}
function _readSettings() {
try {
const raw = localStorage.getItem('Comfy.Settings');
if (!raw) return;
const s = JSON.parse(raw);
if (s['Bangtrix.HWMonitor.Theme'] && THEMES[s['Bangtrix.HWMonitor.Theme']]) curTheme = s['Bangtrix.HWMonitor.Theme'];
if (s['Bangtrix.HWMonitor.RefreshRate'] != null) curRefreshMs = Number(s['Bangtrix.HWMonitor.RefreshRate']) || 1000;
if (s['Bangtrix.HWMonitor.ShowOnStartup'] != null) curShowOnStartup = !!s['Bangtrix.HWMonitor.ShowOnStartup'];
if (s['Bangtrix.HWMonitor.BgOpacity'] != null) curBgOpacity = Number(s['Bangtrix.HWMonitor.BgOpacity']) || 0.92;
if (s['Bangtrix.HWMonitor.CompactMode'] != null) curCompactMode = !!s['Bangtrix.HWMonitor.CompactMode'];
} catch(e) {}
}
// ================================================================
// D Y N A M I C C S S
// ================================================================
function buildDynamicCss() {
if (document.getElementById('bangtrix-hw-dynamic-css')) return;
dynamicCss = document.createElement('style');
dynamicCss.id = 'bangtrix-hw-dynamic-css';
document.head.appendChild(dynamicCss);
_updateDynamicCss();
}
function _updateDynamicCss() {
if (!dynamicCss) return;
const t = THEMES[curTheme] || THEMES["Default (Dark Green)"];
dynamicCss.textContent =
'#bangtrix-hw-monitor{background:rgba(18,18,24,' + Number(curBgOpacity).toFixed(2) + ');' +
'border:1px solid rgba(' + t.accentRgb + ',0.3);}' +
'#bangtrix-hw-monitor.hidden{display:none}' +
'#bangtrix-hw-monitor.minimized .hw-body{display:none}' +
'.hw-header{background:' + t.headerBgHex + ';}' +
'.hw-gpu-name{color:' + t.gpuName + '}' +
'.hw-vendor-line{color:' + t.vendorColor + '}' +
'.hw-stat-label{color:' + t.labelColor + '}' +
'.hw-stat-value{color:' + t.statValueColor + '}' +
'.hw-stat-value.warn{color:' + t.accentWarm + '}' +
'.hw-stat-value.crit{color:' + t.accentCrit + '}' +
'.hw-fill{background:' + t.fillBar + '}' +
'.hw-fill.hw-temp-fill{background:' + t.tempGrad + '}' +
'.hw-status.live{color:' + t.liveColor + '}' +
'.hw-status.err{color:' + t.accentCrit + '}';
}
function _applyTheme() { _updateDynamicCss(); }
function _applyBgOpacity() {
if (!widget) return;
const t = THEMES[curTheme] || THEMES["Default (Dark Green)"];
widget.style.background = 'rgba(18,18,24,' + Number(curBgOpacity).toFixed(2) + ')';
widget.style.borderColor = 'rgba(' + t.accentRgb + ',0.3)';
}
function _applyCompactMode() {
if (!widget) return;
const sc = widget.querySelector('.hw-sparkline-container');
if (sc) sc.style.display = curCompactMode ? 'none' : '';
}
// ================================================================
// W I D G E T
// ================================================================
function buildWidget() {
if (document.getElementById('bangtrix-hw-monitor')) return;
widget = document.createElement('div');
widget.id = 'bangtrix-hw-monitor';
widget.innerHTML =
'<div class="hw-header">' +
'<span class="hw-icon">🖥️</span>' +
'<span class="hw-title">HW Monitor</span>' +
'<div class="hw-controls">' +
'<button class="hw-btn hw-btn-settings" id="hw-btn-settings" title="Open Settings">⚙</button>' +
'<button class="hw-btn" id="hw-btn-min">−</button>' +
'<button class="hw-btn hw-btn-close" id="hw-btn-close">✕</button>' +
'</div>' +
'</div>' +
'<div class="hw-body">' +
'<div class="hw-gpu-name" id="hw-gpu-name">Detecting...</div>' +
'<div class="hw-vendor-line" id="hw-vendor-line"></div>' +
'<div class="hw-grid">' +
'<div class="hw-stat"><div class="hw-stat-label">GPU</div><div class="hw-stat-value" id="hw-gpu-util">--</div><div class="hw-bar"><div class="hw-fill" id="hw-gpu-bar"></div></div></div>' +
'<div class="hw-stat"><div class="hw-stat-label">VRAM</div><div class="hw-stat-value" id="hw-vram">--</div><div class="hw-bar"><div class="hw-fill" id="hw-vram-bar"></div></div></div>' +
'<div class="hw-stat"><div class="hw-stat-label">Temp</div><div class="hw-stat-value" id="hw-temp">--</div><div class="hw-bar"><div class="hw-fill hw-temp-fill" id="hw-temp-bar"></div></div></div>' +
'<div class="hw-stat"><div class="hw-stat-label">Fan</div><div class="hw-stat-value" id="hw-fan">--</div><div class="hw-bar"><div class="hw-fill" id="hw-fan-bar"></div></div></div>' +
'</div>' +
'<div class="hw-sparkline-container">' +
'<canvas class="hw-sparkline" id="hw-sparkline" width="228" height="36"></canvas>' +
'</div>' +
'<div class="hw-status-bar">' +
'<span class="hw-status" id="hw-status">Starting...</span>' +
'<span class="hw-method" id="hw-method"></span>' +
'</div>' +
'</div>';
const baseCss = document.createElement('style');
baseCss.id = 'bangtrix-hw-base-css';
baseCss.textContent =
'#bangtrix-hw-monitor{position:fixed;top:60px;right:20px;width:260px;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:11px;color:#e0e0e0;z-index:99999;box-shadow:0 8px 32px rgba(0,0,0,0.4);backdrop-filter:blur(8px);user-select:none;border-radius:10px;}' +
'.hw-header{display:flex;align-items:center;padding:8px 12px;gap:8px;cursor:move;border-radius:10px 10px 0 0;border-bottom:1px solid rgba(255,255,255,0.04)}' +
'.hw-icon{font-size:12px;animation:pulse 1.5s infinite}' +
'@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.4}}' +
'.hw-title{flex:1;font-weight:600;font-size:12px;color:#fff}' +
'.hw-controls{display:flex;gap:4px}' +
'.hw-btn{background:rgba(255,255,255,0.08);border:none;color:#ccc;width:20px;height:20px;border-radius:4px;cursor:pointer;font-size:11px;line-height:20px;text-align:center}' +
'.hw-btn:hover{background:rgba(255,255,255,0.2)}' +
'.hw-btn-close:hover{background:rgba(220,60,60,0.4)!important}' +
'.hw-body{padding:8px 12px 10px}' +
'.hw-gpu-name{text-align:center;font-size:11px;font-weight:600;padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.03);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.hw-vendor-line{text-align:center;font-size:9px;margin:2px 0 6px}' +
'.hw-grid{display:grid;grid-template-columns:1fr 1fr;gap:6px 10px}' +
'.hw-stat{display:flex;flex-direction:column;gap:1px}' +
'.hw-stat-label{font-size:9px;text-transform:uppercase}' +
'.hw-stat-value{font-weight:600;font-size:12px;transition:color 0.2s}' +
'.hw-bar{height:3px;background:rgba(255,255,255,0.08);border-radius:2px;overflow:hidden}' +
'.hw-fill{height:100%;width:0%;border-radius:2px;transition:width 0.3s ease}' +
'.hw-sparkline-container{margin-top:6px}' +
'.hw-sparkline{width:100%;height:36px;border-radius:4px;background:rgba(0,0,0,0.2)}' +
'.hw-status-bar{display:flex;justify-content:space-between;margin-top:6px;padding-top:6px;border-top:1px solid rgba(255,255,255,0.04);font-size:9px}' +
'.hw-status.live{animation:livePulse 1.5s infinite}' +
'@keyframes livePulse{0%,100%{color:inherit}50%{opacity:0.5}}';
document.head.appendChild(baseCss);
document.body.appendChild(widget);
makeDraggable();
}
// ================================================================
// D R A G
// ================================================================
function makeDraggable() {
const header = widget.querySelector('.hw-header');
if (!header) return;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.hw-btn')) return;
isDragging = true;
dragStart.x = e.clientX - widget.offsetLeft;
dragStart.y = e.clientY - widget.offsetTop;
widget.style.transition = 'none';
document.body.style.cursor = 'grabbing';
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
});
function onDrag(e) {
if (!isDragging) return;
widget.style.left = Math.max(10, Math.min(e.clientX - dragStart.x, innerWidth - widget.offsetWidth + 10)) + 'px';
widget.style.top = Math.max(10, Math.min(e.clientY - dragStart.y, innerHeight - widget.offsetHeight + 10)) + 'px';
widget.style.right = 'auto';
}
function stopDrag() {
isDragging = false;
widget.style.transition = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
}
}
// ================================================================
// E V E N T S
// ================================================================
function bindEvents() {
const minBtn = document.getElementById('hw-btn-min');
if (minBtn) minBtn.onclick = function() {
isMinimized = !isMinimized;
widget.classList.toggle('minimized', isMinimized);
this.textContent = isMinimized ? '+' : '\u2212';
};
const settingsBtn = document.getElementById('hw-btn-settings');
if (settingsBtn) settingsBtn.onclick = function(e) {
e.stopPropagation();
// Open built-in settings panel
_showSettingsPanel();
};
const closeBtn = document.getElementById('hw-btn-close');
if (closeBtn) closeBtn.onclick = function() {
isVisible = false;
widget.classList.add('hidden');
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
};
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
e.preventDefault();
isVisible = !isVisible;
widget.classList.toggle('hidden', !isVisible);
if (isVisible) startPolling();
}
});
}
// ================================================================
// S E T T I N G S P A N E L (built-in, self-contained)
// ================================================================
let settingsPanel = null;
function _showSettingsPanel() {
if (settingsPanel) {
settingsPanel.style.display = settingsPanel.style.display === 'none' ? 'block' : 'none';
return;
}
settingsPanel = document.createElement('div');
settingsPanel.id = 'bangtrix-hw-settings';
settingsPanel.innerHTML =
'<div class="hws-header">HW Monitor Settings <span class="hws-close" id="hws-close">\u2715</span></div>' +
'<div class="hws-body">' +
'<div class="hws-row"><label>Theme</label><select id="hws-theme">' +
Object.keys(THEMES).map(function(t) { return '<option value="' + t + '">' + t + '</option>'; }).join('') +
'</select></div>' +
'<div class="hws-row"><label>Refresh Rate</label><select id="hws-refresh">' +
'<option value="500">500ms</option><option value="1000">1s</option><option value="2000">2s</option>' +
'</select></div>' +
'<div class="hws-row"><label>Show on Startup</label><input type="checkbox" id="hws-startup"></div>' +
'<div class="hws-row"><label>Bg Opacity</label><input type="range" id="hws-opacity" min="0.1" max="1.0" step="0.05"></div>' +
'<div class="hws-row"><label>Compact Mode</label><input type="checkbox" id="hws-compact"></div>' +
'</div>';
const hwsCss = document.createElement('style');
hwsCss.textContent =
'#bangtrix-hw-settings{position:fixed;top:120px;right:30px;width:240px;background:rgba(18,18,24,0.96);border:1px solid rgba(255,255,255,0.15);border-radius:10px;z-index:100000;color:#ccc;font-size:11px;box-shadow:0 4px 20px rgba(0,0,0,0.5);}' +
'.hws-header{display:flex;justify-content:space-between;padding:8px 12px;font-weight:600;color:#fff;border-bottom:1px solid rgba(255,255,255,0.06);}' +
'.hws-close{cursor:pointer;color:#888;}' +
'.hws-close:hover{color:#f44;}' +
'.hws-body{padding:10px 12px;}' +
'.hws-row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;}' +
'.hws-row label{color:#999;}' +
'.hws-row select,.hws-row input{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.1);color:#ddd;border-radius:4px;padding:2px 6px;font-size:11px;}' +
'.hws-row select{min-width:160px;}' +
'.hws-row input[type=range]{width:100px;}';
document.head.appendChild(hwsCss);
document.body.appendChild(settingsPanel);
_syncSettingsPanel();
// Bind events
document.getElementById('hws-close').onclick = function() { settingsPanel.style.display = 'none'; };
document.getElementById('hws-theme').onchange = function() { curTheme = this.value; _saveSetting('Bangtrix.HWMonitor.Theme', curTheme); _applyTheme(); _applyBgOpacity(); };
document.getElementById('hws-refresh').onchange = function() { curRefreshMs = Number(this.value) || 1000; _saveSetting('Bangtrix.HWMonitor.RefreshRate', curRefreshMs); restartPolling(); };
document.getElementById('hws-startup').onchange = function() { curShowOnStartup = !!this.checked; _saveSetting('Bangtrix.HWMonitor.ShowOnStartup', curShowOnStartup); };
document.getElementById('hws-opacity').oninput = function() { curBgOpacity = Number(this.value); _saveSetting('Bangtrix.HWMonitor.BgOpacity', curBgOpacity); _applyBgOpacity(); };
document.getElementById('hws-compact').onchange = function() { curCompactMode = !!this.checked; _saveSetting('Bangtrix.HWMonitor.CompactMode', curCompactMode); _applyCompactMode(); };
}
function _syncSettingsPanel() {
if (!settingsPanel) return;
document.getElementById('hws-theme').value = curTheme;
document.getElementById('hws-refresh').value = curRefreshMs;
document.getElementById('hws-startup').checked = curShowOnStartup;
document.getElementById('hws-opacity').value = curBgOpacity;
document.getElementById('hws-compact').checked = curCompactMode;
}
// ================================================================
// P O L L I N G
// ================================================================
function restartPolling() {
if (!isVisible) return;
if (pollInterval) clearInterval(pollInterval);
pollInterval = null;
startPolling();
}
function startPolling() {
setStatus("Connecting...", "");
fetchStats();
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(fetchStats, curRefreshMs);
}
function fetchStats() {
fetch('/bangtrix/hw/stats')
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
.then(function(data) {
pollRetries = 0;
if (data && data.type === 'hw_stats') {
setStatus("\u25CF LIVE", "live");
setMethod("REST " + (curRefreshMs / 1000) + "s");
updateDisplay(data);
}
})
.catch(function(err) {
pollRetries++;
setStatus("Retry " + pollRetries, "err");
setMethod("");
if (pollRetries === 5) startWebSocket();
});
}
// ================================================================
// W E B S O C K E T F A L L B A C K
// ================================================================
let ws = null, wsRetries = 0, MAX_WS_RETRIES = 10;
function startWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) return;
const url = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws/hw_monitor';
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
ws = new WebSocket(url);
ws.onopen = function() { setStatus("\u25CF LIVE", "live"); setMethod("WS 1s"); wsRetries = 0; };
ws.onmessage = function(ev) { try { var d = JSON.parse(ev.data); if (d.type === 'hw_stats') updateDisplay(d); } catch(e) {} };
ws.onclose = function() { setStatus("WS Disc.", "err"); if (wsRetries < MAX_WS_RETRIES) { wsRetries++; setTimeout(startWebSocket, 2000); } };
}
// ================================================================
// D I S P L A Y U P D A T E
// ================================================================
function updateDisplay(d) {
var el;
el = $id('hw-gpu-name'); if (el) el.textContent = d.gpu_name || ('GPU ' + (d.gpu_id || 0));
el = $id('hw-vendor-line');
if (el) {
var parts = [];
if (d.vendor) parts.push(d.vendor.toUpperCase());
if (d.driver) parts.push(d.driver);
if (d.is_apu) parts.push('APU');
if (d.os_type) parts.push(d.os_type);
el.textContent = parts.join(' | ') || '-';
}
if (!d.is_available) {
setUtil('hw-gpu-util', d.error || '--', 0); setUtil('hw-vram', d.error || '--', 0);
setUtil('hw-temp', d.error || '--', 0); setUtil('hw-fan', d.error || '--', 0);
setBar('hw-gpu-bar', 0); setBar('hw-vram-bar', 0); setBar('hw-temp-bar', 0); setBar('hw-fan-bar', 0);
setStatus(d.error || 'Unavailable', 'err');
return;
}
var util = d.gpu_utilization || 0;
setUtil('hw-gpu-util', Number(util).toFixed(1) + '%', util);
setBar('hw-gpu-bar', util);
var vramUsed = d.vram_used_mb || 0, vramTotal = d.vram_total_mb || 0, vramPct = d.vram_usage_pct || 0;
if (vramTotal > 0) { setUtil('hw-vram', (vramUsed / 1024).toFixed(2) + ' / ' + (vramTotal / 1024).toFixed(1) + ' GB', vramPct); setBar('hw-vram-bar', vramPct); }
else { setUtil('hw-vram', 'N/A', 0); setBar('hw-vram-bar', 0); }
var temp = d.temperature || 0;
if (temp > 0) { setUtil('hw-temp', Number(temp).toFixed(1) + '\u00B0C', temp); setBar('hw-temp-bar', Math.min(temp, 100)); }
else { setUtil('hw-temp', 'N/A', 0); setBar('hw-temp-bar', 0); }
var fan = d.fan_speed || 0;
if (fan > 0) { setUtil('hw-fan', fan + '%', fan); setBar('hw-fan-bar', fan); }
else { setUtil('hw-fan', 'N/A', 0); setBar('hw-fan-bar', 0); }
var info = d.driver || 'unknown';
if (d.core_clock_mhz > 0) info += ' | ' + d.core_clock_mhz + 'MHz';
setMethod(info);
if (!curCompactMode && d.history && d.history.length > 0) drawSparkline(d.history);
}
function setUtil(id, text, pct) {
var el = $id(id); if (!el) return;
el.textContent = text; el.className = 'hw-stat-value';
if (pct > 85) el.classList.add('crit'); else if (pct > 70) el.classList.add('warn');
}
function setBar(id, pct) { var el = $id(id); if (el) el.style.width = Math.min(100, Math.max(0, pct || 0)) + '%'; }
function setStatus(text, cls) {
var el = $id('hw-status'); if (el) { el.textContent = text; el.className = 'hw-status ' + (cls || ''); }
if (widget) { var icon = widget.querySelector('.hw-icon'); if (icon) icon.textContent = cls === 'live' ? '\uD83D\uDFE2' : cls === 'err' ? '\uD83D\uDD34' : '\uD83D\uDFE1'; }
}
function setMethod(text) { var el = $id('hw-method'); if (el) el.textContent = text || ''; }
// ================================================================
// S P A R K L I N E
// ================================================================
function drawSparkline(values) {
var canvas = $id('hw-sparkline');
if (!canvas || values.length < 2) return;
var t = THEMES[curTheme] || THEMES["Default (Dark Green)"];
var ctx = canvas.getContext('2d'), w = canvas.width, h = canvas.height, pad = 3;
ctx.clearRect(0, 0, w, h);
var dataMax = Math.max.apply(null, values), max = dataMax > 80 ? 100 : (dataMax < 1 ? 100 : dataMax * 1.2);
var pw = w - pad * 2, ph = h - pad * 2;
ctx.beginPath(); ctx.moveTo(pad, h - pad);
for (var i = 0; i < values.length; i++) { var x = pad + (i / (values.length - 1)) * pw; var y = h - pad - (values[i] / max) * ph; ctx.lineTo(x, y); }
ctx.lineTo(pad + pw, h - pad); ctx.closePath();
var grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, t.sparklineTop); grad.addColorStop(1, t.sparklineBot);
ctx.fillStyle = grad; ctx.fill();
ctx.beginPath(); ctx.strokeStyle = t.sparklineLine; ctx.lineWidth = 1.5; ctx.lineJoin = 'round';
for (var i = 0; i < values.length; i++) { var x = pad + (i / (values.length - 1)) * pw; var y = h - pad - (values[i] / max) * ph; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }
ctx.stroke();
var lx = pad + pw, ly = h - pad - (values[values.length - 1] / max) * ph;
ctx.beginPath(); ctx.arc(lx, ly, 3, 0, Math.PI * 2); ctx.fillStyle = t.sparklineLine; ctx.fill();
}
// ================================================================
// C O M F Y U I S E T T I N G S R E G I S T R A T I O N
// ================================================================
// Also register with ComfyUI's native settings dialog so they appear
// under Settings -> BangtrixToolkit
function _registerComfyUISettings() {
if (!app || !app.ui || !app.ui.settings || !app.ui.settings.addSetting) {
setTimeout(_registerComfyUISettings, 200);
return;
}
try {
// 1. Theme
app.ui.settings.addSetting({
id: "Bangtrix.HWMonitor.Theme",
name: "\uD83C\uDFA8 HW Monitor Theme",
type: "combo",
defaultValue: "Default (Dark Green)",
options: ["Default (Dark Green)", "Neon Blue", "Crimson Red", "Hacker (Black & Bright Green)"],
onChange: function(v) { curTheme = v; _saveSetting("Bangtrix.HWMonitor.Theme", v); _applyTheme(); _applyBgOpacity(); _syncSettingsPanel(); }
});
// 2. Refresh Rate
app.ui.settings.addSetting({
id: "Bangtrix.HWMonitor.RefreshRate",
name: "\u23F1\uFE0F HW Monitor Refresh Rate",
type: "combo",
defaultValue: 1000,
options: [500, 1000, 2000],
onChange: function(v) { curRefreshMs = Number(v) || 1000; _saveSetting("Bangtrix.HWMonitor.RefreshRate", curRefreshMs); _syncSettingsPanel(); restartPolling(); }
});
// 3. Show on Startup
app.ui.settings.addSetting({
id: "Bangtrix.HWMonitor.ShowOnStartup",
name: "\uD83D\uDC41\uFE0F Show HW Monitor on Startup",
type: "boolean",
defaultValue: true,
onChange: function(v) { curShowOnStartup = !!v; _saveSetting("Bangtrix.HWMonitor.ShowOnStartup", curShowOnStartup); _syncSettingsPanel(); }
});
// 4. Background Opacity
app.ui.settings.addSetting({
id: "Bangtrix.HWMonitor.BgOpacity",
name: "\uD83D\uDD32 HW Monitor Background Opacity",
type: "slider",
defaultValue: 0.92,
attrs: { min: 0.1, max: 1.0, step: 0.05 },
onChange: function(v) { curBgOpacity = Number(v) || 0.92; _saveSetting("Bangtrix.HWMonitor.BgOpacity", curBgOpacity); _syncSettingsPanel(); _applyBgOpacity(); }
});
// 5. Compact Mode
app.ui.settings.addSetting({
id: "Bangtrix.HWMonitor.CompactMode",
name: "\uD83D\uDCE6 HW Monitor Compact Mode",
type: "boolean",
defaultValue: false,
onChange: function(v) { curCompactMode = !!v; _saveSetting("Bangtrix.HWMonitor.CompactMode", curCompactMode); _syncSettingsPanel(); _applyCompactMode(); }
});
console.log("\uD83D\uDDA5\uFE0F Bangtrix HW Monitor: 5 ComfyUI settings registered \u2705");
} catch(e) {
console.warn("\uD83D\uDDA5\uFE0F ComfyUI settings registration deferred:", e.message);
setTimeout(_registerComfyUISettings, 500);
}
}
// ================================================================
// I N I T
// ================================================================
function initWidget() {
if (document.getElementById('bangtrix-hw-monitor')) return;
_readSettings();
buildDynamicCss();
buildWidget();
bindEvents();
var checkEl = $id('hw-gpu-name'); if (checkEl) checkEl.textContent = "Loading...";
if (!curShowOnStartup) { isVisible = false; widget.classList.add('hidden'); }
_applyTheme(); _applyBgOpacity(); _applyCompactMode();
if (isVisible) startPolling();
console.log("\uD83D\uDDA5\uFE0F Bangtrix HW Monitor: widget initialized \u2705");
}
// ================================================================
// S T A R T
// ================================================================
initWidget();
_registerComfyUISettings();
console.log("\uD83D\uDDA5\uFE0F Bangtrix HW Monitor: loaded \u2705");
})();