The server name is already shown in the navbar — no need to repeat it as a heading on the page. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
188 lines
7.0 KiB
JavaScript
188 lines
7.0 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 id="si-fields" class="col-sm-12 row"><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>' +
|
|
'<div class="col-sm-3">' +
|
|
'<div class="card mb-3">' +
|
|
'<div class="card-header">' +
|
|
'<h4 class="card-title mb-0">Players</h4>' +
|
|
'</div>' +
|
|
'<div id="playersCollapse" class="collapse show bottom-row">' +
|
|
'<div class="card-body"><svg id="chart-players" style="height:250px;width:100%"></svg></div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="col-sm-9">' +
|
|
'<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=".bottom-row">toggle</button>' +
|
|
'<h4 class="card-title mb-0">Server Networking</h4>' +
|
|
'</div>' +
|
|
'<div id="networkCollapse" class="collapse show bottom-row">' +
|
|
'<div class="card-body"><svg id="chart-network" 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(['#e879f9', '#22d3ee']);
|
|
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 })
|
|
.color(['#c084fc', '#fbbf24']);
|
|
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)
|
|
.donutLabelsOutside(false)
|
|
.duration(500)
|
|
.height(250)
|
|
.color(['#a78bfa', '#7c3aed', '#e879f9', '#2d1f3d'])
|
|
.margin({ top: 5, right: 5, bottom: 5, left: 5 });
|
|
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 ----
|
|
// Subscribe to header's serverinfo updates (no redundant RCON request)
|
|
function onServerInfo(e) { updateData(e.detail); }
|
|
document.addEventListener('serverinfo-update', onServerInfo);
|
|
|
|
// If header already has cached data, use it immediately
|
|
if (window._lastServerInfo) updateData(window._lastServerInfo);
|
|
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
|
|
return function cleanup() {
|
|
document.removeEventListener('serverinfo-update', onServerInfo);
|
|
window._perfChart = null;
|
|
window._netChart = null;
|
|
window._playersChart = null;
|
|
};
|
|
}
|