Files
Anonymzx-BangtrixToolkit/web/amd_monitor.js
T
2026-05-16 05:41:56 +07:00

579 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 - AMD Monitor Overlay v3.2
* Real-time AMD GPU monitoring overlay for ComfyUI
* Features: GPU name, sparkline, generation stats, settings, multi-GPU
*/
(function () {
"use strict";
var CONFIG = {
storageKey: 'bangtrix_amd_monitor_config',
};
var ws = null;
var reconnectTimer = null;
var reconnectAttempts = 0;
var MAX_RECONNECT = 15;
var widget = null;
var isMinimized = false;
var isVisible = true;
var isDragging = false;
var dragOffset = { x: 0, y: 0 };
var gpuDataMap = {};
var selectedGpuId = 0;
var gpuCount = 1;
var historyData = [];
var savedConfig = {};
// ===== UTILITY =====
function $(id) { return document.getElementById(id); }
// ===== INIT =====
function init() {
console.log('[AMD Monitor] Initializing v3.2...');
loadConfig();
createWidget();
applyConfigToWidget();
setupEventListeners();
connectWebSocket();
}
// ===== WIDGET CREATION =====
function createWidget() {
widget = document.createElement('div');
widget.id = 'bangtrix-amd-monitor';
widget.className = 'amd-monitor-widget';
widget.innerHTML =
'<div class="amd-header">' +
'<div class="amd-title">' +
'<span class="amd-icon">🔴</span>' +
'<span class="amd-text">AMD Monitor</span>' +
'</div>' +
'<div class="amd-controls">' +
'<button class="amd-btn amd-btn-settings" title="Settings">⚙</button>' +
'<button class="amd-btn amd-btn-minimize" title="Minimize">−</button>' +
'<button class="amd-btn amd-btn-close" title="Hide">✕</button>' +
'</div>' +
'</div>' +
'<div class="amd-body">' +
'<div class="amd-gpu-selector" id="amd-gpu-selector" style="display:none;">' +
'<span class="amd-label-small">GPU:</span>' +
'<div class="amd-chip-group" id="amd-gpu-chips"></div>' +
'</div>' +
'<div class="amd-gpu-name" id="amd-gpu-name">AMD GPU</div>' +
'<div class="amd-grid">' +
'<div class="amd-stat"><span class="amd-label">GPU Load</span><span class="amd-value" id="amd-gpu-util">--</span><div class="amd-bar"><div class="amd-fill" id="amd-gpu-bar"></div></div></div>' +
'<div class="amd-stat"><span class="amd-label">VRAM</span><span class="amd-value" id="amd-vram">-- / -- GB</span><div class="amd-bar"><div class="amd-fill" id="amd-vram-bar"></div></div></div>' +
'<div class="amd-stat"><span class="amd-label">Temp</span><span class="amd-value" id="amd-temp">N/A</span><div class="amd-bar"><div class="amd-fill amd-temp" id="amd-temp-bar"></div></div></div>' +
'<div class="amd-stat"><span class="amd-label">Fan</span><span class="amd-value" id="amd-fan">N/A</span><div class="amd-bar"><div class="amd-fill" id="amd-fan-bar"></div></div></div>' +
'</div>' +
'<div class="amd-sparkline-container" id="amd-sparkline-container" style="display:none;">' +
'<div class="amd-label-small">GPU Load History</div>' +
'<canvas class="amd-sparkline" id="amd-sparkline" width="232" height="40"></canvas>' +
'</div>' +
'<div class="amd-process" id="amd-process">' +
'<div class="amd-process-header">' +
'<span class="amd-label-small">Generation Stats</span>' +
'<span class="amd-process-status" id="amd-process-status">● idle</span>' +
'</div>' +
'<div class="amd-process-body">' +
'<div class="amd-stat-row"><span class="amd-label-xs">Duration</span><span class="amd-value-sm" id="amd-gen-duration">--</span></div>' +
'<div class="amd-stat-row"><span class="amd-label-xs">RAM Peak</span><span class="amd-value-sm" id="amd-gen-ram-peak">--</span></div>' +
'<div class="amd-stat-row"><span class="amd-label-xs">RAM Delta</span><span class="amd-value-sm" id="amd-gen-ram-delta">--</span></div>' +
'<div class="amd-stat-row"><span class="amd-label-xs">CPU Peak</span><span class="amd-value-sm" id="amd-gen-cpu-peak">--</span></div>' +
'</div>' +
'</div>' +
'<div class="amd-alert" id="amd-alert" style="display:none;">' +
'<span class="amd-alert-icon">⚠️</span><span class="amd-alert-text" id="amd-alert-text">VRAM Alert</span>' +
'</div>' +
'<div class="amd-settings" id="amd-settings" style="display:none;">' +
'<div class="amd-settings-row"><span class="amd-label-small">Interval</span><select class="amd-select" id="amd-settings-interval"><option value="1">1s</option><option value="5" selected>5s</option><option value="10">10s</option><option value="30">30s</option></select></div>' +
'<div class="amd-settings-row"><span class="amd-label-small">VRAM Alert %</span><input type="number" class="amd-input" id="amd-settings-vram-threshold" value="90" min="50" max="100" /></div>' +
'<div class="amd-settings-row"><span class="amd-label-small">Temp Unit</span><select class="amd-select" id="amd-settings-temp-unit"><option value="celsius" selected>°C</option><option value="fahrenheit">°F</option></select></div>' +
'<div class="amd-settings-row"><span class="amd-label-small">Sparkline</span><label class="amd-toggle"><input type="checkbox" id="amd-settings-sparkline" checked /><span class="amd-toggle-slider"></span></label></div>' +
'</div>' +
'<div class="amd-footer"><span class="amd-status" id="amd-status">Initializing...</span><span class="amd-method" id="amd-method">--</span></div>' +
'</div>';
var style = document.createElement('style');
style.id = 'bangtrix-amd-monitor-styles';
style.textContent =
'#bangtrix-amd-monitor{position:fixed;top:60px;right:20px;width:260px;' +
'background:rgba(18,18,24,0.92);border:1px solid rgba(196,48,43,0.3);border-radius:10px;' +
'font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;font-size:11px;' +
'color:#e0e0e0;z-index:9999;box-shadow:0 8px 32px rgba(0,0,0,0.4);' +
'backdrop-filter:blur(8px);user-select:none;}' +
'#bangtrix-amd-monitor.hidden{display:none}' +
'#bangtrix-amd-monitor.minimized .amd-body{display:none}' +
'.amd-header{display:flex;justify-content:space-between;align-items:center;padding:10px 14px;' +
'background:linear-gradient(135deg,rgba(196,48,43,0.2)0%,rgba(139,32,29,0.1)100%);' +
'border-radius:10px 10px 0 0;cursor:move;border-bottom:1px solid rgba(255,255,255,0.05)}' +
'.amd-title{display:flex;align-items:center;gap:8px;font-weight:600;font-size:12px;color:#fff}' +
'.amd-icon{font-size:14px;animation:pulse 2s infinite}' +
'@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.6}}' +
'.amd-controls{display:flex;gap:4px}' +
'.amd-btn{background:rgba(255,255,255,0.1);border:none;color:#ccc;width:22px;height:22px;border-radius:4px;cursor:pointer;font-size:12px}' +
'.amd-btn:hover{background:rgba(255,255,255,0.2);color:#fff}' +
'.amd-btn-close:hover{background:rgba(255,80,80,0.3)}' +
'.amd-body{padding:12px 14px}' +
'.amd-gpu-name{text-align:center;font-size:11px;font-weight:600;color:#ff6b6b;margin-bottom:8px;' +
'padding:4px 8px;border-radius:4px;background:rgba(255,255,255,0.04);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}' +
'.amd-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px 12px}' +
'.amd-stat{display:flex;flex-direction:column;gap:2px}' +
'.amd-label{color:#888;font-size:10px;text-transform:uppercase}' +
'.amd-label-small{color:#888;font-size:10px}' +
'.amd-value{font-weight:600;font-size:13px;color:#00e676}' +
'.amd-value.na{color:#666}' +
'.amd-value.warning{color:#ffaa00}' +
'.amd-value.critical{color:#ff4444}' +
'.amd-bar{height:4px;background:rgba(255,255,255,0.1);border-radius:2px;overflow:hidden;margin-top:2px}' +
'.amd-fill{height:100%;border-radius:2px;transition:width 0.3s ease;width:0%;background:linear-gradient(90deg,#00e676,#00c853)}' +
'.amd-fill.amd-temp{background:linear-gradient(90deg,#00e676,#ffaa00,#ff4444)}' +
'.amd-sparkline-container{margin-top:8px}' +
'.amd-sparkline{width:100%;height:40px;border-radius:4px;background:rgba(0,0,0,0.3)}' +
'.amd-process{margin-top:8px;padding:6px 8px;background:rgba(0,0,0,0.2);border-radius:6px;border:1px solid rgba(255,255,255,0.06)}' +
'.amd-process-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:4px}' +
'.amd-process-status{font-size:10px}' +
'.amd-process-status.generating{color:#00e676}' +
'.amd-process-status.idle{color:#888}' +
'.amd-process-body{display:grid;grid-template-columns:1fr 1fr;gap:2px 8px}' +
'.amd-stat-row{display:flex;justify-content:space-between}' +
'.amd-label-xs{color:#666;font-size:9px}' +
'.amd-value-sm{color:#ccc;font-size:10px}' +
'.amd-alert{display:flex;align-items:center;gap:6px;padding:6px 10px;margin-top:8px;' +
'background:rgba(255,80,80,0.15);border:1px solid rgba(255,80,80,0.3);border-radius:6px;font-size:11px;color:#ff6b6b}' +
'.amd-settings{margin-top:8px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05)}' +
'.amd-settings-row{display:flex;justify-content:space-between;align-items:center;margin-bottom:6px}' +
'.amd-select,.amd-input{background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.15);color:#e0e0e0;padding:2px 6px;border-radius:4px;font-size:10px;width:70px}' +
'.amd-input{width:50px}' +
'.amd-toggle{position:relative;width:32px;height:16px;cursor:pointer}' +
'.amd-toggle input{display:none}' +
'.amd-toggle-slider{position:absolute;top:0;left:0;right:0;bottom:0;background:rgba(255,255,255,0.15);border-radius:8px;transition:0.2s}' +
'.amd-toggle-slider:before{content:"";position:absolute;width:12px;height:12px;left:2px;bottom:2px;background:#ccc;border-radius:50%;transition:0.2s}' +
'.amd-toggle input:checked+.amd-toggle-slider{background:rgba(196,48,43,0.6)}' +
'.amd-toggle input:checked+.amd-toggle-slider:before{transform:translateX(16px);background:#fff}' +
'.amd-footer{display:flex;justify-content:space-between;margin-top:10px;padding-top:8px;border-top:1px solid rgba(255,255,255,0.05);font-size:10px;color:#666}' +
'.amd-status{transition:color 0.2s}' +
'.amd-status.connected{color:#00e676}' +
'.amd-status.disconnected{color:#ff4444}' +
'.amd-status.warning{color:#ffaa00}' +
'.amd-method{color:#888;font-family:monospace;font-size:9px}';
document.head.appendChild(style);
document.body.appendChild(widget);
setupDrag();
}
// ===== DRAG =====
function setupDrag() {
var header = widget.querySelector('.amd-header');
if (!header) return;
header.addEventListener('mousedown', function(e) {
if (e.target.closest('.amd-btn')) return;
isDragging = true;
var rect = widget.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
widget.style.transition = 'none';
document.body.style.cursor = 'grabbing';
document.addEventListener('mousemove', onDrag);
document.addEventListener('mouseup', stopDrag);
});
function onDrag(e) {
if (!isDragging) return;
var maxX = window.innerWidth - widget.offsetWidth + 20;
var maxY = window.innerHeight - widget.offsetHeight + 20;
widget.style.left = Math.max(10, Math.min(e.clientX - dragOffset.x, maxX)) + 'px';
widget.style.top = Math.max(10, Math.min(e.clientY - dragOffset.y, maxY)) + 'px';
widget.style.right = 'auto';
}
function stopDrag() {
isDragging = false;
widget.style.transition = '';
document.body.style.cursor = '';
document.removeEventListener('mousemove', onDrag);
document.removeEventListener('mouseup', stopDrag);
saveConfig();
}
}
// ===== EVENT LISTENERS =====
function setupEventListeners() {
var minBtn = widget.querySelector('.amd-btn-minimize');
if (minBtn) minBtn.addEventListener('click', function() {
isMinimized = !isMinimized;
widget.classList.toggle('minimized', isMinimized);
minBtn.textContent = isMinimized ? '+' : '−';
saveConfig();
});
var closeBtn = widget.querySelector('.amd-btn-close');
if (closeBtn) closeBtn.addEventListener('click', function() {
isVisible = false;
widget.classList.add('hidden');
saveConfig();
showToast('Hidden. Ctrl+Shift+M to show.');
});
var settingsBtn = widget.querySelector('.amd-btn-settings');
var settingsPanel = $('amd-settings');
if (settingsBtn && settingsPanel) settingsBtn.addEventListener('click', function() {
var isOpen = settingsPanel.style.display !== 'none';
settingsPanel.style.display = isOpen ? 'none' : 'block';
settingsBtn.classList.toggle('active', !isOpen);
});
var intervalSelect = $('amd-settings-interval');
if (intervalSelect) intervalSelect.addEventListener('change', function(e) {
sendCommand({type:'set_interval',interval:parseFloat(e.target.value)});
saveConfig();
});
var vramInput = $('amd-settings-vram-threshold');
if (vramInput) vramInput.addEventListener('change', function(e) {
savedConfig.vramThreshold = parseInt(e.target.value) || 90;
saveConfig();
});
var tempSelect = $('amd-settings-temp-unit');
if (tempSelect) tempSelect.addEventListener('change', function(e) {
savedConfig.tempUnit = e.target.value;
saveConfig();
if (gpuDataMap[selectedGpuId]) refreshDisplay(selectedGpuId);
});
var sparkCheck = $('amd-settings-sparkline');
if (sparkCheck) sparkCheck.addEventListener('change', function(e) {
var container = $('amd-sparkline-container');
if (container) container.style.display = e.target.checked ? 'block' : 'none';
saveConfig();
});
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.shiftKey && e.key === 'M') {
e.preventDefault();
isVisible = !isVisible;
widget.classList.toggle('hidden', !isVisible);
if (isVisible) connectWebSocket();
saveConfig();
}
});
}
// ===== CONFIG =====
function loadConfig() {
try { var saved = localStorage.getItem(CONFIG.storageKey); if (saved) savedConfig = JSON.parse(saved); } catch(e) {}
}
function applyConfigToWidget() {
if (!widget) return;
var c = savedConfig;
if (c.position) {
if (c.position.left) { widget.style.left = c.position.left; widget.style.right = 'auto'; }
if (c.position.top) widget.style.top = c.position.top;
}
if (c.minimized !== undefined) {
isMinimized = c.minimized;
widget.classList.toggle('minimized', isMinimized);
var btn = widget.querySelector('.amd-btn-minimize');
if (btn) btn.textContent = isMinimized ? '+' : '−';
}
if (c.visible !== undefined) { isVisible = c.visible; widget.classList.toggle('hidden', !isVisible); }
var sel = $('amd-settings-interval'); if (sel) sel.value = String(c.updateInterval || 5);
var inp = $('amd-settings-vram-threshold'); if (inp) inp.value = c.vramThreshold || 90;
var sel2 = $('amd-settings-temp-unit'); if (sel2) sel2.value = c.tempUnit || 'celsius';
var chk = $('amd-settings-sparkline');
if (chk) { chk.checked = c.showSparkline !== false; var cont = $('amd-sparkline-container'); if (cont) cont.style.display = chk.checked ? 'block' : 'none'; }
}
function saveConfig() {
try {
var config = {
position: { top: widget.style.top, left: widget.style.left },
minimized: isMinimized, visible: isVisible,
updateInterval: parseFloat($('amd-settings-interval')?.value) || 5,
vramThreshold: parseInt($('amd-settings-vram-threshold')?.value) || 90,
tempUnit: $('amd-settings-temp-unit')?.value || 'celsius',
showSparkline: $('amd-settings-sparkline')?.checked !== false
};
localStorage.setItem(CONFIG.storageKey, JSON.stringify(config));
} catch(e) {}
}
// ===== WEBSOCKET =====
function connectWebSocket() {
if (ws && ws.readyState === WebSocket.OPEN) return;
if (!isVisible) return;
var protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
var url = protocol + '//' + window.location.host + '/ws/amd_monitor';
console.log('[AMD Monitor] Connecting...');
updateStatus('Connecting...', 'warning');
ws = new WebSocket(url);
ws.onopen = function() {
console.log('[AMD Monitor] Connected');
updateStatus('Connected', 'connected');
updateMethod('Live');
reconnectAttempts = 0;
if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
};
ws.onmessage = function(event) {
try { var data = JSON.parse(event.data); if (data.type === 'amd_stats') handleStatsData(data); } catch(e) {}
};
ws.onclose = function() {
updateStatus('Disconnected', 'disconnected');
updateMethod('--');
if (reconnectAttempts < MAX_RECONNECT) {
reconnectAttempts++;
var delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000);
reconnectTimer = setTimeout(connectWebSocket, delay);
}
};
ws.onerror = function() {};
}
function sendCommand(cmd) { if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(cmd)); }
// ===== DATA HANDLING =====
function handleStatsData(data) {
gpuDataMap[data.gpu_id] = data;
if (data.gpu_count && gpuCount !== data.gpu_count) { gpuCount = data.gpu_count; updateGpuSelector(); }
if (data.gpu_id === selectedGpuId) refreshDisplay(selectedGpuId);
}
function refreshDisplay(gpuId) {
var data = gpuDataMap[gpuId];
if (!data) return;
var nameEl = $('amd-gpu-name');
if (nameEl) nameEl.textContent = data.gpu_name || ('AMD GPU ' + gpuId);
if (!data.is_available) {
['amd-gpu-util','amd-vram','amd-temp','amd-fan'].forEach(function(id) { setStat(id, 'N/A'); });
['amd-gpu-bar','amd-vram-bar','amd-temp-bar','amd-fan-bar'].forEach(function(id) { setBar(id, 0); });
updateStatus(data.error || 'Unavailable', 'warning');
updateMethod(data.method || '--');
return;
}
// GPU Load
var gpuUtil = data.gpu_utilization !== undefined ? data.gpu_utilization : 0;
setStat('amd-gpu-util', gpuUtil.toFixed(1) + '%', gpuUtil);
setBar('amd-gpu-bar', gpuUtil);
// VRAM
var vramUsedMB = data.vram_used_mb || 0;
var vramTotalMB = data.vram_total_mb || 0;
var vramPct = data.vram_usage_pct || (vramTotalMB > 0 ? (vramUsedMB / vramTotalMB * 100) : 0);
if (vramTotalMB > 0) {
setStat('amd-vram', (vramUsedMB / 1024).toFixed(1) + ' / ' + (vramTotalMB / 1024).toFixed(1) + ' GB', vramPct);
setBar('amd-vram-bar', vramPct);
} else { setStat('amd-vram', 'N/A'); setBar('amd-vram-bar', 0); }
// Temperature
var temp = data.temperature || 0;
if (temp > 0) {
var tUnit = savedConfig.tempUnit || 'celsius';
var tempDisplay = temp;
var tempUnitStr = '°C';
if (tUnit === 'fahrenheit') { tempDisplay = temp * 9/5 + 32; tempUnitStr = '°F'; }
setStat('amd-temp', tempDisplay.toFixed(1) + tempUnitStr, temp);
setBar('amd-temp-bar', Math.min(temp, 100));
} else { setStat('amd-temp', 'N/A'); setBar('amd-temp-bar', 0); }
// Fan
var fan = data.fan_speed || 0;
if (fan > 0) { setStat('amd-fan', fan + '%', fan); setBar('amd-fan-bar', fan); }
else { setStat('amd-fan', 'N/A'); setBar('amd-fan-bar', 0); }
// Status
var methodInfo = data.method || 'unknown';
if (data.core_clock_mhz > 0) methodInfo += ' | ' + data.core_clock_mhz + 'MHz';
if (data.power_draw_watts > 0) methodInfo += ' | ' + data.power_draw_watts + 'W';
updateMethod(methodInfo);
var statusText = data.gpu_name || ('GPU ' + gpuId);
if (gpuCount > 1) statusText += ' (' + (gpuId + 1) + '/' + gpuCount + ')';
updateStatus(statusText, 'connected');
// Sparkline
if (data.history && data.history.length > 0) {
historyData = data.history;
drawSparkline(historyData);
var sparkContainer = $('amd-sparkline-container');
if (sparkContainer) {
var chk = $('amd-settings-sparkline');
sparkContainer.style.display = (chk && chk.checked) ? 'block' : 'none';
}
}
// Process stats
updateProcessDisplay(data);
checkVramAlert(vramPct);
}
function updateProcessDisplay(data) {
var process = data.process;
if (!process) return;
var statusEl = $('amd-process-status');
var durEl = $('amd-gen-duration');
var ramPeakEl = $('amd-gen-ram-peak');
var ramDeltaEl = $('amd-gen-ram-delta');
var cpuPeakEl = $('amd-gen-cpu-peak');
if (!statusEl) return;
if (process.is_generating && process.generation) {
var gen = process.generation;
statusEl.textContent = '● generating';
statusEl.className = 'amd-process-status generating';
if (durEl) durEl.textContent = gen.duration + 's';
if (ramPeakEl) ramPeakEl.textContent = gen.ram_peak_mb + ' MB';
if (ramDeltaEl) ramDeltaEl.textContent = '+' + (gen.ram_peak_mb - gen.ram_start_mb) + ' MB';
if (cpuPeakEl) cpuPeakEl.textContent = gen.cpu_peak + '%';
} else if (process.last_generation) {
var last = process.last_generation;
statusEl.textContent = '● idle (' + (process.generation_count || 0) + ' gens)';
statusEl.className = 'amd-process-status idle';
if (durEl) durEl.textContent = last.duration + 's';
if (ramPeakEl) ramPeakEl.textContent = last.ram_peak_mb + ' MB';
if (ramDeltaEl) ramDeltaEl.textContent = '+' + last.vram_delta_mb + ' MB';
if (cpuPeakEl) cpuPeakEl.textContent = last.cpu_peak + '%';
} else {
statusEl.textContent = '● idle';
statusEl.className = 'amd-process-status idle';
if (durEl) durEl.textContent = '--'; if (ramPeakEl) ramPeakEl.textContent = '--';
if (ramDeltaEl) ramDeltaEl.textContent = '--'; if (cpuPeakEl) cpuPeakEl.textContent = '--';
}
}
function updateGpuSelector() {
var selector = $('amd-gpu-selector');
var chips = $('amd-gpu-chips');
if (!selector || !chips) return;
if (gpuCount <= 1) { selector.style.display = 'none'; return; }
selector.style.display = 'block';
chips.innerHTML = '';
for (var i = 0; i < gpuCount; i++) (function(idx) {
var chip = document.createElement('span');
chip.className = 'amd-chip' + (idx === selectedGpuId ? ' active' : '');
chip.textContent = 'GPU ' + idx;
chip.addEventListener('click', function() {
selectedGpuId = idx;
chips.querySelectorAll('.amd-chip').forEach(function(c) { c.classList.remove('active'); });
chip.classList.add('active');
refreshDisplay(selectedGpuId);
});
chips.appendChild(chip);
})(i);
}
// ===== SPARKLINE =====
function drawSparkline(values) {
var canvas = $('amd-sparkline');
if (!canvas) return;
var ctx = canvas.getContext('2d');
var w = canvas.width, h = canvas.height, padding = 2;
ctx.clearRect(0, 0, w, h);
if (values.length < 2 || values.every(function(v) { return v === 0; })) {
ctx.fillStyle = '#555'; ctx.font = '10px monospace'; ctx.textAlign = 'center';
ctx.fillText('awaiting data...', w / 2, h / 2 + 3); return;
}
var max = Math.max.apply(null, values); if (max < 1) max = 1;
var stepX = (w - padding * 2) / (values.length - 1);
ctx.beginPath(); ctx.strokeStyle = '#00e676'; ctx.lineWidth = 1.5; ctx.lineJoin = 'round';
for (var i = 0; i < values.length; i++) {
var x = padding + i * stepX;
var y = h - padding - (values[i] / max) * (h - padding * 2);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
ctx.lineTo(padding + (values.length - 1) * stepX, h - padding);
ctx.lineTo(padding, h - padding); ctx.closePath();
ctx.fillStyle = 'rgba(0,230,118,0.1)'; ctx.fill();
var lastVal = values[values.length - 1];
ctx.fillStyle = '#00e676'; ctx.font = 'bold 9px monospace'; ctx.textAlign = 'right';
ctx.fillText(lastVal.toFixed(0) + '%', w - padding, 10);
}
// ===== ALERTS =====
var lastAlertTime = 0;
function checkVramAlert(vramPct) {
var threshold = savedConfig.vramThreshold || 90;
if (vramPct >= threshold && Date.now() - lastAlertTime > 10000) {
lastAlertTime = Date.now();
var alertEl = $('amd-alert');
var alertText = $('amd-alert-text');
if (alertEl && alertText) {
alertText.textContent = 'VRAM ' + vramPct.toFixed(0) + '% >= ' + threshold + '%';
alertEl.style.display = 'flex';
setTimeout(function() { if (alertEl) alertEl.style.display = 'none'; }, 5000);
}
}
}
// ===== UI HELPERS =====
function setStat(elementId, value, numericValue) {
var el = $(elementId);
if (!el) return;
el.textContent = value;
el.classList.remove('warning', 'critical', 'na');
if (value === 'N/A') { el.classList.add('na'); }
else if (numericValue !== null && numericValue !== undefined) {
if ((elementId === 'amd-temp') && numericValue > 80) el.classList.add('critical');
else if ((elementId === 'amd-temp') && numericValue > 65) el.classList.add('warning');
else if ((elementId === 'amd-gpu-util' || elementId.indexOf('vram') >= 0) && numericValue > 90) el.classList.add('critical');
else if ((elementId === 'amd-gpu-util' || elementId.indexOf('vram') >= 0) && numericValue > 75) el.classList.add('warning');
}
}
function setBar(elementId, percent) {
var el = $(elementId); if (!el) return;
el.style.width = Math.min(100, Math.max(0, percent)) + '%';
}
function updateStatus(message, className) {
var el = $('amd-status'); if (!el) return;
el.textContent = message; el.className = 'amd-status ' + className;
var icon = widget.querySelector('.amd-icon');
if (icon) icon.textContent = className === 'connected' ? '🟢' : className === 'warning' ? '🟡' : '🔴';
}
function updateMethod(method) { var el = $('amd-method'); if (el) el.textContent = method || ''; }
function showToast(message, duration) {
duration = duration || 2000;
var old = document.querySelector('.bangtrix-toast'); if (old) old.remove();
var toast = document.createElement('div');
toast.className = 'bangtrix-toast';
toast.textContent = message;
toast.style.cssText = 'position:fixed;bottom:20px;right:20px;background:rgba(30,30,40,0.95);color:#fff;padding:10px 16px;border-radius:6px;font-size:12px;z-index:10001;border:1px solid rgba(196,48,43,0.5);animation:slideIn 0.2s ease;max-width:300px;';
document.body.appendChild(toast);
setTimeout(function() { toast.style.animation = 'slideOut 0.2s ease'; setTimeout(function() { toast.remove(); }, 200); }, duration);
}
// ===== ANIMATIONS =====
if (!document.getElementById('amd-monitor-animations')) {
var s = document.createElement('style');
s.id = 'amd-monitor-animations';
s.textContent = '@keyframes slideIn{from{transform:translateX(100%);opacity:0}to{transform:translateX(0);opacity:1}}@keyframes slideOut{from{transform:translateX(0);opacity:1}to{transform:translateX(100%);opacity:0}}';
document.head.appendChild(s);
}
// ===== AMD CHIP SELECTOR =====
if (!document.getElementById('amd-chip-styles')) {
var cs = document.createElement('style');
cs.id = 'amd-chip-styles';
cs.textContent = '.amd-chip{padding:2px 8px;border-radius:10px;font-size:10px;background:rgba(255,255,255,0.08);cursor:pointer;transition:all 0.15s;border:1px solid transparent}.amd-chip:hover{background:rgba(255,255,255,0.15)}.amd-chip.active{background:rgba(196,48,43,0.3);border-color:rgba(196,48,43,0.5);color:#fff}.amd-gpu-selector{margin-bottom:6px}.amd-chip-group{display:flex;gap:4px;flex-wrap:wrap}';
document.head.appendChild(cs);
}
// ===== START =====
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
window.addEventListener('beforeunload', function() { if (ws) ws.close(); if (reconnectTimer) clearTimeout(reconnectTimer); });
})();