AMD WORK NOW
This commit is contained in:
+67
-20
@@ -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}")
|
||||
+18
-17
@@ -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:
|
||||
|
||||
+85
-136
@@ -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
|
||||
"""Get singleton instance."""
|
||||
return HardwareMonitorServer()
|
||||
+246
-190
@@ -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 =
|
||||
'<div class="hw-header">' +
|
||||
'<span class="hw-icon">🖥️</span>' +
|
||||
'<span class="hw-title">HW Monitor</span>' +
|
||||
@@ -59,19 +78,22 @@
|
||||
'<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>' +
|
||||
'<canvas class="hw-sparkline" id="hw-sparkline" width="232" height="36"></canvas>' +
|
||||
'<div class="hw-sparkline-container">' +
|
||||
'<canvas class="hw-sparkline" id="hw-sparkline" width="232" height="36"></canvas>' +
|
||||
'</div>' +
|
||||
'<div class="hw-status-bar">' +
|
||||
'<span class="hw-status" id="hw-status">Connecting...</span>' +
|
||||
'<span class="hw-status" id="hw-status">Starting...</span>' +
|
||||
'<span class="hw-method" id="hw-method"></span>' +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
|
||||
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 ✅");
|
||||
})();
|
||||
Reference in New Issue
Block a user