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 tok = getCookie("panel_token");
var ws = new WebSocket("ws://" + location.host + base + "/api/logs/ws?token=" + (tok || ""));
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;
}
};