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
+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); }
}