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:
@@ -1,17 +0,0 @@
|
||||
<div ng-controller="ChatController" id="ChatController">
|
||||
|
||||
<div style="height: 600px" class="Output scrollable">
|
||||
<div ng-repeat="line in Output" ng-class="line.Class">
|
||||
<span style="min-width: 60px; display:inline-block;">{{(line.Time * 1000) | date:'shortTime'}}</span>
|
||||
<span style="min-width: 130px; display:inline-block; text-align: right;"><a href="#/{{address}}/player/{{line.UserId}}/">{{line.Username}} :</a></span>
|
||||
<span style="display:inline-block;">{{line.Message}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form ng-submit="SubmitCommand()" class="md-inline-form">
|
||||
<div>
|
||||
<input class="form-control" ng-model="Command" style="width: 100%">
|
||||
</div>
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@@ -1,54 +0,0 @@
|
||||
<div ng-controller="ConnectionController" class="row">
|
||||
|
||||
<div class="col-md-offset-4 col-md-6 panel jumbotron" style="margin-top: 128px">
|
||||
|
||||
<h2>Connect</h2>
|
||||
|
||||
<div class="alert alert-danger" ng-show="LastErrorMessage != null">
|
||||
{{LastErrorMessage}}
|
||||
</div>
|
||||
|
||||
<p>Enter the address (including rcon port) and the rcon password below to connect.</p>
|
||||
|
||||
<form ng-submit="Connect()">
|
||||
|
||||
<div class="form-group">
|
||||
<label for="conncetion-adress">Address</label>
|
||||
<input required minlength=5 ng-model="Address" name="Address" class="form-control" placeholder="xxx.xxx.xxx.xxx:28016" id="conncetion-adress">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="conncetion-password">Password</label>
|
||||
<input required minlength=2 type="password" ng-model="Password" name="Password" class="form-control" id="conncetion-password">
|
||||
</div>
|
||||
|
||||
<div class="checkbox pull-right">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="SaveConnection"> Save Connection
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-block" ng-click="Connect()">Connect</button>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="col-md-offset-4 col-md-6 panel jumbotron">
|
||||
|
||||
<div ng-show="PreviousConnects.length > 0">
|
||||
<h2>Saved Connections</h2>
|
||||
<ul ng-repeat="c in PreviousConnects | orderBy:array:true | limitTo:connectionsLimit">
|
||||
<li>
|
||||
<a ng-click="ConnectTo( c )" href="#/{{c.Address}}/info">{{c.Address}}</a> - {{c.date | date:'short'}}
|
||||
</li>
|
||||
</ul>
|
||||
<div ng-show="PreviousConnects.length > connectionsLimit || connectionsLimit === undefined">
|
||||
<button type="submit" class="btn btn-primary btn-block" ng-click="toggleConnectionsLimit()">
|
||||
Show {{(connectionsLimit === undefined) ? 'less' : 'more'}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,11 +0,0 @@
|
||||
<div ng-controller="ConsoleController" id="ConsoleController">
|
||||
|
||||
<div style="height: 600px" class="Output scrollable">
|
||||
<div ng-repeat="line in Output" ng-class="line.Class">{{line.Message}}</div>
|
||||
</div>
|
||||
|
||||
<form ng-submit="SubmitCommand()">
|
||||
<input type="text" class="form-control" ng-model="Command" ng-keyup="KeyUp($event)" />
|
||||
</form>
|
||||
|
||||
</div>
|
||||
@@ -1,40 +0,0 @@
|
||||
<nav class="navbar navbar-default" ng-controller="ServerInfoController" ng-show="serverinfo">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navbar-collapse-header" aria-expanded="false">
|
||||
<span class="sr-only">Toggle navigation</span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
<span class="icon-bar"></span>
|
||||
</button>
|
||||
|
||||
<a class="navbar-brand" href="#">{{serverinfo.Hostname}}</a>
|
||||
</div>
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse-header">
|
||||
<p class="navbar-text" title="Uptime">
|
||||
<span class="glyphicon glyphicon-time"></span>
|
||||
{{serverinfo.Uptime|SecondsToDuration}}
|
||||
</p>
|
||||
<p class="navbar-text" title="Map Name">
|
||||
<span class="glyphicon glyphicon-globe"></span>
|
||||
{{serverinfo.Map}}
|
||||
</p>
|
||||
<p class="navbar-text" title="Current Players/Maximum Players">
|
||||
<span class="glyphicon glyphicon-user"></span>
|
||||
{{serverinfo.Players|number}}/{{serverinfo.MaxPlayers|number}}
|
||||
</p>
|
||||
<p class="navbar-text" title="Framerate">
|
||||
<span class="glyphicon glyphicon-hourglass"></span>
|
||||
{{serverinfo.Framerate|number}}fps
|
||||
</p>
|
||||
<p class="navbar-text" title="Entity Count">
|
||||
<span class="glyphicon glyphicon-oil"></span>
|
||||
{{serverinfo.EntityCount|number}}
|
||||
</p>
|
||||
|
||||
<button class="btn btn-danger navbar-btn navbar-right" type="button" ng-click="Disconnect()">Disconnect</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -1,41 +0,0 @@
|
||||
|
||||
<div ng-controller="PlayerInfoController" id="PlayerInfoController">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-lg-12">
|
||||
|
||||
<h2 class="center">
|
||||
Player: {{getUsername()}}
|
||||
<div class="btn-group pull-right">
|
||||
<button ng-click="refresh()" class="btn btn-default" type="button"><i class="glyphicon glyphicon-refresh"></i></button>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<div class="btn-group btn-group-lg btn-group-justified">
|
||||
<div class="btn-group">
|
||||
<a href="http://www.steamcommunity.com/profiles/{{userid}}" target="_blank" class="btn btn-default">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
Steam Profile
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="col-lg-12">
|
||||
<h2 class="center">
|
||||
Info
|
||||
</h2>
|
||||
|
||||
<div class="alert alert-warning" ng-show="!info">
|
||||
This Player is currently not connected to this server!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" ng-repeat="(key, value) in info">
|
||||
<p>{{key}}: <span class="badge">{{value}}</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,84 +0,0 @@
|
||||
<div ng-controller="PlayerListController" id="PlayerListController">
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-6">
|
||||
<input ng-model="FilterText" class="form-control" placeholder="Filter..">
|
||||
</div>
|
||||
<div class="col-xs-6">
|
||||
<button ng-click="Refresh()" class="btn btn-default pull-right" type="button"><span class="glyphicon glyphicon-refresh"></span></button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-12">
|
||||
<div class="table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Actions</th>
|
||||
|
||||
<th ng-click="Order( 'DisplayName' )" ng-class="SortClass( 'DisplayName' )">Username</th>
|
||||
<th class="text-right" ng-click="Order( 'SteamID' )" ng-class="SortClass( 'SteamID' )">Steam ID</th>
|
||||
<th class="text-right" ng-click="Order( 'VoiationLevel' )" ng-class="SortClass( 'VoiationLevel' )">Violation Level</th>
|
||||
<th class="text-right" ng-click="Order( 'Address' )" ng-class="SortClass( 'Address' )">Address</th>
|
||||
<th class="text-right" ng-click="Order( 'Ping' )" ng-class="SortClass( 'Ping' )">Ping</th>
|
||||
<th class="text-right" ng-click="Order( 'ConnectedSeconds' )" ng-class="SortClass( 'ConnectedSeconds' )">Duration</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tr ng-repeat="line in Players | orderBy: OrderBy | filter:FilterText" href="#/{{address}}/player/{{line.SteamID}}/">
|
||||
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-default dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||
<span class="glyphicon glyphicon-cog"></span> <span class="caret"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu">
|
||||
<li>
|
||||
<a href="http://www.steamcommunity.com/profiles/{{line.SteamID}}" target="_blank">
|
||||
<span class="glyphicon glyphicon-info-sign"></span>
|
||||
Steam Profile
|
||||
</a>
|
||||
</li>
|
||||
<li class="divider"></li>
|
||||
<li>
|
||||
<a href ng-click="KickPlayer(line.SteamID)">
|
||||
<span class="glyphicon glyphicon-trash"></span>
|
||||
Kick
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a href="#/{{address}}/player/{{line.SteamID}}/">{{line.DisplayName}}</a>
|
||||
</td>
|
||||
|
||||
<td style="text-align: right">
|
||||
<a href="#/{{address}}/player/{{line.SteamID}}/">{{line.SteamID}}</a>
|
||||
</td>
|
||||
|
||||
<td class="text-right" ng-class="{fade: line.VoiationLevel == 0 }">
|
||||
{{line.VoiationLevel}}
|
||||
</td>
|
||||
|
||||
<td class="text-right">
|
||||
{{line.Address}}
|
||||
</td>
|
||||
|
||||
<td class="text-right">
|
||||
{{line.Ping}}
|
||||
</td>
|
||||
|
||||
<td class="text-right">
|
||||
{{line.ConnectedSeconds | SecondsToDuration}}
|
||||
</td>
|
||||
|
||||
<md-divider></md-divider>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1,92 +0,0 @@
|
||||
<div ng-controller="ServerInfoController" id="ServerInfoController" data-ng-init="useCharts=true;">
|
||||
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12">
|
||||
<h2>{{serverinfo.Hostname}}</h2>
|
||||
</div>
|
||||
|
||||
<div class="col-sm-4" ng-repeat="(key, value) in serverinfo">
|
||||
<p>
|
||||
<span class="text-muted">{{key}}:</span>
|
||||
<span>{{value}}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="row">
|
||||
|
||||
<div class="col-sm-12">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" id="headingPerformance">
|
||||
<button
|
||||
class="btn btn-xs btn-primary pull-right"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#performanceCollapse"
|
||||
aria-expanded="true"
|
||||
aria-controls="performanceCollapse">
|
||||
toggle
|
||||
</button>
|
||||
<h4 class="panel-title">
|
||||
Server Performance
|
||||
</h4>
|
||||
</div>
|
||||
<div id="performanceCollapse" class="panel-collapse collapse in" aria-labelledby="headingPerformance">
|
||||
<div class="panel-body">
|
||||
<nvd3 options="performanceChart.options" data="performanceChart.data"></nvd3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" id="headingNetwork">
|
||||
|
||||
<button
|
||||
class="btn btn-xs btn-primary pull-right"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#networkCollapse"
|
||||
aria-expanded="true"
|
||||
aria-controls="networkCollapse">
|
||||
toggle
|
||||
</button>
|
||||
<h4 class="panel-title">
|
||||
Server Networking
|
||||
</h4>
|
||||
</div>
|
||||
<div id="networkCollapse" class="panel-collapse collapse in" aria-labelledby="headingNetwork">
|
||||
<div class="panel-body">
|
||||
<nvd3 options="netChart.options" data="netChart.data"></nvd3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" id="headingPlayers">
|
||||
<button
|
||||
class="btn btn-xs btn-primary pull-right"
|
||||
role="button"
|
||||
data-toggle="collapse"
|
||||
data-target="#playersCollapse"
|
||||
aria-expanded="true"
|
||||
aria-controls="playersCollapse">
|
||||
toggle
|
||||
</button>
|
||||
<h4 class="panel-title">
|
||||
Players
|
||||
</h4>
|
||||
</div>
|
||||
<div id="playersCollapse" class="panel-collapse collapse in" aria-labelledby="headingPlayers">
|
||||
<div class="panel-body">
|
||||
<nvd3 options="playersChart.options" data="playersChart.data"></nvd3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M17,12L12,17V14H8V10H12V7L17,12M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L5,8.09V15.91L12,19.85L19,15.91V8.09L12,4.15Z" /></svg>
|
||||
|
Before Width: | Height: | Size: 667 B |
@@ -1 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M3,6H21V8H3V6M3,11H21V13H3V11M3,16H21V18H3V16Z" /></svg>
|
||||
|
Before Width: | Height: | Size: 341 B |
+47
-50
@@ -1,71 +1,68 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" ng-app="RconApp">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Websocket Rcon</title>
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.2/nv.d3.min.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.css" rel="stylesheet">
|
||||
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap-theme.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.css" rel="stylesheet">
|
||||
<link href="rcon.css" rel="stylesheet">
|
||||
</head>
|
||||
<body ng-controller="RconController">
|
||||
|
||||
<div ng-include="'html/header.html'" ng-show="Connected"></div>
|
||||
|
||||
<div class="container">
|
||||
|
||||
<div ng-include="'html/connect.html'" ng-show="!Connected"></div>
|
||||
|
||||
<div ng-show="Connected" class="col-sm-2 sidebar">
|
||||
|
||||
<ul class="nav nav-sidebar">
|
||||
<li ng-repeat="page in pages" ng-class="{'active' : $route.current.Title == page.Title }">
|
||||
<a href="#{{Nav( page.originalPath )}}">{{page.Title}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
<body class="bg-dark text-light">
|
||||
|
||||
<!-- Server info header (hidden until connected) -->
|
||||
<nav class="navbar navbar-dark bg-primary" id="server-header" style="display:none">
|
||||
<div class="container">
|
||||
<a class="navbar-brand" href="#" id="header-hostname"></a>
|
||||
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbar-collapse-header">
|
||||
<span class="navbar-toggler-icon"></span>
|
||||
</button>
|
||||
<div class="collapse navbar-collapse" id="navbar-collapse-header">
|
||||
<span class="navbar-text me-3" title="Uptime" id="header-uptime"></span>
|
||||
<span class="navbar-text me-3" title="Map Name" id="header-map"></span>
|
||||
<span class="navbar-text me-3" title="Players" id="header-players"></span>
|
||||
<span class="navbar-text me-3" title="Framerate" id="header-fps"></span>
|
||||
<span class="navbar-text me-3" title="Entity Count" id="header-entities"></span>
|
||||
<button class="btn btn-danger ms-auto" type="button" id="btn-disconnect">Disconnect</button>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="col-sm-10" ng-show="Connected">
|
||||
|
||||
<h1 class="page-header">{{$route.current.Title}}</h1>
|
||||
<div class="row" ng-view></div>
|
||||
|
||||
<!-- Main layout: hidden until connected -->
|
||||
<div id="app-connected" style="display:none">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-2">
|
||||
<ul class="nav flex-column nav-sidebar" id="sidebar-nav"></ul>
|
||||
</div>
|
||||
<div class="col-md-10">
|
||||
<h1 id="page-title"></h1>
|
||||
<div id="app-view"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
|
||||
<!-- Connect page: shown when not connected -->
|
||||
<div id="app-connect" class="container"></div>
|
||||
|
||||
<!-- AngularJS -->
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-route.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-animate.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-aria.min.js"></script>
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.8/angular-messages.min.js"></script>
|
||||
|
||||
<!-- Bootstrap -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.js"></script>
|
||||
|
||||
<!-- For charts -->
|
||||
<!-- Bootstrap 5 JS (vanilla, no jQuery needed) -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
|
||||
<!-- Charts -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.16/d3.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/nvd3/1.8.1/nv.d3.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/angular-nvd3/1.0.9/angular-nvd3.min.js"></script>
|
||||
|
||||
<!-- Data manipulation -->
|
||||
<script src="js/linq.min.js"></script>
|
||||
|
||||
<!-- Our stuff -->
|
||||
<script src="js/rconService.js"></script>
|
||||
<!-- App scripts -->
|
||||
<script src="js/utils.js"></script>
|
||||
<script src="js/rcon.js"></script>
|
||||
<script src="js/router.js"></script>
|
||||
<script src="js/pages/connect.js"></script>
|
||||
<script src="js/pages/serverInfo.js"></script>
|
||||
<script src="js/pages/console.js"></script>
|
||||
<script src="js/pages/chat.js"></script>
|
||||
<script src="js/pages/playerlist.js"></script>
|
||||
<script src="js/pages/playerInfo.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
<script src="js/connection.js"></script>
|
||||
<script src="js/serverInfo.js"></script>
|
||||
<script src="js/console.js"></script>
|
||||
<script src="js/playerlist.js"></script>
|
||||
<script src="js/playerInfo.js"></script>
|
||||
<script src="js/chat.js"></script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,114 +1,110 @@
|
||||
var app = angular.module('RconApp', ['ngRoute', 'nvd3']);
|
||||
/**
|
||||
* Main application — layout management, header updates, sidebar nav, routing.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
app.service('rconService', [RconService]);
|
||||
var elConnect = document.getElementById('app-connect');
|
||||
var elConnected = document.getElementById('app-connected');
|
||||
var elHeader = document.getElementById('server-header');
|
||||
var elSidebar = document.getElementById('sidebar-nav');
|
||||
var elTitle = document.getElementById('page-title');
|
||||
|
||||
app.config(function($routeProvider) {
|
||||
$routeProvider.when("/home", {Title: "Home"});
|
||||
$routeProvider.when("/:address/info", {
|
||||
Title: "Server",
|
||||
templateUrl: "html/serverInfo.html",
|
||||
Nav: true
|
||||
});
|
||||
$routeProvider.when("/:address/console", {
|
||||
Title: "Console",
|
||||
templateUrl: "html/console.html",
|
||||
Nav: true
|
||||
});
|
||||
$routeProvider.when("/:address/chat", {
|
||||
Title: "Chat",
|
||||
templateUrl: "html/chat.html",
|
||||
Nav: true
|
||||
});
|
||||
$routeProvider.when("/:address/playerlist", {
|
||||
Title: "Player List",
|
||||
templateUrl: "html/playerlist.html",
|
||||
Nav: true
|
||||
});
|
||||
$routeProvider.when("/:address/player/:userid", {
|
||||
Title: "Player Info",
|
||||
templateUrl: "html/playerInfo.html"
|
||||
});
|
||||
$routeProvider.otherwise({redirectTo: '/home'});
|
||||
});
|
||||
// ---- Sidebar navigation ----
|
||||
var navPages = [
|
||||
{ title: 'Server', path: '/:address/info' },
|
||||
{ title: 'Console', path: '/:address/console' },
|
||||
{ title: 'Chat', path: '/:address/chat' },
|
||||
{ title: 'Player List', path: '/:address/playerlist' }
|
||||
];
|
||||
|
||||
app.controller('RconController', RconController);
|
||||
|
||||
function RconController($scope, $rootScope, rconService, $timeout, $route) {
|
||||
$scope.$route = $route;
|
||||
|
||||
$scope.pages = $.map($route.routes, function(value, index) {
|
||||
if (value.Nav) {
|
||||
return [value];
|
||||
function renderSidebar(address) {
|
||||
var html = '';
|
||||
for (var i = 0; i < navPages.length; i++) {
|
||||
var url = navPages[i].path.replace(':address', address);
|
||||
var linkCls = (window._currentPage === navPages[i].title) ? ' nav-link active' : 'nav-link';
|
||||
html += '<li class="nav-item"><a class="' + linkCls + '" href="#' + url + '">' + navPages[i].title + '</a></li>';
|
||||
}
|
||||
});
|
||||
|
||||
$scope.OpenLeftMenu = function() {
|
||||
$mdSidenav('left').toggle();
|
||||
};
|
||||
|
||||
$scope.IsConnected = function() {
|
||||
return rconService.IsConnected();
|
||||
elSidebar.innerHTML = html;
|
||||
}
|
||||
|
||||
$rootScope.Nav = function(url) {
|
||||
return url.replace(":address", rconService.Address);
|
||||
function setActivePage(title) {
|
||||
window._currentPage = title;
|
||||
elTitle.textContent = title;
|
||||
if (Rcon.isConnected()) renderSidebar(Rcon.getAddress());
|
||||
}
|
||||
|
||||
$rootScope.$on('$stateChangeStart', function(next, current) {
|
||||
console.log(next);
|
||||
});
|
||||
// ---- Header update ----
|
||||
var headerTimer = null;
|
||||
|
||||
rconService.OnOpen = function() {
|
||||
$scope.Connected = true;
|
||||
$scope.$broadcast("OnConnected");
|
||||
$scope.$digest();
|
||||
$scope.address = rconService.Address;
|
||||
}
|
||||
|
||||
rconService.OnClose = function(ev) {
|
||||
$scope.$broadcast("OnDisconnected", ev);
|
||||
$scope.$digest();
|
||||
}
|
||||
|
||||
rconService.OnError = function(ev) {
|
||||
$scope.$broadcast("OnConnectionError", ev);
|
||||
$scope.$digest();
|
||||
}
|
||||
|
||||
rconService.OnMessage = function(msg) {
|
||||
$scope.$apply(function() {
|
||||
$scope.$broadcast("OnMessage", msg);
|
||||
function updateHeader() {
|
||||
Rcon.request('serverinfo', function (msg) {
|
||||
var data = JSON.parse(msg.Message);
|
||||
document.getElementById('header-hostname').textContent = data.Hostname || 'Rust Server';
|
||||
document.getElementById('header-uptime').innerHTML = '<i class="bi bi-clock"></i> ' + secondsToDuration(data.Uptime);
|
||||
document.getElementById('header-map').innerHTML = '<i class="bi bi-globe2"></i> ' + (data.Map || '');
|
||||
document.getElementById('header-players').innerHTML = '<i class="bi bi-people-fill"></i> ' + formatNumber(data.Players) + '/' + formatNumber(data.MaxPlayers);
|
||||
document.getElementById('header-fps').innerHTML = '<i class="bi bi-speedometer2"></i> ' + formatNumber(data.Framerate) + 'fps';
|
||||
document.getElementById('header-entities').innerHTML = '<i class="bi bi-boxes"></i> ' + formatNumber(data.EntityCount);
|
||||
});
|
||||
}
|
||||
|
||||
$scope.Disconnect = function() {
|
||||
function startHeaderUpdates() {
|
||||
updateHeader();
|
||||
headerTimer = setInterval(updateHeader, 1000);
|
||||
}
|
||||
|
||||
function stopHeaderUpdates() {
|
||||
if (headerTimer) { clearInterval(headerTimer); headerTimer = null; }
|
||||
}
|
||||
|
||||
// ---- Disconnect button ----
|
||||
document.getElementById('btn-disconnect').addEventListener('click', function () {
|
||||
if (confirm('Do you really want to disconnect?')) {
|
||||
rconService.Disconnect();
|
||||
$scope.Connected = false;
|
||||
|
||||
$scope.address = '#/home';
|
||||
Rcon.disconnect();
|
||||
}
|
||||
});
|
||||
|
||||
// ---- Layout toggling ----
|
||||
function showConnected(address) {
|
||||
elConnect.innerHTML = '';
|
||||
elConnect.style.display = 'none';
|
||||
elConnected.style.display = 'block';
|
||||
elHeader.style.display = 'block';
|
||||
renderSidebar(address);
|
||||
startHeaderUpdates();
|
||||
}
|
||||
}
|
||||
|
||||
app.filter('SecondsToDuration', [SecondsToDuration]);
|
||||
|
||||
function SecondsToDuration() {
|
||||
return function(input) {
|
||||
input = parseInt(input);
|
||||
|
||||
var out = "";
|
||||
var hours = Math.floor(input / 3600);
|
||||
if (input > 3600)
|
||||
out += hours + "h";
|
||||
|
||||
var minutes = Math.floor(input / 60) % 60;
|
||||
if (input > 60)
|
||||
out += minutes + "m";
|
||||
|
||||
var seconds = input % 60;
|
||||
out += seconds + "s";
|
||||
|
||||
return out;
|
||||
function showDisconnected() {
|
||||
stopHeaderUpdates();
|
||||
elConnected.style.display = 'none';
|
||||
elHeader.style.display = 'none';
|
||||
elConnect.style.display = 'block';
|
||||
// Clear any stale router content
|
||||
document.getElementById('app-view').innerHTML = '';
|
||||
ConnectPage([], elConnect);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Events ----
|
||||
Rcon.on('connected', function () {
|
||||
var addr = Rcon.getAddress();
|
||||
showConnected(addr);
|
||||
Router.go('#/' + addr + '/info');
|
||||
});
|
||||
|
||||
Rcon.on('disconnected', function () {
|
||||
showDisconnected();
|
||||
});
|
||||
|
||||
// ---- Router setup ----
|
||||
Router.add(/^\/([^/]+)\/info$/, 'Server', function (p, c) { setActiveSidebar('Server'); ServerInfoPage(p, c); });
|
||||
Router.add(/^\/([^/]+)\/console$/, 'Console', function (p, c) { setActiveSidebar('Console'); ConsolePage(p, c); });
|
||||
Router.add(/^\/([^/]+)\/chat$/, 'Chat', function (p, c) { setActiveSidebar('Chat'); ChatPage(p, c); });
|
||||
Router.add(/^\/([^/]+)\/playerlist$/, 'Player List', function (p, c) { setActiveSidebar('Player List'); PlayerListPage(p, c); });
|
||||
Router.add(/^\/([^/]+)\/player\/(.+)$/, 'Player Info', function (p, c) { setActiveSidebar('Player Info'); PlayerInfoPage(p, c); });
|
||||
|
||||
Router.start('#app-view');
|
||||
|
||||
// ---- Initial state ----
|
||||
showDisconnected();
|
||||
})();
|
||||
|
||||
-87
@@ -1,87 +0,0 @@
|
||||
|
||||
app.controller( 'ChatController', ChatController );
|
||||
|
||||
function ChatController( $scope, rconService, $timeout )
|
||||
{
|
||||
$scope.Output = [];
|
||||
|
||||
$scope.SubmitCommand = function ()
|
||||
{
|
||||
rconService.Command( "say " + $scope.Command, 1 );
|
||||
$scope.Command = "";
|
||||
}
|
||||
|
||||
$scope.$on( "OnMessage", function ( event, msg )
|
||||
{
|
||||
if ( msg.Type !== "Chat" ) return;
|
||||
|
||||
$scope.OnMessage( JSON.parse( msg.Message ) );
|
||||
});
|
||||
|
||||
$scope.OnMessage = function( msg )
|
||||
{
|
||||
msg.Message = stripHtml(msg.Message);
|
||||
msg.Username = stripHtml(msg.Username);
|
||||
|
||||
$scope.Output.push( msg );
|
||||
|
||||
if($scope.isOnBottom()) {
|
||||
$scope.ScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
$scope.ScrollToBottom = function()
|
||||
{
|
||||
var element = $( "#ChatController .Output" );
|
||||
|
||||
$timeout( function() {
|
||||
element.scrollTop( element.prop('scrollHeight') );
|
||||
}, 50 );
|
||||
}
|
||||
|
||||
$scope.isOnBottom = function()
|
||||
{
|
||||
// get jquery element
|
||||
var element = $( "#ChatController .Output" );
|
||||
|
||||
// height of the element
|
||||
var height = element.height();
|
||||
|
||||
// scroll position from top position
|
||||
var scrollTop = element.scrollTop();
|
||||
|
||||
// full height of the element
|
||||
var scrollHeight = element.prop('scrollHeight');
|
||||
|
||||
if((scrollTop + height) > (scrollHeight - 10)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// Calls console.tail - which returns the last 256 entries from the console.
|
||||
// This is then added to the console
|
||||
//
|
||||
$scope.GetHistory = function ()
|
||||
{
|
||||
rconService.Request( "chat.tail 512", $scope, function ( msg )
|
||||
{
|
||||
var messages = JSON.parse( msg.Message );
|
||||
|
||||
messages.forEach( function ( message ) {
|
||||
$scope.OnMessage( message );
|
||||
});
|
||||
|
||||
$scope.ScrollToBottom();
|
||||
} );
|
||||
}
|
||||
|
||||
rconService.InstallService( $scope, $scope.GetHistory )
|
||||
}
|
||||
|
||||
function stripHtml( text )
|
||||
{
|
||||
return text ? String(text).replace( /<[^>]+>/gm, '' ) : '';
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
|
||||
app.controller( 'ConnectionController', ConnectionController );
|
||||
|
||||
function ConnectionController( $scope, rconService, $routeParams, $timeout, $location )
|
||||
{
|
||||
$scope.Address = "";
|
||||
$scope.Password = "";
|
||||
$scope.SaveConnection = true;
|
||||
|
||||
$scope.connectionsLimit = 5;
|
||||
|
||||
function _loadFromLocalStorage() {
|
||||
var connections = [];
|
||||
|
||||
// load and parse from local storage
|
||||
if ( localStorage && localStorage.previousConnections ) {
|
||||
connections = angular.fromJson( localStorage.previousConnections );
|
||||
}
|
||||
|
||||
// double check
|
||||
if ( !connections ) {
|
||||
connections = [];
|
||||
}
|
||||
|
||||
return connections;
|
||||
}
|
||||
|
||||
function _addWithoutDuplicates(connections, connection) {
|
||||
// prepare new array
|
||||
var filteredConnections = [];
|
||||
|
||||
for(var i in connections) {
|
||||
if(connections[i].Address !== connection.Address && connections[i].Password !== connection.Password) {
|
||||
// add old connection info to our new array
|
||||
filteredConnections.push(connections[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// add new connection
|
||||
filteredConnections.push(connection);
|
||||
|
||||
return filteredConnections;
|
||||
}
|
||||
|
||||
$scope.toggleConnectionsLimit = function ()
|
||||
{
|
||||
// toggle limit between undefined and 5
|
||||
// undefined sets limit to max
|
||||
if($scope.connectionsLimit === undefined) {
|
||||
$scope.connectionsLimit = 5;
|
||||
} else {
|
||||
$scope.connectionsLimit = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.Connect = function ()
|
||||
{
|
||||
$scope.Address = $scope.Address.trim();
|
||||
$scope.Password = $scope.Password.trim();
|
||||
|
||||
$scope.LastErrorMessage = null;
|
||||
rconService.Connect( $scope.Address, $scope.Password );
|
||||
|
||||
$location.path('/' + $scope.Address + '/info');
|
||||
}
|
||||
|
||||
$scope.ConnectTo = function ( c )
|
||||
{
|
||||
$scope.SaveConnection = false;
|
||||
$scope.LastErrorMessage = null;
|
||||
rconService.Connect( c.Address, c.Password );
|
||||
}
|
||||
|
||||
$scope.$on( "OnDisconnected", function ( x, ev )
|
||||
{
|
||||
console.log( ev );
|
||||
$scope.LastErrorMessage = "Connection was closed - Error " + ev.code;
|
||||
$scope.$digest();
|
||||
} );
|
||||
|
||||
$scope.$on( "OnConnected", function ( x, ev )
|
||||
{
|
||||
if ( $scope.SaveConnection )
|
||||
{
|
||||
// new connection to add
|
||||
var connection = { Address: $scope.Address, Password: $scope.Password, date: new Date() };
|
||||
|
||||
// remove old entries and add our new connection info
|
||||
var connections = _addWithoutDuplicates(_loadFromLocalStorage(), connection);
|
||||
|
||||
// push to scope and save data
|
||||
$scope.PreviousConnects = connections;
|
||||
localStorage.previousConnections = angular.toJson( $scope.PreviousConnects );
|
||||
}
|
||||
} );
|
||||
|
||||
$scope.PreviousConnects = _loadFromLocalStorage();
|
||||
|
||||
//
|
||||
// If a server address is passed in.. try to connect if we have a saved entry
|
||||
//
|
||||
$timeout( function ()
|
||||
{
|
||||
$scope.Address = $routeParams.address;
|
||||
|
||||
//
|
||||
// If a password was passed as a search param, use that
|
||||
//
|
||||
var pw = $location.search().password;
|
||||
if ( pw )
|
||||
{
|
||||
$scope.Password = pw;
|
||||
$location.search( "password", null );
|
||||
}
|
||||
|
||||
if ( $scope.Address != null )
|
||||
{
|
||||
// If we have a password (passed as a search param) then connect using that
|
||||
if ( $scope.Password != "" )
|
||||
{
|
||||
$scope.Connect();
|
||||
return;
|
||||
}
|
||||
|
||||
var foundAddress = Enumerable.From( $scope.PreviousConnects ).Where( function ( x ) { return x.Address == $scope.Address } ).First();
|
||||
if ( foundAddress != null )
|
||||
{
|
||||
$scope.ConnectTo( foundAddress );
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}, 20 );
|
||||
|
||||
}
|
||||
-147
@@ -1,147 +0,0 @@
|
||||
|
||||
app.controller( 'ConsoleController', ConsoleController );
|
||||
|
||||
function ConsoleController( $scope, rconService, $timeout )
|
||||
{
|
||||
$scope.Output = [];
|
||||
$scope.commandHistory = [];
|
||||
$scope.commandHistoryIndex = 0;
|
||||
|
||||
$scope.KeyUp = function (event)
|
||||
{
|
||||
switch(event.keyCode) {
|
||||
|
||||
// Arrow Key Up
|
||||
case 38:
|
||||
|
||||
// rotate through commandHistory
|
||||
$scope.commandHistoryIndex--;
|
||||
if($scope.commandHistoryIndex < 0) {
|
||||
$scope.commandHistoryIndex = $scope.commandHistory.length;
|
||||
}
|
||||
|
||||
// set command from history
|
||||
if($scope.commandHistory[$scope.commandHistoryIndex]) {
|
||||
$scope.Command = $scope.commandHistory[$scope.commandHistoryIndex];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
// Arrow Key Down
|
||||
case 40:
|
||||
|
||||
// rotate through commandHistory
|
||||
$scope.commandHistoryIndex++;
|
||||
if($scope.commandHistoryIndex >= $scope.commandHistory.length) {
|
||||
$scope.commandHistoryIndex = 0;
|
||||
}
|
||||
|
||||
// set command from history
|
||||
if($scope.commandHistory[$scope.commandHistoryIndex]) {
|
||||
$scope.Command = $scope.commandHistory[$scope.commandHistoryIndex];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
// reset command history index
|
||||
$scope.commandHistoryIndex = $scope.commandHistory.length;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.SubmitCommand = function ()
|
||||
{
|
||||
$scope.OnMessage( { Message: $scope.Command, Type: 'Command' } );
|
||||
|
||||
$scope.commandHistory.push($scope.Command);
|
||||
|
||||
rconService.Command( $scope.Command, 1 );
|
||||
$scope.Command = "";
|
||||
$scope.commandHistoryIndex = 0;
|
||||
}
|
||||
$scope.$on( "OnMessage", function ( event, msg ) { $scope.OnMessage( msg ); } );
|
||||
|
||||
$scope.OnMessage = function( msg )
|
||||
{
|
||||
|
||||
if ( msg.Message.startsWith( "[rcon] " ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
switch(msg.Type) {
|
||||
case 'Generic':
|
||||
case 'Log':
|
||||
case 'Error':
|
||||
case 'Warning':
|
||||
$scope.addOutput(msg);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log( msg );
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.ScrollToBottom = function()
|
||||
{
|
||||
var element = $( "#ConsoleController .Output" );
|
||||
|
||||
$timeout( function() {
|
||||
element.scrollTop( element.prop('scrollHeight') );
|
||||
}, 50 );
|
||||
}
|
||||
|
||||
$scope.isOnBottom = function()
|
||||
{
|
||||
// get jquery element
|
||||
var element = $( "#ConsoleController .Output" );
|
||||
|
||||
// height of the element
|
||||
var height = element.height();
|
||||
|
||||
// scroll position from top position
|
||||
var scrollTop = element.scrollTop();
|
||||
|
||||
// full height of the element
|
||||
var scrollHeight = element.prop('scrollHeight');
|
||||
|
||||
if((scrollTop + height) > (scrollHeight - 10)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
//
|
||||
// Calls console.tail - which returns the last 128 entries from the console.
|
||||
// This is then added to the console
|
||||
//
|
||||
$scope.GetHistory = function ()
|
||||
{
|
||||
rconService.Request( "console.tail 128", $scope, function ( msg )
|
||||
{
|
||||
var messages = JSON.parse( msg.Message );
|
||||
|
||||
messages.forEach( function ( msg ) {
|
||||
$scope.OnMessage( msg );
|
||||
});
|
||||
|
||||
$scope.ScrollToBottom();
|
||||
} );
|
||||
}
|
||||
|
||||
$scope.addOutput = function (msg)
|
||||
{
|
||||
msg.Class = msg.Type;
|
||||
msg.Message = stripHtml(msg.Message);
|
||||
|
||||
$scope.Output.push( msg );
|
||||
|
||||
if($scope.isOnBottom()) {
|
||||
$scope.ScrollToBottom();
|
||||
}
|
||||
}
|
||||
|
||||
rconService.InstallService( $scope, $scope.GetHistory )
|
||||
}
|
||||
Vendored
-10
File diff suppressed because one or more lines are too long
@@ -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); };
|
||||
}
|
||||
@@ -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);
|
||||
};
|
||||
}
|
||||
@@ -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); };
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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); }
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
|
||||
app.controller( 'PlayerInfoController', PlayerInfoController );
|
||||
|
||||
function PlayerInfoController( $scope, rconService, $routeParams )
|
||||
{
|
||||
$scope.userid = $routeParams.userid;
|
||||
|
||||
$scope.info = null;
|
||||
|
||||
$scope.refresh = function ()
|
||||
{
|
||||
rconService.getPlayers($scope, function(players) {
|
||||
|
||||
for(var i in players) {
|
||||
if(players[i].SteamID === $scope.userid){
|
||||
|
||||
// set player data
|
||||
$scope.info = players[i];
|
||||
|
||||
// remove xp remnants
|
||||
if ($scope.info['CurrentLevel'] !== undefined)
|
||||
delete $scope.info['CurrentLevel'];
|
||||
if ($scope.info['UnspentXp'] !== undefined)
|
||||
delete $scope.info['UnspentXp'];
|
||||
|
||||
// fix violation level
|
||||
if ($scope.info['VoiationLevel'] !== undefined){
|
||||
var violationLevel = $scope.info['VoiationLevel'];
|
||||
delete $scope.info['VoiationLevel'];
|
||||
$scope.info['ViolationLevel'] = violationLevel;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// player not found
|
||||
// reset data to null
|
||||
$scope.info = null;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.getUsername = function ()
|
||||
{
|
||||
// try to find players name in info
|
||||
if($scope.info && $scope.info.DisplayName) {
|
||||
return $scope.info.DisplayName;
|
||||
}
|
||||
|
||||
// otherwise show the id
|
||||
return $scope.userid;
|
||||
}
|
||||
|
||||
rconService.InstallService( $scope, $scope.refresh )
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
|
||||
app.controller( 'PlayerListController', PlayerListController );
|
||||
|
||||
function PlayerListController( $scope, rconService, $interval )
|
||||
{
|
||||
$scope.Output = [];
|
||||
$scope.OrderBy = '-ConnectedSeconds';
|
||||
|
||||
$scope.Refresh = function ()
|
||||
{
|
||||
rconService.getPlayers($scope, function ( players )
|
||||
{
|
||||
$scope.Players = players;
|
||||
});
|
||||
}
|
||||
|
||||
$scope.Order = function( field )
|
||||
{
|
||||
if ( $scope.OrderBy === field )
|
||||
{
|
||||
field = '-' + field;
|
||||
}
|
||||
|
||||
$scope.OrderBy = field;
|
||||
}
|
||||
|
||||
$scope.SortClass = function( field )
|
||||
{
|
||||
if ( $scope.OrderBy === field ) return "sorting";
|
||||
if ( $scope.OrderBy === "-" + field ) return "sorting descending";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$scope.KickPlayer = function ( id )
|
||||
{
|
||||
rconService.Command( 'kick ' + id );
|
||||
|
||||
$scope.Refresh();
|
||||
}
|
||||
|
||||
rconService.InstallService( $scope, $scope.Refresh )
|
||||
|
||||
// var timer = $interval( function ()
|
||||
// {
|
||||
// //$scope.Refresh();
|
||||
// }.bind( this ), 1000 );
|
||||
|
||||
//$scope.$on( '$destroy', function () { $interval.cancel( timer ) } )
|
||||
}
|
||||
+181
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* Rust WebSocket RCON Client — pure vanilla JS, zero dependencies.
|
||||
*
|
||||
* Protocol:
|
||||
* - Connect: ws://host:port/password
|
||||
* - Outgoing: { Identifier: int, Message: string, Name: "WebRcon" }
|
||||
* - Identifier > 1000 → request/response (callback tracked)
|
||||
* - Identifier <= 1000 → unsolicited server push (Generic/Log/Error/Warning/Chat)
|
||||
*
|
||||
* Usage:
|
||||
* Rcon.on('connected', () => { ... });
|
||||
* Rcon.on('disconnected', (ev) => { ... });
|
||||
* Rcon.on('message', (msg) => { ... });
|
||||
* Rcon.connect('192.168.1.100:28016', 'mypassword');
|
||||
* Rcon.request('serverinfo', (msg) => { console.log(JSON.parse(msg.Message)); });
|
||||
* Rcon.command('say Hello world');
|
||||
* Rcon.getPlayers((players) => { ... });
|
||||
*/
|
||||
var Rcon = (function () {
|
||||
'use strict';
|
||||
|
||||
var ConnectionStatus = {
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
CLOSED: 3
|
||||
};
|
||||
|
||||
var socket = null;
|
||||
var address = null;
|
||||
var callbacks = {};
|
||||
var lastIndex = 1001;
|
||||
var listeners = {};
|
||||
|
||||
/**
|
||||
* Simple pub/sub event emitter.
|
||||
*/
|
||||
function emit(event, data) {
|
||||
var cbs = listeners[event];
|
||||
if (cbs) {
|
||||
for (var i = 0; i < cbs.length; i++) {
|
||||
cbs[i](data);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to a Rust server's WebSocket RCON.
|
||||
* @param {string} addr - "host:port"
|
||||
* @param {string} pass - RCON password
|
||||
*/
|
||||
function connect(addr, pass) {
|
||||
disconnect();
|
||||
address = addr;
|
||||
socket = new WebSocket('ws://' + addr + '/' + pass);
|
||||
|
||||
socket.onopen = function () {
|
||||
emit('connected');
|
||||
};
|
||||
|
||||
socket.onclose = function (ev) {
|
||||
emit('disconnected', ev);
|
||||
};
|
||||
|
||||
socket.onerror = function (ev) {
|
||||
emit('error', ev);
|
||||
};
|
||||
|
||||
socket.onmessage = function (e) {
|
||||
var data = JSON.parse(e.data);
|
||||
|
||||
// Targeted response — match to pending callback
|
||||
if (data.Identifier > 1000) {
|
||||
var cb = callbacks[data.Identifier];
|
||||
if (cb) {
|
||||
cb(data);
|
||||
}
|
||||
delete callbacks[data.Identifier];
|
||||
return;
|
||||
}
|
||||
|
||||
// Unsolicited server push (console, chat, etc.)
|
||||
emit('message', data);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from the server.
|
||||
*/
|
||||
function disconnect() {
|
||||
callbacks = {};
|
||||
if (socket) {
|
||||
socket.close();
|
||||
socket = null;
|
||||
}
|
||||
address = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a fire-and-forget command (no response expected).
|
||||
* @param {string} msg - RCON command
|
||||
* @param {number} [identifier] - Defaults to -1
|
||||
*/
|
||||
function command(msg, identifier) {
|
||||
if (!socket || !isConnected()) return;
|
||||
if (identifier === undefined || identifier === null) {
|
||||
identifier = -1;
|
||||
}
|
||||
socket.send(JSON.stringify({
|
||||
Identifier: identifier,
|
||||
Message: msg,
|
||||
Name: 'WebRcon'
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a command and invoke callback with the response.
|
||||
* @param {string} msg - RCON command
|
||||
* @param {function} cb - callback(response)
|
||||
*/
|
||||
function request(msg, cb) {
|
||||
lastIndex++;
|
||||
callbacks[lastIndex] = cb;
|
||||
command(msg, lastIndex);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the WebSocket is open.
|
||||
*/
|
||||
function isConnected() {
|
||||
return socket !== null && socket.readyState === ConnectionStatus.OPEN;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current connected address (host:port).
|
||||
*/
|
||||
function getAddress() {
|
||||
return address;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience: fetch the player list.
|
||||
* @param {function} cb - callback(playersArray)
|
||||
*/
|
||||
function getPlayers(cb) {
|
||||
request('playerlist', function (response) {
|
||||
cb(JSON.parse(response.Message));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Register an event listener.
|
||||
* Events: 'connected', 'disconnected', 'error', 'message'
|
||||
*/
|
||||
function on(event, cb) {
|
||||
if (!listeners[event]) listeners[event] = [];
|
||||
listeners[event].push(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an event listener.
|
||||
*/
|
||||
function off(event, cb) {
|
||||
var cbs = listeners[event];
|
||||
if (cbs) {
|
||||
listeners[event] = cbs.filter(function (f) { return f !== cb; });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
connect: connect,
|
||||
disconnect: disconnect,
|
||||
command: command,
|
||||
request: request,
|
||||
isConnected: isConnected,
|
||||
getAddress: getAddress,
|
||||
getPlayers: getPlayers,
|
||||
on: on,
|
||||
off: off
|
||||
};
|
||||
})();
|
||||
@@ -1,131 +0,0 @@
|
||||
function RconService() {
|
||||
|
||||
var ConnectionStatus = {
|
||||
'CONNECTING': 0,
|
||||
'OPEN': 1,
|
||||
'CLOSING': 2,
|
||||
'CLOSED': 3
|
||||
};
|
||||
|
||||
var Service = {
|
||||
Socket: null,
|
||||
Address: null,
|
||||
Callbacks: {}
|
||||
};
|
||||
|
||||
var LastIndex = 1001;
|
||||
|
||||
Service.Connect = function(addr, pass) {
|
||||
this.Socket = new WebSocket("ws://" + addr + "/" + pass);
|
||||
this.Address = addr;
|
||||
|
||||
this.Socket.onmessage = function(e) {
|
||||
var data = angular.fromJson(e.data);
|
||||
|
||||
//
|
||||
// This is a targetted message, it has an identifier
|
||||
// So feed it back to the right callback.
|
||||
//
|
||||
if (data.Identifier > 1000) {
|
||||
var cb = Service.Callbacks[data.Identifier];
|
||||
if (cb != null) {
|
||||
cb.scope.$apply(function() {
|
||||
cb.callback(data);
|
||||
});
|
||||
}
|
||||
Service.Callbacks[data.Identifier] = null;
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// Generic console message, let OnMessage catch it
|
||||
//
|
||||
if (Service.OnMessage != null) {
|
||||
Service.OnMessage(data);
|
||||
}
|
||||
};
|
||||
|
||||
this.Socket.onopen = this.OnOpen;
|
||||
this.Socket.onclose = this.OnClose;
|
||||
this.Socket.onerror = this.OnError;
|
||||
}
|
||||
|
||||
Service.Disconnect = function() {
|
||||
if (this.Socket) {
|
||||
this.Socket.close();
|
||||
this.Socket = null;
|
||||
}
|
||||
|
||||
this.Callbacks = {};
|
||||
}
|
||||
|
||||
Service.Command = function(msg, identifier) {
|
||||
if (this.Socket === null)
|
||||
return;
|
||||
|
||||
if (!this.IsConnected())
|
||||
return;
|
||||
|
||||
if (identifier === null)
|
||||
identifier = -1;
|
||||
|
||||
var packet = {
|
||||
Identifier: identifier,
|
||||
Message: msg,
|
||||
Name: "WebRcon"
|
||||
};
|
||||
|
||||
this.Socket.send(JSON.stringify(packet));
|
||||
};
|
||||
|
||||
//
|
||||
// Make a request, call this function when it returns
|
||||
//
|
||||
Service.Request = function(msg, scope, callback) {
|
||||
LastIndex++;
|
||||
this.Callbacks[LastIndex] = {
|
||||
scope: scope,
|
||||
callback: callback
|
||||
};
|
||||
Service.Command(msg, LastIndex);
|
||||
}
|
||||
|
||||
//
|
||||
// Returns true if websocket is connected
|
||||
//
|
||||
Service.IsConnected = function() {
|
||||
if (this.Socket == null)
|
||||
return false;
|
||||
|
||||
return this.Socket.readyState === ConnectionStatus.OPEN;
|
||||
}
|
||||
|
||||
//
|
||||
// Helper for installing connectivity logic
|
||||
//
|
||||
// Basically if not connected, call this function when we are
|
||||
// And if we are - then call it right now.
|
||||
//
|
||||
Service.InstallService = function(scope, func) {
|
||||
scope.$on("OnConnected", function() {
|
||||
func();
|
||||
});
|
||||
|
||||
if (this.IsConnected()) {
|
||||
func();
|
||||
}
|
||||
}
|
||||
|
||||
Service.getPlayers = function(scope, success) {
|
||||
this.Request("playerlist", scope, function(response) {
|
||||
var players = JSON.parse(response.Message);
|
||||
|
||||
if (typeof success === 'function') {
|
||||
success.call(scope, players);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Service;
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Minimal hash-based SPA router.
|
||||
*
|
||||
* Route format: { pattern: RegExp, title: string, template: string, onEnter: function(params, container) }
|
||||
*
|
||||
* The first matching route's onEnter is called with parsed params and a container element.
|
||||
* The container is cleared before each navigation.
|
||||
*
|
||||
* Usage:
|
||||
* Router.add(/^\/(.+)\/info$/, 'Server', 'html/serverInfo.html', ServerInfoPage);
|
||||
* Router.add(/^\/(.+)\/player\/(.+)$/, 'Player Info', 'html/playerInfo.html', PlayerInfoPage);
|
||||
* Router.start('#app-view');
|
||||
*/
|
||||
var Router = (function () {
|
||||
'use strict';
|
||||
|
||||
var routes = [];
|
||||
var viewSelector = null;
|
||||
var currentCleanup = null;
|
||||
|
||||
function add(pattern, title, template, onEnter) {
|
||||
routes.push({ pattern: pattern, title: title, template: template, onEnter: onEnter });
|
||||
}
|
||||
|
||||
function navigate(hash) {
|
||||
// strip leading # and optional /
|
||||
var path = (hash || '').replace(/^#\/?/, '/');
|
||||
if (!path || path === '/') path = '/home';
|
||||
|
||||
for (var i = 0; i < routes.length; i++) {
|
||||
var r = routes[i];
|
||||
var match = path.match(r.pattern);
|
||||
if (match) {
|
||||
// Store params for later use
|
||||
var container = document.querySelector(viewSelector);
|
||||
if (!container) return;
|
||||
|
||||
// Cleanup previous page
|
||||
if (currentCleanup && typeof currentCleanup === 'function') {
|
||||
currentCleanup();
|
||||
currentCleanup = null;
|
||||
}
|
||||
|
||||
// Clear container
|
||||
container.innerHTML = '';
|
||||
|
||||
// Call onEnter with match groups + container
|
||||
if (typeof r.onEnter === 'function') {
|
||||
var result = r.onEnter(match, container);
|
||||
if (typeof result === 'function') {
|
||||
currentCleanup = result;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function start(selector) {
|
||||
viewSelector = selector;
|
||||
window.addEventListener('hashchange', function () {
|
||||
navigate(window.location.hash);
|
||||
});
|
||||
// Initial navigation
|
||||
if (window.location.hash) {
|
||||
navigate(window.location.hash);
|
||||
}
|
||||
}
|
||||
|
||||
function go(hash) {
|
||||
window.location.hash = hash;
|
||||
}
|
||||
|
||||
return {
|
||||
add: add,
|
||||
start: start,
|
||||
go: go,
|
||||
navigate: navigate
|
||||
};
|
||||
})();
|
||||
@@ -1,231 +0,0 @@
|
||||
app.controller('ServerInfoController', ServerInfoController);
|
||||
|
||||
var DATA_LIMIT = 100;
|
||||
var recordedData = [];
|
||||
|
||||
function ServerInfoController($scope, rconService, $routeParams, $interval) {
|
||||
$scope.useCharts = false;
|
||||
|
||||
// TODO: move serverinfo to service
|
||||
$scope.serverinfo = {};
|
||||
|
||||
$scope.playersChart = {
|
||||
options: {
|
||||
chart: {
|
||||
type: 'pieChart',
|
||||
height: 250,
|
||||
margin: {
|
||||
top: 0,
|
||||
right: 75,
|
||||
bottom: 0,
|
||||
left: 75
|
||||
},
|
||||
donut: true,
|
||||
x: function(d) {
|
||||
return d.key;
|
||||
},
|
||||
y: function(d) {
|
||||
return d.y;
|
||||
},
|
||||
yAxis: {
|
||||
axisLabel: 'Slots',
|
||||
tickFormat: function(d) {
|
||||
return d3.format('.d')(d);
|
||||
}
|
||||
},
|
||||
showLabels: true,
|
||||
pie: {
|
||||
startAngle: function(d) {
|
||||
return d.startAngle / 2 - Math.PI / 2
|
||||
},
|
||||
endAngle: function(d) {
|
||||
return d.endAngle / 2 - Math.PI / 2
|
||||
}
|
||||
},
|
||||
duration: 500
|
||||
}
|
||||
},
|
||||
data: []
|
||||
};
|
||||
|
||||
$scope.performanceChart = {
|
||||
options: {
|
||||
chart: {
|
||||
type: 'multiChart',
|
||||
height: 200,
|
||||
margin: {
|
||||
top: 30,
|
||||
right: 75,
|
||||
bottom: 40,
|
||||
left: 75
|
||||
},
|
||||
color: [
|
||||
'darkred', 'yellowgreen'
|
||||
],
|
||||
duration: 0,
|
||||
lines1: {
|
||||
duration: 0
|
||||
},
|
||||
lines2: {
|
||||
duration: 0
|
||||
},
|
||||
useInteractiveGuideline: true,
|
||||
yAxis1: {
|
||||
axisLabel: 'Framerate',
|
||||
tickFormat: function(d) {
|
||||
return d3.format(',d')(d);
|
||||
}
|
||||
},
|
||||
yAxis2: {
|
||||
axisLabel: 'Entities',
|
||||
tickFormat: function(d) {
|
||||
return d3.format(',d')(d);
|
||||
},
|
||||
// axisLabelDistance: 12
|
||||
},
|
||||
xAxis: {
|
||||
axisLabel: "Time",
|
||||
tickFormat: function(d) {
|
||||
return d3.time.format('%H:%M:%S')(new Date(d))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
key: 'Framerate',
|
||||
type: "line",
|
||||
duration: 0,
|
||||
yAxis: 1,
|
||||
values: []
|
||||
}, {
|
||||
key: 'Entities',
|
||||
type: 'line',
|
||||
duration: 0,
|
||||
yAxis: 2,
|
||||
values: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
$scope.netChart = {
|
||||
options: {
|
||||
chart: {
|
||||
type: 'stackedAreaChart',
|
||||
height: 250,
|
||||
margin: {
|
||||
top: 0,
|
||||
right: 75,
|
||||
bottom: 40,
|
||||
left: 75
|
||||
},
|
||||
useVoronoi: false,
|
||||
duration: 0,
|
||||
useInteractiveGuideline: true,
|
||||
yAxis: {
|
||||
axisLabel: 'Network',
|
||||
tickFormat: function(bytes) {
|
||||
var fmt = d3.format('.0f');
|
||||
if (bytes < 1024) {
|
||||
return fmt(bytes) + 'B';
|
||||
} else if (bytes < 1024 * 1024) {
|
||||
return fmt(bytes / 1024) + 'kB';
|
||||
} else if (bytes < 1024 * 1024 * 1024) {
|
||||
return fmt(bytes / 1024 / 1024) + 'MB';
|
||||
} else {
|
||||
return fmt(bytes / 1024 / 1024 / 1024) + 'GB';
|
||||
}
|
||||
}
|
||||
},
|
||||
xAxis: {
|
||||
axisLabel: "Time",
|
||||
tickFormat: function(d) {
|
||||
return d3.time.format('%H:%M:%S')(new Date(d))
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
data: [
|
||||
{
|
||||
key: 'IN',
|
||||
values: []
|
||||
}, {
|
||||
key: 'OUT',
|
||||
values: []
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
rconService.InstallService($scope, _refresh);
|
||||
|
||||
// TODO: move updateinterval to service
|
||||
var timer = $interval(_refresh, 1000);
|
||||
$scope.$on("$destroy", function() {
|
||||
$interval.cancel(timer);
|
||||
$scope.serverinfo = {};
|
||||
});
|
||||
|
||||
function _refresh() {
|
||||
rconService.Request('serverinfo', $scope, function(msg) {
|
||||
_updateData(JSON.parse(msg.Message));
|
||||
});
|
||||
}
|
||||
|
||||
function _updateData(data) {
|
||||
$scope.serverinfo = data;
|
||||
|
||||
if ($scope.useCharts) {
|
||||
_collectChartData(data);
|
||||
_generateChartData();
|
||||
}
|
||||
}
|
||||
|
||||
function _collectChartData(data) {
|
||||
// player chart
|
||||
$scope.playersChart.data = [
|
||||
{
|
||||
key: 'Queued',
|
||||
y: data.Queued
|
||||
}, {
|
||||
key: 'Joining',
|
||||
y: data.Joining
|
||||
}, {
|
||||
key: 'Players',
|
||||
y: data.Players
|
||||
}, {
|
||||
key: 'Free',
|
||||
y: (data.MaxPlayers - (data.Joining + data.Players))
|
||||
}
|
||||
];
|
||||
|
||||
recordedData.push({ts: Date.now(), data: data});
|
||||
if (recordedData.length > DATA_LIMIT) {
|
||||
recordedData = recordedData.slice(Math.max(recordedData.length - DATA_LIMIT, 1));
|
||||
}
|
||||
}
|
||||
|
||||
function _generateChartData() {
|
||||
|
||||
var fpsChartValues = [];
|
||||
var entChartValues = [];
|
||||
|
||||
var netInChartValues = [];
|
||||
var netOutChartValues = [];
|
||||
|
||||
for (var i = 0; i < recordedData.length; i++) {
|
||||
var record = recordedData[i];
|
||||
fpsChartValues.push({x: record.ts, y: record.data.Framerate});
|
||||
entChartValues.push({x: record.ts, y: record.data.EntityCount});
|
||||
|
||||
netInChartValues.push({x: record.ts, y: record.data.NetworkIn});
|
||||
netOutChartValues.push({x: record.ts, y: record.data.NetworkOut});
|
||||
}
|
||||
|
||||
$scope.performanceChart.data[0].values = fpsChartValues;
|
||||
$scope.performanceChart.data[1].values = entChartValues;
|
||||
|
||||
$scope.netChart.data[0].values = netInChartValues;
|
||||
$scope.netChart.data[1].values = netOutChartValues;
|
||||
}
|
||||
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* Utility functions shared across pages.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Convert seconds to a human-readable duration string.
|
||||
* e.g., 3723 → "1h2m3s"
|
||||
*/
|
||||
function secondsToDuration(input) {
|
||||
input = parseInt(input, 10);
|
||||
if (isNaN(input)) return '0s';
|
||||
|
||||
var out = '';
|
||||
var hours = Math.floor(input / 3600);
|
||||
if (hours > 0) out += hours + 'h';
|
||||
|
||||
var minutes = Math.floor((input % 3600) / 60);
|
||||
if (minutes > 0) out += minutes + 'm';
|
||||
|
||||
var seconds = input % 60;
|
||||
out += seconds + 's';
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip HTML tags from a string.
|
||||
*/
|
||||
function stripHtml(text) {
|
||||
return text ? String(text).replace(/<[^>]+>/gm, '') : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a number with locale-aware separators (fallback).
|
||||
*/
|
||||
function formatNumber(n) {
|
||||
n = parseInt(n, 10);
|
||||
if (isNaN(n)) return '0';
|
||||
return n.toLocaleString ? n.toLocaleString() : String(n);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if user is scrolled to the bottom of an element.
|
||||
*/
|
||||
function isScrolledToBottom(el) {
|
||||
return (el.scrollTop + el.clientHeight) >= (el.scrollHeight - 10);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
# Example nginx config for WebRcon
|
||||
# Copy the project files to /var/www/webrcon (or wherever you like)
|
||||
# and symlink this file into /etc/nginx/sites-enabled/
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name rcon.yourdomain.com;
|
||||
|
||||
root /var/www/webrcon;
|
||||
index index.html;
|
||||
|
||||
# SPA routing — all paths serve index.html
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Cache static assets aggressively (all deps are CDN-loaded anyway)
|
||||
location ~* \.(css|js|svg|png|jpg|jpeg|gif|ico)$ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# No backend — browser talks WebSocket directly to the game server
|
||||
}
|
||||
Generated
-250
@@ -1,250 +0,0 @@
|
||||
{
|
||||
"name": "webrcon",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "webrcon",
|
||||
"devDependencies": {
|
||||
"http-server": "^0.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.9.0.tgz",
|
||||
"integrity": "sha512-XQJ3MipmCHAIBBMFfu2jaSetneOrXbSyyqeU3Nod867oNOpS+i9FEms5PWgjMxSgBybRf2IVVLtr1YfrDO+okg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/colors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/colors/-/colors-1.0.3.tgz",
|
||||
"integrity": "sha512-pFGrxThWcWQ2MsAz6RtgeWe4NK2kUE1WfsrvvlctdII745EW9I0yflqhe7++M5LEc7bV2c/9/5zc8sFcpL0Drw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.1.90"
|
||||
}
|
||||
},
|
||||
"node_modules/corser": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz",
|
||||
"integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ecstatic": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/ecstatic/-/ecstatic-1.4.1.tgz",
|
||||
"integrity": "sha512-VC4nmLt4/3KmKbSXxREmJMs+iAB+VdOzMyugV4YzG6d/2NcHa+b0J5/zKGRWS7efwNLEZP2AIodUUm12wOglng==",
|
||||
"deprecated": "This package is unmaintained and deprecated. See the GH Issue 259.",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"he": "^0.5.0",
|
||||
"mime": "^1.2.11",
|
||||
"minimist": "^1.1.0",
|
||||
"url-join": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"ecstatic": "lib/ecstatic.js"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz",
|
||||
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://github.com/sponsors/RubenVerborgh"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"debug": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/he": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-0.5.0.tgz",
|
||||
"integrity": "sha512-DoufbNNOFzwRPy8uecq+j+VCPQ+JyDelHTmSgygrA5TsR8Cbw4Qcir5sGtWiusB4BdT89nmlaVDhSJOqC/33vw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy": {
|
||||
"version": "1.18.1",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
|
||||
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eventemitter3": "^4.0.0",
|
||||
"follow-redirects": "^1.0.0",
|
||||
"requires-port": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/http-server": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/http-server/-/http-server-0.9.0.tgz",
|
||||
"integrity": "sha512-ufsBg3xl4RRokW37Kg1uGS4MKkpfXqRrtuKYDT9QaBxqix9IHZ2QbOk2RDTOM8MJjWRzjozCC5heZX/azjKB6Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"colors": "1.0.3",
|
||||
"corser": "~2.0.0",
|
||||
"ecstatic": "^1.4.0",
|
||||
"http-proxy": "^1.8.1",
|
||||
"opener": "~1.4.0",
|
||||
"optimist": "0.6.x",
|
||||
"portfinder": "0.4.x",
|
||||
"union": "~0.4.3"
|
||||
},
|
||||
"bin": {
|
||||
"hs": "bin/http-server",
|
||||
"http-server": "bin/http-server"
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimist": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
|
||||
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/opener": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/opener/-/opener-1.4.3.tgz",
|
||||
"integrity": "sha512-4Im9TrPJcjAYyGR5gBe3yZnBzw5n3Bfh1ceHHGNOpMurINKc6RdSIPXMyon4BZacJbJc36lLkhipioGbWh5pwg==",
|
||||
"dev": true,
|
||||
"license": "(WTFPL OR MIT)",
|
||||
"bin": {
|
||||
"opener": "opener.js"
|
||||
}
|
||||
},
|
||||
"node_modules/optimist": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz",
|
||||
"integrity": "sha512-snN4O4TkigujZphWLN0E//nQmm7790RYaE53DdL7ZYwee2D8DDo9/EyYiKUfN3rneWUjhJnueija3G9I2i0h3g==",
|
||||
"dev": true,
|
||||
"license": "MIT/X11",
|
||||
"dependencies": {
|
||||
"minimist": "~0.0.1",
|
||||
"wordwrap": "~0.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/optimist/node_modules/minimist": {
|
||||
"version": "0.0.10",
|
||||
"resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz",
|
||||
"integrity": "sha512-iotkTvxc+TwOm5Ieim8VnSNvCDjCK9S8G3scJ50ZthspSxa7jx50jkhYduuAtAjvfDUwSgOwf8+If99AlOEhyw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/portfinder": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/portfinder/-/portfinder-0.4.0.tgz",
|
||||
"integrity": "sha512-SZ3hp61WVhwNSS0gf0Fdrx5Yb/wl35qisHuPVM1S0StV8t5XlVZmmJy7/417OELJA7t6ecEmeEzvOaBwq3kCiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT/X11",
|
||||
"dependencies": {
|
||||
"async": "0.9.0",
|
||||
"mkdirp": "0.5.x"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-2.3.3.tgz",
|
||||
"integrity": "sha512-f5M0HQqZWkzU8GELTY8LyMrGkr3bPjKoFtTkwUEqJQbcljbeK8M7mliP9Ia2xoOI6oMerp+QPS7oYJtpGmWe/A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/union": {
|
||||
"version": "0.4.6",
|
||||
"resolved": "https://registry.npmjs.org/union/-/union-0.4.6.tgz",
|
||||
"integrity": "sha512-2qtrvSgD0GKotLRCNYkIMUOzoaHjXKCtbAP0kc5Po6D+RWTBb+BxlcHlHCYcf+Y+YM7eQicPgAg9mnWQvtoFVA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"qs": "~2.3.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-join": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/url-join/-/url-join-1.1.0.tgz",
|
||||
"integrity": "sha512-zz1wZk4Lb5PTVwZ3HWDmm8XnlPvmOof6/fjdDPA5yBrUcbtV64U6bV832Zf1BtU2WkBBWaUT46wCs+l0HP5nhg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz",
|
||||
"integrity": "sha512-1tMA907+V4QmxV7dbRvb4/8MaRALK6q9Abid3ndMYnbyo8piisCmeONVqVSXqQA3KaP4SLt5b7ud6E2sqP8TFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"name": "webrcon",
|
||||
"description": "Game server Rcon, using websockets",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/Facepunch/webrcon"
|
||||
},
|
||||
"scripts": {
|
||||
"server": "http-server -p 8888",
|
||||
"start": "npm run server"
|
||||
},
|
||||
"devDependencies": {
|
||||
"http-server": "^0.9.0"
|
||||
}
|
||||
}
|
||||
@@ -1,79 +1,63 @@
|
||||
@import url(https://fonts.googleapis.com/css?family=Roboto:400,700);
|
||||
|
||||
body
|
||||
{
|
||||
:root {
|
||||
--rcon-bg: #252830;
|
||||
--rcon-text: #cfd2da;
|
||||
--rcon-accent: #1ca8dd;
|
||||
--rcon-card-bg: #434857;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Roboto", sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
line-height: 1.5;
|
||||
color: #cfd2da;
|
||||
background-color: #252830;
|
||||
}
|
||||
|
||||
a
|
||||
{
|
||||
color: #1ca8dd;
|
||||
a { color: var(--rcon-accent); }
|
||||
a:hover { color: #fff; text-decoration: none; }
|
||||
|
||||
/* Console output */
|
||||
#ConsoleController .Output {
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
a:hover
|
||||
{
|
||||
color: #fff;
|
||||
text-decoration: none;
|
||||
.scrollable { overflow: auto; }
|
||||
|
||||
/* Sidebar navigation */
|
||||
.nav-sidebar {
|
||||
margin-top: 64px;
|
||||
}
|
||||
|
||||
.center
|
||||
{
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#ConsoleController .Output
|
||||
{
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.scrollable
|
||||
{
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
|
||||
.nav-sidebar
|
||||
{
|
||||
margin-top: 64px;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.nav-sidebar > li > a
|
||||
{
|
||||
padding-right: 20px;
|
||||
padding-left: 20px;
|
||||
.nav-sidebar .nav-link {
|
||||
padding: 8px 20px;
|
||||
border-radius: 5px;
|
||||
color: var(--rcon-text);
|
||||
}
|
||||
|
||||
.nav-sidebar > .active > a,
|
||||
.nav-sidebar > .active > a:hover,
|
||||
.nav-sidebar > .active > a:focus
|
||||
{
|
||||
.nav-sidebar .nav-link:hover,
|
||||
.nav-sidebar .nav-link.active {
|
||||
color: #fff;
|
||||
background-color: #428bca;
|
||||
}
|
||||
|
||||
.fade
|
||||
{
|
||||
opacity: 0.5;
|
||||
/* Fade utility */
|
||||
.fade { opacity: 0.5; }
|
||||
|
||||
/* Table styles */
|
||||
.table > tbody > tr:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.table > tbody > tr:hover
|
||||
{
|
||||
background-color: rgba( 255, 255, 255, 0.3 );
|
||||
.table > thead > tr > th.sorting {
|
||||
color: #1bc98e;
|
||||
border-color: #1bc98e;
|
||||
}
|
||||
|
||||
.table > thead > tr > th.sorting
|
||||
{
|
||||
color: #1bc98e;
|
||||
border-color: #1bc98e;
|
||||
.table > thead > tr > th.sorting.descending {
|
||||
color: #e05d6f;
|
||||
border-color: #e05d6f;
|
||||
}
|
||||
|
||||
.table-responsive {
|
||||
@@ -81,71 +65,60 @@ a:hover
|
||||
overflow-y: visible !important;
|
||||
}
|
||||
|
||||
.btn-default, .btn-default:active, .form-control
|
||||
{
|
||||
background: rgba( 255, 255, 255, 0.3 );
|
||||
border: none;
|
||||
color: #cfd2da;
|
||||
text-shadow: none;
|
||||
/* Form controls dark theme */
|
||||
.form-control {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
color: var(--rcon-text);
|
||||
}
|
||||
|
||||
.btn-default:hover
|
||||
{
|
||||
background: #1ca8dd;
|
||||
color: #fff;
|
||||
.form-control:focus {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
color: #fff;
|
||||
border-color: var(--rcon-accent);
|
||||
box-shadow: 0 0 0 0.2rem rgba(28, 168, 221, 0.25);
|
||||
}
|
||||
|
||||
.panel
|
||||
{
|
||||
background-color: #434857;
|
||||
/* Card dark theme */
|
||||
.card {
|
||||
background-color: var(--rcon-card-bg);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.chatname
|
||||
{
|
||||
display: block;
|
||||
width: 200px;
|
||||
float: left;
|
||||
text-align: right;
|
||||
padding-right: 8px;
|
||||
/* Dropdown dark theme */
|
||||
.dropdown-menu {
|
||||
background-color: var(--rcon-card-bg);
|
||||
border-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.navbar-default
|
||||
{
|
||||
background-color: #428bca;
|
||||
background-image: none;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
color: white;
|
||||
.dropdown-menu .dropdown-item {
|
||||
color: var(--rcon-text);
|
||||
}
|
||||
|
||||
.navbar-default .navbar-brand, .navbar-default .navbar-brand:hover
|
||||
{
|
||||
color: #fff;
|
||||
.dropdown-menu .dropdown-item:hover {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.navbar-default .navbar-text,
|
||||
.navbar-default .navbar-nav > li > a, .navbar-default .navbar-nav > li > a:hover, .navbar-default .navbar-nav > li > a:focus
|
||||
{
|
||||
color: #fff;
|
||||
.dropdown-menu .dropdown-divider {
|
||||
border-color: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav > li > a:hover
|
||||
{
|
||||
background-color: rgba( 255, 255, 255, 0.25 );
|
||||
/* Navbar text color override for dark theme */
|
||||
.navbar-dark .navbar-text {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.navbar-default .navbar-nav > .open > a, .navbar-default .navbar-nav > .active > a
|
||||
{
|
||||
background-color: white;
|
||||
background-image: none;
|
||||
border: none;
|
||||
color: #428bca;
|
||||
/* Page title */
|
||||
#page-title {
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
|
||||
.navbar-info
|
||||
{
|
||||
position: relative;
|
||||
display: block;
|
||||
padding: 15px;
|
||||
line-height: 20px;
|
||||
/* Chart panels */
|
||||
#chart-performance,
|
||||
#chart-network,
|
||||
#chart-players {
|
||||
min-height: 200px;
|
||||
}
|
||||
Reference in New Issue
Block a user