feat: full de-Angularization with Bootstrap 5 migration

- Removed AngularJS 1.4.8 completely — no more framework dependency
- Extracted core RCON logic into pure vanilla JS (js/rcon.js)
- Added lightweight hash-based SPA router (js/router.js)
- Rewrote all 6 pages as vanilla JS modules in js/pages/
- Migrated Bootstrap 3 → Bootstrap 5 (cards instead of panels, bootstrap-icons instead of glyphicons)
- Removed jQuery dependency (Bootstrap 5 is vanilla JS)
- Purged all Angular templates from html/
- Removed npm artifacts (package.json, node_modules)
- Added nginx.example.conf for static hosting
- Updated rcon.css dark theme for BS5 variables
- Static file only — no Node.js backend required

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-06-13 17:51:50 +02:00
co-authored by Copilot
parent 0fd94044a8
commit 0a178d17b9
32 changed files with 1242 additions and 1702 deletions
+73
View File
@@ -0,0 +1,73 @@
/**
* Chat page — live chat viewer with message sending.
*/
function ChatPage(params, container) {
var address = Rcon.getAddress() || '';
var html =
'<div id="ChatController">' +
'<div id="chat-output" style="height:600px" class="Output scrollable"></div>' +
'<form id="chat-form">' +
'<input id="chat-input" class="form-control" style="width:100%" placeholder="Type a message...">' +
'</form>' +
'</div>';
container.innerHTML = html;
var outputEl = document.getElementById('chat-output');
var inputEl = document.getElementById('chat-input');
function addMessage(msg) {
var div = document.createElement('div');
var t = new Date((msg.Time || 0) * 1000);
var timeStr = t.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
var ts = document.createElement('span');
ts.style.cssText = 'min-width:60px;display:inline-block;';
ts.textContent = timeStr;
div.appendChild(ts);
var ns = document.createElement('span');
ns.style.cssText = 'min-width:130px;display:inline-block;text-align:right;';
var a = document.createElement('a');
a.href = '#/' + address + '/player/' + (msg.UserId || '');
a.textContent = stripHtml(msg.Username || '') + ' :';
ns.appendChild(a);
div.appendChild(ns);
var ms = document.createElement('span');
ms.style.display = 'inline-block';
ms.textContent = stripHtml(msg.Message || '');
div.appendChild(ms);
outputEl.appendChild(div);
if (isScrolledToBottom(outputEl)) outputEl.scrollTop = outputEl.scrollHeight;
}
function onSubmit() {
var cmd = inputEl.value.trim();
if (!cmd) return;
Rcon.command('say ' + cmd, 1);
inputEl.value = '';
}
function onMessage(msg) {
if (msg.Type !== 'Chat') return;
addMessage(JSON.parse(msg.Message));
}
function fetchHistory() {
Rcon.request('chat.tail 512', function (response) {
var messages = JSON.parse(response.Message);
for (var i = 0; i < messages.length; i++) addMessage(messages[i]);
outputEl.scrollTop = outputEl.scrollHeight;
});
}
document.getElementById('chat-form').addEventListener('submit', function (e) { e.preventDefault(); onSubmit(); });
Rcon.on('message', onMessage);
if (Rcon.isConnected()) fetchHistory();
else { var _c = function () { fetchHistory(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
return function cleanup() { Rcon.off('message', onMessage); };
}
+142
View File
@@ -0,0 +1,142 @@
/**
* Connection page — connect form with localStorage persistence.
*/
function ConnectPage(params, container) {
var html =
'<div class="row">' +
'<div class="offset-md-4 col-md-6" style="margin-top: 128px">' +
'<div class="card p-4 mb-4">' +
'<h2>Connect</h2>' +
'<div class="alert alert-danger" id="connect-error" style="display:none"></div>' +
'<p>Enter the address (including rcon port) and the rcon password below to connect.</p>' +
'<form id="connect-form">' +
'<div class="mb-3">' +
'<label for="connection-address" class="form-label">Address</label>' +
'<input required minlength="5" id="connection-address" class="form-control" placeholder="xxx.xxx.xxx.xxx:28016">' +
'</div>' +
'<div class="mb-3">' +
'<label for="connection-password" class="form-label">Password</label>' +
'<input required minlength="2" type="password" id="connection-password" class="form-control">' +
'</div>' +
'<div class="form-check float-end">' +
'<input type="checkbox" class="form-check-input" id="connection-save" checked>' +
'<label class="form-check-label" for="connection-save">Save Connection</label>' +
'</div>' +
'<button type="submit" class="btn btn-primary w-100">Connect</button>' +
'</form>' +
'</div>' +
'<div class="card p-4">' +
'<div id="saved-connections"></div>' +
'</div>' +
'</div>' +
'</div>';
container.innerHTML = html;
var addrInput = document.getElementById('connection-address');
var passInput = document.getElementById('connection-password');
var saveCheck = document.getElementById('connection-save');
var errorDiv = document.getElementById('connect-error');
var savedDiv = document.getElementById('saved-connections');
var connectionsLimit = 5;
function loadConnections() {
try { return JSON.parse(localStorage.previousConnections || '[]'); }
catch (e) { return []; }
}
function saveConnections(conns) {
localStorage.previousConnections = JSON.stringify(conns);
}
function showError(msg) {
errorDiv.textContent = msg;
errorDiv.style.display = 'block';
}
function hideError() { errorDiv.style.display = 'none'; }
function addConnection(addr, pass) {
var conns = loadConnections();
conns = conns.filter(function (c) { return c.Address !== addr; });
conns.push({ Address: addr, Password: pass, date: new Date().toISOString() });
saveConnections(conns);
}
function doConnect(addr, pass) {
hideError();
Rcon.connect(addr, pass);
}
function renderSaved() {
var conns = loadConnections();
if (conns.length === 0) { savedDiv.innerHTML = ''; return; }
var display = conns.slice().reverse();
var shown = connectionsLimit || display.length;
var h = '<h2>Saved Connections</h2><ul>';
for (var i = 0; i < Math.min(shown, display.length); i++) {
var c = display[i];
h += '<li><a href="#" class="saved-conn" data-addr="' + c.Address + '" data-pass="' + c.Password + '">' + c.Address + '</a> — ' + new Date(c.date).toLocaleString() + '</li>';
}
h += '</ul>';
if (conns.length > 5) {
h += '<button type="button" class="btn btn-primary btn-block" id="toggle-conn-limit">Show ' + (connectionsLimit ? 'more' : 'less') + '</button>';
}
savedDiv.innerHTML = h;
var links = savedDiv.querySelectorAll('.saved-conn');
for (var j = 0; j < links.length; j++) {
links[j].addEventListener('click', function (e) {
e.preventDefault();
doConnect(this.dataset.addr, this.dataset.pass);
});
}
var toggle = document.getElementById('toggle-conn-limit');
if (toggle) {
toggle.addEventListener('click', function () {
connectionsLimit = connectionsLimit ? undefined : 5;
renderSaved();
});
}
}
document.getElementById('connect-form').addEventListener('submit', function (e) {
e.preventDefault();
var addr = addrInput.value.trim();
var pass = passInput.value.trim();
if (!addr || !pass) return;
if (saveCheck.checked) addConnection(addr, pass);
doConnect(addr, pass);
});
function onConnected() {
window.location.hash = '#/' + Rcon.getAddress() + '/info';
}
function onDisconnected(ev) {
showError('Connection was closed — Error ' + (ev ? ev.code : 'unknown'));
}
Rcon.on('connected', onConnected);
Rcon.on('disconnected', onDisconnected);
renderSaved();
// Auto-connect from URL — check hash for address and password
var hash = window.location.hash;
var pwMatch = hash.match(/[?&]password=([^&]+)/);
if (pwMatch) {
passInput.value = decodeURIComponent(pwMatch[1]);
window.location.hash = hash.replace(/[?&]password=[^&]+/, '');
}
var addrMatch = hash.match(/^#\/([^/]+)\//);
if (addrMatch) addrInput.value = addrMatch[1];
if (addrInput.value && passInput.value) {
if (saveCheck.checked) addConnection(addrInput.value.trim(), passInput.value.trim());
doConnect(addrInput.value.trim(), passInput.value.trim());
}
return function cleanup() {
Rcon.off('connected', onConnected);
Rcon.off('disconnected', onDisconnected);
};
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Console page — live console output with command input and history.
*/
function ConsolePage(params, container) {
var html =
'<div id="ConsoleController">' +
'<div id="console-output" style="height:600px" class="Output scrollable"></div>' +
'<form id="console-form">' +
'<input type="text" id="console-input" class="form-control" autocomplete="off">' +
'</form>' +
'</div>';
container.innerHTML = html;
var outputEl = document.getElementById('console-output');
var inputEl = document.getElementById('console-input');
var commandHistory = [];
var historyIndex = 0;
function addOutput(msg) {
var div = document.createElement('div');
div.className = msg.Type;
div.textContent = stripHtml(msg.Message);
outputEl.appendChild(div);
if (isScrolledToBottom(outputEl)) outputEl.scrollTop = outputEl.scrollHeight;
}
function onSubmit() {
var cmd = inputEl.value.trim();
if (!cmd) return;
addOutput({ Message: cmd, Type: 'Command' });
commandHistory.push(cmd);
historyIndex = commandHistory.length;
Rcon.command(cmd, 1);
inputEl.value = '';
}
function onKeyDown(e) {
if (commandHistory.length === 0) return;
if (e.keyCode === 38) { // Up
historyIndex--;
if (historyIndex < 0) historyIndex = commandHistory.length - 1;
inputEl.value = commandHistory[historyIndex] || '';
e.preventDefault();
} else if (e.keyCode === 40) { // Down
historyIndex++;
if (historyIndex >= commandHistory.length) historyIndex = 0;
inputEl.value = commandHistory[historyIndex] || '';
e.preventDefault();
} else {
historyIndex = commandHistory.length;
}
}
function onMessage(msg) {
if (msg.Message && msg.Message.indexOf('[rcon] ') === 0) return;
switch (msg.Type) {
case 'Generic': case 'Log': case 'Error': case 'Warning':
addOutput(msg); break;
}
}
function fetchHistory() {
Rcon.request('console.tail 128', function (response) {
var messages = JSON.parse(response.Message);
for (var i = 0; i < messages.length; i++) onMessage(messages[i]);
outputEl.scrollTop = outputEl.scrollHeight;
});
}
document.getElementById('console-form').addEventListener('submit', function (e) {
e.preventDefault(); onSubmit();
});
inputEl.addEventListener('keydown', onKeyDown);
Rcon.on('message', onMessage);
if (Rcon.isConnected()) fetchHistory();
else { var _c = function () { fetchHistory(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
return function cleanup() { Rcon.off('message', onMessage); };
}
+70
View File
@@ -0,0 +1,70 @@
/**
* Player info page — single player detail with quirk fixes.
*/
function PlayerInfoPage(params, container) {
var userid = params[2]; // SteamID from URL
var html =
'<div id="PlayerInfoController">' +
'<div class="row">' +
'<div class="col-lg-12">' +
'<h2 class="text-center">' +
'Player: <span id="pi-username">' + userid + '</span>' +
'<button id="pi-refresh" class="btn btn-secondary float-end" type="button"><i class="bi bi-arrow-clockwise"></i></button>' +
'</h2>' +
'<div class="d-grid mb-3">' +
'<a href="http://www.steamcommunity.com/profiles/' + userid + '" target="_blank" class="btn btn-secondary">' +
'<i class="bi bi-info-circle-fill"></i> Steam Profile' +
'</a>' +
'</div>' +
'</div>' +
'<div class="col-lg-12">' +
'<h2 class="text-center">Info</h2>' +
'<div id="pi-notfound" class="alert alert-warning" style="display:none">This Player is currently not connected to this server!</div>' +
'</div>' +
'<div id="pi-fields" class="col-sm-12"></div>' +
'</div>' +
'</div>';
container.innerHTML = html;
function refresh() {
Rcon.getPlayers(function (players) {
var info = null;
for (var i = 0; i < players.length; i++) {
if (players[i].SteamID === userid) { info = players[i]; break; }
}
if (!info) {
document.getElementById('pi-notfound').style.display = 'block';
document.getElementById('pi-fields').innerHTML = '';
return;
}
document.getElementById('pi-notfound').style.display = 'none';
// Fix known Rust RCON quirk: VoiationLevel → ViolationLevel
if (info.VoiationLevel !== undefined) {
info.ViolationLevel = info.VoiationLevel;
delete info.VoiationLevel;
}
// Remove XP remnants
delete info.CurrentLevel;
delete info.UnspentXp;
document.getElementById('pi-username').textContent = info.DisplayName || userid;
var fieldsHtml = '';
for (var key in info) {
if (info.hasOwnProperty(key)) {
fieldsHtml += '<div class="col-sm-4"><p>' + key + ': <span class="badge">' + info[key] + '</span></p></div>';
}
}
document.getElementById('pi-fields').innerHTML = fieldsHtml;
});
}
document.getElementById('pi-refresh').addEventListener('click', refresh);
if (Rcon.isConnected()) refresh();
else { var _c = function () { refresh(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
}
+141
View File
@@ -0,0 +1,141 @@
/**
* Player list page — table with sort, filter, and kick.
*/
function PlayerListPage(params, container) {
var address = Rcon.getAddress() || '';
var html =
'<div id="PlayerListController">' +
'<div class="row mb-3">' +
'<div class="col-6">' +
'<input id="player-filter" class="form-control" placeholder="Filter..">' +
'</div>' +
'<div class="col-6">' +
'<button id="player-refresh" class="btn btn-secondary float-end" type="button"><i class="bi bi-arrow-clockwise"></i></button>' +
'</div>' +
'</div>' +
'<div class="row">' +
'<div class="col-sm-12">' +
'<div class="table-responsive">' +
'<table class="table" id="player-table">' +
'<thead>' +
'<tr>' +
'<th>Actions</th>' +
'<th class="sortable" data-field="DisplayName">Username</th>' +
'<th class="sortable text-end" data-field="SteamID">Steam ID</th>' +
'<th class="sortable text-end" data-field="VoiationLevel">Violation Level</th>' +
'<th class="sortable text-end" data-field="Address">Address</th>' +
'<th class="sortable text-end" data-field="Ping">Ping</th>' +
'<th class="sortable text-end" data-field="ConnectedSeconds">Duration</th>' +
'</tr>' +
'</thead>' +
'<tbody id="player-tbody"></tbody>' +
'</table>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
container.innerHTML = html;
var players = [];
var orderBy = '-ConnectedSeconds';
var filterText = '';
function sortPlayers(a, b) {
var desc = orderBy[0] === '-';
var field = desc ? orderBy.substring(1) : orderBy;
var va = a[field], vb = b[field];
if (typeof va === 'string') va = va.toLowerCase();
if (typeof vb === 'string') vb = vb.toLowerCase();
if (va < vb) return desc ? 1 : -1;
if (va > vb) return desc ? -1 : 1;
return 0;
}
function matchesFilter(p) {
if (!filterText) return true;
var ft = filterText.toLowerCase();
return (p.DisplayName && p.DisplayName.toLowerCase().indexOf(ft) !== -1) ||
(p.SteamID && p.SteamID.indexOf(ft) !== -1) ||
(p.Address && p.Address.toLowerCase().indexOf(ft) !== -1);
}
function render() {
var filtered = players.filter(matchesFilter).sort(sortPlayers);
var tbody = document.getElementById('player-tbody');
var html = '';
for (var i = 0; i < filtered.length; i++) {
var p = filtered[i];
var fadeClass = (p.VoiationLevel === 0) ? ' class="fade"' : '';
html +=
'<tr>' +
'<td>' +
'<div class="btn-group">' +
'<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown">' +
'<i class="bi bi-gear-fill"></i>' +
'</button>' +
'<ul class="dropdown-menu">' +
'<li><a class="dropdown-item" href="http://www.steamcommunity.com/profiles/' + p.SteamID + '" target="_blank"><i class="bi bi-info-circle-fill"></i> Steam Profile</a></li>' +
'<li><hr class="dropdown-divider"></li>' +
'<li><a class="dropdown-item kick-player" href="#" data-id="' + p.SteamID + '"><i class="bi bi-trash-fill"></i> Kick</a></li>' +
'</ul>' +
'</div>' +
'</td>' +
'<td><a href="#/' + address + '/player/' + p.SteamID + '/">' + (p.DisplayName || '') + '</a></td>' +
'<td class="text-end"><a href="#/' + address + '/player/' + p.SteamID + '/">' + p.SteamID + '</a></td>' +
'<td class="text-end"' + fadeClass + '>' + (p.VoiationLevel || 0) + '</td>' +
'<td class="text-end">' + (p.Address || '') + '</td>' +
'<td class="text-end">' + (p.Ping || 0) + '</td>' +
'<td class="text-end">' + secondsToDuration(p.ConnectedSeconds || 0) + '</td>' +
'</tr>';
}
tbody.innerHTML = html;
// Bind kick buttons
var kicks = tbody.querySelectorAll('.kick-player');
for (var j = 0; j < kicks.length; j++) {
kicks[j].addEventListener('click', function (e) {
e.preventDefault();
Rcon.command('kick ' + this.dataset.id);
refresh();
});
}
}
function refresh() {
Rcon.getPlayers(function (list) {
players = list;
render();
});
}
function updateSortHeaders() {
var headers = document.querySelectorAll('#player-table .sortable');
for (var i = 0; i < headers.length; i++) {
headers[i].className = headers[i].className.replace(/sorting(\s+descending)?/g, '').trim();
var field = headers[i].dataset.field;
if (orderBy === field) headers[i].className += ' sorting';
else if (orderBy === '-' + field) headers[i].className += ' sorting descending';
}
}
document.getElementById('player-refresh').addEventListener('click', refresh);
document.getElementById('player-filter').addEventListener('input', function () {
filterText = this.value;
render();
});
// Sort header clicks
var sortHeaders = document.querySelectorAll('#player-table .sortable');
for (var k = 0; k < sortHeaders.length; k++) {
sortHeaders[k].addEventListener('click', function () {
var field = this.dataset.field;
orderBy = (orderBy === field) ? '-' + field : field;
updateSortHeaders();
render();
});
}
if (Rcon.isConnected()) { refresh(); updateSortHeaders(); }
else { var _c = function () { refresh(); updateSortHeaders(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
}
+188
View File
@@ -0,0 +1,188 @@
/**
* 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"></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"></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"></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"></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'])
.useInteractiveGuideline(true)
.duration(0);
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 })
.useInteractiveGuideline(true)
.duration(0);
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) {
// 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-muted">' + key + ':</span> <span>' + val + '</span></p></div>';
}
}
document.getElementById('si-fields').innerHTML = fieldsHtml;
if (data.Hostname) document.getElementById('si-hostname').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) {
// Players pie chart
if (window._playersChart) {
d3.select('#chart-players').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);
}
if (Rcon.isConnected()) startPolling();
else { var _c = function () { startPolling(); Rcon.off('connected', _c); }; Rcon.on('connected', _c); }
return function cleanup() {
if (pollTimer) clearInterval(pollTimer);
window._perfChart = null;
window._netChart = null;
window._playersChart = null;
};
}