1013 lines
56 KiB
JavaScript
1013 lines
56 KiB
JavaScript
/**
|
||
* BangtrixToolkit — Universal Hardware Monitor Overlay
|
||
* ComfyUI Extension
|
||
*
|
||
* Strategy: REST API polling via GET /btx/hw/stats.
|
||
* Fallback: WebSocket /ws/hw_monitor.
|
||
* Toggle: Ctrl+Shift+M
|
||
*/
|
||
|
||
(function() {
|
||
"use strict";
|
||
console.log("🖥️ Bangtrix HW Monitor: loading...");
|
||
|
||
// ================================================================
|
||
// T H E M E S (Dark + Light variants for each)
|
||
// ================================================================
|
||
const THEMES = {
|
||
"Default Green": {
|
||
dark: {
|
||
bg: "rgba(12,12,18,0.92)", accent: "#00e676", accentRgb: "0,230,118",
|
||
accentWarm: "#ffaa00", accentCrit: "#ff4444", gpuName: "#66aaff",
|
||
fillBar: "#00e676", tempGrad: "linear-gradient(90deg,#00e676,#ffaa00,#ff4444)",
|
||
sparklineLine: "#00e676", sparklineTop: "#00e67644", sparklineBot: "#00e67608",
|
||
liveColor: "#00e676", vendorColor: "#888", labelColor: "#888",
|
||
statValueColor: "#e0e0e0", headerBg: "rgba(0,230,118,0.08)",
|
||
glowColor: "0,230,118", neonIntensity: 0
|
||
},
|
||
light: {
|
||
bg: "rgba(240,240,248,0.92)", accent: "#008844", accentRgb: "0,136,68",
|
||
accentWarm: "#cc7700", accentCrit: "#cc2222", gpuName: "#2266cc",
|
||
fillBar: "#008844", tempGrad: "linear-gradient(90deg,#008844,#cc7700,#cc2222)",
|
||
sparklineLine: "#008844", sparklineTop: "#00884433", sparklineBot: "#00884408",
|
||
liveColor: "#008844", vendorColor: "#666", labelColor: "#666",
|
||
statValueColor: "#222", headerBg: "rgba(0,136,68,0.08)",
|
||
glowColor: "0,136,68", neonIntensity: 0
|
||
}
|
||
},
|
||
"Neon Blue": {
|
||
dark: {
|
||
bg: "rgba(8,12,28,0.92)", accent: "#00bfff", accentRgb: "0,191,255",
|
||
accentWarm: "#ff7700", accentCrit: "#ff3355", gpuName: "#66ddff",
|
||
fillBar: "#00bfff", tempGrad: "linear-gradient(90deg,#00bfff,#ff7700,#ff3355)",
|
||
sparklineLine: "#00bfff", sparklineTop: "#00bfff44", sparklineBot: "#00bfff08",
|
||
liveColor: "#00bfff", vendorColor: "#7799bb", labelColor: "#7799bb",
|
||
statValueColor: "#e0e0e0", headerBg: "rgba(0,180,255,0.08)",
|
||
glowColor: "0,191,255", neonIntensity: 1
|
||
},
|
||
light: {
|
||
bg: "rgba(235,242,250,0.92)", accent: "#0066aa", accentRgb: "0,102,170",
|
||
accentWarm: "#b85a00", accentCrit: "#bb2244", gpuName: "#3377cc",
|
||
fillBar: "#0066aa", tempGrad: "linear-gradient(90deg,#0066aa,#b85a00,#bb2244)",
|
||
sparklineLine: "#0066aa", sparklineTop: "#0066aa33", sparklineBot: "#0066aa08",
|
||
liveColor: "#0066aa", vendorColor: "#556677", labelColor: "#556677",
|
||
statValueColor: "#222", headerBg: "rgba(0,102,170,0.08)",
|
||
glowColor: "0,102,170", neonIntensity: 0
|
||
}
|
||
},
|
||
"Crimson Red": {
|
||
dark: {
|
||
bg: "rgba(20,8,8,0.92)", accent: "#ff4444", accentRgb: "255,68,68",
|
||
accentWarm: "#ff8844", accentCrit: "#cc0000", gpuName: "#ff6666",
|
||
fillBar: "#ff4444", tempGrad: "linear-gradient(90deg,#ff4444,#ff8844,#cc0000)",
|
||
sparklineLine: "#ff4444", sparklineTop: "#ff444444", sparklineBot: "#ff444408",
|
||
liveColor: "#ff4444", vendorColor: "#aa7777", labelColor: "#aa7777",
|
||
statValueColor: "#e0e0e0", headerBg: "rgba(255,68,68,0.08)",
|
||
glowColor: "255,68,68", neonIntensity: 1
|
||
},
|
||
light: {
|
||
bg: "rgba(252,238,238,0.92)", accent: "#bb2222", accentRgb: "187,34,34",
|
||
accentWarm: "#bb6622", accentCrit: "#990000", gpuName: "#cc4444",
|
||
fillBar: "#bb2222", tempGrad: "linear-gradient(90deg,#bb2222,#bb6622,#990000)",
|
||
sparklineLine: "#bb2222", sparklineTop: "#bb222233", sparklineBot: "#bb222208",
|
||
liveColor: "#bb2222", vendorColor: "#885555", labelColor: "#885555",
|
||
statValueColor: "#222", headerBg: "rgba(187,34,34,0.08)",
|
||
glowColor: "187,34,34", neonIntensity: 0
|
||
}
|
||
},
|
||
"Hacker Green": {
|
||
dark: {
|
||
bg: "rgba(0,0,0,0.92)", accent: "#00ff00", accentRgb: "0,255,0",
|
||
accentWarm: "#88ff00", accentCrit: "#ff0000", gpuName: "#00ff00",
|
||
fillBar: "#00ff00", tempGrad: "linear-gradient(90deg,#00ff00,#88ff00,#ff0000)",
|
||
sparklineLine: "#00ff00", sparklineTop: "#00ff0044", sparklineBot: "#00ff0008",
|
||
liveColor: "#00ff00", vendorColor: "#336633", labelColor: "#336633",
|
||
statValueColor: "#c0c0c0", headerBg: "rgba(0,255,0,0.05)",
|
||
glowColor: "0,255,0", neonIntensity: 1
|
||
},
|
||
light: {
|
||
bg: "rgba(240,248,240,0.92)", accent: "#007700", accentRgb: "0,119,0",
|
||
accentWarm: "#669900", accentCrit: "#cc0000", gpuName: "#005500",
|
||
fillBar: "#007700", tempGrad: "linear-gradient(90deg,#007700,#669900,#cc0000)",
|
||
sparklineLine: "#007700", sparklineTop: "#00770033", sparklineBot: "#00770008",
|
||
liveColor: "#007700", vendorColor: "#446644", labelColor: "#446644",
|
||
statValueColor: "#222", headerBg: "rgba(0,119,0,0.08)",
|
||
glowColor: "0,119,0", neonIntensity: 0
|
||
}
|
||
},
|
||
"Cyberpunk (Yellow/Cyan)": {
|
||
dark: {
|
||
bg: "rgba(10,0,20,0.95)", accent: "#ffdd00", accentRgb: "255,221,0",
|
||
accentWarm: "#ff8800", accentCrit: "#ff0055", gpuName: "#00ffcc",
|
||
fillBar: "#ffdd00", tempGrad: "linear-gradient(90deg,#00ffcc,#ffdd00,#ff0055)",
|
||
sparklineLine: "#ffdd00", sparklineTop: "#ffdd0044", sparklineBot: "#ffdd0008",
|
||
liveColor: "#00ffcc", vendorColor: "#ff88cc", labelColor: "#aa66cc",
|
||
statValueColor: "#f0e0ff", headerBg: "rgba(255,221,0,0.08)",
|
||
glowColor: "255,221,0", neonIntensity: 2
|
||
},
|
||
light: {
|
||
bg: "rgba(250,245,235,0.92)", accent: "#886600", accentRgb: "136,102,0",
|
||
accentWarm: "#aa5500", accentCrit: "#cc0044", gpuName: "#007766",
|
||
fillBar: "#886600", tempGrad: "linear-gradient(90deg,#007766,#886600,#cc0044)",
|
||
sparklineLine: "#886600", sparklineTop: "#88660033", sparklineBot: "#88660008",
|
||
liveColor: "#007766", vendorColor: "#886655", labelColor: "#886655",
|
||
statValueColor: "#222", headerBg: "rgba(136,102,0,0.08)",
|
||
glowColor: "136,102,0", neonIntensity: 0
|
||
}
|
||
},
|
||
"Synthwave (Pink/Purple)": {
|
||
dark: {
|
||
bg: "rgba(16,0,28,0.95)", accent: "#ff44cc", accentRgb: "255,68,204",
|
||
accentWarm: "#ffaa00", accentCrit: "#00ffaa", gpuName: "#aa66ff",
|
||
fillBar: "#ff44cc", tempGrad: "linear-gradient(90deg,#ff44cc,#ffaa00,#00ffaa)",
|
||
sparklineLine: "#ff44cc", sparklineTop: "#ff44cc44", sparklineBot: "#ff44cc08",
|
||
liveColor: "#ff44cc", vendorColor: "#aa88cc", labelColor: "#8866aa",
|
||
statValueColor: "#f0e0ff", headerBg: "rgba(255,68,204,0.08)",
|
||
glowColor: "255,68,204", neonIntensity: 2
|
||
},
|
||
light: {
|
||
bg: "rgba(248,240,248,0.92)", accent: "#993377", accentRgb: "153,51,119",
|
||
accentWarm: "#aa7700", accentCrit: "#007755", gpuName: "#7744bb",
|
||
fillBar: "#993377", tempGrad: "linear-gradient(90deg,#993377,#aa7700,#007755)",
|
||
sparklineLine: "#993377", sparklineTop: "#99337733", sparklineBot: "#99337708",
|
||
liveColor: "#993377", vendorColor: "#775566", labelColor: "#775566",
|
||
statValueColor: "#222", headerBg: "rgba(153,51,119,0.08)",
|
||
glowColor: "153,51,119", neonIntensity: 0
|
||
}
|
||
},
|
||
"Bangtrix Signature": {
|
||
dark: {
|
||
bg: "rgba(6,6,18,0.95)", accent: "#ff6600", accentRgb: "255,102,0",
|
||
accentWarm: "#ffcc00", accentCrit: "#00ccff", gpuName: "#ff8844",
|
||
fillBar: "#ff6600", tempGrad: "linear-gradient(90deg,#ff6600,#ffcc00,#00ccff)",
|
||
sparklineLine: "#ff6600", sparklineTop: "#ff660044", sparklineBot: "#ff660008",
|
||
liveColor: "#ff6600", vendorColor: "#ff8844", labelColor: "#ffaa44",
|
||
statValueColor: "#fff0e0", headerBg: "rgba(255,102,0,0.08)",
|
||
glowColor: "255,102,0", neonIntensity: 1
|
||
},
|
||
light: {
|
||
bg: "rgba(252,244,236,0.92)", accent: "#cc5500", accentRgb: "204,85,0",
|
||
accentWarm: "#aa8800", accentCrit: "#0088aa", gpuName: "#cc7733",
|
||
fillBar: "#cc5500", tempGrad: "linear-gradient(90deg,#cc5500,#aa8800,#0088aa)",
|
||
sparklineLine: "#cc5500", sparklineTop: "#cc550033", sparklineBot: "#cc550008",
|
||
liveColor: "#cc5500", vendorColor: "#aa6633", labelColor: "#aa6633",
|
||
statValueColor: "#222", headerBg: "rgba(204,85,0,0.08)",
|
||
glowColor: "204,85,0", neonIntensity: 0
|
||
}
|
||
},
|
||
"Custom": {
|
||
dark: {
|
||
bg: "rgba(12,12,18,0.92)", accent: "#00ff00", accentRgb: "0,255,0",
|
||
accentWarm: "#ffaa00", accentCrit: "#ff4444", gpuName: "#66aaff",
|
||
fillBar: "#00ff00", tempGrad: "linear-gradient(90deg,#00ff00,#ffaa00,#ff4444)",
|
||
sparklineLine: "#00ff00", sparklineTop: "#00ff0044", sparklineBot: "#00ff0008",
|
||
liveColor: "#00ff00", vendorColor: "#888", labelColor: "#888",
|
||
statValueColor: "#e0e0e0", headerBg: "rgba(0,255,0,0.08)",
|
||
glowColor: "0,255,0", neonIntensity: 1
|
||
},
|
||
light: {
|
||
bg: "rgba(240,240,248,0.92)", accent: "#008800", accentRgb: "0,136,0",
|
||
accentWarm: "#cc7700", accentCrit: "#cc2222", gpuName: "#2266cc",
|
||
fillBar: "#008800", tempGrad: "linear-gradient(90deg,#008800,#cc7700,#cc2222)",
|
||
sparklineLine: "#008800", sparklineTop: "#00880033", sparklineBot: "#00880008",
|
||
liveColor: "#008800", vendorColor: "#666", labelColor: "#666",
|
||
statValueColor: "#222", headerBg: "rgba(0,136,0,0.08)",
|
||
glowColor: "0,136,0", neonIntensity: 0
|
||
}
|
||
}
|
||
};
|
||
const THEME_NAMES = Object.keys(THEMES);
|
||
|
||
// ================================================================
|
||
// 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 Green", curBaseMode = "Dark", curRefreshMs = 500;
|
||
let curShowOnStartup = true, curBgOpacity = 0.92, curCompactMode = false, curGhostMode = false;
|
||
let curCustomAccent = "#00ff00", curCustomText = "#ffffff";
|
||
let curUiScale = 1.0;
|
||
|
||
// ================================================================
|
||
// T H E M E H E L P E R
|
||
// ================================================================
|
||
function _getTheme() {
|
||
const t = THEMES[curTheme] || THEMES["Default Green"];
|
||
const mode = curBaseMode === "Light" ? "light" : "dark";
|
||
let colors = t[mode];
|
||
// If Custom theme, override accent & color with user values
|
||
if (curTheme === "Custom") {
|
||
const mode2 = curBaseMode === "Light" ? "light" : "dark";
|
||
let base = THEMES["Default Green"][mode2];
|
||
// Parse custom accent to RGB
|
||
let acRgb = "0,255,0";
|
||
try {
|
||
const c = document.createElement('span');
|
||
c.style.color = curCustomAccent;
|
||
const m = c.style.color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)/);
|
||
if (m) acRgb = m[1] + "," + m[2] + "," + m[3];
|
||
} catch(e) {}
|
||
// Custom theme: ALWAYS use curCustomText for text colors — never force black.
|
||
// The user explicitly chose this color, respect it in both Light and Dark modes.
|
||
const txtColor = curCustomText;
|
||
const valColor = curCustomText;
|
||
colors = {
|
||
bg: base.bg, accent: curCustomAccent, accentRgb: acRgb,
|
||
accentWarm: base.accentWarm, accentCrit: base.accentCrit,
|
||
gpuName: txtColor, fillBar: curCustomAccent,
|
||
tempGrad: "linear-gradient(90deg," + curCustomAccent + "," + base.accentWarm + "," + base.accentCrit + ")",
|
||
sparklineLine: curCustomAccent, sparklineTop: "rgba(" + acRgb + ",0.27)",
|
||
sparklineBot: "rgba(" + acRgb + ",0.03)",
|
||
liveColor: txtColor, vendorColor: txtColor,
|
||
labelColor: txtColor, statValueColor: valColor,
|
||
headerBg: "rgba(" + acRgb + ",0.08)",
|
||
glowColor: acRgb, neonIntensity: 1
|
||
};
|
||
}
|
||
return colors;
|
||
}
|
||
|
||
// ================================================================
|
||
// U T I L S
|
||
// ================================================================
|
||
function $id(id) {
|
||
const el = document.getElementById(id);
|
||
if (!el) console.error("🖥️ DOM Element:", id);
|
||
return el;
|
||
}
|
||
// === COLOR VALIDATION ===
|
||
// ``curCustomAccent`` and ``curCustomText`` are concatenated into CSS
|
||
// template strings (``dynamicCss.textContent``). A malformed value such
|
||
// as ``"} body{background:red}/*`` would let a hostile localStorage
|
||
// payload escape the property and inject arbitrary CSS. Reject anything
|
||
// that isn't a strict 6-digit hex color so the value reaching the CSS
|
||
// is provably safe to embed.
|
||
const _HEX_COLOR_RE = /^#[0-9a-fA-F]{6}$/;
|
||
function _isValidColor(v) {
|
||
return typeof v === 'string' && _HEX_COLOR_RE.test(v);
|
||
}
|
||
function _safeColor(v, fallback) {
|
||
return _isValidColor(v) ? v : fallback;
|
||
}
|
||
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.BaseMode']) curBaseMode = s['Bangtrix.HWMonitor.BaseMode'];
|
||
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'];
|
||
if (s['Bangtrix.HWMonitor.GhostMode'] != null) curGhostMode = !!s['Bangtrix.HWMonitor.GhostMode'];
|
||
if (s['Bangtrix.HWMonitor.CustomAccent']) curCustomAccent = _safeColor(s['Bangtrix.HWMonitor.CustomAccent'], curCustomAccent);
|
||
if (s['Bangtrix.HWMonitor.CustomText']) curCustomText = _safeColor(s['Bangtrix.HWMonitor.CustomText'], curCustomText);
|
||
if (s['Bangtrix.HWMonitor.UiScale'] != null) curUiScale = Number(s['Bangtrix.HWMonitor.UiScale']) || 1.0;
|
||
} 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 = _getTheme();
|
||
const isTextDark = curBaseMode === "Light";
|
||
const textMain = isTextDark ? "#1a1a1a" : "#e0e0e0";
|
||
const headerBorderBottom = "rgba(255,255,255,0.04)";
|
||
const sparklineBg = isTextDark ? "rgba(0,0,0,0.08)" : "rgba(0,0,0,0.2)";
|
||
const barBg = isTextDark ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.08)";
|
||
const statusBorderTop = isTextDark ? "rgba(0,0,0,0.06)" : "rgba(255,255,255,0.04)";
|
||
const gpuNameBg = isTextDark ? "rgba(0,0,0,0.03)" : "rgba(255,255,255,0.03)";
|
||
|
||
let glowExtra = "";
|
||
if (t.neonIntensity >= 1 && !curGhostMode) {
|
||
// Subtle glow on accent elements
|
||
glowExtra =
|
||
'.hw-fill{box-shadow:0 0 8px rgba(' + t.glowColor + ',0.4)}' +
|
||
'.hw-stat-value{text-shadow:0 0 8px rgba(' + t.glowColor + ',0.15)}';
|
||
}
|
||
if (t.neonIntensity >= 2 && !curGhostMode) {
|
||
glowExtra +=
|
||
'.hw-gpu-name{text-shadow:0 0 12px rgba(' + t.glowColor + ',0.25)}' +
|
||
'.hw-title{text-shadow:0 0 10px rgba(' + t.glowColor + ',0.2)}';
|
||
}
|
||
|
||
const btnColor = isTextDark ? '#000' : '#ccc';
|
||
const btnClearColor = isTextDark ? '#000' : '#ccc';
|
||
const methodColor = isTextDark ? '#000' : '#ccc';
|
||
const liveColor = isTextDark ? '#000' : t.liveColor;
|
||
const iconColor = isTextDark ? '#000' : 'inherit';
|
||
|
||
dynamicCss.textContent =
|
||
'#bangtrix-hw-monitor{background:' + t.bg + ';' +
|
||
'border:1px solid rgba(' + t.accentRgb + ',0.25);' +
|
||
'backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);' +
|
||
'transition:all 0.3s ease;}' +
|
||
'#bangtrix-hw-monitor.hidden{display:none}' +
|
||
'#bangtrix-hw-monitor.minimized .hw-body{display:none}' +
|
||
'.hw-header{background:' + t.headerBg + ';border-bottom:1px solid ' + headerBorderBottom + ';transition:all 0.3s ease;}' +
|
||
'.hw-gpu-name{color:' + t.gpuName + ';background:' + gpuNameBg + ';transition:all 0.3s ease;}' +
|
||
'.hw-vendor-line{color:' + t.vendorColor + ';transition:color 0.3s ease;}' +
|
||
'.hw-stat-label{color:' + t.labelColor + ';transition:color 0.3s ease;}' +
|
||
'.hw-stat-value{color:' + t.statValueColor + ';transition:all 0.3s ease;}' +
|
||
'.hw-stat-value.warn{color:' + t.accentWarm + '}' +
|
||
'.hw-stat-value.crit{color:' + t.accentCrit + '}' +
|
||
'.hw-fill{background:' + t.fillBar + ';transition:all 0.3s ease;}' +
|
||
'.hw-fill.hw-temp-fill{background:' + t.tempGrad + '}' +
|
||
'.hw-bar{background:' + barBg + ';transition:background 0.3s ease;}' +
|
||
'#bangtrix-hw-monitor,.hw-header,.hw-body{transition:all 0.3s ease;}' +
|
||
'.hw-status.live{color:' + liveColor + '!important;transition:color 0.3s ease;}' +
|
||
'.hw-status.err{color:' + t.accentCrit + '}' +
|
||
'.hw-sparkline{background:' + sparklineBg + ';transition:background 0.3s ease;}' +
|
||
'.hw-status-bar{border-top:1px solid ' + statusBorderTop + ';transition:border-color 0.3s ease;}' +
|
||
'.hw-btn{color:' + btnColor + '!important;transition:all 0.2s ease;}' +
|
||
'.hw-btn:hover{color:' + btnColor + '!important;}' +
|
||
'.hw-btn-close:hover{color:' + btnColor + '!important;}' +
|
||
'.hw-btn-clear{color:' + btnClearColor + '!important;}' +
|
||
'.hw-btn-clear:hover{color:' + btnClearColor + '!important;}' +
|
||
'.hw-method{color:' + methodColor + '!important;}' +
|
||
'.hw-title{color:' + textMain + ';transition:color 0.3s ease;}' +
|
||
'.hw-icon{color:' + iconColor + '!important;transition:color 0.3s ease;}' +
|
||
glowExtra;
|
||
}
|
||
function _applyTheme() {
|
||
_updateDynamicCss();
|
||
_applyBgOpacity();
|
||
_applyGhostMode();
|
||
}
|
||
function _applyBgOpacity() {
|
||
if (!widget) return;
|
||
const t = _getTheme();
|
||
if (!curGhostMode) {
|
||
widget.style.background = curBaseMode === "Light"
|
||
? 'rgba(240,240,248,' + Number(curBgOpacity).toFixed(2) + ')'
|
||
: 'rgba(12,12,18,' + Number(curBgOpacity).toFixed(2) + ')';
|
||
widget.style.borderColor = 'rgba(' + t.accentRgb + ',0.25)';
|
||
}
|
||
}
|
||
function _applyCompactMode() {
|
||
if (!widget) return;
|
||
const sc = widget.querySelector('.hw-sparkline-container');
|
||
if (sc) sc.style.display = curCompactMode ? 'none' : '';
|
||
}
|
||
function _applyUiScale() {
|
||
if (!widget) return;
|
||
var s = Math.max(0.5, Math.min(2.0, curUiScale || 1.0));
|
||
widget.style.transform = 'scale(' + s + ')';
|
||
widget.style.transformOrigin = 'top right';
|
||
}
|
||
function _applyGhostMode() {
|
||
if (!widget) return;
|
||
if (curGhostMode) {
|
||
widget.style.background = 'transparent';
|
||
widget.style.border = 'none';
|
||
widget.style.boxShadow = 'none';
|
||
widget.style.backdropFilter = 'none';
|
||
widget.style.webkitBackdropFilter = 'none';
|
||
} else {
|
||
const t = _getTheme();
|
||
widget.style.background = curBaseMode === "Light"
|
||
? 'rgba(240,240,248,' + Number(curBgOpacity).toFixed(2) + ')'
|
||
: 'rgba(12,12,18,' + Number(curBgOpacity).toFixed(2) + ')';
|
||
widget.style.border = '1px solid rgba(' + t.accentRgb + ',0.25)';
|
||
widget.style.boxShadow = '0 8px 32px rgba(0,0,0,0.35), 0 2px 8px rgba(0,0,0,0.15)';
|
||
widget.style.backdropFilter = 'blur(10px)';
|
||
widget.style.webkitBackdropFilter = 'blur(10px)';
|
||
}
|
||
}
|
||
|
||
// ================================================================
|
||
// 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" id="hw-vram-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>' +
|
||
'<button class="hw-btn hw-btn-clear" id="hw-btn-clear" title="Free VRAM & RAM">🧹 Clear</button>' +
|
||
'</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;z-index:99999;box-shadow:0 8px 32px rgba(0,0,0,0.35),0 2px 8px rgba(0,0,0,0.15);backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);user-select:none;border-radius:12px;overflow:hidden}' +
|
||
'.hw-header{display:flex;align-items:center;padding:8px 12px;gap:8px;cursor:move;border-radius:12px 12px 0 0}' +
|
||
'.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}' +
|
||
'.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;transition:all 0.3s ease}' +
|
||
'.hw-gpu-name{text-align:center;font-size:11px;font-weight:600;padding:4px 8px;border-radius:4px;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:all 0.2s ease-in-out}' +
|
||
'.hw-bar{height:4px;border-radius:2px;overflow:hidden}' +
|
||
'.hw-fill{height:100%;width:0%;border-radius:2px}' +
|
||
'.hw-sparkline-container{margin-top:6px}' +
|
||
'.hw-sparkline{width:100%;height:36px;border-radius:4px}' +
|
||
'.hw-status-bar{display:flex;align-items:center;margin-top:6px;padding-top:6px;font-size:9px}' +
|
||
'.hw-method{flex:1;text-align:center;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;margin:0 4px}' +
|
||
'.hw-btn-clear{flex-shrink:0;width:auto;padding:0 6px;font-size:10px;background:rgba(255,255,255,0.06)}' +
|
||
'.hw-btn-clear:hover{background:rgba(0,255,100,0.2)!important;color:#0f0}' +
|
||
'.hw-btn-clear:disabled{background:rgba(255,255,255,0.04)}' +
|
||
'.hw-status.live{animation:livePulse 1.5s infinite}' +
|
||
'.hw-status.loading{color:#ffaa00;animation:loadingPulse 1s infinite}' +
|
||
'@keyframes loadingPulse{0%,100%{opacity:1}50%{opacity:0.4}}' +
|
||
'@keyframes livePulse{0%,100%{color:inherit}50%{opacity:0.5}}';
|
||
document.head.appendChild(baseCss);
|
||
document.body.appendChild(widget);
|
||
makeDraggable();
|
||
_applyUiScale();
|
||
}
|
||
|
||
// ================================================================
|
||
// 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();
|
||
_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; }
|
||
};
|
||
|
||
// --- Free Memory button ---
|
||
const clearBtn = document.getElementById('hw-btn-clear');
|
||
if (clearBtn) clearBtn.onclick = function() {
|
||
var btn = this;
|
||
btn.disabled = true;
|
||
btn.textContent = '⏳';
|
||
btn.style.opacity = '0.5';
|
||
btn.style.cursor = 'wait';
|
||
fetch('/btx/free_memory', { method: 'POST' })
|
||
.then(function(r) { return r.json(); })
|
||
.then(function(data) {
|
||
btn.textContent = data.status === 'success' ? '✅' : '❌';
|
||
btn.style.opacity = '1';
|
||
btn.style.cursor = '';
|
||
setTimeout(function() {
|
||
btn.textContent = '🧹 Clear';
|
||
btn.disabled = false;
|
||
}, 2000);
|
||
})
|
||
.catch(function() {
|
||
btn.textContent = '❌';
|
||
btn.style.opacity = '1';
|
||
btn.style.cursor = '';
|
||
setTimeout(function() {
|
||
btn.textContent = '🧹 Clear';
|
||
btn.disabled = false;
|
||
}, 2000);
|
||
});
|
||
};
|
||
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();
|
||
} else {
|
||
// Stop background work when the widget is hidden so we
|
||
// don't burn CPU/network on a widget the user can't see.
|
||
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
||
// Close any active WebSocket and cancel its retry loop.
|
||
if (ws) {
|
||
try { ws.close(); } catch (_) {}
|
||
ws = null;
|
||
}
|
||
wsRetries = MAX_WS_RETRIES; // stop retry timer
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// ================================================================
|
||
// S E T T I N G S P A N E L
|
||
// ================================================================
|
||
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>Base Mode</label><select id="hws-basemode">' +
|
||
'<option value="Dark">Dark</option><option value="Light">Light</option>' +
|
||
'</select></div>' +
|
||
'<div class="hws-row"><label>Theme</label><select id="hws-theme">' +
|
||
THEME_NAMES.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><option value="250">250ms</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 class="hws-row"><label>Ghost Mode</label><input type="checkbox" id="hws-ghost"></div>' +
|
||
'<div class="hws-row" id="hws-row-accent" style="display:none"><label>Custom Accent</label><input type="color" id="hws-custom-accent" value="#00ff00"></div>' +
|
||
'<div class="hws-row" id="hws-row-text" style="display:none"><label>Custom Text</label><input type="color" id="hws-custom-text" value="#ffffff"></div>' +
|
||
'<div class="hws-row"><label>UI Scale</label><input type="range" id="hws-scale" min="0.5" max="2.0" step="0.1"><span id="hws-scale-val" style="min-width:32px;text-align:right;color:#ddd;font-size:11px">1.0x</span></div>' +
|
||
'</div>';
|
||
const hwsCss = document.createElement('style');
|
||
hwsCss.textContent =
|
||
'#bangtrix-hw-settings{position:fixed;top:120px;right:30px;width:260px;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);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);}' +
|
||
'.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:5px 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:170px;}' +
|
||
'.hws-row input[type=range]{width:100px;}' +
|
||
'.hws-row input[type=color]{width:40px;height:22px;padding:1px;cursor:pointer}';
|
||
document.head.appendChild(hwsCss);
|
||
document.body.appendChild(settingsPanel);
|
||
_syncSettingsPanel();
|
||
// Show custom color rows only when Custom theme is selected
|
||
function _toggleCustomRows() {
|
||
var show = document.getElementById('hws-theme').value === 'Custom';
|
||
document.getElementById('hws-row-accent').style.display = show ? 'flex' : 'none';
|
||
document.getElementById('hws-row-text').style.display = show ? 'flex' : 'none';
|
||
}
|
||
document.getElementById('hws-close').onclick = function() { settingsPanel.style.display = 'none'; };
|
||
document.getElementById('hws-basemode').onchange = function() {
|
||
curBaseMode = this.value;
|
||
_saveSetting('Bangtrix.HWMonitor.BaseMode', curBaseMode);
|
||
_applyTheme();
|
||
_syncSettingsPanel();
|
||
};
|
||
document.getElementById('hws-theme').onchange = function() {
|
||
curTheme = this.value;
|
||
_saveSetting('Bangtrix.HWMonitor.Theme', curTheme);
|
||
_toggleCustomRows();
|
||
_applyTheme();
|
||
_syncSettingsPanel();
|
||
};
|
||
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();
|
||
};
|
||
document.getElementById('hws-ghost').onchange = function() {
|
||
curGhostMode = !!this.checked;
|
||
_saveSetting('Bangtrix.HWMonitor.GhostMode', curGhostMode);
|
||
_applyGhostMode();
|
||
_updateDynamicCss();
|
||
};
|
||
document.getElementById('hws-custom-accent').oninput = function() {
|
||
var v = _safeColor(this.value, curCustomAccent);
|
||
if (v !== curCustomAccent) this.value = v; // reject malformed
|
||
curCustomAccent = v;
|
||
_saveSetting('Bangtrix.HWMonitor.CustomAccent', curCustomAccent);
|
||
_applyTheme();
|
||
};
|
||
document.getElementById('hws-scale').oninput = function() {
|
||
curUiScale = Number(this.value);
|
||
_saveSetting('Bangtrix.HWMonitor.UiScale', curUiScale);
|
||
document.getElementById('hws-scale-val').textContent = curUiScale.toFixed(1) + 'x';
|
||
_applyUiScale();
|
||
};
|
||
document.getElementById('hws-custom-text').oninput = function() {
|
||
var v = _safeColor(this.value, curCustomText);
|
||
if (v !== curCustomText) this.value = v; // reject malformed
|
||
curCustomText = v;
|
||
_saveSetting('Bangtrix.HWMonitor.CustomText', curCustomText);
|
||
_applyTheme();
|
||
};
|
||
_toggleCustomRows();
|
||
}
|
||
function _syncSettingsPanel() {
|
||
if (!settingsPanel) return;
|
||
document.getElementById('hws-basemode').value = curBaseMode;
|
||
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;
|
||
document.getElementById('hws-ghost').checked = curGhostMode;
|
||
document.getElementById('hws-custom-accent').value = curCustomAccent;
|
||
document.getElementById('hws-custom-text').value = curCustomText;
|
||
document.getElementById('hws-scale').value = curUiScale;
|
||
document.getElementById('hws-scale-val').textContent = curUiScale.toFixed(1) + 'x';
|
||
}
|
||
|
||
// ================================================================
|
||
// 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('/btx/hw/stats')
|
||
.then(function(r) { if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); })
|
||
.then(function(data) {
|
||
try {
|
||
pollRetries = 0;
|
||
if (data && data.type === 'hw_stats') {
|
||
if (data.is_loading) {
|
||
setStatus("\u25A0 DETECTING", "loading");
|
||
setMethod("Initializing...");
|
||
updateDisplay(data);
|
||
} else if (data.is_available) {
|
||
setStatus("\u25CF LIVE", "live");
|
||
setMethod("REST " + (curRefreshMs / 1000) + "s");
|
||
updateDisplay(data);
|
||
} else {
|
||
setStatus(data.gpu_name || "Offline", "err");
|
||
setMethod(data.driver || "");
|
||
updateDisplay(data);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error("Bangtrix HW Monitor UI Error:", error);
|
||
}
|
||
})
|
||
.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() {
|
||
// Don't burn CPU/network reconnecting if the widget is hidden.
|
||
if (!isVisible) return;
|
||
if (ws && ws.readyState === WebSocket.OPEN) return;
|
||
const url = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/btx/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");
|
||
// Stop retrying if widget was hidden during the lifetime of
|
||
// this WS connection.
|
||
if (!isVisible || wsRetries >= MAX_WS_RETRIES) return;
|
||
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(' | ') || '-';
|
||
}
|
||
// CRITICAL: When is_loading is true, DO NOT enter the error branch.
|
||
// Just render placeholder values. The status/method is already
|
||
// set by fetchStats -> setStatus("■ DETECTING", "loading").
|
||
if (d.is_loading) {
|
||
setUtil('hw-gpu-util', '--', 0); setUtil('hw-vram', '--', 0);
|
||
setUtil('hw-temp', '--', 0); setUtil('hw-fan', '--', 0);
|
||
setBar('hw-gpu-bar', 0); setBar('hw-vram-bar', 0); setBar('hw-temp-bar', 0); setBar('hw-fan-bar', 0);
|
||
// Do NOT overwrite status — fetchStats already set it.
|
||
// Do NOT return — let the stat labels update so the overlay shows something.
|
||
// But skip the rest of the real stats rendering.
|
||
if (!curCompactMode && d.history && d.history.length > 0) drawSparkline(d.history);
|
||
return;
|
||
}
|
||
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);
|
||
// Only overwrite status if it wasn't already set by caller
|
||
// (caller already set status in fetchStats for unavail case)
|
||
setStatus(d.error || 'Unavailable', 'err');
|
||
return;
|
||
}
|
||
// Dynamically update VRAM label for APU vs dedicated GPU
|
||
var vramLabel = $id('hw-vram-label');
|
||
if (vramLabel) {
|
||
vramLabel.textContent = d.is_apu ? 'GPU MEM' : 'VRAM';
|
||
}
|
||
var util = Number(d.gpu_utilization) || 0;
|
||
setUtil('hw-gpu-util', util.toFixed(1) + '%', util);
|
||
setBar('hw-gpu-bar', util);
|
||
var vramUsed = Number(d.vram_used_mb) || 0;
|
||
var vramTotal = Number(d.vram_total_mb) || 1; // minimum 1 prevents div/0
|
||
var vramPct = Number(d.vram_usage_pct) || 0;
|
||
// Cap pct so bar never exceeds 100%
|
||
if (vramPct > 100) vramPct = 100;
|
||
setUtil('hw-vram', (vramUsed / 1024).toFixed(2) + ' / ' + (vramTotal / 1024).toFixed(1) + ' GB', vramPct);
|
||
setBar('hw-vram-bar', vramPct);
|
||
// OOM Warning: if VRAM >= 90%, color the bar and value red
|
||
var vramBar = $id('hw-vram-bar');
|
||
var vramVal = $id('hw-vram');
|
||
if (vramPct >= 90) {
|
||
if (vramBar) vramBar.style.background = '#ff4444';
|
||
if (vramVal) vramVal.style.color = '#ff4444';
|
||
} else {
|
||
if (vramBar) vramBar.style.background = '';
|
||
if (vramVal) vramVal.style.color = '';
|
||
}
|
||
// === Temperature ===
|
||
// null-aware: 0°C is a legitimate reading (cold boot, sub-zero bench),
|
||
// so distinguish "no sensor data" (null/undefined/-1) from "real zero".
|
||
var tempNum = (d.temperature === null || d.temperature === undefined || d.temperature === -1)
|
||
? NaN : Number(d.temperature);
|
||
if (!Number.isFinite(tempNum)) {
|
||
setUtil('hw-temp', 'N/A', 0);
|
||
setBar('hw-temp-bar', 0);
|
||
} else {
|
||
setUtil('hw-temp', tempNum.toFixed(1) + '\u00B0C', tempNum);
|
||
setBar('hw-temp-bar', Math.min(tempNum, 100));
|
||
}
|
||
// === Fan ===
|
||
// Same null-aware treatment. Always render the duty cycle, even when
|
||
// 0 — on RDNA3 (e.g. RX 7700/7800 XT) the driver can stop the fan
|
||
// entirely at idle (zero-RPM mode) while the target PWM is still
|
||
// meaningful. Showing "0%" is more honest than "N/A".
|
||
var fanNum = (d.fan_speed === null || d.fan_speed === undefined || d.fan_speed === -1)
|
||
? NaN : Number(d.fan_speed);
|
||
if (!Number.isFinite(fanNum)) {
|
||
setUtil('hw-fan', 'N/A', 0);
|
||
setBar('hw-fan-bar', 0);
|
||
} else {
|
||
fanNum = Math.max(0, Math.min(100, fanNum));
|
||
setUtil('hw-fan', fanNum + '%', fanNum);
|
||
setBar('hw-fan-bar', fanNum);
|
||
}
|
||
|
||
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' : cls === 'loading' ? '\u23F3' : '\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;
|
||
const t = _getTheme();
|
||
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
|
||
// ================================================================
|
||
function _registerComfyUISettings() {
|
||
if (!app || !app.ui || !app.ui.settings || !app.ui.settings.addSetting) {
|
||
setTimeout(_registerComfyUISettings, 200);
|
||
return;
|
||
}
|
||
try {
|
||
app.ui.settings.addSetting({
|
||
id: "Bangtrix.HWMonitor.BaseMode",
|
||
name: "\uD83C\uDF11 HW Monitor Base Mode",
|
||
type: "combo",
|
||
defaultValue: "Dark",
|
||
options: ["Dark", "Light"],
|
||
onChange: function(v) { curBaseMode = v; _saveSetting("Bangtrix.HWMonitor.BaseMode", v); _applyTheme(); _syncSettingsPanel(); }
|
||
});
|
||
app.ui.settings.addSetting({
|
||
id: "Bangtrix.HWMonitor.Theme",
|
||
name: "\uD83C\uDFA8 HW Monitor Theme",
|
||
type: "combo",
|
||
defaultValue: "Default Green",
|
||
options: THEME_NAMES,
|
||
onChange: function(v) { curTheme = v; _saveSetting("Bangtrix.HWMonitor.Theme", v); _applyTheme(); _syncSettingsPanel(); }
|
||
});
|
||
app.ui.settings.addSetting({
|
||
id: "Bangtrix.HWMonitor.RefreshRate",
|
||
name: "\u23F1\uFE0F HW Monitor Refresh Rate",
|
||
type: "combo",
|
||
defaultValue: 1000,
|
||
options: [250, 500, 1000, 2000],
|
||
onChange: function(v) { curRefreshMs = Number(v) || 1000; _saveSetting("Bangtrix.HWMonitor.RefreshRate", curRefreshMs); _syncSettingsPanel(); restartPolling(); }
|
||
});
|
||
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(); }
|
||
});
|
||
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(); }
|
||
});
|
||
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(); }
|
||
});
|
||
app.ui.settings.addSetting({
|
||
id: "Bangtrix.HWMonitor.GhostMode",
|
||
name: "\uD83D\uDC7B HW Monitor Ghost Mode (Borderless)",
|
||
type: "boolean",
|
||
defaultValue: false,
|
||
onChange: function(v) { curGhostMode = !!v; _saveSetting("Bangtrix.HWMonitor.GhostMode", curGhostMode); _syncSettingsPanel(); _applyGhostMode(); _updateDynamicCss(); }
|
||
});
|
||
app.ui.settings.addSetting({
|
||
id: "Bangtrix.HWMonitor.CustomAccent",
|
||
name: "\uD83C\uDFA8 Custom Accent Color (Custom theme only)",
|
||
type: "text",
|
||
defaultValue: "#00ff00",
|
||
onChange: function(v) { curCustomAccent = _safeColor(v, "#00ff00"); _saveSetting("Bangtrix.HWMonitor.CustomAccent", curCustomAccent); _syncSettingsPanel(); if (curTheme === 'Custom') _applyTheme(); }
|
||
});
|
||
app.ui.settings.addSetting({
|
||
id: "Bangtrix.HWMonitor.CustomText",
|
||
name: "\uD83D\uDD8C\uFE0F Custom Text Color (Custom theme only)",
|
||
type: "text",
|
||
defaultValue: "#ffffff",
|
||
onChange: function(v) { curCustomText = _safeColor(v, "#ffffff"); _saveSetting("Bangtrix.HWMonitor.CustomText", curCustomText); _syncSettingsPanel(); if (curTheme === 'Custom') _applyTheme(); }
|
||
});
|
||
console.log("\uD83D\uDDA5\uFE0F Bangtrix HW Monitor: 9 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(); _applyCompactMode(); _applyGhostMode();
|
||
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");
|
||
})(); |