Files
rust-rcon/js/pages/serverInfo.js
T
angryzeroandCopilot 7ea16d4677 fix: defer startPolling until all NVD3 charts initialized
nv.addGraph uses setTimeout internally, so chart models may not exist yet
when the first WebSocket response arrives. Moved startPolling() into the
last nv.addGraph callback to guarantee charts are ready before data flows.

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

197 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;
// ---- 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;
// All charts ready — start polling data
if (Rcon.isConnected()) startPolling();
else { var _c = function () { startPolling(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
});
// ---- 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;
};
}