Files
rust-rcon/js/pages/serverInfo.js
T
angryzeroandCopilot 27d67f73aa fix: start polling synchronously, decoupled from nv.addGraph
Move startPolling() before nv.addGraph calls so it fires immediately
after DOM setup. This eliminates the race where nv.addGraph's async
render loop could delay or silently swallow the polling start. Charts
still initialize via nv.addGraph and have null guards in updateCharts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-06-13 18:19:03 +02:00

200 lines
7.2 KiB
JavaScript

/**
* Server info page — live dashboard with D3/NVD3 charts.
*/
function ServerInfoPage(params, container) {
var DATA_LIMIT = 100;
var recordedData = [];
var html =
'<div id="ServerInfoController">' +
'<div class="row">' +
'<div class="col-sm-12"><h2 id="si-hostname">Server Info</h2></div>' +
'<div id="si-fields" class="col-sm-12"><span class="text-white-50">Loading…</span></div>' +
'</div>' +
'<div class="row">' +
'<div class="col-sm-12">' +
'<div class="card mb-3">' +
'<div class="card-header">' +
'<button class="btn btn-sm btn-outline-primary float-end" role="button" data-bs-toggle="collapse" data-bs-target="#performanceCollapse">toggle</button>' +
'<h4 class="card-title mb-0">Server Performance</h4>' +
'</div>' +
'<div id="performanceCollapse" class="collapse show">' +
'<div class="card-body"><svg id="chart-performance" style="height:200px;width:100%"></svg></div>' +
'</div>' +
'</div>' +
'<div class="card mb-3">' +
'<div class="card-header">' +
'<button class="btn btn-sm btn-outline-primary float-end" role="button" data-bs-toggle="collapse" data-bs-target="#networkCollapse">toggle</button>' +
'<h4 class="card-title mb-0">Server Networking</h4>' +
'</div>' +
'<div id="networkCollapse" class="collapse show">' +
'<div class="card-body"><svg id="chart-network" style="height:250px;width:100%"></svg></div>' +
'</div>' +
'</div>' +
'<div class="card mb-3">' +
'<div class="card-header">' +
'<button class="btn btn-sm btn-outline-primary float-end" role="button" data-bs-toggle="collapse" data-bs-target="#playersCollapse">toggle</button>' +
'<h4 class="card-title mb-0">Players</h4>' +
'</div>' +
'<div id="playersCollapse" class="collapse show">' +
'<div class="card-body"><svg id="chart-players" style="height:250px;width:100%"></svg></div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
container.innerHTML = html;
// Start data polling immediately — charts will render when ready
if (Rcon.isConnected()) startPolling();
else {
var _c = function () { startPolling(); Rcon.off('connected', _c); };
Rcon.on('connected', _c);
}
// ---- NVD3 Charts ----
nv.addGraph(function () {
var perfChart = nv.models.multiChart()
.margin({ top: 30, right: 75, bottom: 40, left: 75 })
.color(['darkred', 'yellowgreen']);
perfChart.yAxis1.axisLabel('Framerate').tickFormat(d3.format(',d'));
perfChart.yAxis2.axisLabel('Entities').tickFormat(d3.format(',d'));
perfChart.xAxis.axisLabel('Time').tickFormat(function (d) { return d3.time.format('%H:%M:%S')(new Date(d)); });
perfChart.lines1.duration(0);
perfChart.lines2.duration(0);
d3.select('#chart-performance').datum([
{ key: 'Framerate', type: 'line', yAxis: 1, values: [] },
{ key: 'Entities', type: 'line', yAxis: 2, values: [] }
]).call(perfChart);
window._perfChart = perfChart;
});
nv.addGraph(function () {
var netChart = nv.models.stackedAreaChart()
.margin({ top: 0, right: 75, bottom: 40, left: 75 });
netChart.yAxis.axisLabel('Network').tickFormat(function (bytes) {
var fmt = d3.format('.0f');
if (bytes < 1024) return fmt(bytes) + 'B';
if (bytes < 1048576) return fmt(bytes / 1024) + 'kB';
if (bytes < 1073741824) return fmt(bytes / 1048576) + 'MB';
return fmt(bytes / 1073741824) + 'GB';
});
netChart.xAxis.axisLabel('Time').tickFormat(function (d) { return d3.time.format('%H:%M:%S')(new Date(d)); });
d3.select('#chart-network').datum([
{ key: 'IN', values: [] },
{ key: 'OUT', values: [] }
]).call(netChart);
window._netChart = netChart;
});
nv.addGraph(function () {
var playersChart = nv.models.pieChart()
.x(function (d) { return d.key; })
.y(function (d) { return d.y; })
.showLabels(true)
.donut(true)
.duration(500)
.height(250)
.margin({ top: 0, right: 75, bottom: 0, left: 75 });
playersChart.pie.startAngle(function (d) { return d.startAngle / 2 - Math.PI / 2; });
playersChart.pie.endAngle(function (d) { return d.endAngle / 2 - Math.PI / 2; });
d3.select('#chart-players').datum([]).call(playersChart);
window._playersChart = playersChart;
});
// ---- Data polling ----
var pollTimer = null;
function refresh() {
Rcon.request('serverinfo', function (msg) {
updateData(JSON.parse(msg.Message));
});
}
function updateData(data) {
// Guard: element may be gone if navigated away
var fieldsEl = document.getElementById('si-fields');
if (!fieldsEl) return;
// Update text fields
var fieldsHtml = '';
for (var key in data) {
if (data.hasOwnProperty(key)) {
var val = data[key];
if (typeof val === 'number') val = formatNumber(val);
fieldsHtml += '<div class="col-sm-4"><p><span class="text-white-50">' + key + ':</span> <span>' + val + '</span></p></div>';
}
}
fieldsEl.innerHTML = fieldsHtml;
if (data.Hostname) {
var hn = document.getElementById('si-hostname');
if (hn) hn.textContent = data.Hostname;
}
// Collect chart data
recordedData.push({ ts: Date.now(), data: data });
if (recordedData.length > DATA_LIMIT) {
recordedData = recordedData.slice(recordedData.length - DATA_LIMIT);
}
updateCharts(data);
}
function updateCharts(data) {
// Guard: SVG elements may be gone if navigated away
// Players pie chart
if (window._playersChart) {
var pc = d3.select('#chart-players');
if (!pc.empty()) {
pc.datum([
{ key: 'Queued', y: data.Queued || 0 },
{ key: 'Joining', y: data.Joining || 0 },
{ key: 'Players', y: data.Players || 0 },
{ key: 'Free', y: (data.MaxPlayers || 0) - ((data.Joining || 0) + (data.Players || 0)) }
]).call(window._playersChart);
}
}
// Performance chart
var fpsVals = [], entVals = [], netInVals = [], netOutVals = [];
for (var i = 0; i < recordedData.length; i++) {
var r = recordedData[i];
fpsVals.push({ x: r.ts, y: r.data.Framerate || 0 });
entVals.push({ x: r.ts, y: r.data.EntityCount || 0 });
netInVals.push({ x: r.ts, y: r.data.NetworkIn || 0 });
netOutVals.push({ x: r.ts, y: r.data.NetworkOut || 0 });
}
if (window._perfChart) {
d3.select('#chart-performance').datum([
{ key: 'Framerate', type: 'line', yAxis: 1, values: fpsVals },
{ key: 'Entities', type: 'line', yAxis: 2, values: entVals }
]).call(window._perfChart);
}
if (window._netChart) {
d3.select('#chart-network').datum([
{ key: 'IN', values: netInVals },
{ key: 'OUT', values: netOutVals }
]).call(window._netChart);
}
}
function startPolling() {
refresh();
pollTimer = setInterval(refresh, 1000);
}
return function cleanup() {
if (pollTimer) clearInterval(pollTimer);
window._perfChart = null;
window._netChart = null;
window._playersChart = null;
};
}