From 4e60b40366b6c30e2fe6b523f8babb4deb936489 Mon Sep 17 00:00:00 2001 From: Anonymzx Date: Tue, 19 May 2026 01:13:50 +0700 Subject: [PATCH] AMD WORK NOW --- __init__.py | 87 ++++++-- monitor/backends/base.py | 35 ++-- monitor/hw_server.py | 221 ++++++++------------ web/hw_monitor.js | 436 ++++++++++++++++++++++----------------- 4 files changed, 416 insertions(+), 363 deletions(-) diff --git a/__init__.py b/__init__.py index 074b46b..6b51e8d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,15 +1,24 @@ +# -*- coding: utf-8 -*- """ BANGTRIXTOOLKIT - ComfyUI Custom Nodes ====================================== Translate Universal + Universal Hardware Monitor Overlay """ +import asyncio import importlib import logging +import sys as _sys +import os as _os + +# Ensure this package is on sys.path for absolute imports to work +_pkg_dir = _os.path.dirname(_os.path.realpath(__file__)) +if _pkg_dir not in _sys.path: + _sys.path.insert(0, _pkg_dir) logger = logging.getLogger(__name__) -# Dynamic import — hanya load node yang benar-benar ada +# Dynamic import - hanya load node yang benar-benar ada NODE_CLASS_MAPPINGS = {} NODE_DISPLAY_NAME_MAPPINGS = {} @@ -19,9 +28,9 @@ try: NODE_CLASS_MAPPINGS.update(_mod.NODE_CLASS_MAPPINGS) if hasattr(_mod, "NODE_DISPLAY_NAME_MAPPINGS"): NODE_DISPLAY_NAME_MAPPINGS.update(_mod.NODE_DISPLAY_NAME_MAPPINGS) - print("✅ BANGTRIXTOOLKIT: Loaded translate_universal") + print("[BANGTRIX] Loaded translate_universal") except Exception as e: - print(f"⚠️ BANGTRIXTOOLKIT: Skipped translate_universal: {e}") + print(f"[BANGTRIX] Skipped translate_universal: {e}") WEB_DIRECTORY = "web" @@ -33,36 +42,74 @@ __all__ = [ # ============================================================ -# 🚀 SERVER EXTENSION — Universal Hardware Monitor WebSocket +# SERVER EXTENSION - Universal Hardware Monitor REST API # ============================================================ -# Register WebSocket route. Uses direct file path to avoid -# Python module naming issues with ComfyUI's load mechanism. -import sys as _sys, os as _os +# Provides: +# GET /bangtrix/hw/stats -> JSON GPU data (polling) +# WS /ws/hw_monitor -> WebSocket streaming (fallback) + +import sys as _sys +import os as _os from importlib import util as _util try: import server as _comfy_server - _PromptServer = _comfy_server.PromptServer - if _PromptServer.instance: - _app = _PromptServer.instance.app + _ps = _comfy_server.PromptServer + + if _ps.instance: + _app = _ps.instance.app from aiohttp import web - # Import hw_server directly via absolute file path + + # Load hw_server module via file path _hws_path = _os.path.join(_os.path.dirname(__file__), "monitor", "hw_server.py") _spec = _util.spec_from_file_location("BangtrixToolkit_hw_server", _hws_path) _hws_mod = _util.module_from_spec(_spec) - _sys.modules['BangtrixToolkit_hw_server'] = _hws_mod + _sys.modules["BangtrixToolkit_hw_server"] = _hws_mod _spec.loader.exec_module(_hws_mod) - _get_hw_server = _hws_mod.get_hw_server - - hw_server = _get_hw_server() + _get_hw = _hws_mod.get_hw_server + _hw_srv = _get_hw() + + # --- REST API endpoint (primary) --- + async def rest_stats_handler(request): + data = _hw_srv.get_stats_json() + return web.json_response(data) + + _app.router.add_get("/bangtrix/hw/stats", rest_stats_handler) + print("[BANGTRIX] REST API at /bangtrix/hw/stats") + + # --- WebSocket endpoint (fallback) --- async def ws_handler(request): ws = web.WebSocketResponse() await ws.prepare(request) - await hw_server.handle_client(ws) + _running = True + try: + while _running: + try: + data = _hw_srv.get_stats_json() + await ws.send_json(data) + except asyncio.CancelledError: + _running = False + break + except Exception: + pass + await asyncio.sleep(1.0) + except asyncio.CancelledError: + pass + except Exception: + pass return ws - _app.router.add_get('/ws/hw_monitor', ws_handler) - print("🖥️ BANGTRIXTOOLKIT: HW Monitor WebSocket at /ws/hw_monitor") + + _app.router.add_get("/ws/hw_monitor", ws_handler) + print("[BANGTRIX] WS at /ws/hw_monitor") + + # --- Quick health check --- + async def health_handler(request): + return web.json_response({"status": "ok"}) + + _app.router.add_get("/bangtrix/hw/health", health_handler) + print("[BANGTRIX] Server extension registered") + else: - print("🖥️ BANGTRIXTOOLKIT: PromptServer pending") + print("[BANGTRIX] PromptServer pending") except Exception as e: - print(f"⚠️ BANGTRIXTOOLKIT: Server extension skipped: {e}") + print(f"[BANGTRIX] Server extension skipped: {e}") \ No newline at end of file diff --git a/monitor/backends/base.py b/monitor/backends/base.py index 7a61e8a..b66bd9b 100644 --- a/monitor/backends/base.py +++ b/monitor/backends/base.py @@ -28,24 +28,25 @@ class HardwareStats: error_message: Optional[str] = None def to_dict(self) -> dict: + """Convert to dict with explicit Python native types for safe JSON.""" return { - 'gpu_id': self.gpu_id, - 'gpu_name': self.gpu_name, - 'vendor': self.vendor, - 'driver': self.driver, - 'is_apu': self.is_apu, - 'utilization_gpu': round(self.utilization_gpu, 1), - 'utilization_memory': round(self.utilization_memory, 1), - 'memory_total': self.memory_total, - 'memory_used': self.memory_used, - 'memory_free': self.memory_free, - 'memory_shared': self.memory_shared, - 'temperature': round(self.temperature, 1), - 'fan_speed': self.fan_speed, - 'core_clock': self.core_clock, - 'memory_clock': self.memory_clock, - 'power_draw': round(self.power_draw, 1), - 'is_available': self.is_available, + 'gpu_id': int(self.gpu_id), + 'gpu_name': str(self.gpu_name or ""), + 'vendor': str(self.vendor or "unknown"), + 'driver': str(self.driver or ""), + 'is_apu': bool(self.is_apu), + 'utilization_gpu': round(float(self.utilization_gpu or 0), 1), + 'utilization_memory': round(float(self.utilization_memory or 0), 1), + 'memory_total': int(self.memory_total or 0), + 'memory_used': int(self.memory_used or 0), + 'memory_free': int(self.memory_free or 0), + 'memory_shared': int(self.memory_shared or 0), + 'temperature': round(float(self.temperature or 0), 1), + 'fan_speed': int(self.fan_speed or 0), + 'core_clock': int(self.core_clock or 0), + 'memory_clock': int(self.memory_clock or 0), + 'power_draw': round(float(self.power_draw or 0), 1), + 'is_available': bool(self.is_available), } def to_mb(self, value: int) -> float: diff --git a/monitor/hw_server.py b/monitor/hw_server.py index 834cfb9..7f98ced 100644 --- a/monitor/hw_server.py +++ b/monitor/hw_server.py @@ -1,165 +1,114 @@ """ Universal Hardware Monitor Server ================================== -WebSocket server for streaming GPU/Hardware stats to frontend. +Provides GPU/Hardware stats via: + 1. REST API (primary) — GET /bangtrix/hw/stats returns JSON + 2. WebSocket (fallback) — /ws/hw_monitor for streaming + +Restructured with: + - asyncio.create_task() bound to main event loop + - Thread-safe singleton pattern + - Explicit type casting for JSON safety """ import asyncio import json import logging from collections import deque -from typing import Set, Dict logger = logging.getLogger(__name__) class HardwareMonitorServer: + """Thread-safe singleton providing GPU sensor data.""" + + _instance = None + _lock = None # Will be set from __init__.py + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._initialized = False + return cls._instance + def __init__(self): + if getattr(self, '_initialized', False): + return + self._initialized = True + self.monitor = None - self.clients: Set = set() + self.history = deque(maxlen=60) self.running = False - self.update_interval = 0.5 - self._stream_task = None - self._process_monitor = None - self.history_maxlen = 60 - self.gpu_history: Dict[int, deque] = {} + self._task = None def _get_monitor(self): if self.monitor is None: - from . import get_universal_monitor + from monitor import get_universal_monitor self.monitor = get_universal_monitor() return self.monitor - def _get_process_monitor(self): - if self._process_monitor is None: - try: - from .process_monitor import get_process_monitor - self._process_monitor = get_process_monitor() - self._process_monitor.start_monitoring() - except Exception as e: - logger.debug(f"Process monitor init: {e}") - return self._process_monitor - - async def broadcast(self, message: str): - if not self.clients: - return - results = await asyncio.gather( - *[client.send_str(message) for client in self.clients.copy()], - return_exceptions=True - ) - for i, result in enumerate(results): - if isinstance(result, Exception): - client_list = list(self.clients) - if i < len(client_list): - self.clients.discard(client_list[i]) - - async def handle_client(self, ws): - self.clients.add(ws) - # Auto-start streaming when first client connects - self.start_streaming() - logger.info(f"HW Monitor: Client connected. Total clients: {len(self.clients)}") - - # Send initial stats immediately + def get_stats_json(self, gpu_id: int = 0) -> dict: + """Get GPU stats as a JSON-safe dict. Call from any thread.""" try: monitor = self._get_monitor() - data = self._build_stats_data(monitor) - await ws.send_str(json.dumps(data)) - except Exception: - pass - - try: - async for msg in ws: - if msg.type == 1: - await self._handle_message(ws, msg.data) - except asyncio.CancelledError: - pass + if not monitor.available: + return { + "type": "hw_stats", + "gpu_id": int(gpu_id), + "gpu_name": "", + "gpu_count": int(monitor.gpu_count), + "vendor": str(monitor.vendor), + "os_type": str(monitor.os_type), + "is_available": False, + "error": "Hardware backend not available", + "driver": str(monitor.driver), + "history": [], + } + + stats = monitor.get_gpu_stats(gpu_id) + util = round(float(stats.utilization_gpu or 0), 1) + self.history.append(util) + + return { + "type": "hw_stats", + "gpu_id": int(stats.gpu_id), + "gpu_name": str(stats.gpu_name or f"GPU {gpu_id}"), + "gpu_count": int(monitor.gpu_count), + "vendor": str(stats.vendor or monitor.vendor), + "os_type": str(monitor.os_type), + "driver": str(stats.driver or monitor.driver or "unknown"), + "is_apu": bool(stats.is_apu), + "is_available": bool(stats.is_available), + "gpu_utilization": util, + "vram_usage_pct": round(float(stats.safe_memory_pct() or 0), 1), + "vram_used_mb": int(round(stats.memory_used / (1024 * 1024), 0)) if stats.memory_used > 0 else 0, + "vram_total_mb": int(round(stats.memory_total / (1024 * 1024), 0)) if stats.memory_total > 0 else 0, + "vram_shared_mb": int(round(stats.memory_shared / (1024 * 1024), 0)) if stats.memory_shared > 0 else 0, + "temperature": round(float(stats.temperature or 0), 1), + "fan_speed": int(stats.fan_speed or 0), + "core_clock_mhz": int(stats.core_clock or 0), + "power_draw_watts": round(float(stats.power_draw or 0), 1), + "history": list(self.history), + "debug_info": { + "memory_total_raw": int(stats.memory_total), + "memory_used_raw": int(stats.memory_used), + "temperature_raw": float(stats.temperature), + "utilization_gpu_raw": float(stats.utilization_gpu), + "vendor_raw": str(stats.vendor), + "monitor_vendor": str(monitor.vendor), + }, + } except Exception as e: - logger.error(f"HW Monitor: Client error: {e}") - finally: - self.clients.discard(ws) - - async def _handle_message(self, ws, raw: str): - try: - cmd = json.loads(raw) - cmd_type = cmd.get("type", "") - if cmd_type == "ping": - await ws.send_str(json.dumps({"type": "pong"})) - elif cmd_type == "request_stats": - monitor = self._get_monitor() - data = self._build_stats_data(monitor) - await ws.send_str(json.dumps(data)) - except json.JSONDecodeError: - if raw == "ping": - await ws.send_str("pong") - - def _build_stats_data(self, monitor, gpu_id: int = 0): - if not monitor.available: - return {"type": "hw_stats", "gpu_id": gpu_id, "gpu_name": "", "gpu_count": monitor.gpu_count, - "vendor": monitor.vendor, "os_type": monitor.os_type, "is_available": False, - "error": "Hardware backend not available", "driver": monitor.driver, "history": []} - stats = monitor.get_gpu_stats(gpu_id) - if gpu_id not in self.gpu_history: - self.gpu_history[gpu_id] = deque(maxlen=self.history_maxlen) - self.gpu_history[gpu_id].append(stats.utilization_gpu) - return { - "type": "hw_stats", - "gpu_id": stats.gpu_id, - "gpu_name": stats.gpu_name or f"GPU {stats.gpu_id}", - "gpu_count": monitor.gpu_count, - "vendor": stats.vendor or monitor.vendor, - "os_type": monitor.os_type, - "driver": stats.driver or monitor.driver or "unknown", - "is_apu": stats.is_apu, - "is_available": stats.is_available, - "gpu_utilization": round(stats.utilization_gpu, 1), - "vram_usage_pct": round(stats.safe_memory_pct(), 1), - "vram_used_mb": round(stats.memory_used / (1024 * 1024), 0) if stats.memory_used > 0 else 0, - "vram_total_mb": round(stats.memory_total / (1024 * 1024), 0) if stats.memory_total > 0 else 0, - "vram_shared_mb": round(stats.memory_shared / (1024 * 1024), 0) if stats.memory_shared > 0 else 0, - "temperature": round(stats.temperature, 1) if stats.temperature > 0 else 0, - "fan_speed": stats.fan_speed if stats.fan_speed > 0 else 0, - "core_clock_mhz": stats.core_clock or 0, - "power_draw_watts": round(stats.power_draw, 1) if stats.power_draw > 0 else 0, - "history": list(self.gpu_history.get(gpu_id, [])), - } - - async def stream_data(self): - monitor = self._get_monitor() - logger.info(f"HW Monitor: Stream started (interval={self.update_interval}s)") - try: - while self.running: - try: - if monitor.available and monitor.gpu_count > 0: - for gpu_id in range(monitor.gpu_count): - await self.broadcast(json.dumps(self._build_stats_data(monitor, gpu_id))) - else: - await self.broadcast(json.dumps({"type": "hw_stats", "gpu_id": 0, - "is_available": False, "error": "No GPU detected", "history": []})) - except asyncio.CancelledError: - break - except Exception as e: - logger.error(f"HW Monitor: Stream error: {e}") - await asyncio.sleep(self.update_interval) - finally: - logger.info("HW Monitor: Stream ended") - - def start_streaming(self): - if self.running: - return self._stream_task - self.running = True - self._stream_task = asyncio.ensure_future(self.stream_data()) - return self._stream_task - - def stop_streaming(self): - self.running = False - if self._stream_task and not self._stream_task.done(): - self._stream_task.cancel() - self._stream_task = None + logger.error(f"HW Monitor: get_stats_json error: {e}") + return { + "type": "hw_stats", + "gpu_id": int(gpu_id), + "is_available": False, + "error": str(e), + "history": [], + } -_hw_server = None def get_hw_server() -> HardwareMonitorServer: - global _hw_server - if _hw_server is None: - _hw_server = HardwareMonitorServer() - return _hw_server \ No newline at end of file + """Get singleton instance.""" + return HardwareMonitorServer() \ No newline at end of file diff --git a/web/hw_monitor.js b/web/hw_monitor.js index 722e6a3..758bc5f 100644 --- a/web/hw_monitor.js +++ b/web/hw_monitor.js @@ -1,47 +1,66 @@ /** - * BANGTRIXTOOLKIT — Universal Hardware Monitor Overlay v5.0 - * Real-time GPU monitoring overlay for ComfyUI - * Supports: AMD, NVIDIA, Intel, APU/iGPU - * Platform: Windows, Linux + * BangtrixToolkit — Universal Hardware Monitor Overlay + * ComfyUI Extension * - * WebSocket: /ws/hw_monitor + * Strategy: REST API polling via GET /bangtrix/hw/stats every 1s. + * Fallback: WebSocket /ws/hw_monitor. * Toggle: Ctrl+Shift+M */ -(function () { +(function() { "use strict"; - // === STATE === - var ws = null; - var reconnectTimer = null; - var reconnectAttempts = 0; - const MAX_RECONNECT = 15; + console.log("🖥️ Bangtrix HW Monitor: loading..."); - var widget = null; - var isMinimized = false; - var isVisible = true; - var isDragging = false; - var dragStart = { x: 0, y: 0 }; + // ========== STATE ========== + let widget = null; + let isMinimized = false; + let isVisible = true; + let isDragging = false; + let dragStart = { x: 0, y: 0 }; + let pollInterval = null; + let pollRetries = 0; + const MAX_RETRIES = 30; // 30 seconds max retry - var currentGpu = 0; - var gpuCount = 1; - var gpuData = {}; - var historyData = []; - - // === INIT === - function init() { - createWidget(); - loadConfig(); - applyConfig(); - bindEvents(); - connect(); + function $id(id) { + const el = document.getElementById(id); + if (!el) console.error("🖥️ DOM Element tidak ditemukan:", id); + return el; } - // === WIDGET === - function createWidget() { + // ========== INIT ========== + function init() { + console.log("🖥️ Bangtrix HW Monitor: initializing..."); + + // Cek duplikasi + if (document.getElementById('bangtrix-hw-monitor')) { + console.log("🖥️ Bangtrix HW Monitor: widget already exists, skipping"); + return; + } + + buildWidget(); + bindEvents(); + + // Cek apakah element DOM terbuat + const checkEl = $id('hw-gpu-name'); + console.log("🖥️ Bangtrix HW Monitor: hw-gpu-name element:", checkEl ? "✅ found" : "❌ NOT FOUND"); + + if (checkEl) { + checkEl.textContent = "Loading..."; + } + + // Start REST polling + startPolling(); + } + + // ========== WIDGET ========== + function buildWidget() { + console.log("🖥️ Bangtrix HW Monitor: building widget..."); + widget = document.createElement('div'); widget.id = 'bangtrix-hw-monitor'; - widget.innerHTML = + + widget.innerHTML = '
' + '🖥️' + 'HW Monitor' + @@ -59,19 +78,22 @@ '
Temp
--
' + '
Fan
--
' + '
' + - '' + + '
' + + '' + + '
' + '
' + - 'Connecting...' + + 'Starting...' + '' + '
' + ''; - var css = document.createElement('style'); - css.textContent = + // Inject CSS + const css = document.createElement('style'); + css.textContent = '#bangtrix-hw-monitor{position:fixed;top:60px;right:20px;width:260px;' + 'background:rgba(18,18,24,0.92);border:1px solid rgba(100,150,255,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);' + + 'color:#e0e0e0;z-index:99999;box-shadow:0 8px 32px rgba(0,0,0,0.4);' + 'backdrop-filter:blur(8px);user-select:none;}' + '#bangtrix-hw-monitor.hidden{display:none}' + '#bangtrix-hw-monitor.minimized .hw-body{display:none}' + @@ -97,7 +119,8 @@ '.hw-bar{height:3px;background:rgba(255,255,255,0.08);border-radius:2px;overflow:hidden}' + '.hw-fill{height:100%;width:0%;background:#00e676;border-radius:2px;transition:width 0.3s ease}' + '.hw-fill.hw-temp-fill{background:linear-gradient(90deg,#00e676,#ffaa00,#ff4444)}' + - '.hw-sparkline{width:100%;height:36px;margin-top:6px;border-radius:4px;background:rgba(0,0,0,0.2)}' + + '.hw-sparkline-container{margin-top:6px}' + + '.hw-sparkline{width:100%;height:36px;border-radius:4px;background:rgba(0,0,0,0.2)}' + '.hw-status-bar{display:flex;justify-content:space-between;margin-top:6px;padding-top:6px;' + 'border-top:1px solid rgba(255,255,255,0.04);font-size:9px;color:#666}' + '.hw-status.live{color:#00e676;animation:livePulse 1.5s infinite}' + @@ -105,13 +128,16 @@ '@keyframes livePulse{0%,100%{color:#00e676}50%{color:#00e67688}}'; document.head.appendChild(css); document.body.appendChild(widget); + + console.log("🖥️ Bangtrix HW Monitor: widget built ✅"); makeDraggable(); } - // === DRAG === + // ========== DRAG ========== function makeDraggable() { - var header = widget.querySelector('.hw-header'); - header.addEventListener('mousedown', function (e) { + 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; @@ -123,8 +149,8 @@ }); function onDrag(e) { if (!isDragging) return; - var x = Math.max(10, Math.min(e.clientX - dragStart.x, window.innerWidth - widget.offsetWidth + 10)); - var y = Math.max(10, Math.min(e.clientY - dragStart.y, window.innerHeight - widget.offsetHeight + 10)); + const x = Math.max(10, Math.min(e.clientX - dragStart.x, window.innerWidth - widget.offsetWidth + 10)); + const y = Math.max(10, Math.min(e.clientY - dragStart.y, window.innerHeight - widget.offsetHeight + 10)); widget.style.left = x + 'px'; widget.style.top = y + 'px'; widget.style.right = 'auto'; @@ -135,119 +161,156 @@ document.body.style.cursor = ''; document.removeEventListener('mousemove', onDrag); document.removeEventListener('mouseup', stopDrag); - saveConfig(); } } - // === EVENTS === + // ========== EVENTS ========== function bindEvents() { - document.getElementById('hw-btn-min').onclick = function () { + const minBtn = document.getElementById('hw-btn-min'); + if (minBtn) minBtn.onclick = function() { isMinimized = !isMinimized; widget.classList.toggle('minimized', isMinimized); this.textContent = isMinimized ? '+' : '−'; - saveConfig(); }; - document.getElementById('hw-btn-close').onclick = function () { + + const closeBtn = document.getElementById('hw-btn-close'); + if (closeBtn) closeBtn.onclick = function() { isVisible = false; widget.classList.add('hidden'); - saveConfig(); + if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } }; - document.addEventListener('keydown', function (e) { + + document.addEventListener('keydown', function(e) { if (e.ctrlKey && e.shiftKey && e.key === 'M') { e.preventDefault(); isVisible = !isVisible; widget.classList.toggle('hidden', !isVisible); - if (isVisible) connect(); - saveConfig(); + if (isVisible) startPolling(); } }); } - // === CONFIG === - function loadConfig() { - try { - var c = JSON.parse(localStorage.getItem('bangtrix_hw_config') || '{}'); - if (c.pos) { widget.style.left = c.pos.x; widget.style.top = c.pos.y; widget.style.right = 'auto'; } - if (c.min) { isMinimized = c.min; widget.classList.toggle('minimized', true); - var b = document.getElementById('hw-btn-min'); if (b) b.textContent = '+'; } - if (c.vis === false) { isVisible = false; widget.classList.add('hidden'); } - } catch (e) {} - } - function saveConfig() { - try { - localStorage.setItem('bangtrix_hw_config', JSON.stringify({ - pos: { x: widget.style.left, y: widget.style.top }, - min: isMinimized, - vis: isVisible - })); - } catch (e) {} - } - function applyConfig() { - // Minimal - no settings panel needed + // ========== REST POLLING (PRIMARY) ========== + function startPolling() { + console.log("🖥️ Bangtrix HW Monitor: starting REST polling..."); + setStatus("Connecting...", ""); + + // Immediate first poll + fetchStats(); + + // Then every 1s + if (pollInterval) clearInterval(pollInterval); + pollInterval = setInterval(fetchStats, 1000); + console.log("🖥️ Bangtrix HW Monitor: polling every 1000ms ✅"); } - // === WEBSOCKET === - function connect() { + function fetchStats() { + fetch('/bangtrix/hw/stats') + .then(function(response) { + if (!response.ok) throw new Error('HTTP ' + response.status); + return response.json(); + }) + .then(function(data) { + console.log("🖥️ Fetch HW data:", JSON.stringify(data).substring(0, 200) + "..."); + pollRetries = 0; + + if (data && data.type === 'hw_stats') { + setStatus("● LIVE", "live"); + setMethod("REST 1s"); + updateDisplay(data); + } else { + console.warn("🖥️ Fetch HW data: unexpected format", data); + } + }) + .catch(function(err) { + pollRetries++; + console.warn("🖥️ Fetch HW error (" + pollRetries + "/" + MAX_RETRIES + "):", err.message); + setStatus("Retry " + pollRetries, "err"); + setMethod(""); + + // After 5 failed retries with REST, try WebSocket fallback + if (pollRetries === 5) { + console.log("🖥️ REST failed, trying WebSocket fallback..."); + startWebSocket(); + } + }); + } + + // ========== WEBSOCKET FALLBACK ========== + let ws = null; + let wsRetries = 0; + const MAX_WS_RETRIES = 10; + + function startWebSocket() { if (ws && ws.readyState === WebSocket.OPEN) return; - if (!isVisible) return; - var url = (location.protocol === 'https:' ? 'wss:' : 'ws:') + '//' + location.host + '/ws/hw_monitor'; + const protocol = location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = protocol + '//' + location.host + '/ws/hw_monitor'; + + console.log("🖥️ Trying WebSocket fallback:", url); + if (pollInterval) { clearInterval(pollInterval); pollInterval = null; } - setStatus('Connecting...', ''); ws = new WebSocket(url); - ws.onopen = function () { - setStatus('● LIVE', 'live'); - setMethod('500ms'); - reconnectAttempts = 0; + ws.onopen = function() { + console.log("🖥️ WebSocket connected ✅"); + setStatus("● LIVE", "live"); + setMethod("WS 1s"); + wsRetries = 0; }; - ws.onmessage = function (ev) { + + ws.onmessage = function(ev) { try { - var d = JSON.parse(ev.data); - if (d.type === 'hw_stats') onStats(d); - } catch (e) {} - }; - ws.onclose = function () { - setStatus('Disconnected', 'err'); - setMethod(''); - if (reconnectAttempts < MAX_RECONNECT) { - reconnectAttempts++; - setTimeout(connect, Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000)); + const d = JSON.parse(ev.data); + console.log("🖥️ WS data received"); + if (d.type === 'hw_stats') updateDisplay(d); + } catch(e) { + console.warn("🖥️ WS JSON error:", e.message); } }; - ws.onerror = function () { - if (reconnectAttempts === 0) setTimeout(connect, 2000); + + ws.onclose = function() { + console.log("🖥️ WebSocket disconnected"); + setStatus("WS Disc.", "err"); + if (wsRetries < MAX_WS_RETRIES) { + wsRetries++; + setTimeout(startWebSocket, 2000); + } + }; + + ws.onerror = function() { + console.warn("🖥️ WebSocket error"); }; } - // === DATA === - function onStats(d) { - gpuData[d.gpu_id] = d; - if (d.gpu_count && d.gpu_count !== gpuCount) gpuCount = d.gpu_count; - if (d.gpu_id === currentGpu) updateDisplay(d); - } - - function $id(id) { return document.getElementById(id); } - + // ========== DISPLAY UPDATE ========== function updateDisplay(d) { - var nameEl = $id('hw-gpu-name'); - var vendorEl = $id('hw-vendor-line'); - - if (nameEl) nameEl.textContent = d.gpu_name || ('GPU ' + d.gpu_id); + // === GPU Name === + const nameEl = $id('hw-gpu-name'); + if (nameEl) { + const newName = d.gpu_name || ('GPU ' + (d.gpu_id || 0)); + nameEl.textContent = newName; + console.log("🖥️ GPU name set to:", newName); + } + + // === Vendor === + const vendorEl = $id('hw-vendor-line'); if (vendorEl) { - var parts = []; + const 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); - vendorEl.textContent = parts.join(' | '); + vendorEl.textContent = parts.join(' | ') || '-'; } + // === Unavailable === if (!d.is_available) { - setUtil('hw-gpu-util', '--', 0); - setUtil('hw-vram', '--', 0); - setUtil('hw-temp', '--', 0); - setUtil('hw-fan', '--', 0); + console.warn("🖥️ GPU unavailable:", d.error || "no error"); + 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); @@ -256,43 +319,37 @@ return; } - // GPU Util - var util = d.gpu_utilization || 0; - setUtil('hw-gpu-util', util.toFixed(1) + '%', util); + // === GPU === + const util = d.gpu_utilization || 0; + setUtil('hw-gpu-util', Number(util).toFixed(1) + '%', util); setBar('hw-gpu-bar', util); + console.log("🖥️ GPU util:", util); - // VRAM - var vramUsed = d.vram_used_mb || 0; - var vramTotal = d.vram_total_mb || 0; - var vramShared = d.vram_shared_mb || 0; - var vramPct = d.vram_usage_pct || 0; - - if (vramTotal > 0 && vramUsed > 0) { - setUtil('hw-vram', (vramUsed/1024).toFixed(2) + ' / ' + (vramTotal/1024).toFixed(1) + ' GB', vramPct); + // === VRAM === + const vramUsed = d.vram_used_mb || 0; + const vramTotal = d.vram_total_mb || 0; + const vramPct = d.vram_usage_pct || 0; + if (vramTotal > 0) { + setUtil('hw-vram', (vramUsed / 1024).toFixed(2) + ' / ' + (vramTotal / 1024).toFixed(1) + ' GB', vramPct); setBar('hw-vram-bar', vramPct); - } else if (vramShared > 0) { - setUtil('hw-vram', (vramUsed/1024).toFixed(2) + ' / ' + (vramShared/1024).toFixed(1) + ' GB (shared)', vramPct); - setBar('hw-vram-bar', vramPct); - } else if (vramTotal > 0) { - setUtil('hw-vram', '0 / ' + (vramTotal/1024).toFixed(1) + ' GB', 0); - setBar('hw-vram-bar', 0); + console.log("🖥️ VRAM:", vramUsed, "/", vramTotal, "MB =", vramPct, "%"); } else { setUtil('hw-vram', 'N/A', 0); setBar('hw-vram-bar', 0); } - // Temp - var temp = d.temperature || 0; + // === Temp === + const temp = d.temperature || 0; if (temp > 0) { - setUtil('hw-temp', temp.toFixed(1) + '°C', temp); + setUtil('hw-temp', Number(temp).toFixed(1) + '°C', temp); setBar('hw-temp-bar', Math.min(temp, 100)); } else { setUtil('hw-temp', 'N/A', 0); setBar('hw-temp-bar', 0); } - // Fan - var fan = d.fan_speed || 0; + // === Fan === + const fan = d.fan_speed || 0; if (fan > 0) { setUtil('hw-fan', fan + '%', fan); setBar('hw-fan-bar', fan); @@ -301,110 +358,109 @@ setBar('hw-fan-bar', 0); } - // Method info - var info = d.driver || 'unknown'; + // === Method info === + let info = d.driver || 'unknown'; if (d.core_clock_mhz > 0) info += ' | ' + d.core_clock_mhz + 'MHz'; - if (d.power_draw_watts > 0) info += ' | ' + d.power_draw_watts + 'W'; setMethod(info); - setStatus('● LIVE', 'live'); - // Sparkline + // === Sparkline === if (d.history && d.history.length > 0) { - historyData = d.history; - drawSparkline(historyData); + drawSparkline(d.history); } } function setUtil(id, text, pct) { - var el = $id(id); - if (!el) return; + const el = $id(id); + if (!el) { + console.error("🖥️ DOM Element untuk", id, "tidak ditemukan!"); + return; + } el.textContent = text; el.className = 'hw-stat-value'; - if (pct !== undefined && pct !== null) { - if (pct > 85) el.classList.add('crit'); - else if (pct > 70) el.classList.add('warn'); - } + if (pct > 85) el.classList.add('crit'); + else if (pct > 70) el.classList.add('warn'); } function setBar(id, pct) { - var el = $id(id); + const 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'); + const el = $id('hw-status'); if (el) { el.textContent = text; el.className = 'hw-status ' + (cls || ''); } - var icon = widget.querySelector('.hw-icon'); - if (icon) icon.textContent = cls === 'live' ? '🟢' : cls === 'err' ? '🔴' : '🟡'; + if (widget) { + const icon = widget.querySelector('.hw-icon'); + if (icon) icon.textContent = cls === 'live' ? '🟢' : cls === 'err' ? '🔴' : '🟡'; + } } function setMethod(text) { - var el = $id('hw-method'); + const el = $id('hw-method'); if (el) el.textContent = text || ''; } - // === SPARKLINE === + // ========== SPARKLINE ========== function drawSparkline(values) { - var canvas = $id('hw-sparkline'); - if (!canvas || values.length < 2) return; - var ctx = canvas.getContext('2d'); - var w = canvas.width, h = canvas.height, pad = 3; + const canvas = $id('hw-sparkline'); + if (!canvas || !values || values.length < 2) return; + + const ctx = canvas.getContext('2d'); + const w = canvas.width, h = canvas.height, pad = 3; ctx.clearRect(0, 0, w, h); - ctx.strokeStyle = 'rgba(255,255,255,0.04)'; - ctx.lineWidth = 1; - for (var y = 0; y <= 3; y++) { - ctx.beginPath(); ctx.moveTo(0, pad + (y/3)*(h-pad*2)); ctx.lineTo(w, pad + (y/3)*(h-pad*2)); ctx.stroke(); - } + const dataMax = Math.max.apply(null, values); + const max = dataMax > 80 ? 100 : (dataMax < 1 ? 100 : dataMax * 1.2); + const pw = w - pad * 2, ph = h - pad * 2; - var dataMax = Math.max.apply(null, values); - var max = dataMax > 80 ? 100 : (dataMax < 1 ? 100 : dataMax * 1.2); - var pw = w - pad * 2, ph = h - pad * 2; - - // Area fill 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; + for (let i = 0; i < values.length; i++) { + const x = pad + (i / (values.length - 1)) * pw; + const 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); + const grad = ctx.createLinearGradient(0, 0, 0, h); grad.addColorStop(0, '#00e67633'); grad.addColorStop(1, '#00e67605'); ctx.fillStyle = grad; ctx.fill(); - // Line ctx.beginPath(); ctx.strokeStyle = '#00e676'; 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; + for (let i = 0; i < values.length; i++) { + const x = pad + (i / (values.length - 1)) * pw; + const y = h - pad - (values[i] / max) * ph; if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); } ctx.stroke(); - // Dot - var lx = pad + pw; - var ly = h - pad - (values[values.length-1] / max) * ph; - ctx.beginPath(); ctx.arc(lx, ly, 3, 0, Math.PI * 2); + const lx = pad + pw; + const ly = h - pad - (values[values.length - 1] / max) * ph; + ctx.beginPath(); + ctx.arc(lx, ly, 3, 0, Math.PI * 2); ctx.fillStyle = '#00e676'; ctx.fill(); - - // Label - ctx.fillStyle = '#00e676'; - ctx.font = 'bold 10px monospace'; - ctx.textAlign = 'right'; - ctx.textBaseline = 'top'; - ctx.fillText(values[values.length-1].toFixed(0) + '%', w - pad, pad); } - // === START === - if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); - else init(); + // ========== START ========== + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + // Fallback startup - try again if first attempt missed + setTimeout(function() { + if (!document.getElementById('bangtrix-hw-monitor')) { + console.log("🖥️ Bangtrix HW Monitor: fallback init..."); + init(); + } + }, 3000); + + console.log("🖥️ Bangtrix HW Monitor: loaded ✅"); })(); \ No newline at end of file