window.LogsModule = { ws: null, reconnectTimer: null, reconnectDelay: 1000, maxLines: 500, lineCount: 0, batchBuffer: [], batchTimer: null, init: function() { var self = this; self.box = document.getElementById("log-box"); if (!self.box) return; self.box.innerHTML = ""; self.lineCount = 0; self._connect(); }, _connect: function() { var self = this; if (self.ws) { self.ws.onclose = null; self.ws.close(); self.ws = null; } var base = window.location.pathname.split("/").slice(0, 2).join("/"); var proto = location.protocol === 'https:' ? 'wss://' : 'ws://'; var ws = new WebSocket(proto + location.host + base + "/api/logs/ws"); self.ws = ws; ws.onopen = function() { self.reconnectDelay = 1000; self._appendHTML('
🟢 Connected
'); }; ws.onmessage = function(e) { try { var d = JSON.parse(e.data); if (d.type === "log") { self._bufferLog(d); } } catch(ex) {} }; ws.onclose = function() { self.ws = null; self._appendHTML('
🔴 Disconnected — reconnecting...
'); self.reconnectTimer = setTimeout(function() { self.reconnectDelay = Math.min(self.reconnectDelay * 1.5, 15000); self._connect(); }, self.reconnectDelay); }; ws.onerror = function() { ws.close(); }; }, /* Buffer log entries then flush at ~30fps to avoid DOM thrashing */ _bufferLog: function(d) { var self = this; self.batchBuffer.push(d); if (!self.batchTimer) { self.batchTimer = setTimeout(function() { self._flush(); self.batchTimer = null; }, 33); // ~30fps flush } }, _flush: function() { var self = this; var batch = self.batchBuffer; self.batchBuffer = []; if (!batch.length || !self.box) return; var html = ''; for (var i = 0; i < batch.length; i++) { var d = batch[i]; var cls = d.level === "ERROR" ? "log-ERROR" : d.level === "WARNING" ? "log-WARNING" : "log-INFO"; var t = d.timestamp ? new Date(d.timestamp * 1000).toLocaleTimeString() : "--"; html += '
' + '' + t + '' + '[' + d.level + '] ' + self._escapeHtml(d.message || '') + '
'; } self._appendHTML(html); }, _appendHTML: function(html) { var self = this; if (!self.box) return; // Estimate line count from
tags var newLines = (html.match(/
self.maxLines && self.box.firstChild) { // Count lines in the first child var removed = 1; if (self.box.firstChild.nodeType === 1) { var inner = self.box.firstChild.innerHTML || ''; removed = Math.max(1, (inner.match(/
/g, '>') .replace(/"/g, '"') .replace(/'/g, '''); }, destroy: function() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.batchTimer) { clearTimeout(this.batchTimer); this.batchTimer = null; } if (this.ws) { this.ws.onclose = null; this.ws.close(); this.ws = null; } this.batchBuffer = []; this.box = null; } };