diff --git a/__init__.py b/__init__.py index 317696d..d5fadce 100644 --- a/__init__.py +++ b/__init__.py @@ -1,12 +1,72 @@ +""" +BANGTRIXTOOLKIT - ComfyUI Custom Nodes +====================================== +AMD Monitor & Translate Universal +""" + from .nodes.translate_universal import ( NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS ) +from .nodes.amd_monitor import ( + NODE_CLASS_MAPPINGS as AMD_NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as AMD_NODE_DISPLAY_NAME_MAPPINGS +) + +# Gabungkan semua node mappings +NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **AMD_NODE_CLASS_MAPPINGS} +NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **AMD_NODE_DISPLAY_NAME_MAPPINGS} + WEB_DIRECTORY = "./web" __all__ = [ "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY" -] \ No newline at end of file +] + + +# ============================================================ +# šŸš€ SERVER EXTENSION - WebSocket untuk Real-Time Monitoring +# ============================================================ + +async def on_app_started(app): + """Register WebSocket endpoint for AMD Monitor (FIXED: 1 argument only)""" + import asyncio + from aiohttp import web + from .server.amd_server import get_amd_server + + amd_server = get_amd_server() + + async def websocket_handler(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + await amd_server.handle_client(ws) + return ws + + # Register route (safe: won't error if already registered) + try: + app.router.add_get('/ws/amd_monitor', websocket_handler) + print("šŸ”“ BANGTRIXTOOLKIT: AMD Monitor WebSocket at /ws/amd_monitor") + except RuntimeError: + pass # Route already exists + + # Start monitoring in background + if not amd_server.running: + amd_server.running = True + asyncio.create_task(amd_server.stream_data()) + print("āœ… BANGTRIXTOOLKIT: AMD Monitor streaming started") + + +# Register with ComfyUI (FIXED: lambda to match signature) +try: + from server import PromptServer + prompt_server = PromptServer.instance + + # Append our async handler (ComfyUI only passes 'app') + prompt_server.app.on_startup.append(on_app_started) + print("āœ… BANGTRIXTOOLKIT: Server extension registered") + +except Exception as e: + print(f"āš ļø BANGTRIXTOOLKIT: Server extension skipped: {e}") \ No newline at end of file diff --git a/nodes/__init__.py b/nodes/__init__.py new file mode 100644 index 0000000..e4337b0 --- /dev/null +++ b/nodes/__init__.py @@ -0,0 +1,18 @@ +from .translate_universal import ( + NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS +) + +from .amd_monitor import ( + NODE_CLASS_MAPPINGS as AMD_CLASS, + NODE_DISPLAY_NAME_MAPPINGS as AMD_DISPLAY +) + +# Gabungkan mappings +NODE_CLASS_MAPPINGS = {**NODE_CLASS_MAPPINGS, **AMD_CLASS} +NODE_DISPLAY_NAME_MAPPINGS = {**NODE_DISPLAY_NAME_MAPPINGS, **AMD_DISPLAY} + +__all__ = [ + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS" +] \ No newline at end of file diff --git a/nodes/_init_.py b/nodes/_init_.py deleted file mode 100644 index dc8c771..0000000 --- a/nodes/_init_.py +++ /dev/null @@ -1,9 +0,0 @@ -from .translate_universal import ( - NODE_CLASS_MAPPINGS, - NODE_DISPLAY_NAME_MAPPINGS -) - -__all__ = [ - "NODE_CLASS_MAPPINGS", - "NODE_DISPLAY_NAME_MAPPINGS" -] \ No newline at end of file diff --git a/nodes/amd_monitor.py b/nodes/amd_monitor.py new file mode 100644 index 0000000..9d80652 --- /dev/null +++ b/nodes/amd_monitor.py @@ -0,0 +1,57 @@ +from ..utils.amd_utils import get_amd_monitor + + +class AMDMonitorNode: + """ + A node to monitor AMD GPU statistics. + Outputs: GPU Usage (%), VRAM Used (MB), VRAM Total (MB), Temperature (C) + """ + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "gpu_index": ("INT", {"default": 0, "min": 0, "max": 10, "step": 1}), + } + } + + RETURN_TYPES = ("FLOAT", "FLOAT", "FLOAT", "INT", "INT", "STRING") + RETURN_NAMES = ("gpu_utilization", "vram_usage_pct", "temperature", "vram_used_mb", "vram_total_mb", "status_msg") + FUNCTION = "monitor" + CATEGORY = "BANGTRIXTOOLKIT/System" + + def monitor(self, gpu_index): + monitor = get_amd_monitor() + + if not monitor.available: + return (0.0, 0.0, 0.0, 0, 0, "Error: AMD Backend not found") + + stats = monitor.get_gpu_stats(gpu_index) + + if not stats.is_available: + return (0.0, 0.0, 0.0, 0, 0, f"Error: {stats.error_message}") + + # Convert bytes to MB for easier reading + vram_used_mb = stats.memory_used / (1024 * 1024) + vram_total_mb = stats.memory_total / (1024 * 1024) + + status = "OK" + + return ( + float(stats.utilization_gpu), + float(stats.utilization_memory), + float(stats.temperature), + int(vram_used_mb), + int(vram_total_mb), + status + ) + + +# Registry +NODE_CLASS_MAPPINGS = { + "BANGTRIX_AMD_Monitor": AMDMonitorNode +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "BANGTRIX_AMD_Monitor": "AMD Monitor (BANGTRIX)" +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 663bd1f..68220a3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,15 @@ -requests \ No newline at end of file +# HTTP & WebSocket Server (Required for AMD Monitor) +aiohttp>=3.8.0 +async-timeout>=4.0.0 + +# HTTP Requests (Optional - for translate_universal) +requests>=2.31.0 + +# System Monitoring +psutil>=5.9.0 + +# AMD GPU Monitoring - Linux (ROCm) +pyrsmi>=0.2.0; sys_platform == "linux" + +# AMD GPU Monitoring - Windows (ADL) +pyadl>=1.0.0; sys_platform == "win32" \ No newline at end of file diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..aa59ec2 --- /dev/null +++ b/server/__init__.py @@ -0,0 +1,3 @@ +from .amd_server import AMDMonitorServer, get_amd_server + +__all__ = ["AMDMonitorServer", "get_amd_server"] \ No newline at end of file diff --git a/server/amd_server.py b/server/amd_server.py new file mode 100644 index 0000000..fffeb7a --- /dev/null +++ b/server/amd_server.py @@ -0,0 +1,107 @@ +""" +AMD Monitor Server +================== +WebSocket server for streaming AMD GPU stats to frontend +""" + +import asyncio +import json +import logging +from typing import Set + +logger = logging.getLogger(__name__) + + +class AMDMonitorServer: + def __init__(self): + self.monitor = None # Lazy load + self.clients: Set = set() + self.running = False + self.update_interval = 1.0 # seconds + + def _get_monitor(self): + """Lazy load monitor to avoid import issues at startup""" + if self.monitor is None: + from ..utils.amd_utils import get_amd_monitor + self.monitor = get_amd_monitor() + return self.monitor + + async def broadcast(self, message: str): + """Send data to all connected clients""" + if self.clients: + await asyncio.gather( + *[client.send_str(message) for client in self.clients], + return_exceptions=True + ) + + async def handle_client(self, ws): + """Handle WebSocket client connection""" + self.clients.add(ws) + logger.info(f"AMD Monitor: Client connected. Total clients: {len(self.clients)}") + + try: + async for msg in ws: + if msg.type == 1: # Text message + if msg.data == "ping": + await ws.send_str("pong") + except Exception as e: + logger.error(f"AMD Monitor: Client error: {e}") + finally: + self.clients.discard(ws) + logger.info(f"AMD Monitor: Client disconnected. Total clients: {len(self.clients)}") + + async def stream_data(self): + """Stream GPU stats periodically""" + monitor = self._get_monitor() + + while self.running: + try: + if monitor.available: + stats = monitor.get_gpu_stats(0) # GPU 0 + + data = { + "type": "amd_stats", + "gpu_id": stats.gpu_id, + "gpu_utilization": stats.utilization_gpu, + "vram_usage_pct": stats.utilization_memory, + "vram_used_mb": stats.memory_used / (1024 * 1024), + "vram_total_mb": stats.memory_total / (1024 * 1024), + "temperature": stats.temperature, + "fan_speed": stats.fan_speed, + "is_available": stats.is_available, + "method": monitor.method, + "gpu_count": monitor.gpu_count + } + else: + data = { + "type": "amd_stats", + "is_available": False, + "error": "AMD backend not available", + "method": None, + "gpu_count": 0 + } + + await self.broadcast(json.dumps(data)) + + except Exception as e: + logger.error(f"AMD Monitor: Stream error: {e}") + + await asyncio.sleep(self.update_interval) + + def start(self, loop=None): + """Start the monitoring loop safely""" + if self.running: + return + + self.running = True + logger.info("AMD Monitor: Starting data stream") + + +# Singleton instance +_amd_server = None + +def get_amd_server() -> AMDMonitorServer: + global _amd_server + if _amd_server is None: + _amd_server = AMDMonitorServer() + return _amd_server \ No newline at end of file diff --git a/test_amd_monitor.py b/test_amd_monitor.py new file mode 100644 index 0000000..fcaface --- /dev/null +++ b/test_amd_monitor.py @@ -0,0 +1,54 @@ +""" +Quick test script for AMD Monitor backend +Run: python_embeded\python.exe test_amd_monitor.py +""" + +import sys +import os + +# Add project to path +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +print("=== BANGTRIXTOOLKIT AMD Monitor Test ===\n") + +# Test 1: Import utils +print("1. Testing amd_utils import...") +try: + from utils.amd_utils import get_amd_monitor, AMDGPUStats + print(" āœ… amd_utils imported") +except Exception as e: + print(f" āŒ amd_utils import failed: {e}") + sys.exit(1) + +# Test 2: Initialize monitor +print("\n2. Initializing AMD monitor...") +monitor = get_amd_monitor() +print(f" Available: {monitor.available}") +print(f" Method: {monitor.method}") +print(f" GPU Count: {monitor.gpu_count}") + +# Test 3: Get stats +if monitor.available: + print("\n3. Fetching GPU stats...") + stats = monitor.get_gpu_stats(0) + print(f" GPU ID: {stats.gpu_id}") + print(f" Utilization: {stats.utilization_gpu}%") + print(f" VRAM: {stats.memory_used / 1024 / 1024:.0f} MB / {stats.memory_total / 1024 / 1024:.0f} MB") + print(f" Temperature: {stats.temperature}°C") + print(f" Fan: {stats.fan_speed}%") + print(f" Status: {'āœ… OK' if stats.is_available else 'āŒ Error: ' + str(stats.error_message)}") +else: + print("\nāš ļø AMD backend not available - check requirements & drivers") + +# Test 4: Check dependencies +print("\n4. Dependency check:") +deps = ['aiohttp', 'psutil', 'pyadl', 'pyrsmi'] +for dep in deps: + try: + __import__(dep) + ver = getattr(__import__(dep), '__version__', 'unknown') + print(f" āœ… {dep}: {ver}") + except ImportError: + print(f" ⚪ {dep}: not installed") + +print("\n=== Test Complete ===") \ No newline at end of file diff --git a/utils/amd_utils.py b/utils/amd_utils.py new file mode 100644 index 0000000..e61f5f8 --- /dev/null +++ b/utils/amd_utils.py @@ -0,0 +1,212 @@ +import platform +import logging +from dataclasses import dataclass +from typing import Optional + +logger = logging.getLogger(__name__) + + +@dataclass +class AMDGPUStats: + gpu_id: int + utilization_gpu: float + utilization_memory: float + memory_total: int # Bytes + memory_used: int # Bytes + memory_free: int # Bytes + temperature: float + fan_speed: int + is_available: bool + error_message: Optional[str] = None + + +class AMDMonitor: + def __init__(self): + self.available = False + self.method = None + self.gpu_count = 0 + self._backend = None + self._initialize() + + def _initialize(self): + system = platform.system() + + # --- LINUX IMPLEMENTATION (ROCm) --- + if system == "Linux": + try: + import pyrsmi + self._backend = pyrsmi + pyrsmi.rocm_smi_initialize() + self.method = "pyrsmi" + self.available = True + self.gpu_count = pyrsmi.rocm_smi_get_num_devices() + logger.info(f"AMD Monitor: Linux detected, using pyrsmi. Found {self.gpu_count} GPUs.") + return + except ImportError: + logger.warning("Linux detected but 'pyrsmi' not installed.") + except Exception as e: + logger.warning(f"Linux pyrsmi init failed: {e}") + + # --- WINDOWS IMPLEMENTATION (ADL) --- + elif system == "Windows": + try: + import pyadl + self._backend = pyadl + + # Try init without custom allocator (safer) + try: + res = pyadl.ADL2_Main_Control_Create(0) # Use default allocator + except: + res = 0 # Assume success if function doesn't exist + + if res == 0: + adapters = pyadl.ADL2_Adapter_AdapterInfo_Get_All() + self.gpu_count = len([a for a in adapters if getattr(a, 'is_present', True)]) + self.method = "pyadl" + self.available = True + logger.info(f"AMD Monitor: Windows + pyadl. Found {self.gpu_count} GPUs.") + return + except ImportError: + logger.warning("Windows: pyadl not installed") + except Exception as e: + logger.warning(f"Windows pyadl error: {e}") + + # Fallback: Try psutil for basic system info + try: + import psutil + self.method = "psutil-fallback" + self.available = True # Partial availability + self.gpu_count = 1 # Assume 1 GPU for fallback + logger.info("AMD Monitor: Using psutil fallback (limited AMD info)") + return + except ImportError: + pass + + logger.error("No AMD monitoring backend available. Check requirements.") + + def get_gpu_stats(self, gpu_id: int = 0) -> AMDGPUStats: + if not self.available: + return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, "Backend not available") + + try: + if self.method == "pyrsmi": + return self._get_stats_linux(gpu_id) + elif self.method == "pyadl": + return self._get_stats_windows(gpu_id) + elif self.method == "psutil-fallback": + return self._get_stats_windows_fallback(gpu_id) + except Exception as e: + return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, str(e)) + + def _get_stats_linux(self, gpu_id: int) -> AMDGPUStats: + rsmi = self._backend + # Utilization + util = rsmi.rocm_smi_get_gpu_utilization(gpu_id) + + # Memory (ROCm returns KB, convert to Bytes) + mem_total_kb = rsmi.rocm_smi_get_gpu_memory_total(gpu_id) + mem_used_kb = rsmi.rocm_smi_get_gpu_memory_used(gpu_id) + + mem_total = mem_total_kb * 1024 + mem_used = mem_used_kb * 1024 + mem_free = mem_total - mem_used + mem_util = (mem_used / mem_total) * 100 if mem_total > 0 else 0 + + # Temp (ROCm returns mC, convert to C) + temp = rsmi.rocm_smi_get_temp(gpu_id, 0) / 1000.0 + + # Fan + fan = rsmi.rocm_smi_get_fan_speed(gpu_id, 0) + + return AMDGPUStats( + gpu_id=gpu_id, + utilization_gpu=util, + utilization_memory=mem_util, + memory_total=mem_total, + memory_used=mem_used, + memory_free=mem_free, + temperature=temp, + fan_speed=fan, + is_available=True + ) + + def _get_stats_windows(self, gpu_id: int) -> AMDGPUStats: + adl = self._backend + adapters = adl.ADL2_Adapter_AdapterInfo_Get_All() + active_adapters = [a for a in adapters if getattr(a, 'is_present', True)] + + if gpu_id >= len(active_adapters): + return AMDGPUStats(gpu_id, 0, 0, 0, 0, 0, 0, 0, False, "GPU Index out of range") + + adapter = active_adapters[gpu_id] + adapter_index = adapter.adapter_index + + # Utilization (Overdrive6 is for GCN and newer) + gpu_util = 0 + try: + usage = adl.ADL2_Overdrive6_CurrentUsage_Get(adapter_index, 0) + gpu_util = usage.iEngineClock + except: + pass + + # Memory (VRAM) - ADL doesn't expose this easily + mem_used = 0 + mem_total = 0 + mem_util = 0 + + # Temperature + temp = 0 + try: + temp_data = adl.ADL2_Overdrive6_Temperature_Get(adapter_index, 0) + temp = temp_data.iTemperature / 1000.0 # ADL returns in milli-Celsius + except: + pass + + # Fan Speed + fan = 0 + try: + fan_data = adl.ADL2_Overdrive6_FanSpeed_Get(adapter_index, 0) + fan = fan_data.iFanSpeedPercent + except: + pass + + return AMDGPUStats( + gpu_id=gpu_id, + utilization_gpu=gpu_util, + utilization_memory=mem_util, + memory_total=mem_total, + memory_used=mem_used, + memory_free=0, + temperature=temp, + fan_speed=fan, + is_available=True + ) + + def _get_stats_windows_fallback(self, gpu_id: int) -> AMDGPUStats: + """Fallback using psutil when ADL fails""" + import psutil + + # Basic system memory (not GPU-specific, but better than nothing) + svmem = psutil.virtual_memory() + + return AMDGPUStats( + gpu_id=gpu_id, + utilization_gpu=0, + utilization_memory=(svmem.used / svmem.total * 100), + memory_total=svmem.total, + memory_used=svmem.used, + memory_free=svmem.available, + temperature=0, + fan_speed=0, + is_available=True + ) + + +# Singleton +_amd_monitor = None + +def get_amd_monitor(): + global _amd_monitor + if _amd_monitor is None: + _amd_monitor = AMDMonitor() + return _amd_monitor \ No newline at end of file diff --git a/web/amd_monitor.js b/web/amd_monitor.js new file mode 100644 index 0000000..9248b9a --- /dev/null +++ b/web/amd_monitor.js @@ -0,0 +1,540 @@ +/** + * BANGTRIXTOOLKIT - AMD Monitor Floating Widget + * Real-time AMD GPU monitoring overlay for ComfyUI + * Cross-platform: Windows (ADL) / Linux (ROCm) + */ + +(function() { + "use strict"; + + // ===== CONFIGURATION ===== + const CONFIG = { + wsUrl: `ws://${window.location.host}/ws/amd_monitor`, + updateInterval: 1000, + storageKey: 'bangtrix_amd_monitor_config', + defaultPosition: { top: '60px', right: '20px' } + }; + + // ===== STATE ===== + let ws = null; + let reconnectTimer = null; + let reconnectAttempts = 0; + const MAX_RECONNECT = 5; + + // Widget state + let widget = null; + let isMinimized = false; + let isVisible = true; + let isDragging = false; + let dragOffset = { x: 0, y: 0 }; + + // ===== INITIALIZATION ===== + function init() { + if (document.getElementById('bangtrix-amd-monitor')) { + console.log('AMD Monitor: Already initialized'); + return; + } + + console.log('AMD Monitor: Initializing floating widget...'); + createWidget(); + setupEventListeners(); + loadConfig(); + connectWebSocket(); + } + + // ===== WIDGET CREATION ===== + function createWidget() { + widget = document.createElement('div'); + widget.id = 'bangtrix-amd-monitor'; + widget.className = 'amd-monitor-widget'; + + widget.innerHTML = ` +
+
+ šŸ”“ + AMD Monitor +
+
+ + + +
+
+ +
+
+
+ GPU Load + -- +
+
+
+ VRAM + -- / -- GB +
+
+
+ Temperature + --°C +
+
+
+ Fan Speed + --% +
+
+
+ + +
+ `; + + injectStyles(); + document.body.appendChild(widget); + setupDrag(); + + return widget; + } + + function injectStyles() { + const style = document.createElement('style'); + style.id = 'bangtrix-amd-monitor-styles'; + style.textContent = ` + #bangtrix-amd-monitor { + position: fixed; + top: 60px; + right: 20px; + width: 240px; + 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; + transition: opacity 0.2s, transform 0.2s; + } + + #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; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.15s; + } + + .amd-btn:hover { background: rgba(255,255,255,0.2); color: #fff; } + .amd-btn-close:hover { background: rgba(255, 80, 80, 0.3); color: #fff; } + + .amd-body { padding: 12px 14px; } + .amd-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 14px; } + + .amd-stat { + display: flex; + flex-direction: column; + gap: 4px; + } + + .amd-label { + color: #888; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.3px; + } + + .amd-value { + font-weight: 600; + font-size: 13px; + color: #00e676; + transition: color 0.2s; + } + + .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%; + background: linear-gradient(90deg, #00e676, #00c853); + border-radius: 2px; + transition: width 0.3s ease; + width: 0%; + } + + .amd-fill.amd-temp { + background: linear-gradient(90deg, #00e676, #ffaa00, #ff4444); + } + + .amd-footer { + display: flex; + justify-content: space-between; + margin-top: 14px; + padding-top: 10px; + 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; } + .amd-header:active { cursor: grabbing; } + + @media (max-width: 768px) { + #bangtrix-amd-monitor { width: 200px; font-size: 10px; } + .amd-grid { grid-template-columns: 1fr; } + } + `; + document.head.appendChild(style); + } + + // ===== DRAG FUNCTIONALITY ===== + function setupDrag() { + const header = widget.querySelector('.amd-header'); + + header.addEventListener('mousedown', (e) => { + if (e.target.closest('.amd-btn')) return; + + isDragging = true; + const 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; + + const x = e.clientX - dragOffset.x; + const y = e.clientY - dragOffset.y; + + const maxX = window.innerWidth - widget.offsetWidth + 20; + const maxY = window.innerHeight - widget.offsetHeight + 20; + + widget.style.left = Math.max(10, Math.min(x, maxX)) + 'px'; + widget.style.top = Math.max(10, Math.min(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() { + widget.querySelector('.amd-btn-minimize').addEventListener('click', () => { + isMinimized = !isMinimized; + widget.classList.toggle('minimized', isMinimized); + widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : 'āˆ’'; + saveConfig(); + }); + + widget.querySelector('.amd-btn-close').addEventListener('click', () => { + isVisible = !isVisible; + widget.classList.toggle('hidden', !isVisible); + saveConfig(); + }); + + widget.querySelector('.amd-btn-settings').addEventListener('click', () => { + showToast('Settings coming soon!'); + }); + } + + // ===== CONFIGURATION PERSISTENCE ===== + function loadConfig() { + try { + const saved = localStorage.getItem(CONFIG.storageKey); + if (saved) { + const config = JSON.parse(saved); + if (config.position) { + if (config.position.left) { + widget.style.left = config.position.left; + widget.style.right = 'auto'; + } + if (config.position.top) { + widget.style.top = config.position.top; + } + } + if (config.minimized !== undefined) { + isMinimized = config.minimized; + widget.classList.toggle('minimized', isMinimized); + widget.querySelector('.amd-btn-minimize').textContent = isMinimized ? '+' : 'āˆ’'; + } + if (config.visible !== undefined) { + isVisible = config.visible; + widget.classList.toggle('hidden', !isVisible); + } + } + } catch (e) { + console.warn('AMD Monitor: Failed to load config:', e); + } + } + + function saveConfig() { + try { + const config = { + position: { + top: widget.style.top, + left: widget.style.left + }, + minimized: isMinimized, + visible: isVisible + }; + localStorage.setItem(CONFIG.storageKey, JSON.stringify(config)); + } catch (e) { + console.warn('AMD Monitor: Failed to save config:', e); + } + } + + // ===== WEBSOCKET CONNECTION ===== + function connectWebSocket() { + if (ws?.readyState === WebSocket.OPEN) return; + + console.log('AMD Monitor: Connecting to', CONFIG.wsUrl); + updateStatus('Connecting...', 'warning'); + + ws = new WebSocket(CONFIG.wsUrl); + + ws.onopen = () => { + console.log('AMD Monitor: Connected'); + updateStatus('Connected', 'connected'); + updateMethod('Live'); + reconnectAttempts = 0; + if (reconnectTimer) { + clearTimeout(reconnectTimer); + reconnectTimer = null; + } + }; + + ws.onmessage = (event) => { + try { + const data = JSON.parse(event.data); + if (data.type === 'amd_stats') { + updateDisplay(data); + } + } catch (e) { + console.error('AMD Monitor: Parse error:', e); + } + }; + + ws.onclose = () => { + console.log('AMD Monitor: Disconnected'); + updateStatus('Disconnected', 'disconnected'); + updateMethod('--'); + + if (reconnectAttempts < MAX_RECONNECT) { + reconnectAttempts++; + const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 8000); + console.log(`AMD Monitor: Reconnecting in ${delay}ms (attempt ${reconnectAttempts})`); + reconnectTimer = setTimeout(connectWebSocket, delay); + } + }; + + ws.onerror = (error) => { + console.error('AMD Monitor: WebSocket error:', error); + updateStatus('Connection error', 'disconnected'); + }; + } + + // ===== DISPLAY UPDATES ===== + function updateDisplay(data) { + if (!data.is_available) { + setStat('amd-gpu-util', 'N/A'); + setStat('amd-vram', 'N/A'); + setStat('amd-temp', 'N/A'); + setStat('amd-fan', 'N/A'); + setBar('amd-gpu-bar', 0); + setBar('amd-vram-bar', 0); + setBar('amd-temp-bar', 0); + setBar('amd-fan-bar', 0); + updateStatus(data.error || 'AMD backend unavailable', 'warning'); + updateMethod(data.method || '--'); + return; + } + + const gpuUtil = data.gpu_utilization || 0; + setStat('amd-gpu-util', `${gpuUtil.toFixed(1)}%`, gpuUtil); + setBar('amd-gpu-bar', gpuUtil); + + const vramUsedGB = (data.vram_used_mb || 0) / 1024; + const vramTotalGB = (data.vram_total_mb || 0) / 1024; + const vramPct = data.vram_usage_pct || (vramTotalGB > 0 ? (vramUsedGB / vramTotalGB * 100) : 0); + setStat('amd-vram', `${vramUsedGB.toFixed(1)} / ${vramTotalGB.toFixed(1)} GB`, vramPct); + setBar('amd-vram-bar', vramPct); + + const temp = data.temperature || 0; + setStat('amd-temp', `${temp.toFixed(1)}°C`, temp); + setBar('amd-temp-bar', Math.min(temp, 100)); + + const fan = data.fan_speed || 0; + setStat('amd-fan', `${fan}%`, fan); + setBar('amd-fan-bar', fan); + + updateStatus(`GPU ${data.gpu_id}`, 'connected'); + updateMethod(data.method || 'unknown'); + } + + function setStat(elementId, value, numericValue = null) { + const el = document.getElementById(elementId); + if (!el) return; + el.textContent = value; + + el.classList.remove('warning', 'critical'); + if (numericValue !== null) { + 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 === 'amd-vram') && numericValue > 90) { + el.classList.add('critical'); + } else if ((elementId === 'amd-gpu-util' || elementId === 'amd-vram') && numericValue > 75) { + el.classList.add('warning'); + } + } + } + + function setBar(elementId, percent) { + const el = document.getElementById(elementId); + if (!el) return; + el.style.width = `${Math.min(100, Math.max(0, percent))}%`; + } + + function updateStatus(message, className) { + const el = document.getElementById('amd-status'); + if (!el) return; + el.textContent = message; + el.className = `amd-status ${className}`; + + const icon = widget.querySelector('.amd-icon'); + if (icon) { + icon.textContent = className === 'connected' ? '🟢' : + className === 'warning' ? '🟔' : 'šŸ”“'; + } + } + + function updateMethod(method) { + const el = document.getElementById('amd-method'); + if (!el) return; + el.textContent = method ? `[${method}]` : ''; + } + + // ===== UTILITIES ===== + function showToast(message, duration = 2000) { + const toast = document.createElement('div'); + 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; + `; + document.body.appendChild(toast); + + setTimeout(() => { + toast.style.animation = 'slideOut 0.2s ease'; + setTimeout(() => toast.remove(), 200); + }, duration); + } + + if (!document.getElementById('amd-monitor-animations')) { + const animStyle = document.createElement('style'); + animStyle.id = 'amd-monitor-animations'; + animStyle.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(animStyle); + } + + // ===== START ===== + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + window.addEventListener('beforeunload', () => { + if (ws) ws.close(); + if (reconnectTimer) clearTimeout(reconnectTimer); + }); + +})(); \ No newline at end of file diff --git a/web/js/amd_monitor.js b/web/js/amd_monitor.js deleted file mode 100644 index e241cbe..0000000 --- a/web/js/amd_monitor.js +++ /dev/null @@ -1,56 +0,0 @@ -import { app } from "../../scripts/app.js"; - -app.registerExtension({ - name: "Bangtrix.AMDMonitor", - - async setup() { - - const panel = document.createElement("div"); - - panel.id = "bangtrix-amd-monitor"; - - panel.style.position = "fixed"; - panel.style.top = "20px"; - panel.style.right = "20px"; - panel.style.width = "220px"; - panel.style.padding = "12px"; - panel.style.background = "rgba(20,20,20,0.9)"; - panel.style.color = "white"; - panel.style.fontSize = "14px"; - panel.style.borderRadius = "12px"; - panel.style.zIndex = "99999"; - panel.style.backdropFilter = "blur(10px)"; - panel.style.boxShadow = "0 0 10px rgba(0,0,0,0.5)"; - - panel.innerHTML = ` -

AMD Monitor šŸ“Š

- -
GPU Usage: 0%
-
VRAM: 0 GB
-
RAM: 0 GB
-
Timer: 0s
- `; - - document.body.appendChild(panel); - - let seconds = 0; - - setInterval(() => { - - seconds++; - - document.getElementById("gpu_usage").innerText = - Math.floor(Math.random() * 100) + "%"; - - document.getElementById("vram_usage").innerText = - (Math.random() * 16).toFixed(2) + " GB"; - - document.getElementById("ram_usage").innerText = - (Math.random() * 32).toFixed(2) + " GB"; - - document.getElementById("timer").innerText = - seconds + "s"; - - }, 1000); - } -}); \ No newline at end of file