Minetrack 5 (#143)
* remove unused #getServer methods, inline #roundToPoint * replace #safeName regex with incremental ids * remove legacy #setInterval based #updateMojangServices handling * add Tooltip class, move faviconSize to css instead of js * move server id assignment to ServerRegistry * move printPort logic to formatMinecraftServerAddress, add MINECRAFT_DEFAULT_PORTS * simplify ping tracking * rework perc-bar tooltip to not use mousemove event * begin moving graphing logic to GraphDisplayManager * begin merge graph point tracking into graphDisplayManager * centralizing graphing logic into GraphDisplayManager * properly reset GraphDisplayManager when handling disconnects * move individual server graph data into ServerGraph class * constantly run sortServers loop to simplify logic * inline #updateMojangServices method * resize performance improvements * remove legacy bootTime refresh behavior, require manual user refresh * move class defs to core.js * remove unused #isGraphDataVisible arg * remove #toggleControlsDrawer * dont call #updatePercentageBar in #updateServerStatus calls * centralize caption handling * inline #msToTime * remove hackish seconds handling for timestamps * reduce #forEach calls with filter/map * safely fallback to errorMessage if errno/description does not match * Add /images/missing_favicon.png path instead of putting base64 in js * remove debug * cleanup mojang status handling * move historyPlot instance into GraphDisplayManager * cleanup checkbox html generation * cleanup #updateServerStatus * fix up tooltip styling * move jquery code out of core.js * fix add server race condition when initially pinging servers * send error.placeholder=true for pending pings so the frontend can discard later * filter placeholder pings sent by the backend * del assets/images/logo_2014.png * move graph code into graph.js * merge pingTracker into ServerRegistry+ServerGraph * remove todos * simplify getVisibleGraphData * fix potential sortServers race condition when adding * use #show instead of #fadeIn(0) * remove publicConfig.json, send over socket * update docs/CHANGELOG.md * getOrAssign -> getOrCreateId * dont delete graph controls when disconnected * early work cleaning up HTML+CSS structures * cleanup server css elements * cleanup graph control css elements * move base CSS color values into @media(prefers-color-scheme: light) * move CSS magic colors to vars * reduce duplicated CSS color rules * inline body text color CSS * WIP replacing jQuery calls with vanilla JS * WIP replacing jQuery calls with vanilla JS * replace getElementsByClass with querySelectorAll * typeMarker -> serverTypeHTML * use jQuery slim for remaining flot.js dependency * merge setAllGraphVisibility into GraphDisplayManager * break apart element update and redraw logic * add eslint + parcel bundler * auto lint assets/js when building * statically serve favicons/ for faviconOverrides outside of dist/ * only send favicons when changed * move faviconOverride behavior into entry in servers.json * add warning to backend server files * remove .server-favicon-missing class * add Minetrack 5 migration guide * add npm run build step to install.sh * adjust package.json version to 5.0.0 * remove js references from index.html * move logic and behavior out of site.js * cleanup ServerRegistry methods * prevent multiple history graph redraws * add comments * cleanup #addServer usage, move to App * move graph control bindings into GraphDisplayManager * site.js -> main.js, core.js -> servers.js * move Tooltip/Caption into util.js * spacing tweak * format index.html * ensure the frontend does not handling updateHistoryGraph events * prevent versions/record updates if the same value * avoid empty percbar updates, ensure versions are sorted * only include main.js ref in index.html * serve minified copy of font awesome directly * bundle icons.css into main.css, remove Open Sans 400 * add new SVG logo * update docs/CHANGELOG.md * new design, server version grouping * remove start.sh call from install.sh * move graph controls into header with new button * move #handleSettingsToggle back to graph * fix legacy code behavior of currentVersionIndex applying globally * fix header text color in light mode * fix mojang status text color in light mode * fix toggle settings and checkbox colors * tweak button hover color * tweak button hover color * add new status-overlay to avoid complicated DOM management during loading * fix initial graph rendering bug * add comments * update default graph tick sizes * prevent #tooltip from overflowing page * remove localhost spec * prevent minor connection errors from reshuffling layout * update CHANGELOG.md * add message/button for manually loading historical graph on mobile devices * send isGraphVisible to frontend to prevent alert if logToDatabase: false * send timestamp data with record * update docs/CHANGELOG.md * remove clock icon * remove 24h peak timestamp * Only check favicon if present * safely handle undefined/empty knownVersions in #formatMinecraftVersions * merge config.versions and minecraft.json into minecraft_versions.json, simplify index matching behavior * remove localhost url in socket.io config * stub methods/linkage for FocusManager * add #isObjectEqual hack, add event proxying to FocusManager * wip extended stats box * remove server-type badging * tweak mojang unstable color * serve socket.io-client using parcel * fix incorrect mojang status colors * remove legacy capitalization design * redesign focus boxes * update docs/CHANGELOG.md * remove localhost ref * color clock icon * use background-color for hover effect, remove unused var * improve stats focus box icons * change mojang sessions icon to globe * Add favorites system * remove focus boxes * update docs/CHANGELOG.md * remove focus icons from font * simplify graph related event binding * Add Sort By button * store current sortOption in localStorage * update docs/CHANGELOG.md * move magic 0 sortOption to SORT_OPTION_INDEX_DEFAULT * remove localhost ref * merge #settings-toggle, #sort-by and .mojang-status CSS * remove .focus-box CSS * use sortedServerIds for _lastSortedServers * tweak --color-blue * new missing_favicon design to match logo * edit footer CSS/text, remove github icon * replace player count diff counter with GROWTH sort option * italize non-default sort options * add Only Favorites button to auto sync favorites to the visible graph data * add icons to graph control buttons * update docs/CHANGELOG.md * use * to denote non-default sort option instead * remove localhost url in socket.io config * add value highlighting to make sort by easier to read * remove last remaining uppercase text * remove serverTypesVisible from config.json * simplify header CSS, fix spacing with logToDatabase=false * fix inverted text color on highlighted values * remove localhost url in socket.io config * break header into rows on mobile devices Co-authored-by: Hugo Manrique <contact@hugmanrique.me>
This commit is contained in:
parent
8c5e25b259
commit
f875361bc7
5
.babelrc
Normal file
5
.babelrc
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"plugins": [
|
||||
"@babel/plugin-proposal-class-properties"
|
||||
]
|
||||
}
|
20
.eslintrc.json
Normal file
20
.eslintrc.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es6": true,
|
||||
"jquery": true
|
||||
},
|
||||
"extends": [
|
||||
"standard"
|
||||
],
|
||||
"globals": {
|
||||
"Atomics": "readonly",
|
||||
"SharedArrayBuffer": "readonly"
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2018
|
||||
},
|
||||
"rules": {
|
||||
},
|
||||
"parser": "babel-eslint"
|
||||
}
|
4
.gitignore
vendored
4
.gitignore
vendored
@ -3,4 +3,6 @@ minetrack.log
|
||||
.idea/
|
||||
database.sql
|
||||
database.sql-journal
|
||||
.DS_Store
|
||||
.DS_Store
|
||||
dist/
|
||||
.cache/
|
||||
|
@ -2,6 +2,9 @@
|
||||
Minetrack is a Minecraft PC/PE server tracker that lets you focus on what's happening *now*.
|
||||
Built to be lightweight and durable, you can easily adapt it to monitor BungeeCord or server instances.
|
||||
|
||||
#### Migrating to Minetrack 5
|
||||
See our Minetrack 5 [migration guide](docs/MIGRATING.md).
|
||||
|
||||
#### This project is not actively maintained!
|
||||
This project and the offical website are not actively maintained anymore, but you are welcome to run your own instances of Minetrack.
|
||||
I will however review and accept pull-requests, so please share any improvements you are making so everybody can benefit from them.
|
||||
|
223
app.js
223
app.js
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
var server = require('./lib/server');
|
||||
var ping = require('./lib/ping');
|
||||
var logger = require('./lib/logger');
|
||||
@ -7,22 +12,44 @@ var db = require('./lib/database');
|
||||
|
||||
var config = require('./config.json');
|
||||
var servers = require('./servers.json');
|
||||
var minecraftVersions = require('./minecraft_versions.json');
|
||||
|
||||
var networkHistory = [];
|
||||
var connectedClients = 0;
|
||||
|
||||
var currentVersionIndex = {
|
||||
'PC': 0,
|
||||
'PE': 0
|
||||
};
|
||||
|
||||
var networkVersions = [];
|
||||
|
||||
const lastFavicons = [];
|
||||
|
||||
var graphData = [];
|
||||
var highestPlayerCount = {};
|
||||
var lastGraphPush = [];
|
||||
var graphPeaks = {};
|
||||
|
||||
const serverProtocolVersionIndexes = []
|
||||
|
||||
function getNextProtocolVersion (server) {
|
||||
// Minecraft Bedrock Edition does not have protocol versions
|
||||
if (server.type === 'PE') {
|
||||
return {
|
||||
protocolId: 0,
|
||||
protocolIndex: 0
|
||||
}
|
||||
}
|
||||
const protocolVersions = minecraftVersions[server.type]
|
||||
let nextProtocolVersion = serverProtocolVersionIndexes[server.name]
|
||||
if (typeof nextProtocolVersion === 'undefined' || nextProtocolVersion + 1 >= protocolVersions.length) {
|
||||
nextProtocolVersion = 0
|
||||
} else {
|
||||
nextProtocolVersion++
|
||||
}
|
||||
serverProtocolVersionIndexes[server.name] = nextProtocolVersion
|
||||
return {
|
||||
protocolId: protocolVersions[nextProtocolVersion].protocolId,
|
||||
protocolIndex: nextProtocolVersion
|
||||
}
|
||||
}
|
||||
|
||||
function pingAll() {
|
||||
for (var i = 0; i < servers.length; i++) {
|
||||
// Make sure we lock our scope.
|
||||
@ -32,7 +59,7 @@ function pingAll() {
|
||||
network.color = util.stringToColor(network.name);
|
||||
}
|
||||
|
||||
var attemptedVersion = config.versions[network.type][currentVersionIndex[network.type]];
|
||||
const attemptedVersion = getNextProtocolVersion(network)
|
||||
ping.ping(network.ip, network.port, network.type, config.rates.connectTimeout, function(err, res) {
|
||||
// Handle our ping results, if it succeeded.
|
||||
if (err) {
|
||||
@ -40,27 +67,14 @@ function pingAll() {
|
||||
}
|
||||
|
||||
// If we have favicon override specified, use it.
|
||||
if (res && config.faviconOverride && config.faviconOverride[network.name]) {
|
||||
res.favicon = config.faviconOverride[network.name];
|
||||
if (network.favicon) {
|
||||
res.favicon = network.favicon
|
||||
}
|
||||
|
||||
handlePing(network, res, err, attemptedVersion);
|
||||
}, attemptedVersion);
|
||||
}, attemptedVersion.protocolId);
|
||||
})(servers[i]);
|
||||
}
|
||||
|
||||
currentVersionIndex['PC']++;
|
||||
currentVersionIndex['PE']++;
|
||||
|
||||
if (currentVersionIndex['PC'] >= config.versions['PC'].length) {
|
||||
// Loop around
|
||||
currentVersionIndex['PC'] = 0;
|
||||
}
|
||||
|
||||
if (currentVersionIndex['PE'] >= config.versions['PE'].length) {
|
||||
// Loop around
|
||||
currentVersionIndex['PE'] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
// This is where the result of a ping is feed.
|
||||
@ -76,18 +90,31 @@ function handlePing(network, res, err, attemptedVersion) {
|
||||
networkVersions[network.name] = [];
|
||||
}
|
||||
|
||||
const serverVersionHistory = networkVersions[network.name]
|
||||
|
||||
// If the result version matches the attempted version, the version is supported
|
||||
var _networkVersions = networkVersions[network.name];
|
||||
if (res && res.version !== undefined) {
|
||||
const indexOf = serverVersionHistory.indexOf(attemptedVersion.protocolIndex)
|
||||
|
||||
// Test indexOf to avoid inserting previously recorded protocolIndex values
|
||||
if (res.version === attemptedVersion.protocolId && indexOf === -1) {
|
||||
serverVersionHistory.push(attemptedVersion.protocolIndex)
|
||||
} else if (res.version !== attemptedVersion.protocolId && indexOf >= 0) {
|
||||
serverVersionHistory.splice(indexOf, 1)
|
||||
}
|
||||
}
|
||||
|
||||
const timestamp = util.getCurrentTimeMs()
|
||||
|
||||
if (res) {
|
||||
if (res.version == attemptedVersion) {
|
||||
if (_networkVersions.indexOf(res.version) == -1) {
|
||||
_networkVersions.push(res.version);
|
||||
}
|
||||
} else {
|
||||
// Mismatch, so remove the version from the supported version list
|
||||
var index = _networkVersions.indexOf(attemptedVersion);
|
||||
if (index != -1) {
|
||||
_networkVersions.splice(index, 1);
|
||||
const recordData = highestPlayerCount[network.ip]
|
||||
|
||||
// Validate that we have logToDatabase enabled otherwise in memory pings
|
||||
// will create a record that's only valid for the runtime duration.
|
||||
if (config.logToDatabase && (!recordData || res.players.online > recordData.playerCount)) {
|
||||
highestPlayerCount[network.ip] = {
|
||||
playerCount: res.players.online,
|
||||
timestamp: timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -96,20 +123,25 @@ function handlePing(network, res, err, attemptedVersion) {
|
||||
var networkSnapshot = {
|
||||
info: {
|
||||
name: network.name,
|
||||
timestamp: util.getCurrentTimeMs(),
|
||||
timestamp: timestamp,
|
||||
type: network.type
|
||||
},
|
||||
versions: _networkVersions,
|
||||
record: highestPlayerCount[network.ip]
|
||||
versions: serverVersionHistory,
|
||||
recordData: highestPlayerCount[network.ip]
|
||||
};
|
||||
|
||||
if (res) {
|
||||
networkSnapshot.result = res;
|
||||
|
||||
// Validate that we have logToDatabase enabled otherwise in memory pings
|
||||
// will create a record that's only valid for the runtime duration.
|
||||
if (config.logToDatabase && res.players.online > highestPlayerCount[network.ip]) {
|
||||
highestPlayerCount[network.ip] = res.players.online;
|
||||
// Only emit updated favicons
|
||||
// Favicons will otherwise be explicitly emitted during the handshake process
|
||||
if (res.favicon) {
|
||||
const lastFavicon = lastFavicons[network.name]
|
||||
if (lastFavicon !== res.favicon) {
|
||||
lastFavicons[network.name] = res.favicon
|
||||
networkSnapshot.favicon = res.favicon // Send updated favicon directly on object
|
||||
}
|
||||
delete res.favicon // Never store favicons in memory outside lastFavicons
|
||||
}
|
||||
} else if (err) {
|
||||
networkSnapshot.error = err;
|
||||
@ -121,18 +153,15 @@ function handlePing(network, res, err, attemptedVersion) {
|
||||
|
||||
// Remove our previous data that we don't need anymore.
|
||||
for (var i = 0; i < _networkHistory.length; i++) {
|
||||
delete _networkHistory[i].versions
|
||||
delete _networkHistory[i].info;
|
||||
|
||||
if (_networkHistory[i].result) {
|
||||
delete _networkHistory[i].result.favicon;
|
||||
}
|
||||
}
|
||||
|
||||
_networkHistory.push({
|
||||
error: err,
|
||||
result: res,
|
||||
versions: _networkVersions,
|
||||
timestamp: util.getCurrentTimeMs(),
|
||||
versions: serverVersionHistory,
|
||||
timestamp: timestamp,
|
||||
info: {
|
||||
ip: network.ip,
|
||||
port: network.port,
|
||||
@ -148,18 +177,16 @@ function handlePing(network, res, err, attemptedVersion) {
|
||||
|
||||
// Log it to the database if needed.
|
||||
if (config.logToDatabase) {
|
||||
db.log(network.ip, util.getCurrentTimeMs(), res ? res.players.online : 0);
|
||||
db.log(network.ip, timestamp, res ? res.players.online : 0);
|
||||
}
|
||||
|
||||
// Push it to our graphs.
|
||||
var timeMs = util.getCurrentTimeMs();
|
||||
|
||||
// The same mechanic from trimUselessPings is seen here.
|
||||
// If we dropped the ping, then to avoid destroying the graph, ignore it.
|
||||
// However if it's been too long since the last successful ping, we'll send it anyways.
|
||||
if (config.logToDatabase) {
|
||||
if (!lastGraphPush[network.ip] || (timeMs - lastGraphPush[network.ip] >= 60 * 1000 && res) || timeMs - lastGraphPush[network.ip] >= 70 * 1000) {
|
||||
lastGraphPush[network.ip] = timeMs;
|
||||
if (!lastGraphPush[network.ip] || (timestamp - lastGraphPush[network.ip] >= 60 * 1000 && res) || timestamp - lastGraphPush[network.ip] >= 70 * 1000) {
|
||||
lastGraphPush[network.ip] = timestamp;
|
||||
|
||||
// Don't have too much data!
|
||||
util.trimOldPings(graphData);
|
||||
@ -168,14 +195,14 @@ function handlePing(network, res, err, attemptedVersion) {
|
||||
graphData[network.name] = [];
|
||||
}
|
||||
|
||||
graphData[network.name].push([timeMs, res ? res.players.online : 0]);
|
||||
graphData[network.name].push([timestamp, res ? res.players.online : 0]);
|
||||
|
||||
// Send the update.
|
||||
server.io.sockets.emit('updateHistoryGraph', {
|
||||
ip: network.ip,
|
||||
name: network.name,
|
||||
players: (res ? res.players.online : 0),
|
||||
timestamp: timeMs
|
||||
timestamp: timestamp
|
||||
});
|
||||
}
|
||||
|
||||
@ -234,10 +261,6 @@ function startServices() {
|
||||
|
||||
logger.log('info', '%s connected, total clients: %d', util.getRemoteAddr(client.request), connectedClients);
|
||||
|
||||
// We send the boot time (also sent in publicConfig.json) to the frontend to validate they have the same config.
|
||||
// If so, they'll send back "requestListing" event, otherwise they will pull the new config and retry.
|
||||
client.emit('bootTime', util.getBootTime());
|
||||
|
||||
// Attach our listeners.
|
||||
client.on('disconnect', function() {
|
||||
connectedClients -= 1;
|
||||
@ -257,41 +280,54 @@ function startServices() {
|
||||
}
|
||||
});
|
||||
|
||||
client.on('requestListing', function() {
|
||||
// Send them our previous data, so they have somewhere to start.
|
||||
client.emit('updateMojangServices', mojang.toMessage());
|
||||
const minecraftVersionNames = {}
|
||||
Object.keys(minecraftVersions).forEach(function (key) {
|
||||
minecraftVersionNames[key] = minecraftVersions[key].map(version => version.name)
|
||||
})
|
||||
|
||||
// Remap our associative array into just an array.
|
||||
var networkHistoryKeys = Object.keys(networkHistory);
|
||||
|
||||
networkHistoryKeys.sort();
|
||||
|
||||
// Send each individually, this should look cleaner than waiting for one big array to transfer.
|
||||
for (var i = 0; i < servers.length; i++) {
|
||||
var server = servers[i];
|
||||
|
||||
if (!(server.name in networkHistory) || networkHistory[server.name].length < 1) {
|
||||
// This server hasn't been ping'd yet. Send a hacky placeholder.
|
||||
client.emit('add', [[{
|
||||
error: {
|
||||
description: 'Waiting'
|
||||
},
|
||||
result: null,
|
||||
timestamp: util.getCurrentTimeMs(),
|
||||
info: {
|
||||
ip: server.ip,
|
||||
port: server.port,
|
||||
type: server.type,
|
||||
name: server.name
|
||||
}
|
||||
}]]);
|
||||
} else {
|
||||
client.emit('add', [networkHistory[networkHistoryKeys[i]]]);
|
||||
}
|
||||
}
|
||||
|
||||
client.emit('syncComplete');
|
||||
// Send configuration data for rendering the page
|
||||
client.emit('setPublicConfig', {
|
||||
graphDuration: config.graphDuration,
|
||||
servers: servers,
|
||||
minecraftVersions: minecraftVersionNames,
|
||||
isGraphVisible: config.logToDatabase
|
||||
});
|
||||
|
||||
// Send them our previous data, so they have somewhere to start.
|
||||
client.emit('updateMojangServices', mojang.toMessage());
|
||||
|
||||
// Send each individually, this should look cleaner than waiting for one big array to transfer.
|
||||
for (var i = 0; i < servers.length; i++) {
|
||||
var server = servers[i];
|
||||
|
||||
if (!(server.name in networkHistory) || networkHistory[server.name].length < 1) {
|
||||
// This server hasn't been ping'd yet. Send a hacky placeholder.
|
||||
client.emit('add', [[{
|
||||
error: {
|
||||
description: 'Waiting...',
|
||||
placeholder: true
|
||||
},
|
||||
result: null,
|
||||
timestamp: util.getCurrentTimeMs(),
|
||||
info: {
|
||||
ip: server.ip,
|
||||
port: server.port,
|
||||
type: server.type,
|
||||
name: server.name
|
||||
}
|
||||
}]]);
|
||||
} else {
|
||||
// Append the lastFavicon to the last ping entry
|
||||
const serverHistory = networkHistory[server.name];
|
||||
const lastFavicon = lastFavicons[server.name];
|
||||
if (lastFavicon) {
|
||||
serverHistory[serverHistory.length - 1].favicon = lastFavicon
|
||||
}
|
||||
client.emit('add', [serverHistory])
|
||||
}
|
||||
}
|
||||
|
||||
client.emit('syncComplete');
|
||||
});
|
||||
|
||||
startMainLoop();
|
||||
@ -332,10 +368,13 @@ if (config.logToDatabase) {
|
||||
}
|
||||
|
||||
(function(server) {
|
||||
db.getTotalRecord(server.ip, function(record) {
|
||||
logger.log('info', 'Computed total record %s (%d)', server.ip, record);
|
||||
db.getTotalRecord(server.ip, function(playerCount, timestamp) {
|
||||
logger.log('info', 'Computed total record %s (%d) @ %d', server.ip, playerCount, timestamp);
|
||||
|
||||
highestPlayerCount[server.ip] = record;
|
||||
highestPlayerCount[server.ip] = {
|
||||
playerCount: playerCount,
|
||||
timestamp: timestamp
|
||||
};
|
||||
|
||||
completedQueries += 1;
|
||||
|
||||
|
59
assets/css/icons.css
Normal file
59
assets/css/icons.css
Normal file
@ -0,0 +1,59 @@
|
||||
@font-face {
|
||||
font-family: 'icomoon';
|
||||
src:
|
||||
url('../fonts/icomoon.ttf?gn52nv') format('truetype'),
|
||||
url('../fonts/icomoon.woff?gn52nv') format('woff'),
|
||||
url('../fonts/icomoon.svg?gn52nv#icomoon') format('svg');
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
}
|
||||
|
||||
[class^="icon-"], [class*=" icon-"] {
|
||||
/* use !important to prevent issues with browser extensions that change fonts */
|
||||
font-family: 'icomoon' !important;
|
||||
speak: none;
|
||||
font-style: normal;
|
||||
font-weight: normal;
|
||||
font-variant: normal;
|
||||
text-transform: none;
|
||||
line-height: 1;
|
||||
|
||||
/* Better Font Rendering =========== */
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
.icon-star:before {
|
||||
content: "\f005";
|
||||
}
|
||||
.icon-star-o:before {
|
||||
content: "\f006";
|
||||
}
|
||||
.icon-lock:before {
|
||||
content: "\f023";
|
||||
}
|
||||
.icon-eye:before {
|
||||
content: "\f06e";
|
||||
}
|
||||
.icon-eye-slash:before {
|
||||
content: "\f070";
|
||||
}
|
||||
.icon-cogs:before {
|
||||
content: "\f085";
|
||||
}
|
||||
.icon-gears:before {
|
||||
content: "\f085";
|
||||
}
|
||||
.icon-globe:before {
|
||||
content: "\f0ac";
|
||||
}
|
||||
.icon-code:before {
|
||||
content: "\f121";
|
||||
}
|
||||
.icon-sort-amount-desc:before {
|
||||
content: "\f161";
|
||||
}
|
||||
.icon-street-view:before {
|
||||
content: "\f21d";
|
||||
}
|
@ -1,16 +1,62 @@
|
||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300,400);
|
||||
@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300);
|
||||
@import url(../css/icons.css);
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
body {
|
||||
:root {
|
||||
--color-dark-gray: #A3A3A3;
|
||||
--color-gold: #FFD700;
|
||||
--color-dark-purple: #6c5ce7;
|
||||
--color-light-purple: #a29bfe;
|
||||
--color-dark-blue: #0984e3;
|
||||
--color-light-blue: #74b9ff;
|
||||
|
||||
--theme-color-dark: #3B3738;
|
||||
--theme-color-light: #EBEBEB;
|
||||
|
||||
--border-radius: 1px;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--color-purple: var(--color-light-purple);
|
||||
--color-blue: var(--color-light-blue);
|
||||
--background-color: var(--theme-color-light);
|
||||
--text-decoration-color: var(--theme-color-dark);
|
||||
--text-color: #000;
|
||||
--text-color-inverted: #FFF;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #212021;
|
||||
color: #FFF;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 300 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--color-purple: var(--color-dark-purple);
|
||||
--color-blue: var(--color-dark-blue);
|
||||
--background-color: var(--theme-color-dark);
|
||||
--text-decoration-color: var(--theme-color-light);
|
||||
--text-color: #FFF;
|
||||
--text-color-inverted: #000;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #1c1b1c;
|
||||
color: #FFF;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-size: 18px;
|
||||
font-weight: 300;
|
||||
min-width: 800px;
|
||||
}
|
||||
|
||||
/* Page layout */
|
||||
@ -19,195 +65,287 @@ html, body {
|
||||
}
|
||||
|
||||
a {
|
||||
cursor: pointer;
|
||||
cursor: pointer;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
#push {
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
display: none;
|
||||
position: relative;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Logo */
|
||||
.logo-text {
|
||||
letter-spacing: -3px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
#header {
|
||||
background: #EBEBEB;
|
||||
color: #3B3738;
|
||||
overflow: auto;
|
||||
header {
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
#header-wrapper {
|
||||
overflow: auto;
|
||||
min-width: 850px;
|
||||
header .column-left {
|
||||
float: left;
|
||||
}
|
||||
|
||||
#header .column {
|
||||
display: inline-block;
|
||||
float: left;
|
||||
header .column-right {
|
||||
float: right;
|
||||
}
|
||||
|
||||
#header .column h1 {
|
||||
margin: -6px 0;
|
||||
header .logo-image {
|
||||
--fixed-logo-image-size: 36px;
|
||||
width: var(--fixed-logo-image-size);
|
||||
height: var(--fixed-logo-image-size);
|
||||
margin-right: 5px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#header .slogan {
|
||||
font-size: 20px;
|
||||
text-align: left;
|
||||
header .logo-text {
|
||||
font-size: 48px;
|
||||
margin: -6px 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
#header .subslogan {
|
||||
font-size: 19px;
|
||||
header .logo-status {
|
||||
font-size: 21px;
|
||||
}
|
||||
|
||||
#header a, #footer a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
border-bottom: 1px dashed #3B3738;
|
||||
header a {
|
||||
border-bottom: 1px dashed var(--text-decoration-color);
|
||||
}
|
||||
|
||||
#header a:hover, #footer a:hover {
|
||||
border-bottom: 1px dashed transparent;
|
||||
header a:hover {
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
|
||||
#header > h1 {
|
||||
font-size: 42px;
|
||||
header .header-button {
|
||||
color: var(--text-color);
|
||||
width: 83px;
|
||||
height: 83px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
margin-left: -5px;
|
||||
}
|
||||
|
||||
#header > #column-center {
|
||||
width: 1480px;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
header .header-button > span:first-of-type {
|
||||
display: block;
|
||||
margin-top: 10px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
header .header-button-group {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
header .header-button-group:first-of-type {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
header .header-button-group:last-of-type {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
header .header-button-single {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
border-radius: var(--border-radius);
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
header .header-button-single:hover {
|
||||
background: var(--background-color) !important;
|
||||
}
|
||||
|
||||
/* Footer */
|
||||
#footer {
|
||||
font-size: 16px;
|
||||
text-transform: uppercase;
|
||||
background: #EBEBEB;
|
||||
color: #3B3738;
|
||||
padding: 15px 0;
|
||||
min-width: 950px;
|
||||
margin-top: 15px;
|
||||
footer {
|
||||
display: none;
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
padding: 10px 0 15px 0;
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
#footer a {
|
||||
font-weight: 700;
|
||||
border-bottom: none !important;
|
||||
footer a {
|
||||
border-bottom: 1px dashed var(--text-decoration-color);
|
||||
}
|
||||
|
||||
#footer a:hover {
|
||||
border-bottom: 1px dashed #000 !important;
|
||||
footer a:hover {
|
||||
border-bottom: 1px dashed transparent;
|
||||
}
|
||||
|
||||
/* Tagline */
|
||||
#tagline-text {
|
||||
padding-top: 20px;
|
||||
text-align: center;
|
||||
/* Status overly */
|
||||
#status-overlay {
|
||||
padding: 20px 0;
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius);
|
||||
text-align: center;
|
||||
height: 150px;
|
||||
width: 350px;
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
margin-top: -95px;
|
||||
margin-left: -175px;
|
||||
}
|
||||
|
||||
#status-overlay .logo-image {
|
||||
--fixed-logo-image-size: 72px;
|
||||
width: var(--fixed-logo-image-size);
|
||||
height: var(--fixed-logo-image-size);
|
||||
}
|
||||
|
||||
/* Floating tooltip */
|
||||
#tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 5px 8px;
|
||||
border-radius: var(--border-radius);
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
/* Server listing */
|
||||
.server-container {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
#server-list {
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.server {
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px;
|
||||
width: 800px;
|
||||
border: 1px solid transparent;
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
margin: 0 5px;
|
||||
width: 800px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.version {
|
||||
font-size: 12px;
|
||||
.server .column {
|
||||
float: left;
|
||||
}
|
||||
|
||||
/*.server:hover {
|
||||
background: #282828;
|
||||
border: 1px solid #444;
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
}*/
|
||||
|
||||
.server > .column > img {
|
||||
border-radius: 2px;
|
||||
margin-top: 5px;
|
||||
.server .column-favicon {
|
||||
width: 80px;
|
||||
}
|
||||
|
||||
.server > .column {
|
||||
float: left;
|
||||
display: inline-block;
|
||||
.server .column-favicon .server-favicon {
|
||||
--fixed-server-favicon-size: 64px;
|
||||
width: var(--fixed-server-favicon-size);
|
||||
height: var(--fixed-server-favicon-size);
|
||||
border-radius: var(--border-radius);
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.server > .column > .rank {
|
||||
width: 64px;
|
||||
padding-top: 4px;
|
||||
.server .column-favicon .server-rank {
|
||||
display: block;
|
||||
width: 64px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.server > .column > h3 > .type {
|
||||
padding: 1px 5px;
|
||||
border-radius: 2px;
|
||||
border: 1px solid #A09E9E;
|
||||
font-size: 14px;
|
||||
margin-bottom: 2px;
|
||||
.server .column-status {
|
||||
width: 282px;
|
||||
}
|
||||
|
||||
.server-meta {
|
||||
font-size: 16px !important;
|
||||
.server .column-status .server-name {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Charts */
|
||||
.chart {
|
||||
height: 100px;
|
||||
width: 400px;
|
||||
margin-right: -3px;
|
||||
margin-bottom: 5px;
|
||||
.server .column-status .server-is-favorite {
|
||||
cursor: pointer;
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
#tooltip {
|
||||
display: none;
|
||||
position: absolute;
|
||||
padding: 5px;
|
||||
border-radius: 3px;
|
||||
background: rgba(0, 0, 0, 0.65);
|
||||
z-index: 10000;
|
||||
.server .column-status .server-is-favorite:hover::before {
|
||||
content: "\f006";
|
||||
}
|
||||
|
||||
/* Existing elements */
|
||||
h3 {
|
||||
text-transform: uppercase;
|
||||
.server .column-status .server-is-not-favorite {
|
||||
cursor: pointer;
|
||||
color: var(--background-color);
|
||||
}
|
||||
|
||||
/* Basic classes used randomly */
|
||||
.color-gray {
|
||||
color: #C4C4C4;
|
||||
.server .column-status .server-is-not-favorite:hover {
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
.color-dark-gray {
|
||||
color: #A3A3A3;
|
||||
.server .column-status .server-error {
|
||||
display: none;
|
||||
color: #e74c3c;
|
||||
}
|
||||
|
||||
.color-red {
|
||||
color: #e74c3c;
|
||||
.server .column-status .server-label {
|
||||
color: var(--color-dark-gray);
|
||||
font-size: 16px;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-uppercase {
|
||||
text-transform: uppercase;
|
||||
.server .column-status .server-value {
|
||||
color: var(--color-dark-gray);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.text-center-align {
|
||||
text-align: center;
|
||||
.server .column-graph {
|
||||
float: right;
|
||||
height: 100px;
|
||||
width: 400px;
|
||||
margin-right: -3px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
/* Highlighted values */
|
||||
.server-highlighted-label {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.server-highlighted-value {
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Global stats */
|
||||
.global-stat {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
/* Sort by */
|
||||
#sort-by {
|
||||
background: var(--color-purple);
|
||||
}
|
||||
|
||||
/* Settings toggle */
|
||||
#settings-toggle {
|
||||
background: var(--color-blue);
|
||||
}
|
||||
|
||||
/* Historical graph */
|
||||
#big-graph-mobile-load-request {
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
padding: 10px 0;
|
||||
text-align: center;
|
||||
display: none;
|
||||
width: 100%;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
#big-graph-mobile-load-request a {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* The big graph */
|
||||
#big-graph, #big-graph-controls, #big-graph-checkboxes {
|
||||
width: 90%;
|
||||
margin: 15px auto 0 auto;
|
||||
}
|
||||
|
||||
#big-graph-checkboxes > table {
|
||||
@ -215,119 +353,105 @@ h3 {
|
||||
}
|
||||
|
||||
#big-graph {
|
||||
margin-top: 20px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
#big-graph-controls {
|
||||
margin: 10px auto;
|
||||
margin: 10px auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#big-graph-controls a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
text-transform: uppercase;
|
||||
border-bottom: 1px dashed #FFF;
|
||||
font-size: 16px;
|
||||
#big-graph-controls .icon-star {
|
||||
color: var(--color-gold);
|
||||
}
|
||||
|
||||
#big-graph-controls a:hover {
|
||||
border-bottom: 1px dashed transparent;
|
||||
#big-graph-controls-drawer {
|
||||
background: var(--background-color);
|
||||
color: var(--text-color);
|
||||
border-radius: var(--border-radius);
|
||||
padding-bottom: 10px;
|
||||
overflow: auto;
|
||||
display: none;
|
||||
}
|
||||
|
||||
#big-graph-controls-drawer .graph-controls-setall {
|
||||
text-align: center;
|
||||
display: block;
|
||||
margin: 15px 0;
|
||||
}
|
||||
|
||||
#big-graph-checkboxes {
|
||||
margin: 15px auto 0 auto;
|
||||
}
|
||||
|
||||
/* Basic elements */
|
||||
.button {
|
||||
background: #3498db;
|
||||
border-radius: 2px;
|
||||
text-shadow: 0 0 0 #000;
|
||||
width: 85px;
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
margin: 0 auto;
|
||||
background: var(--color-blue);
|
||||
border-radius: var(--border-radius);
|
||||
font-size: 16px;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
background: #ecf0f1;
|
||||
color: #3498db;
|
||||
cursor: pointer;
|
||||
background: var(--text-color);
|
||||
color: var(--text-color-inverted);
|
||||
}
|
||||
|
||||
/* Percentage bar */
|
||||
#perc-bar {
|
||||
height: 35px;
|
||||
position: relative;
|
||||
height: 35px;
|
||||
position: relative;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
#perc-bar > .perc-bar-part {
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
transition: 0.1s all;
|
||||
}
|
||||
|
||||
#perc-bar > .perc-bar-part:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* Mojang Status */
|
||||
.mojang-status {
|
||||
width: 85px;
|
||||
height: 106px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.mojang-status > strong {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.mojang-status > i {
|
||||
margin-top: 20px;
|
||||
font-size: 22px;
|
||||
#perc-bar .perc-bar-part {
|
||||
height: 100%;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
/* Mojang status colors */
|
||||
.mojang-status-online {
|
||||
background: #87D37C;
|
||||
@media (prefers-color-scheme: light) {
|
||||
.mojang-status-online {
|
||||
background: #87D37C;
|
||||
}
|
||||
|
||||
.mojang-status-unstable {
|
||||
background: #f1c40f;
|
||||
}
|
||||
|
||||
.mojang-status-offline {
|
||||
background: #DE5749;
|
||||
}
|
||||
}
|
||||
|
||||
.mojang-status-unstable {
|
||||
background: #f1c40f;
|
||||
}
|
||||
|
||||
.mojang-status-offline {
|
||||
background: #DE5749;
|
||||
}
|
||||
|
||||
/* Dark color scheme overrides */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body {
|
||||
background: #1c1b1c;
|
||||
}
|
||||
.mojang-status-online {
|
||||
background: #66aa5a;
|
||||
}
|
||||
|
||||
#header, #footer {
|
||||
background: #3B3738;
|
||||
color: #FFF;
|
||||
}
|
||||
.mojang-status-unstable {
|
||||
background: #cc8a4f;
|
||||
}
|
||||
|
||||
#footer a, #header a:hover {
|
||||
border-bottom: 1px dashed transparent !important;
|
||||
}
|
||||
.mojang-status-offline {
|
||||
background: #A6453B;
|
||||
}
|
||||
}
|
||||
|
||||
#footer a:hover, #header a {
|
||||
border-bottom: 1px dashed #EBEBEB !important;
|
||||
}
|
||||
/* Header rows */
|
||||
@media only screen and (max-width: 1050px) {
|
||||
header {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mojang-status-online {
|
||||
background: #6b9963;
|
||||
}
|
||||
.header-possible-row-break {
|
||||
padding-top: 20px;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mojang-status-unstable {
|
||||
background: #a87b4b;
|
||||
}
|
||||
|
||||
.mojang-status-offline {
|
||||
background: #A6453B;
|
||||
}
|
||||
}
|
||||
.header-possible-row-break:last-of-type {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
20
assets/fonts/icomoon.svg
Normal file
20
assets/fonts/icomoon.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 17 KiB |
BIN
assets/fonts/icomoon.ttf
Normal file
BIN
assets/fonts/icomoon.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/icomoon.woff
Normal file
BIN
assets/fonts/icomoon.woff
Normal file
Binary file not shown.
@ -2,98 +2,97 @@
|
||||
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<head>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="css/main.css">
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.6.3/css/font-awesome.min.css">
|
||||
<link rel="stylesheet" type="text/css" href="../css/main.css">
|
||||
|
||||
<link rel="icon" type="image/png" href="/images/compass.png">
|
||||
<link rel="icon" type="image/svg+xml" href="../images/logo.svg">
|
||||
|
||||
<title>Minetrack</title>
|
||||
<meta charset="UTF-8">
|
||||
|
||||
</head>
|
||||
<title>Minetrack</title>
|
||||
|
||||
<body>
|
||||
</head>
|
||||
|
||||
<div id="tooltip"></div>
|
||||
<body>
|
||||
|
||||
<div id="push">
|
||||
<div id="tooltip"></div>
|
||||
|
||||
<div id="header">
|
||||
<div id="status-overlay">
|
||||
<img class="logo-image" src="../images/logo.svg">
|
||||
<h1 class="logo-text">Minetrack</h1>
|
||||
<div id="status-text">Connecting...</div>
|
||||
</div>
|
||||
|
||||
<div id="header-wrapper">
|
||||
<div id="push">
|
||||
|
||||
<div class="column" style="padding: 20px;">
|
||||
|
||||
<img src="/images/compass.png" width="28" height="28" style="display: inline-block; margin-right: 5px;">
|
||||
<h1 style="display: inline-block;" class="text-uppercase">Minetrack</h1>
|
||||
<p class="subslogan text-uppercase">Watching <span id="stat_totalPlayers">0</span> players on <span id="stat_networks">0</span> networks.</p>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="mojang-status-list" class="column" style="float: right;">
|
||||
|
||||
<div class="mojang-status" title="Sessions" id="mojang-status_Sessions"><i class="fa fa-gamepad"></i><br /><strong>Sessions</strong><p class="mojang-status-text" id="mojang-status-text_Sessions">...</p></div><div class="mojang-status" title="Skins" id="mojang-status_Skins"><i class="fa fa-user"></i><br /><strong>Skins</strong><p class="mojang-status-text" id="mojang-status-text_Skins">...</p></div><div class="mojang-status" title="Auth" id="mojang-status_Auth"><i class="fa fa-key"></i><br /><strong>Auth</strong><p class="mojang-status-text" id="mojang-status-text_Auth">...</p></div><div class="mojang-status" title="API" id="mojang-status_API"><i class="fa fa-wrench"></i><br /><strong>API</strong><p class="mojang-status-text" id="mojang-status-text_API">...</p></div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div id="perc-bar"></div>
|
||||
|
||||
<header>
|
||||
<div class="header-possible-row-break column-left">
|
||||
<img class="logo-image" src="../images/logo.svg">
|
||||
<h1 class="logo-text">Minetrack</h1>
|
||||
<p class="logo-status">Counting <span class="global-stat" id="stat_totalPlayers">0</span> players on <span class="global-stat" id="stat_networks">0</span> Minecraft servers.</p>
|
||||
</div>
|
||||
|
||||
<div id="perc-bar"></div>
|
||||
|
||||
<div id="tagline-text">Connecting...</div>
|
||||
|
||||
<div id="big-graph"></div>
|
||||
|
||||
<div id="big-graph-controls" style="display: none;">
|
||||
|
||||
<span style="text-align: center; display: block;">
|
||||
|
||||
<a onclick="toggleControlsDrawer();">Click to toggle graph controls</a>
|
||||
|
||||
</span>
|
||||
|
||||
<div id="big-graph-controls-drawer" style="display: none;">
|
||||
|
||||
<div id="big-graph-checkboxes"></div>
|
||||
|
||||
<br />
|
||||
|
||||
<span style="text-align: center; display: block; margin-bottom: 15px;">
|
||||
|
||||
<span onclick="setAllGraphVisibility(true);" class="button">Show All</span>
|
||||
<span onclick="setAllGraphVisibility(false);" class="button">Hide All</span>
|
||||
|
||||
</span>
|
||||
<div class="header-possible-row-break column-right">
|
||||
<div id="sort-by" class="header-button header-button-single"><span class="icon-sort-amount-desc"></span> Sort By<br><strong id="sort-by-text">...</strong></div>
|
||||
|
||||
<div id="settings-toggle" class="header-button header-button-single"><span class="icon-gears"></span> Graph Controls</div>
|
||||
|
||||
<div class="header-button header-button-group" title="Sessions" id="mojang-status_Sessions">
|
||||
<span class="icon-globe"></span><strong>Sessions</strong>
|
||||
<p class="mojang-status-text" id="mojang-status-text_Sessions">...</p>
|
||||
</div>
|
||||
<div class="header-button header-button-group" title="Skins" id="mojang-status_Skins">
|
||||
<span class="icon-street-view"></span><strong>Skins</strong>
|
||||
<p class="mojang-status-text" id="mojang-status-text_Skins">...</p>
|
||||
</div>
|
||||
<div class="header-button header-button-group" title="Auth" id="mojang-status_Auth">
|
||||
<span class="icon-lock"></span><strong>Auth</strong>
|
||||
<p class="mojang-status-text" id="mojang-status-text_Auth">...</p>
|
||||
</div>
|
||||
<div class="header-button header-button-group" title="API" id="mojang-status_API">
|
||||
<span class="icon-code"></span><strong>API</strong>
|
||||
<p class="mojang-status-text" id="mojang-status-text_API">...</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div id="server-container-list" class="server-container"></div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="footer">
|
||||
|
||||
<span style="padding-left: 20px;">Website powered by <a href="https://github.com/Cryptkeeper/Minetrack"><i class="fa fa-github"></i> Minetrack</a>.</span>
|
||||
|
||||
<div id="big-graph-mobile-load-request">
|
||||
<strong>On a mobile device?</strong>
|
||||
<p>Minetrack has skipped automatically loading the historical graph to help save data and power.</p>
|
||||
<br>
|
||||
<a id="big-graph-mobile-load-request-button" class="button">Load Historical Graph</a>
|
||||
</div>
|
||||
|
||||
<!-- External JS assets -->
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script>
|
||||
<div id="big-graph"></div>
|
||||
|
||||
<!-- Internal JS assets -->
|
||||
<script src="js/util.js"></script>
|
||||
<script src="js/graph.js"></script>
|
||||
<script src="js/site.js"></script>
|
||||
<div id="big-graph-controls">
|
||||
<div id="big-graph-controls-drawer">
|
||||
<div id="big-graph-checkboxes"></div>
|
||||
|
||||
<script src="publicConfig.json"></script>
|
||||
<span class="graph-controls-setall">
|
||||
<a minetrack-show-type="all" class="button graph-controls-show"><span class="icon-eye"></span> Show All</a>
|
||||
<a minetrack-show-type="none" class="button graph-controls-show"><span class="icon-eye-slash"></span> Hide All</a>
|
||||
<a minetrack-show-type="favorites" class="button graph-controls-show"><span class="icon-star"></span> Only Favorites</a>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
<div id="server-list"></div>
|
||||
|
||||
</html>
|
||||
</div>
|
||||
|
||||
<footer id="footer">
|
||||
<span class="icon-code"></span> Powered by open source software - <a href="https://github.com/Cryptkeeper/Minetrack">make it your own!</a>
|
||||
</footer>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.1/jquery.slim.min.js"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script>
|
||||
|
||||
<script src="../js/main.js" defer></script>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
Binary file not shown.
Before Width: | Height: | Size: 6.3 KiB |
8
assets/images/logo.svg
Normal file
8
assets/images/logo.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle style="fill: rgb(85, 218, 149);" cx="250" cy="250" r="250"/>
|
||||
<path d="M 0 250 C 0 111.929 111.929 0 250 0 C 369.688 0 469.731 84.108 494.25 196.443 L 300.624 389.846 L 200.592 290.249 L 68.654 422.087 C 26.106 377.264 0 316.681 0 250 Z" style="fill: rgb(178, 255, 161);"/>
|
||||
<path d="M 191.114 130.467 L 217.723 185.377 L 164.504 185.377 L 191.114 130.467 Z" style="fill: rgb(41, 41, 41);" transform="matrix(-1, 0.000057, -0.000057, -1, 391.12207, 407.396851)"/>
|
||||
<rect x="100" y="81.861" width="200" height="147.724" style="fill: rgb(41, 41, 41);" rx="7.498" ry="7.498"/>
|
||||
<path d="M 185.523 118.251 L 185.523 200.341 L 163.003 200.341 L 163.003 156.881 C 163.003 151.601 163.116 146.771 163.343 142.391 C 161.956 144.078 160.233 145.838 158.173 147.671 L 148.853 155.361 L 137.343 141.211 L 165.533 118.251 L 185.523 118.251 ZM 263.346 159.401 C 263.346 173.928 260.912 184.571 256.046 191.331 C 251.179 198.084 243.729 201.461 233.696 201.461 C 223.889 201.461 216.496 197.934 211.516 190.881 C 206.536 183.821 204.046 173.328 204.046 159.401 C 204.046 144.808 206.479 134.104 211.346 127.291 C 216.212 120.478 223.662 117.071 233.696 117.071 C 243.469 117.071 250.852 120.618 255.846 127.711 C 260.846 134.804 263.346 145.368 263.346 159.401 Z M 226.116 159.401 C 226.116 168.688 226.696 175.071 227.856 178.551 C 229.016 182.038 230.962 183.781 233.696 183.781 C 236.469 183.781 238.426 181.981 239.566 178.381 C 240.706 174.788 241.276 168.461 241.276 159.401 C 241.276 150.308 240.696 143.938 239.536 140.291 C 238.376 136.638 236.429 134.811 233.696 134.811 C 230.962 134.811 229.016 136.571 227.856 140.091 C 226.696 143.611 226.116 150.048 226.116 159.401 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); white-space: pre;"/>
|
||||
</svg>
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
Before Width: | Height: | Size: 30 KiB |
8
assets/images/missing_favicon.svg
Normal file
8
assets/images/missing_favicon.svg
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 500 500" width="500" height="500" xmlns="http://www.w3.org/2000/svg">
|
||||
<circle style="fill: rgb(227, 106, 87);" cx="250" cy="250" r="250"/>
|
||||
<path d="M 5.7 172.087 C 5.7 310.158 117.629 422.087 255.7 422.087 C 375.388 422.087 475.431 337.979 499.95 225.644 L 306.324 32.241 L 206.292 131.838 L 74.354 0 C 31.806 44.823 5.7 105.406 5.7 172.087 Z" style="fill: rgb(222, 222, 222);" transform="matrix(-1, 0, 0, -1, 505.700409, 422.087006)"/>
|
||||
<path d="M 191.114 130.467 L 217.723 185.377 L 164.504 185.377 L 191.114 130.467 Z" style="fill: rgb(41, 41, 41);" transform="matrix(-1, 0.000057, -0.000057, -1, 491.12207, 407.396851)"/>
|
||||
<rect x="200" y="81.861" width="200" height="147.724" style="fill: rgb(41, 41, 41);" rx="7.498" ry="7.498"/>
|
||||
<path d="M 304.775 171.28 L 287.535 171.28 L 287.535 166.45 C 287.535 162.777 288.285 159.677 289.785 157.15 C 291.285 154.623 293.942 152.163 297.755 149.77 C 300.788 147.863 302.972 146.14 304.305 144.6 C 305.632 143.067 306.295 141.307 306.295 139.32 C 306.295 137.753 305.585 136.51 304.165 135.59 C 302.738 134.67 300.885 134.21 298.605 134.21 C 292.952 134.21 286.345 136.213 278.785 140.22 L 270.975 124.95 C 280.295 119.637 290.028 116.98 300.175 116.98 C 308.522 116.98 315.072 118.813 319.825 122.48 C 324.578 126.147 326.955 131.143 326.955 137.47 C 326.955 142.003 325.898 145.927 323.785 149.24 C 321.672 152.553 318.295 155.687 313.655 158.64 C 309.722 161.187 307.258 163.05 306.265 164.23 C 305.272 165.41 304.775 166.803 304.775 168.41 L 304.775 171.28 Z M 285.125 190.93 C 285.125 187.483 286.125 184.827 288.125 182.96 C 290.132 181.087 293.082 180.15 296.975 180.15 C 300.715 180.15 303.588 181.093 305.595 182.98 C 307.595 184.873 308.595 187.523 308.595 190.93 C 308.595 194.337 307.558 196.977 305.485 198.85 C 303.405 200.717 300.568 201.65 296.975 201.65 C 293.268 201.65 290.365 200.723 288.265 198.87 C 286.172 197.017 285.125 194.37 285.125 190.93 Z" transform="matrix(1, 0, 0, 1, 0, 0)" style="fill: rgb(255, 255, 255); white-space: pre;"/>
|
||||
</svg>
|
After Width: | Height: | Size: 2.0 KiB |
140
assets/js/app.js
Normal file
140
assets/js/app.js
Normal file
@ -0,0 +1,140 @@
|
||||
import { ServerRegistry } from './servers'
|
||||
import { SortController } from './sort'
|
||||
import { GraphDisplayManager } from './graph'
|
||||
import { MojangUpdater } from './mojang'
|
||||
import { PercentageBar } from './percbar'
|
||||
import { FavoritesManager } from './favorites'
|
||||
import { Tooltip, Caption, formatNumber } from './util'
|
||||
|
||||
export class App {
|
||||
publicConfig
|
||||
|
||||
constructor () {
|
||||
this.tooltip = new Tooltip()
|
||||
this.caption = new Caption()
|
||||
this.serverRegistry = new ServerRegistry(this)
|
||||
this.sortController = new SortController(this)
|
||||
this.graphDisplayManager = new GraphDisplayManager(this)
|
||||
this.mojangUpdater = new MojangUpdater()
|
||||
this.percentageBar = new PercentageBar(this)
|
||||
this.favoritesManager = new FavoritesManager(this)
|
||||
|
||||
this._taskIds = []
|
||||
}
|
||||
|
||||
setPageReady (isReady) {
|
||||
document.getElementById('push').style.display = isReady ? 'block' : 'none'
|
||||
document.getElementById('footer').style.display = isReady ? 'block' : 'none'
|
||||
document.getElementById('status-overlay').style.display = isReady ? 'none' : 'block'
|
||||
}
|
||||
|
||||
setPublicConfig (publicConfig) {
|
||||
this.publicConfig = publicConfig
|
||||
|
||||
this.serverRegistry.assignServers(publicConfig.servers)
|
||||
|
||||
// Start repeating frontend tasks once it has received enough data to be considered active
|
||||
// This simplifies management logic at the cost of each task needing to safely handle empty data
|
||||
this.initTasks()
|
||||
}
|
||||
|
||||
handleSyncComplete () {
|
||||
this.caption.hide()
|
||||
|
||||
// Load favorites since all servers are registered
|
||||
this.favoritesManager.loadLocalStorage()
|
||||
|
||||
// Run a single bulk server sort instead of per-add event since there may be multiple
|
||||
this.sortController.show()
|
||||
this.percentageBar.redraw()
|
||||
}
|
||||
|
||||
initTasks () {
|
||||
this._taskIds.push(setInterval(this.sortController.sortServers, 5000))
|
||||
this._taskIds.push(setInterval(this.updateGlobalStats, 1000))
|
||||
this._taskIds.push(setInterval(this.percentageBar.redraw, 1000))
|
||||
}
|
||||
|
||||
handleDisconnect () {
|
||||
this.tooltip.hide()
|
||||
|
||||
// Reset individual tracker elements to flush any held data
|
||||
this.serverRegistry.reset()
|
||||
this.sortController.reset()
|
||||
this.graphDisplayManager.reset()
|
||||
this.mojangUpdater.reset()
|
||||
this.percentageBar.reset()
|
||||
|
||||
// Undefine publicConfig, resynced during the connection handshake
|
||||
this.publicConfig = undefined
|
||||
|
||||
// Clear all task ids, if any
|
||||
this._taskIds.forEach(clearInterval)
|
||||
|
||||
this._taskIds = []
|
||||
|
||||
// Reset hidden values created by #updateGlobalStats
|
||||
this._lastTotalPlayerCount = undefined
|
||||
this._lastServerRegistrationCount = undefined
|
||||
|
||||
// Reset modified DOM structures
|
||||
document.getElementById('stat_totalPlayers').innerText = 0
|
||||
document.getElementById('stat_networks').innerText = 0
|
||||
|
||||
// Modify page state to display loading overlay
|
||||
this.caption.set('Lost connection!')
|
||||
|
||||
this.setPageReady(false)
|
||||
}
|
||||
|
||||
getTotalPlayerCount () {
|
||||
return this.serverRegistry.getServerRegistrations()
|
||||
.map(serverRegistration => serverRegistration.playerCount)
|
||||
.reduce((sum, current) => sum + current, 0)
|
||||
}
|
||||
|
||||
addServer = (pings) => {
|
||||
// Even if the backend has never pinged the server, the frontend is promised a placeholder object.
|
||||
// result = undefined
|
||||
// error = defined with "Waiting" description
|
||||
// info = safely defined with configured data
|
||||
const latestPing = pings[pings.length - 1]
|
||||
const serverRegistration = this.serverRegistry.createServerRegistration(latestPing.info.name)
|
||||
|
||||
serverRegistration.initServerStatus(latestPing)
|
||||
|
||||
// Push the historical data into the graph
|
||||
// This will trim and format the data so it is ready for the graph to render once init
|
||||
serverRegistration.addGraphPoints(pings)
|
||||
|
||||
// Create the plot instance internally with the restructured and cleaned data
|
||||
serverRegistration.buildPlotInstance()
|
||||
|
||||
// Handle the last known state (if any) as an incoming update
|
||||
// This triggers the main update pipeline and enables centralized update handling
|
||||
serverRegistration.updateServerStatus(latestPing, true, this.publicConfig.minecraftVersions)
|
||||
|
||||
// Allow the ServerRegistration to bind any DOM events with app instance context
|
||||
serverRegistration.initEventListeners()
|
||||
}
|
||||
|
||||
updateGlobalStats = () => {
|
||||
// Only redraw when needed
|
||||
// These operations are relatively cheap, but the site already does too much rendering
|
||||
const totalPlayerCount = this.getTotalPlayerCount()
|
||||
|
||||
if (totalPlayerCount !== this._lastTotalPlayerCount) {
|
||||
this._lastTotalPlayerCount = totalPlayerCount
|
||||
document.getElementById('stat_totalPlayers').innerText = formatNumber(totalPlayerCount)
|
||||
}
|
||||
|
||||
// Only redraw when needed
|
||||
// These operations are relatively cheap, but the site already does too much rendering
|
||||
const serverRegistrationCount = this.serverRegistry.getServerRegistrations().length
|
||||
|
||||
if (serverRegistrationCount !== this._lastServerRegistrationCount) {
|
||||
this._lastServerRegistrationCount = serverRegistrationCount
|
||||
document.getElementById('stat_networks').innerText = serverRegistrationCount
|
||||
}
|
||||
}
|
||||
}
|
69
assets/js/favorites.js
Normal file
69
assets/js/favorites.js
Normal file
@ -0,0 +1,69 @@
|
||||
export const FAVORITE_SERVERS_STORAGE_KEY = 'minetrack_favorite_servers'
|
||||
|
||||
export class FavoritesManager {
|
||||
constructor (app) {
|
||||
this._app = app
|
||||
}
|
||||
|
||||
loadLocalStorage () {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
let serverNames = localStorage.getItem(FAVORITE_SERVERS_STORAGE_KEY)
|
||||
if (serverNames) {
|
||||
serverNames = JSON.parse(serverNames)
|
||||
|
||||
for (let i = 0; i < serverNames.length; i++) {
|
||||
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverNames[i])
|
||||
|
||||
// The serverName may not exist in the backend configuration anymore
|
||||
// Ensure serverRegistration is defined before mutating data or considering valid
|
||||
if (serverRegistration) {
|
||||
serverRegistration.isFavorite = true
|
||||
|
||||
// Update icon since by default it is unfavorited
|
||||
document.getElementById('favorite-toggle_' + serverRegistration.serverId).setAttribute('class', this.getIconClass(serverRegistration.isFavorite))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalStorage () {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
// Mutate the serverIds array into server names for storage use
|
||||
const serverNames = this._app.serverRegistry.getServerRegistrations()
|
||||
.filter(serverRegistration => serverRegistration.isFavorite)
|
||||
.map(serverRegistration => serverRegistration.data.name)
|
||||
|
||||
if (serverNames.length > 0) {
|
||||
// Only save if the array contains data, otherwise clear the item
|
||||
localStorage.setItem(FAVORITE_SERVERS_STORAGE_KEY, JSON.stringify(serverNames))
|
||||
} else {
|
||||
localStorage.removeItem(FAVORITE_SERVERS_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleFavoriteButtonClick = (serverRegistration) => {
|
||||
serverRegistration.isFavorite = !serverRegistration.isFavorite
|
||||
|
||||
// Update the displayed favorite icon
|
||||
document.getElementById('favorite-toggle_' + serverRegistration.serverId).setAttribute('class', this.getIconClass(serverRegistration.isFavorite))
|
||||
|
||||
// Request the app controller instantly re-sort the server listing
|
||||
// This handles the favorite sorting logic internally
|
||||
this._app.sortController.sortServers()
|
||||
|
||||
this._app.graphDisplayManager.handleServerIsFavoriteUpdate(serverRegistration)
|
||||
|
||||
// Write an updated settings payload
|
||||
this.updateLocalStorage()
|
||||
}
|
||||
|
||||
getIconClass (isFavorite) {
|
||||
if (isFavorite) {
|
||||
return 'icon-star server-is-favorite'
|
||||
} else {
|
||||
return 'icon-star-o server-is-not-favorite'
|
||||
}
|
||||
}
|
||||
}
|
@ -1,130 +1,370 @@
|
||||
// Used by the individual server entries
|
||||
var smallChartOptions = {
|
||||
series: {
|
||||
shadowSize: 0
|
||||
},
|
||||
xaxis: {
|
||||
font: {
|
||||
color: "#E3E3E3"
|
||||
},
|
||||
show: false
|
||||
},
|
||||
yaxis: {
|
||||
minTickSize: 75,
|
||||
tickDecimals: 0,
|
||||
show: true,
|
||||
tickLength: 10,
|
||||
tickFormatter: function(value) {
|
||||
return formatNumber(value);
|
||||
},
|
||||
font: {
|
||||
color: "#E3E3E3"
|
||||
},
|
||||
labelWidth: -10
|
||||
},
|
||||
grid: {
|
||||
hoverable: true,
|
||||
color: "#696969"
|
||||
},
|
||||
colors: [
|
||||
"#E9E581"
|
||||
]
|
||||
};
|
||||
import { formatNumber, formatTimestamp, isMobileBrowser } from './util'
|
||||
|
||||
// Used by the one chart to rule them all
|
||||
var bigChartOptions = {
|
||||
series: {
|
||||
shadowSize: 0
|
||||
import { FAVORITE_SERVERS_STORAGE_KEY } from './favorites'
|
||||
|
||||
export const HISTORY_GRAPH_OPTIONS = {
|
||||
series: {
|
||||
shadowSize: 0
|
||||
},
|
||||
xaxis: {
|
||||
font: {
|
||||
color: '#E3E3E3'
|
||||
},
|
||||
xaxis: {
|
||||
font: {
|
||||
color: "#E3E3E3"
|
||||
},
|
||||
show: false
|
||||
show: false
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
tickSize: 5000,
|
||||
tickLength: 10,
|
||||
tickFormatter: formatNumber,
|
||||
font: {
|
||||
color: '#E3E3E3'
|
||||
},
|
||||
yaxis: {
|
||||
show: true,
|
||||
tickSize: 2000,
|
||||
tickLength: 10,
|
||||
tickFormatter: function(value) {
|
||||
return formatNumber(value);
|
||||
},
|
||||
font: {
|
||||
color: "#E3E3E3"
|
||||
},
|
||||
labelWidth: -5,
|
||||
min: 0
|
||||
},
|
||||
grid: {
|
||||
hoverable: true,
|
||||
color: "#696969"
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
labelWidth: -5,
|
||||
min: 0
|
||||
},
|
||||
grid: {
|
||||
hoverable: true,
|
||||
color: '#696969'
|
||||
},
|
||||
legend: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
|
||||
const HIDDEN_SERVERS_STORAGE_KEY = 'minetrack_hidden_servers'
|
||||
const SHOW_FAVORITES_STORAGE_KEY = 'minetrack_show_favorites'
|
||||
|
||||
export class GraphDisplayManager {
|
||||
// Only emit graph data request if not on mobile due to graph data size
|
||||
isVisible = !isMobileBrowser()
|
||||
|
||||
constructor (app) {
|
||||
this._app = app
|
||||
this._graphData = []
|
||||
this._hasLoadedSettings = false
|
||||
this._initEventListenersOnce = false
|
||||
this._showOnlyFavorites = false
|
||||
}
|
||||
|
||||
addGraphPoint (serverId, timestamp, playerCount) {
|
||||
if (!this._hasLoadedSettings) {
|
||||
// _hasLoadedSettings is controlled by #setGraphData
|
||||
// It will only be true once the context has been loaded and initial payload received
|
||||
// #addGraphPoint should not be called prior to that since it means the data is racing
|
||||
// and the application has received updates prior to the initial state
|
||||
return
|
||||
}
|
||||
};
|
||||
|
||||
function toggleControlsDrawer() {
|
||||
var div = $('#big-graph-controls-drawer');
|
||||
// Trim any outdated entries by filtering the array into a new array
|
||||
const startTimestamp = new Date().getTime()
|
||||
const newGraphData = this._graphData[serverId].filter(point => startTimestamp - point[0] <= this._app.publicConfig.graphDuration)
|
||||
|
||||
div.css('display', div.css('display') !== 'none' ? 'none' : 'block');
|
||||
}
|
||||
// Push the new data from the method call request
|
||||
newGraphData.push([timestamp, playerCount])
|
||||
|
||||
function saveGraphControls(displayedServers) {
|
||||
if (typeof(localStorage)) {
|
||||
var json = JSON.stringify(displayedServers);
|
||||
this._graphData[serverId] = newGraphData
|
||||
}
|
||||
|
||||
localStorage.setItem('displayedServers', json);
|
||||
}
|
||||
}
|
||||
loadLocalStorage () {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const showOnlyFavorites = localStorage.getItem(SHOW_FAVORITES_STORAGE_KEY)
|
||||
if (showOnlyFavorites) {
|
||||
this._showOnlyFavorites = true
|
||||
}
|
||||
|
||||
function loadGraphControls() {
|
||||
if (typeof(localStorage)) {
|
||||
var item = localStorage.getItem('displayedServers');
|
||||
// If only favorites mode is active, use the stored favorite servers data instead
|
||||
let serverNames
|
||||
if (this._showOnlyFavorites) {
|
||||
serverNames = localStorage.getItem(FAVORITE_SERVERS_STORAGE_KEY)
|
||||
} else {
|
||||
serverNames = localStorage.getItem(HIDDEN_SERVERS_STORAGE_KEY)
|
||||
}
|
||||
|
||||
if (item) {
|
||||
return JSON.parse(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (serverNames) {
|
||||
serverNames = JSON.parse(serverNames)
|
||||
|
||||
function resetGraphControls() {
|
||||
if (typeof(localStorage)) {
|
||||
localStorage.removeItem('displayedServers');
|
||||
}
|
||||
}
|
||||
|
||||
// Called by flot.js when they hover over a data point.
|
||||
function handlePlotHover(event, pos, item) {
|
||||
if (item) {
|
||||
var text = getTimestamp(item.datapoint[0] / 1000) + '\
|
||||
<br />\
|
||||
' + formatNumber(item.datapoint[1]) + ' Players';
|
||||
|
||||
if (item.series && item.series.label) {
|
||||
text = item.series.label + '<br />' + text;
|
||||
// Iterate over all active serverRegistrations
|
||||
// This merges saved state with current state to prevent desyncs
|
||||
for (const serverRegistration of this._app.serverRegistry.getServerRegistrations()) {
|
||||
// isVisible will be true if showOnlyFavorites && contained in FAVORITE_SERVERS_STORAGE_KEY
|
||||
// OR, if it is NOT contains within HIDDEN_SERVERS_STORAGE_KEY
|
||||
// Checks between FAVORITE/HIDDEN keys are mutually exclusive
|
||||
if (this._showOnlyFavorites) {
|
||||
serverRegistration.isVisible = serverNames.indexOf(serverRegistration.data.name) >= 0
|
||||
} else {
|
||||
serverRegistration.isVisible = serverNames.indexOf(serverRegistration.data.name) < 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
renderTooltip(item.pageX + 5, item.pageY + 5, text);
|
||||
updateLocalStorage () {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
// Mutate the serverIds array into server names for storage use
|
||||
const serverNames = this._app.serverRegistry.getServerRegistrations()
|
||||
.filter(serverRegistration => !serverRegistration.isVisible)
|
||||
.map(serverRegistration => serverRegistration.data.name)
|
||||
|
||||
// Only store if the array contains data, otherwise clear the item
|
||||
// If showOnlyFavorites is true, do NOT store serverNames since the state will be auto managed instead
|
||||
if (serverNames.length > 0 && !this._showOnlyFavorites) {
|
||||
localStorage.setItem(HIDDEN_SERVERS_STORAGE_KEY, JSON.stringify(serverNames))
|
||||
} else {
|
||||
localStorage.removeItem(HIDDEN_SERVERS_STORAGE_KEY)
|
||||
}
|
||||
|
||||
// Only store SHOW_FAVORITES_STORAGE_KEY if true
|
||||
if (this._showOnlyFavorites) {
|
||||
localStorage.setItem(SHOW_FAVORITES_STORAGE_KEY, true)
|
||||
} else {
|
||||
localStorage.removeItem(SHOW_FAVORITES_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the backend data into the schema used by flot.js
|
||||
getVisibleGraphData () {
|
||||
return Object.keys(this._graphData)
|
||||
.map(Number)
|
||||
.map(serverId => this._app.serverRegistry.getServerRegistration(serverId))
|
||||
.filter(serverRegistration => serverRegistration !== undefined && serverRegistration.isVisible)
|
||||
.map(serverRegistration => {
|
||||
return {
|
||||
data: this._graphData[serverRegistration.serverId],
|
||||
yaxis: 1,
|
||||
label: serverRegistration.data.name,
|
||||
color: serverRegistration.data.color
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
buildPlotInstance (graphData) {
|
||||
// Lazy load settings from localStorage, if any and if enabled
|
||||
if (!this._hasLoadedSettings) {
|
||||
this._hasLoadedSettings = true
|
||||
|
||||
this.loadLocalStorage()
|
||||
}
|
||||
|
||||
// Remap the incoming data from being string (serverName) keyed into serverId keys
|
||||
for (const serverName of Object.keys(graphData)) {
|
||||
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverName)
|
||||
this._graphData[serverRegistration.serverId] = graphData[serverName]
|
||||
}
|
||||
|
||||
// Explicitly define a height so flot.js can rescale the Y axis
|
||||
document.getElementById('big-graph').style.height = '400px'
|
||||
|
||||
this._plotInstance = $.plot('#big-graph', this.getVisibleGraphData(), HISTORY_GRAPH_OPTIONS)
|
||||
|
||||
// Show the settings-toggle element
|
||||
document.getElementById('settings-toggle').style.display = 'inline-block'
|
||||
}
|
||||
|
||||
// requestRedraw allows usages to request a redraw that may be performed, or cancelled, sometime later
|
||||
// This allows multiple rapid, but individual updates, to clump into a single redraw instead
|
||||
requestRedraw () {
|
||||
if (this._redrawRequestTimeout) {
|
||||
clearTimeout(this._redrawRequestTimeout)
|
||||
}
|
||||
|
||||
// Schedule new delayed redraw call
|
||||
// This can be cancelled by #requestRedraw, #redraw and #reset
|
||||
this._redrawRequestTimeout = setTimeout(this.redraw, 1000)
|
||||
}
|
||||
|
||||
redraw = () => {
|
||||
// Use drawing as a hint to update settings
|
||||
// This may cause unnessecary localStorage updates, but its a rare and harmless outcome
|
||||
this.updateLocalStorage()
|
||||
|
||||
// Fire calls to the provided graph instance
|
||||
// This allows flot.js to manage redrawing and creates a helper method to reduce code duplication
|
||||
this._plotInstance.setData(this.getVisibleGraphData())
|
||||
this._plotInstance.setupGrid()
|
||||
this._plotInstance.draw()
|
||||
|
||||
// undefine value so #clearTimeout is not called
|
||||
// This is safe even if #redraw is manually called since it removes the pending work
|
||||
if (this._redrawRequestTimeout) {
|
||||
clearTimeout(this._redrawRequestTimeout)
|
||||
}
|
||||
|
||||
this._redrawRequestTimeout = undefined
|
||||
}
|
||||
|
||||
requestResize () {
|
||||
// Only resize when _plotInstance is defined
|
||||
// Set a timeout to resize after resize events have not been fired for some duration of time
|
||||
// This prevents burning CPU time for multiple, rapid resize events
|
||||
if (this._plotInstance) {
|
||||
if (this._resizeRequestTimeout) {
|
||||
clearTimeout(this._resizeRequestTimeout)
|
||||
}
|
||||
|
||||
// Schedule new delayed resize call
|
||||
// This can be cancelled by #requestResize, #resize and #reset
|
||||
this._resizeRequestTimeout = setTimeout(this.resize, 200)
|
||||
}
|
||||
}
|
||||
|
||||
resize = () => {
|
||||
if (this._plotInstance) {
|
||||
this._plotInstance.resize()
|
||||
this._plotInstance.setupGrid()
|
||||
this._plotInstance.draw()
|
||||
}
|
||||
|
||||
// undefine value so #clearTimeout is not called
|
||||
// This is safe even if #resize is manually called since it removes the pending work
|
||||
if (this._resizeRequestTimeout) {
|
||||
clearTimeout(this._resizeRequestTimeout)
|
||||
}
|
||||
|
||||
this._resizeRequestTimeout = undefined
|
||||
}
|
||||
|
||||
// Called by flot.js when they hover over a data point.
|
||||
handlePlotHover = (event, pos, item) => {
|
||||
if (!item) {
|
||||
this._app.tooltip.hide()
|
||||
} else {
|
||||
hideTooltip();
|
||||
let text = formatNumber(item.datapoint[1]) + ' Players<br>' + formatTimestamp(item.datapoint[0])
|
||||
// Prefix text with the series label when possible
|
||||
if (item.series && item.series.label) {
|
||||
text = '<strong>' + item.series.label + '</strong><br>' + text
|
||||
}
|
||||
|
||||
this._app.tooltip.set(item.pageX, item.pageY, 10, 10, text)
|
||||
}
|
||||
}
|
||||
|
||||
initEventListeners () {
|
||||
if (!this._initEventListenersOnce) {
|
||||
this._initEventListenersOnce = true
|
||||
|
||||
// These listeners should only be init once since they attach to persistent elements
|
||||
document.getElementById('settings-toggle').addEventListener('click', this.handleSettingsToggle, false)
|
||||
|
||||
document.querySelectorAll('.graph-controls-show').forEach((element) => {
|
||||
element.addEventListener('click', this.handleShowButtonClick, false)
|
||||
})
|
||||
}
|
||||
|
||||
$('#big-graph').bind('plothover', this.handlePlotHover)
|
||||
|
||||
// These listeners should be bound each #initEventListeners call since they are for newly created elements
|
||||
document.querySelectorAll('.graph-control').forEach((element) => {
|
||||
element.addEventListener('click', this.handleServerButtonClick, false)
|
||||
})
|
||||
}
|
||||
|
||||
handleServerButtonClick = (event) => {
|
||||
const serverId = parseInt(event.target.getAttribute('minetrack-server-id'))
|
||||
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverId)
|
||||
|
||||
if (serverRegistration.isVisible !== event.target.checked) {
|
||||
serverRegistration.isVisible = event.target.checked
|
||||
|
||||
// Any manual changes automatically disables "Only Favorites" mode
|
||||
// Otherwise the auto management might overwrite their manual changes
|
||||
this._showOnlyFavorites = false
|
||||
|
||||
this.redraw()
|
||||
}
|
||||
}
|
||||
|
||||
handleShowButtonClick = (event) => {
|
||||
const showType = event.target.getAttribute('minetrack-show-type')
|
||||
|
||||
// If set to "Only Favorites", set internal state so that
|
||||
// visible graphData is automatically updating when a ServerRegistration's #isVisible changes
|
||||
// This is also saved and loaded by #loadLocalStorage & #updateLocalStorage
|
||||
this._showOnlyFavorites = showType === 'favorites'
|
||||
|
||||
let redraw = false
|
||||
|
||||
this._app.serverRegistry.getServerRegistrations().forEach(function (serverRegistration) {
|
||||
let isVisible
|
||||
if (showType === 'all') {
|
||||
isVisible = true
|
||||
} else if (showType === 'none') {
|
||||
isVisible = false
|
||||
} else if (showType === 'favorites') {
|
||||
isVisible = serverRegistration.isFavorite
|
||||
}
|
||||
|
||||
if (serverRegistration.isVisible !== isVisible) {
|
||||
serverRegistration.isVisible = isVisible
|
||||
redraw = true
|
||||
}
|
||||
})
|
||||
|
||||
if (redraw) {
|
||||
this.redraw()
|
||||
this.updateCheckboxes()
|
||||
}
|
||||
}
|
||||
|
||||
handleSettingsToggle = () => {
|
||||
const element = document.getElementById('big-graph-controls-drawer')
|
||||
|
||||
if (element.style.display !== 'block') {
|
||||
element.style.display = 'block'
|
||||
} else {
|
||||
element.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
handleServerIsFavoriteUpdate = (serverRegistration) => {
|
||||
// When in "Only Favorites" mode, visibility is dependent on favorite status
|
||||
// Redraw and update elements as needed
|
||||
if (this._showOnlyFavorites && serverRegistration.isVisible !== serverRegistration.isFavorite) {
|
||||
serverRegistration.isVisible = serverRegistration.isFavorite
|
||||
|
||||
this.redraw()
|
||||
this.updateCheckboxes()
|
||||
}
|
||||
}
|
||||
|
||||
updateCheckboxes () {
|
||||
document.querySelectorAll('.graph-control').forEach((checkbox) => {
|
||||
const serverId = parseInt(checkbox.getAttribute('minetrack-server-id'))
|
||||
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverId)
|
||||
|
||||
checkbox.checked = serverRegistration.isVisible
|
||||
})
|
||||
}
|
||||
|
||||
reset () {
|
||||
this._graphData = []
|
||||
this._plotInstance = undefined
|
||||
this._hasLoadedSettings = false
|
||||
|
||||
// Fire #clearTimeout if the timeout is currently defined
|
||||
if (this._resizeRequestTimeout) {
|
||||
clearTimeout(this._resizeRequestTimeout)
|
||||
|
||||
this._resizeRequestTimeout = undefined
|
||||
}
|
||||
|
||||
if (this._redrawRequestTimeout) {
|
||||
clearTimeout(this._redrawRequestTimeout)
|
||||
|
||||
this._redrawRequestTimeout = undefined
|
||||
}
|
||||
|
||||
// Reset modified DOM structures
|
||||
document.getElementById('big-graph-checkboxes').innerHTML = ''
|
||||
document.getElementById('big-graph-controls').style.display = 'none'
|
||||
|
||||
document.getElementById('settings-toggle').style.display = 'none'
|
||||
|
||||
const graphElement = document.getElementById('big-graph')
|
||||
|
||||
graphElement.innerHTML = ''
|
||||
graphElement.removeAttribute('style')
|
||||
}
|
||||
}
|
||||
|
||||
// Converts the backend data into the schema used by flot.js
|
||||
function convertGraphData(rawData) {
|
||||
var data = [];
|
||||
|
||||
var keys = Object.keys(rawData);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
data.push({
|
||||
data: rawData[keys[i]],
|
||||
yaxis: 1,
|
||||
label: keys[i],
|
||||
color: getServerColor(keys[i])
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
159
assets/js/main.js
Normal file
159
assets/js/main.js
Normal file
@ -0,0 +1,159 @@
|
||||
import { App } from './app'
|
||||
|
||||
import io from 'socket.io-client'
|
||||
|
||||
const app = new App()
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
const socket = io.connect({
|
||||
reconnect: true,
|
||||
reconnectDelay: 1000,
|
||||
reconnectionAttempts: 10
|
||||
})
|
||||
|
||||
// The backend will automatically push data once connected
|
||||
socket.on('connect', function () {
|
||||
app.caption.set('Loading...')
|
||||
})
|
||||
|
||||
socket.on('disconnect', function () {
|
||||
app.handleDisconnect()
|
||||
|
||||
// Reset modified DOM structures
|
||||
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
|
||||
})
|
||||
|
||||
socket.on('historyGraph', function (data) {
|
||||
// Consider the graph visible since a payload has been received
|
||||
// This is used for the manual graph load request behavior
|
||||
app.graphDisplayManager.isVisible = true
|
||||
|
||||
app.graphDisplayManager.buildPlotInstance(data)
|
||||
|
||||
// Build checkbox elements for graph controls
|
||||
let lastRowCounter = 0
|
||||
let controlsHTML = ''
|
||||
|
||||
Object.keys(data).sort().forEach(function (serverName) {
|
||||
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
|
||||
|
||||
controlsHTML += '<td>' +
|
||||
'<input type="checkbox" class="graph-control" minetrack-server-id="' + serverRegistration.serverId + '" ' + (serverRegistration.isVisible ? 'checked' : '') + '>' +
|
||||
' ' + serverName +
|
||||
'</input></td>'
|
||||
|
||||
// Occasionally break table rows using a magic number
|
||||
if (++lastRowCounter % 6 === 0) {
|
||||
controlsHTML += '</tr><tr>'
|
||||
}
|
||||
})
|
||||
|
||||
// Apply generated HTML and show controls
|
||||
document.getElementById('big-graph-checkboxes').innerHTML = '<table><tr>' +
|
||||
controlsHTML +
|
||||
'</tr></table>'
|
||||
|
||||
document.getElementById('big-graph-controls').style.display = 'block'
|
||||
|
||||
// Bind click event for updating graph data
|
||||
app.graphDisplayManager.initEventListeners()
|
||||
})
|
||||
|
||||
socket.on('updateHistoryGraph', function (data) {
|
||||
// Skip any incoming updates if the graph is disabled
|
||||
// The backend shouldn't send these anyways
|
||||
if (!app.graphDisplayManager.isVisible) {
|
||||
return
|
||||
}
|
||||
|
||||
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
|
||||
|
||||
if (serverRegistration) {
|
||||
app.graphDisplayManager.addGraphPoint(serverRegistration.serverId, data.timestamp, data.players)
|
||||
|
||||
// Only redraw the graph if not mutating hidden data
|
||||
if (serverRegistration.isVisible) {
|
||||
app.graphDisplayManager.requestRedraw()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('add', function (data) {
|
||||
data.forEach(app.addServer)
|
||||
})
|
||||
|
||||
socket.on('update', function (data) {
|
||||
// The backend may send "update" events prior to receiving all "add" events
|
||||
// A server has only been added once it's ServerRegistration is defined
|
||||
// Checking undefined protects from this race condition
|
||||
const serverRegistration = app.serverRegistry.getServerRegistration(data.info.name)
|
||||
|
||||
if (serverRegistration) {
|
||||
serverRegistration.updateServerStatus(data, false, app.publicConfig.minecraftVersions)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('updateMojangServices', function (data) {
|
||||
Object.values(data).forEach(app.mojangUpdater.updateServiceStatus)
|
||||
})
|
||||
|
||||
socket.on('setPublicConfig', function (data) {
|
||||
app.setPublicConfig(data)
|
||||
|
||||
// Display the main page component
|
||||
// Called here instead of syncComplete so the DOM can be drawn prior to the graphs being drawn
|
||||
// Otherwise flot.js will cause visual alignment bugs
|
||||
app.setPageReady(true)
|
||||
|
||||
// Allow the graphDisplayManager to control whether or not the historical graph is loaded
|
||||
// Defer to isGraphVisible from the publicConfig to understand if the frontend will ever receive a graph payload
|
||||
if (data.isGraphVisible) {
|
||||
if (app.graphDisplayManager.isVisible) {
|
||||
socket.emit('requestHistoryGraph')
|
||||
} else {
|
||||
document.getElementById('big-graph-mobile-load-request').style.display = 'block'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Fired once the backend has sent all requested data
|
||||
socket.on('syncComplete', function () {
|
||||
app.handleSyncComplete()
|
||||
})
|
||||
|
||||
socket.on('updatePeak', function (data) {
|
||||
const serverRegistration = app.serverRegistry.getServerRegistration(data.name)
|
||||
|
||||
if (serverRegistration) {
|
||||
serverRegistration.updateServerPeak(data.timestamp, data.players)
|
||||
}
|
||||
})
|
||||
|
||||
socket.on('peaks', function (data) {
|
||||
Object.keys(data).forEach(function (serverName) {
|
||||
const serverRegistration = app.serverRegistry.getServerRegistration(serverName)
|
||||
|
||||
if (serverRegistration) {
|
||||
const graphData = data[serverName]
|
||||
|
||||
// [0] and [1] indexes correspond to flot.js' graphing data structure
|
||||
serverRegistration.updateServerPeak(graphData[0], graphData[1])
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
window.addEventListener('resize', function () {
|
||||
app.percentageBar.redraw()
|
||||
|
||||
// Delegate to GraphDisplayManager which can check if the resize is necessary
|
||||
app.graphDisplayManager.requestResize()
|
||||
}, false)
|
||||
|
||||
document.getElementById('big-graph-mobile-load-request-button').addEventListener('click', function () {
|
||||
// Send a graph data request to the backend
|
||||
socket.emit('requestHistoryGraph')
|
||||
|
||||
// Hide the activation link to avoid multiple requests
|
||||
document.getElementById('big-graph-mobile-load-request').style.display = 'none'
|
||||
}, false)
|
||||
}, false)
|
20
assets/js/mojang.js
Normal file
20
assets/js/mojang.js
Normal file
@ -0,0 +1,20 @@
|
||||
const MOJANG_STATUS_BASE_CLASS = 'header-button header-button-group'
|
||||
|
||||
export class MojangUpdater {
|
||||
updateServiceStatus (status) {
|
||||
// HACK: ensure mojang-status is added for alignment, replace existing class to swap status color
|
||||
document.getElementById('mojang-status_' + status.name).setAttribute('class', MOJANG_STATUS_BASE_CLASS + ' mojang-status-' + status.title.toLowerCase())
|
||||
document.getElementById('mojang-status-text_' + status.name).innerText = status.title
|
||||
}
|
||||
|
||||
reset () {
|
||||
// Strip any mojang-status-* color classes from all mojang-status classes
|
||||
document.querySelectorAll('.mojang-status').forEach(function (element) {
|
||||
element.setAttribute('class', MOJANG_STATUS_BASE_CLASS)
|
||||
})
|
||||
|
||||
document.querySelectorAll('.mojang-status-text').forEach(function (element) {
|
||||
element.innerText = '...'
|
||||
})
|
||||
}
|
||||
}
|
72
assets/js/percbar.js
Normal file
72
assets/js/percbar.js
Normal file
@ -0,0 +1,72 @@
|
||||
import { formatNumber, formatPercent } from './util'
|
||||
|
||||
export class PercentageBar {
|
||||
constructor (app) {
|
||||
this._app = app
|
||||
this._parent = document.getElementById('perc-bar')
|
||||
}
|
||||
|
||||
redraw = () => {
|
||||
const serverRegistrations = this._app.serverRegistry.getServerRegistrations().sort(function (a, b) {
|
||||
return a.playerCount - b.playerCount
|
||||
})
|
||||
|
||||
const totalPlayers = this._app.getTotalPlayerCount()
|
||||
|
||||
let leftPadding = 0
|
||||
|
||||
for (const serverRegistration of serverRegistrations) {
|
||||
const width = Math.round((serverRegistration.playerCount / totalPlayers) * this._parent.offsetWidth)
|
||||
|
||||
// Update position/width
|
||||
// leftPadding is a sum of previous iterations width value
|
||||
const div = document.getElementById('perc-bar-part_' + serverRegistration.serverId) || this.createPart(serverRegistration)
|
||||
|
||||
// Only redraw if needed
|
||||
if (div.style.width !== width + 'px' || div.style.left !== leftPadding + 'px') {
|
||||
div.style.width = width + 'px'
|
||||
div.style.left = leftPadding + 'px'
|
||||
}
|
||||
|
||||
leftPadding += width
|
||||
}
|
||||
}
|
||||
|
||||
createPart (serverRegistration) {
|
||||
const div = document.createElement('div')
|
||||
|
||||
div.id = 'perc-bar-part_' + serverRegistration.serverId
|
||||
div.style.background = serverRegistration.data.color
|
||||
|
||||
div.setAttribute('class', 'perc-bar-part')
|
||||
div.setAttribute('minetrack-server-id', serverRegistration.serverId)
|
||||
|
||||
this._parent.appendChild(div)
|
||||
|
||||
// Define events once during creation
|
||||
div.addEventListener('mouseover', this.handleMouseOver, false)
|
||||
div.addEventListener('mouseout', this.handleMouseOut, false)
|
||||
|
||||
return div
|
||||
}
|
||||
|
||||
handleMouseOver = (event) => {
|
||||
const serverId = parseInt(event.target.getAttribute('minetrack-server-id'))
|
||||
const serverRegistration = this._app.serverRegistry.getServerRegistration(serverId)
|
||||
|
||||
this._app.tooltip.set(event.target.offsetLeft, event.target.offsetTop, 10, this._parent.offsetTop + this._parent.offsetHeight + 10,
|
||||
(typeof serverRegistration.rankIndex !== 'undefined' ? '#' + (serverRegistration.rankIndex + 1) + ' ' : '') +
|
||||
serverRegistration.data.name +
|
||||
'<br>' + formatNumber(serverRegistration.playerCount) + ' Players<br>' +
|
||||
'<strong>' + formatPercent(serverRegistration.playerCount, this._app.getTotalPlayerCount()) + '</strong>')
|
||||
}
|
||||
|
||||
handleMouseOut = () => {
|
||||
this._app.tooltip.hide()
|
||||
}
|
||||
|
||||
reset () {
|
||||
// Reset modified DOM elements
|
||||
this._parent.innerHTML = ''
|
||||
}
|
||||
}
|
304
assets/js/servers.js
Normal file
304
assets/js/servers.js
Normal file
@ -0,0 +1,304 @@
|
||||
import { formatNumber, formatTimestamp, formatDate, formatMinecraftServerAddress, formatMinecraftVersions, isArrayEqual, isObjectEqual } from './util'
|
||||
|
||||
import MISSING_FAVICON from '../images/missing_favicon.svg'
|
||||
|
||||
export const SERVER_GRAPH_OPTIONS = {
|
||||
series: {
|
||||
shadowSize: 0
|
||||
},
|
||||
xaxis: {
|
||||
font: {
|
||||
color: '#E3E3E3'
|
||||
},
|
||||
show: false
|
||||
},
|
||||
yaxis: {
|
||||
minTickSize: 100,
|
||||
tickDecimals: 0,
|
||||
show: true,
|
||||
tickLength: 10,
|
||||
tickFormatter: formatNumber,
|
||||
font: {
|
||||
color: '#E3E3E3'
|
||||
},
|
||||
labelWidth: -10
|
||||
},
|
||||
grid: {
|
||||
hoverable: true,
|
||||
color: '#696969'
|
||||
},
|
||||
colors: [
|
||||
'#E9E581'
|
||||
]
|
||||
}
|
||||
|
||||
export class ServerRegistry {
|
||||
constructor (app) {
|
||||
this._app = app
|
||||
this._serverIdsByName = []
|
||||
this._serverDataById = []
|
||||
this._registeredServers = []
|
||||
}
|
||||
|
||||
assignServers (servers) {
|
||||
for (let i = 0; i < servers.length; i++) {
|
||||
const data = servers[i]
|
||||
this._serverIdsByName[data.name] = i
|
||||
this._serverDataById[i] = data
|
||||
}
|
||||
}
|
||||
|
||||
createServerRegistration (serverName) {
|
||||
const serverId = this._serverIdsByName[serverName]
|
||||
const serverData = this._serverDataById[serverId]
|
||||
const serverRegistration = new ServerRegistration(this._app, serverId, serverData)
|
||||
this._registeredServers[serverId] = serverRegistration
|
||||
return serverRegistration
|
||||
}
|
||||
|
||||
getServerRegistration (serverKey) {
|
||||
if (typeof serverKey === 'string') {
|
||||
const serverId = this._serverIdsByName[serverKey]
|
||||
return this._registeredServers[serverId]
|
||||
} else if (typeof serverKey === 'number') {
|
||||
return this._registeredServers[serverKey]
|
||||
}
|
||||
}
|
||||
|
||||
getServerRankBy (serverRegistration, x, sort) {
|
||||
const records = Object.values(this._registeredServers)
|
||||
.map(x)
|
||||
.filter(val => val !== undefined)
|
||||
|
||||
// Invalidate any results that do not account for all serverRegistrations
|
||||
if (records.length === this._registeredServers.length) {
|
||||
records.sort(sort)
|
||||
|
||||
// Pull matching data from target serverRegistration
|
||||
// Assume indexOf cannot be -1 or val undefined since they have been pre-tested in the map call above
|
||||
const val = x(serverRegistration)
|
||||
const indexOf = records.indexOf(val)
|
||||
return indexOf + 1
|
||||
}
|
||||
}
|
||||
|
||||
getServerRegistrations = () => Object.values(this._registeredServers)
|
||||
|
||||
reset () {
|
||||
this._serverIdsByName = []
|
||||
this._serverDataById = []
|
||||
this._registeredServers = []
|
||||
|
||||
// Reset modified DOM structures
|
||||
document.getElementById('server-list').innerHTML = ''
|
||||
}
|
||||
}
|
||||
|
||||
const SERVER_GRAPH_DATA_MAX_LENGTH = 72
|
||||
|
||||
export class ServerRegistration {
|
||||
playerCount = 0
|
||||
isVisible = true
|
||||
isFavorite = false
|
||||
rankIndex
|
||||
lastRecordData
|
||||
lastVersions = []
|
||||
lastPeakData
|
||||
|
||||
constructor (app, serverId, data) {
|
||||
this._app = app
|
||||
this.serverId = serverId
|
||||
this.data = data
|
||||
this._graphData = []
|
||||
this._failedSequentialPings = 0
|
||||
}
|
||||
|
||||
addGraphPoints (points) {
|
||||
// Test if the first point contains error.placeholder === true
|
||||
// This is sent by the backend when the server hasn't been pinged yet
|
||||
// These points will be disregarded to prevent the graph starting at 0 player count
|
||||
points = points.filter(point => !point.error || !point.error.placeholder)
|
||||
|
||||
// The backend should never return more data elements than the max
|
||||
// but trim the data result regardless for safety and performance purposes
|
||||
if (points.length > SERVER_GRAPH_DATA_MAX_LENGTH) {
|
||||
points.slice(points.length - SERVER_GRAPH_DATA_MAX_LENGTH, points.length)
|
||||
}
|
||||
|
||||
this._graphData = points.map(point => point.result ? [point.timestamp, point.result.players.online] : [point.timestamp, 0])
|
||||
}
|
||||
|
||||
buildPlotInstance () {
|
||||
this._plotInstance = $.plot('#chart_' + this.serverId, [this._graphData], SERVER_GRAPH_OPTIONS)
|
||||
}
|
||||
|
||||
handlePing (payload, pushToGraph) {
|
||||
if (payload.result) {
|
||||
this.playerCount = payload.result.players.online
|
||||
|
||||
if (pushToGraph) {
|
||||
// Only update graph for successful pings
|
||||
// This intentionally pauses the server graph when pings begin to fail
|
||||
this._graphData.push([payload.info.timestamp, this.playerCount])
|
||||
|
||||
// Trim graphData to within the max length by shifting out the leading elements
|
||||
if (this._graphData.length > SERVER_GRAPH_DATA_MAX_LENGTH) {
|
||||
this._graphData.shift()
|
||||
}
|
||||
|
||||
this.redraw()
|
||||
}
|
||||
|
||||
// Reset failed ping counter to ensure the next connection error
|
||||
// doesn't instantly retrigger a layout change
|
||||
this._failedSequentialPings = 0
|
||||
} else {
|
||||
// Attempt to retain a copy of the cached playerCount for up to N failed pings
|
||||
// This prevents minor connection issues from constantly reshuffling the layout
|
||||
if (++this._failedSequentialPings > 5) {
|
||||
this.playerCount = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
redraw () {
|
||||
// Redraw the plot instance
|
||||
this._plotInstance.setData([this._graphData])
|
||||
this._plotInstance.setupGrid()
|
||||
this._plotInstance.draw()
|
||||
}
|
||||
|
||||
updateServerRankIndex (rankIndex) {
|
||||
this.rankIndex = rankIndex
|
||||
|
||||
document.getElementById('ranking_' + this.serverId).innerText = '#' + (rankIndex + 1)
|
||||
}
|
||||
|
||||
updateServerPeak (time, playerCount) {
|
||||
const peakLabelElement = document.getElementById('peak_' + this.serverId)
|
||||
|
||||
// Always set label once any peak data has been received
|
||||
peakLabelElement.style.display = 'block'
|
||||
|
||||
const peakValueElement = document.getElementById('peak-value_' + this.serverId)
|
||||
|
||||
peakValueElement.innerText = formatNumber(playerCount)
|
||||
peakLabelElement.title = 'At ' + formatTimestamp(time)
|
||||
|
||||
this.lastPeakData = {
|
||||
timestamp: time,
|
||||
playerCount: playerCount
|
||||
}
|
||||
}
|
||||
|
||||
updateServerStatus (ping, isInitialUpdate, minecraftVersions) {
|
||||
// Only pushToGraph when initialUpdate === false
|
||||
// Otherwise the ping value is pushed into the graphData when already present
|
||||
this.handlePing(ping, !isInitialUpdate)
|
||||
|
||||
// Compare against a cached value to avoid empty updates
|
||||
// Allow undefined ping.versions inside the if statement for text reset handling
|
||||
if (ping.versions && !isArrayEqual(ping.versions, this.lastVersions)) {
|
||||
this.lastVersions = ping.versions
|
||||
|
||||
const versionsElement = document.getElementById('version_' + this.serverId)
|
||||
|
||||
versionsElement.style.display = 'block'
|
||||
versionsElement.innerText = formatMinecraftVersions(ping.versions, minecraftVersions[ping.info.type]) || ''
|
||||
}
|
||||
|
||||
// Compare against a cached value to avoid empty updates
|
||||
if (ping.recordData !== undefined && !isObjectEqual(ping.recordData, this.lastRecordData, ['playerCount', 'timestamp'])) {
|
||||
this.lastRecordData = ping.recordData
|
||||
|
||||
// Always set label once any record data has been received
|
||||
const recordLabelElement = document.getElementById('record_' + this.serverId)
|
||||
|
||||
recordLabelElement.style.display = 'block'
|
||||
|
||||
const recordValueElement = document.getElementById('record-value_' + this.serverId)
|
||||
|
||||
const recordData = ping.recordData
|
||||
|
||||
// Safely handle legacy recordData that may not include the timestamp payload
|
||||
if (recordData.timestamp !== -1) {
|
||||
recordValueElement.innerHTML = formatNumber(recordData.playerCount) + ' (' + formatDate(recordData.timestamp) + ')'
|
||||
recordLabelElement.title = 'At ' + formatDate(recordData.timestamp) + ' ' + formatTimestamp(recordData.timestamp)
|
||||
} else {
|
||||
recordValueElement.innerText = formatNumber(recordData.playerCount)
|
||||
}
|
||||
}
|
||||
|
||||
const playerCountLabelElement = document.getElementById('player-count_' + this.serverId)
|
||||
const errorElement = document.getElementById('error_' + this.serverId)
|
||||
|
||||
if (ping.error) {
|
||||
// Hide any visible player-count and show the error element
|
||||
playerCountLabelElement.style.display = 'none'
|
||||
errorElement.style.display = 'block'
|
||||
|
||||
// Attempt to find an error cause from documented options
|
||||
errorElement.innerText = ping.error.description || ping.error.errno || 'Unknown error'
|
||||
} else if (ping.result) {
|
||||
// Ensure the player-count element is visible and hide the error element
|
||||
playerCountLabelElement.style.display = 'block'
|
||||
errorElement.style.display = 'none'
|
||||
|
||||
document.getElementById('player-count-value_' + this.serverId).innerText = formatNumber(ping.result.players.online)
|
||||
|
||||
// An updated favicon has been sent, update the src
|
||||
// Ignore calls from 'add' events since they will have explicitly manually handled the favicon update
|
||||
if (!isInitialUpdate && ping.favicon) {
|
||||
document.getElementById('favicon_' + this.serverId).setAttribute('src', ping.favicon)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initServerStatus (latestPing) {
|
||||
const peakHourDuration = Math.floor(this._app.publicConfig.graphDuration / (60 * 60 * 1000)) + 'h Peak: '
|
||||
|
||||
const serverElement = document.createElement('div')
|
||||
|
||||
serverElement.id = 'container_' + this.serverId
|
||||
serverElement.innerHTML = '<div class="column column-favicon">' +
|
||||
'<img class="server-favicon" src="' + (latestPing.favicon || MISSING_FAVICON) + '" id="favicon_' + this.serverId + '" title="' + this.data.name + '\n' + formatMinecraftServerAddress(this.data.ip, this.data.port) + '">' +
|
||||
'<span class="server-rank" id="ranking_' + this.serverId + '"></span>' +
|
||||
'</div>' +
|
||||
'<div class="column column-status">' +
|
||||
'<h3 class="server-name"><span class="' + this._app.favoritesManager.getIconClass(this.isFavorite) + '" id="favorite-toggle_' + this.serverId + '"></span> ' + this.data.name + '</h3>' +
|
||||
'<span class="server-error" id="error_' + this.serverId + '"></span>' +
|
||||
'<span class="server-label" id="player-count_' + this.serverId + '">Players: <span class="server-value" id="player-count-value_' + this.serverId + '"></span></span>' +
|
||||
'<span class="server-label" id="peak_' + this.serverId + '">' + peakHourDuration + '<span class="server-value" id="peak-value_' + this.serverId + '">-</span></span>' +
|
||||
'<span class="server-label" id="record_' + this.serverId + '">Record: <span class="server-value" id="record-value_' + this.serverId + '">-</span></span>' +
|
||||
'<span class="server-label" id="version_' + this.serverId + '"></span>' +
|
||||
'</div>' +
|
||||
'<div class="column column-graph" id="chart_' + this.serverId + '"></div>'
|
||||
|
||||
serverElement.setAttribute('class', 'server')
|
||||
|
||||
document.getElementById('server-list').appendChild(serverElement)
|
||||
}
|
||||
|
||||
updateHighlightedValue (selectedCategory) {
|
||||
['player-count', 'peak', 'record'].forEach((category) => {
|
||||
const labelElement = document.getElementById(category + '_' + this.serverId)
|
||||
const valueElement = document.getElementById(category + '-value_' + this.serverId)
|
||||
|
||||
if (selectedCategory && category === selectedCategory) {
|
||||
labelElement.setAttribute('class', 'server-highlighted-label')
|
||||
valueElement.setAttribute('class', 'server-highlighted-value')
|
||||
} else {
|
||||
labelElement.setAttribute('class', 'server-label')
|
||||
valueElement.setAttribute('class', 'server-value')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
initEventListeners () {
|
||||
$('#chart_' + this.serverId).bind('plothover', this._app.graphDisplayManager.handlePlotHover)
|
||||
|
||||
document.getElementById('favorite-toggle_' + this.serverId).addEventListener('click', () => {
|
||||
this._app.favoritesManager.handleFavoriteButtonClick(this)
|
||||
}, false)
|
||||
}
|
||||
}
|
@ -1,538 +0,0 @@
|
||||
var graphs = [];
|
||||
var lastPlayerEntries = [];
|
||||
|
||||
var historyPlot;
|
||||
var displayedGraphData;
|
||||
var hiddenGraphData = [];
|
||||
|
||||
var isConnected = false;
|
||||
|
||||
var mojangServicesUpdater;
|
||||
var sortServersTask;
|
||||
|
||||
var currentServerHover;
|
||||
var faviconSize = 64;
|
||||
|
||||
function updateServerStatus(lastEntry) {
|
||||
var info = lastEntry.info;
|
||||
|
||||
var div = $('#status_' + safeName(info.name));
|
||||
var versionDiv = $('#version_' + safeName(info.name));
|
||||
|
||||
if (lastEntry.versions) {
|
||||
var versions = '';
|
||||
|
||||
for (var i = 0; i < lastEntry.versions.length; i++) {
|
||||
if (!lastEntry.versions[i]) continue;
|
||||
versions += '<span class="version">' + publicConfig.minecraftVersions[lastEntry.info.type][lastEntry.versions[i]] + '</span> ';
|
||||
}
|
||||
|
||||
versionDiv.html(versions);
|
||||
} else {
|
||||
versionDiv.html('');
|
||||
}
|
||||
|
||||
if (lastEntry.result) {
|
||||
var result = lastEntry.result;
|
||||
var newStatus = 'Players: <span style="font-weight: 500;">' + formatNumber(result.players.online) + '</span>';
|
||||
|
||||
var listing = graphs[lastEntry.info.name].listing;
|
||||
|
||||
if (listing.length > 0) {
|
||||
newStatus += '<span class="color-gray"> (';
|
||||
|
||||
var playerDifference = listing[listing.length - 1][1] - listing[0][1];
|
||||
|
||||
if (playerDifference >= 0) {
|
||||
newStatus += '+';
|
||||
}
|
||||
|
||||
newStatus += playerDifference + ')</span>';
|
||||
}
|
||||
|
||||
lastPlayerEntries[info.name] = result.players.online;
|
||||
|
||||
div.html(newStatus);
|
||||
} else {
|
||||
var newStatus = '<span class="color-red">';
|
||||
|
||||
if (findErrorMessage(lastEntry.error)) {
|
||||
newStatus += findErrorMessage(lastEntry.error);
|
||||
} else {
|
||||
newStatus += 'Failed to ping!';
|
||||
}
|
||||
|
||||
div.html(newStatus + '</span>');
|
||||
}
|
||||
|
||||
var keys = Object.keys(lastPlayerEntries);
|
||||
var totalPlayers = 0;
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
totalPlayers += lastPlayerEntries[keys[i]];
|
||||
}
|
||||
|
||||
$("#stat_totalPlayers").text(formatNumber(totalPlayers));
|
||||
$("#stat_networks").text(formatNumber(keys.length));
|
||||
|
||||
if (lastEntry.record) {
|
||||
$('#record_' + safeName(info.name)).html('Record: ' + formatNumber(lastEntry.record));
|
||||
}
|
||||
|
||||
updatePercentageBar();
|
||||
}
|
||||
|
||||
function sortServers() {
|
||||
var serverNames = [];
|
||||
|
||||
var keys = Object.keys(lastPlayerEntries);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
serverNames.push(keys[i]);
|
||||
}
|
||||
|
||||
serverNames.sort(function(a, b) {
|
||||
return (lastPlayerEntries[b] || 0) - (lastPlayerEntries[a] || 0);
|
||||
});
|
||||
|
||||
for (var i = 0; i < serverNames.length; i++) {
|
||||
$('#container_' + safeName(serverNames[i])).appendTo('#server-container-list');
|
||||
$('#ranking_' + safeName(serverNames[i])).text('#' + (i + 1));
|
||||
}
|
||||
}
|
||||
|
||||
function updatePercentageBar() {
|
||||
var keys = Object.keys(lastPlayerEntries);
|
||||
|
||||
keys.sort(function(a, b) {
|
||||
return lastPlayerEntries[a] - lastPlayerEntries[b];
|
||||
});
|
||||
|
||||
var totalPlayers = getCurrentTotalPlayers();
|
||||
|
||||
var parent = $('#perc-bar');
|
||||
var leftPadding = 0;
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
(function(pos, server, length) {
|
||||
var safeNameCopy = safeName(server);
|
||||
var playerCount = lastPlayerEntries[server];
|
||||
|
||||
var div = $('#perc_bar_part_' + safeNameCopy);
|
||||
|
||||
// Setup the base
|
||||
if (!div.length) {
|
||||
$('<div/>', {
|
||||
id: 'perc_bar_part_' + safeNameCopy,
|
||||
class: 'perc-bar-part',
|
||||
html: '',
|
||||
style: 'background: ' + getServerColor(server) + ';'
|
||||
}).appendTo(parent);
|
||||
|
||||
div = $('#perc_bar_part_' + safeNameCopy);
|
||||
|
||||
div.mouseover(function(e) {
|
||||
currentServerHover = server;
|
||||
});
|
||||
|
||||
div.mouseout(function(e) {
|
||||
hideTooltip();
|
||||
currentServerHover = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
// Update our position/width
|
||||
var width = (playerCount / totalPlayers) * parent.width();
|
||||
|
||||
div.css({
|
||||
width: width + 'px',
|
||||
left: leftPadding + 'px'
|
||||
});
|
||||
|
||||
leftPadding += width;
|
||||
})(i, keys[i], keys.length);
|
||||
}
|
||||
}
|
||||
|
||||
function getCurrentTotalPlayers() {
|
||||
var totalPlayers = 0;
|
||||
var keys = Object.keys(lastPlayerEntries);
|
||||
for (var i = 0; i < keys.length; i++) totalPlayers += lastPlayerEntries[keys[i]]
|
||||
return totalPlayers;
|
||||
}
|
||||
|
||||
function setAllGraphVisibility(visible) {
|
||||
if (visible) {
|
||||
var keys = Object.keys(hiddenGraphData);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
displayedGraphData[keys[i]] = hiddenGraphData[keys[i]];
|
||||
}
|
||||
|
||||
hiddenGraphData = [];
|
||||
} else {
|
||||
var keys = Object.keys(displayedGraphData);
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
hiddenGraphData[keys[i]] = displayedGraphData[keys[i]];
|
||||
}
|
||||
|
||||
displayedGraphData = [];
|
||||
}
|
||||
|
||||
$('.graph-control').each(function(index, item) {
|
||||
item.checked = visible;
|
||||
});
|
||||
|
||||
historyPlot.setData(convertGraphData(displayedGraphData));
|
||||
historyPlot.setupGrid();
|
||||
|
||||
historyPlot.draw();
|
||||
|
||||
// Update our localStorage
|
||||
if (visible) {
|
||||
resetGraphControls();
|
||||
} else {
|
||||
saveGraphControls(Object.keys(displayedGraphData));
|
||||
}
|
||||
}
|
||||
|
||||
function validateBootTime(bootTime, socket) {
|
||||
$('#tagline-text').text('Validating...');
|
||||
|
||||
console.log('Remote bootTime is ' + bootTime + ', local is ' + publicConfig.bootTime);
|
||||
|
||||
if (bootTime === publicConfig.bootTime) {
|
||||
$('#tagline-text').text('Loading...');
|
||||
|
||||
socket.emit('requestListing');
|
||||
|
||||
if (!isMobileBrowser()) socket.emit('requestHistoryGraph');
|
||||
|
||||
isConnected = true;
|
||||
|
||||
// Start any special updating tasks.
|
||||
mojangServicesUpdater = setInterval(updateMojangServices, 1000);
|
||||
sortServersTask = setInterval(sortServers, 10000);
|
||||
} else {
|
||||
$('#tagline-text').text('Updating...');
|
||||
|
||||
$.getScript('/publicConfig.json', function(data, textStatus, xhr) {
|
||||
if (xhr.status === 200) {
|
||||
validateBootTime(publicConfig.bootTime, socket);
|
||||
} else {
|
||||
showCaption('Failed to update! Refresh?');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function printPort(port) {
|
||||
if(port == undefined || port == 25565) {
|
||||
return "";
|
||||
} else {
|
||||
return ":" + port;
|
||||
}
|
||||
}
|
||||
|
||||
function updateServerPeak(name, time, playerCount) {
|
||||
var safeNameCopy = safeName(name);
|
||||
// hack: strip the AM/PM suffix
|
||||
// Javascript doesn't have a nice way to format Dates with AM/PM, so we'll append it manually
|
||||
var timestamp = getTimestamp(time / 1000).split(':');
|
||||
var end = timestamp.pop().split(' ')[1];
|
||||
timestamp = timestamp.join(':');
|
||||
// end may be undefined for other timezones/24 hour times
|
||||
if (end) {
|
||||
timestamp += ' ' + end;
|
||||
}
|
||||
var timeLabel = msToTime(publicConfig.graphDuration);
|
||||
$('#peak_' + safeNameCopy).html(timeLabel + ' Peak: ' + formatNumber(playerCount) + ' @ ' + timestamp);
|
||||
}
|
||||
|
||||
$(document).ready(function() {
|
||||
var socket = io.connect({
|
||||
reconnect: true,
|
||||
reconnectDelay: 1000,
|
||||
reconnectionAttempts: 10
|
||||
});
|
||||
|
||||
socket.on('bootTime', function(bootTime) {
|
||||
validateBootTime(bootTime, socket);
|
||||
});
|
||||
|
||||
socket.on('disconnect', function() {
|
||||
if (mojangServicesUpdater) clearInterval(mojangServicesUpdater);
|
||||
if (sortServersTask) clearInterval(sortServersTask);
|
||||
|
||||
lastMojangServiceUpdate = undefined;
|
||||
|
||||
showCaption('Disconnected! Refresh?');
|
||||
|
||||
lastPlayerEntries = {};
|
||||
graphs = {};
|
||||
|
||||
$('#server-container-list').html('');
|
||||
|
||||
$('#big-graph').html('');
|
||||
$('#big-graph-checkboxes').html('');
|
||||
$('#big-graph-controls').css('display', 'none');
|
||||
|
||||
$('#perc-bar').html('');
|
||||
$('.mojang-status').css('background', 'transparent');
|
||||
$('.mojang-status-text').text('...');
|
||||
|
||||
$("#stat_totalPlayers").text(0);
|
||||
$("#stat_networks").text(0);
|
||||
|
||||
isConnected = false;
|
||||
});
|
||||
|
||||
socket.on('historyGraph', function(rawData) {
|
||||
var shownServers = loadGraphControls();
|
||||
|
||||
if (shownServers) {
|
||||
var keys = Object.keys(rawData);
|
||||
|
||||
hiddenGraphData = [];
|
||||
displayedGraphData = [];
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var name = keys[i];
|
||||
|
||||
if (shownServers.indexOf(name) !== -1) {
|
||||
displayedGraphData[name] = rawData[name];
|
||||
} else {
|
||||
hiddenGraphData[name] = rawData[name];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
displayedGraphData = rawData;
|
||||
}
|
||||
|
||||
$('#big-graph').css('height', '400px');
|
||||
|
||||
historyPlot = $.plot('#big-graph', convertGraphData(displayedGraphData), bigChartOptions);
|
||||
|
||||
$('#big-graph').bind('plothover', handlePlotHover);
|
||||
|
||||
var keys = Object.keys(rawData);
|
||||
|
||||
var sinceBreak = 0;
|
||||
var html = '<table><tr>';
|
||||
|
||||
keys.sort();
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var checkedString = '';
|
||||
|
||||
if (displayedGraphData[keys[i]]) {
|
||||
checkedString = 'checked=checked';
|
||||
}
|
||||
|
||||
html += '<td><input type="checkbox" class="graph-control" id="graph-controls" data-target-network="' + keys[i] + '" ' + checkedString + '> ' + keys[i] + '</input></td>';
|
||||
|
||||
if (sinceBreak >= 7) {
|
||||
sinceBreak = 0;
|
||||
|
||||
html += '</tr><tr>';
|
||||
} else {
|
||||
sinceBreak++;
|
||||
}
|
||||
}
|
||||
|
||||
$('#big-graph-checkboxes').append(html + '</tr></table>');
|
||||
$('#big-graph-controls').css('display', 'block');
|
||||
});
|
||||
|
||||
socket.on('updateHistoryGraph', function(rawData) {
|
||||
// Prevent race conditions.
|
||||
if (!displayedGraphData || !hiddenGraphData) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's not in our display group, use the hidden group instead.
|
||||
var targetGraphData = displayedGraphData[rawData.name] ? displayedGraphData : hiddenGraphData;
|
||||
|
||||
trimOldPings(targetGraphData, publicConfig.graphDuration);
|
||||
|
||||
targetGraphData[rawData.name].push([rawData.timestamp, rawData.players]);
|
||||
|
||||
// Redraw if we need to.
|
||||
if (displayedGraphData[rawData.name]) {
|
||||
historyPlot.setData(convertGraphData(displayedGraphData));
|
||||
historyPlot.setupGrid();
|
||||
|
||||
historyPlot.draw();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('add', function(servers) {
|
||||
for (var i = 0; i < servers.length; i++) {
|
||||
var history = servers[i];
|
||||
var listing = [];
|
||||
|
||||
for (var x = 0; x < history.length; x++) {
|
||||
var point = history[x];
|
||||
|
||||
if (point.result) {
|
||||
listing.push([point.timestamp, point.result.players.online]);
|
||||
} else if (point.error) {
|
||||
listing.push([point.timestamp, 0]);
|
||||
}
|
||||
}
|
||||
|
||||
var lastEntry = history[history.length - 1];
|
||||
var info = lastEntry.info;
|
||||
|
||||
if (lastEntry.error) {
|
||||
lastPlayerEntries[info.name] = 0;
|
||||
} else if (lastEntry.result) {
|
||||
lastPlayerEntries[info.name] = lastEntry.result.players.online;
|
||||
}
|
||||
|
||||
var typeString = publicConfig.serverTypesVisible ? '<span class="type">' + info.type + '</span>' : '';
|
||||
|
||||
var safeNameCopy = safeName(info.name);
|
||||
|
||||
$('<div/>', {
|
||||
id: 'container_' + safeNameCopy,
|
||||
class: 'server',
|
||||
'server-id': safeNameCopy,
|
||||
html: '<div id="server-' + safeNameCopy + '" class="column" style="width: 80px;">\
|
||||
<img id="favicon_' + safeNameCopy + '" title="' + info.name + '\n' + info.ip + printPort(info.port) + '" height="' + faviconSize + '" width="' + faviconSize + '">\
|
||||
<br />\
|
||||
<p class="text-center-align rank" id="ranking_' + safeNameCopy + '"></p>\
|
||||
</div>\
|
||||
<div class="column" style="width: 282px;">\
|
||||
<h3>' + info.name + ' ' + typeString + '</h3>\
|
||||
<span id="status_' + safeNameCopy + '">Waiting</span>\
|
||||
<div id="version_' + safeNameCopy + '" class="color-dark-gray server-meta versions"><span class="version"></span></div>\
|
||||
<span id="peak_' + safeNameCopy + '" class="color-dark-gray server-meta"></span>\
|
||||
<br><span id="record_' + safeNameCopy + '" class="color-dark-gray server-meta"></span>\
|
||||
</div>\
|
||||
<div class="column" style="float: right;">\
|
||||
<div class="chart" id="chart_' + safeNameCopy + '"></div>\
|
||||
</div>'
|
||||
}).appendTo("#server-container-list");
|
||||
|
||||
var favicon = MISSING_FAVICON_BASE64;
|
||||
|
||||
if (lastEntry.result && lastEntry.result.favicon) {
|
||||
favicon = lastEntry.result.favicon;
|
||||
}
|
||||
|
||||
$('#favicon_' + safeName(info.name)).attr('src', favicon);
|
||||
|
||||
graphs[lastEntry.info.name] = {
|
||||
listing: listing,
|
||||
plot: $.plot('#chart_' + safeNameCopy, [listing], smallChartOptions)
|
||||
};
|
||||
|
||||
updateServerStatus(lastEntry);
|
||||
|
||||
$('#chart_' + safeNameCopy).bind('plothover', handlePlotHover);
|
||||
}
|
||||
|
||||
sortServers();
|
||||
updatePercentageBar();
|
||||
});
|
||||
|
||||
socket.on('update', function(update) {
|
||||
// Prevent weird race conditions.
|
||||
if (!graphs[update.info.name]) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We have a new favicon, update the old one.
|
||||
if (update.result && update.result.favicon) {
|
||||
$('#favicon_' + safeName(update.info.name)).attr('src', update.result.favicon);
|
||||
}
|
||||
|
||||
var graph = graphs[update.info.name];
|
||||
|
||||
updateServerStatus(update);
|
||||
|
||||
if (update.result) {
|
||||
graph.listing.push([update.info.timestamp, update.result ? update.result.players.online : 0]);
|
||||
|
||||
if (graph.listing.length > 72) {
|
||||
graph.listing.shift();
|
||||
}
|
||||
|
||||
graph.plot.setData([graph.listing]);
|
||||
graph.plot.setupGrid();
|
||||
|
||||
graph.plot.draw();
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('updateMojangServices', function(data) {
|
||||
if (isConnected) {
|
||||
updateMojangServices(data);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('syncComplete', function() {
|
||||
hideCaption();
|
||||
});
|
||||
|
||||
socket.on('updatePeak', function(data) {
|
||||
updateServerPeak(data.name, data.timestamp, data.players);
|
||||
});
|
||||
|
||||
socket.on('peaks', function(data) {
|
||||
var keys = Object.keys(data);
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var val = data[keys[i]];
|
||||
updateServerPeak(keys[i], val[0], val[1]);
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('click', '.graph-control', function(e) {
|
||||
var serverIp = $(this).attr('data-target-network');
|
||||
|
||||
// Restore it, or delete it - either works.
|
||||
if (!this.checked) {
|
||||
hiddenGraphData[serverIp] = displayedGraphData[serverIp];
|
||||
|
||||
delete displayedGraphData[serverIp];
|
||||
} else {
|
||||
displayedGraphData[serverIp] = hiddenGraphData[serverIp];
|
||||
|
||||
delete hiddenGraphData[serverIp];
|
||||
}
|
||||
|
||||
// Redraw the graph
|
||||
historyPlot.setData(convertGraphData(displayedGraphData));
|
||||
historyPlot.setupGrid();
|
||||
|
||||
historyPlot.draw();
|
||||
|
||||
// Update our localStorage
|
||||
if (Object.keys(hiddenGraphData).length === 0) {
|
||||
resetGraphControls();
|
||||
} else {
|
||||
saveGraphControls(Object.keys(displayedGraphData));
|
||||
}
|
||||
});
|
||||
|
||||
$(document).on('mousemove', function(e) {
|
||||
if (currentServerHover) {
|
||||
var totalPlayers = getCurrentTotalPlayers();
|
||||
var playerCount = lastPlayerEntries[currentServerHover];
|
||||
|
||||
renderTooltip(e.pageX + 10, e.pageY + 10, '<strong>' + currentServerHover + '</strong>: ' + roundToPoint(playerCount / totalPlayers * 100, 10) + '% of ' + formatNumber(totalPlayers) + ' tracked players.<br />(' + formatNumber(playerCount) + ' online.)');
|
||||
}
|
||||
});
|
||||
|
||||
$(window).on('resize', function() {
|
||||
updatePercentageBar();
|
||||
|
||||
if (historyPlot) {
|
||||
historyPlot.resize();
|
||||
historyPlot.setupGrid();
|
||||
historyPlot.draw();
|
||||
}
|
||||
});
|
||||
});
|
185
assets/js/sort.js
Normal file
185
assets/js/sort.js
Normal file
@ -0,0 +1,185 @@
|
||||
import { isArrayEqual } from './util'
|
||||
|
||||
const SORT_OPTIONS = [
|
||||
{
|
||||
getName: () => 'Players',
|
||||
sortFunc: (a, b) => b.playerCount - a.playerCount,
|
||||
highlightedValue: 'player-count'
|
||||
},
|
||||
{
|
||||
getName: (app) => {
|
||||
return Math.floor(app.publicConfig.graphDuration / (60 * 60 * 1000)) + 'h Peak'
|
||||
},
|
||||
sortFunc: (a, b) => {
|
||||
if (!a.lastPeakData && !b.lastPeakData) {
|
||||
return 0
|
||||
} else if (a.lastPeakData && !b.lastPeakData) {
|
||||
return -1
|
||||
} else if (b.lastPeakData && !a.lastPeakData) {
|
||||
return 1
|
||||
}
|
||||
return b.lastPeakData.playerCount - a.lastPeakData.playerCount
|
||||
},
|
||||
testFunc: (app) => {
|
||||
// Require at least one ServerRegistration to have a lastPeakData value defined
|
||||
for (const serverRegistration of app.serverRegistry.getServerRegistrations()) {
|
||||
if (serverRegistration.lastPeakData) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
highlightedValue: 'peak'
|
||||
},
|
||||
{
|
||||
getName: () => 'Record',
|
||||
sortFunc: (a, b) => {
|
||||
if (!a.lastRecordData && !b.lastRecordData) {
|
||||
return 0
|
||||
} else if (a.lastRecordData && !b.lastRecordData) {
|
||||
return -1
|
||||
} else if (b.lastRecordData && !a.lastRecordData) {
|
||||
return 1
|
||||
}
|
||||
return b.lastRecordData.playerCount - a.lastRecordData.playerCount
|
||||
},
|
||||
testFunc: (app) => {
|
||||
// Require at least one ServerRegistration to have a lastRecordData value defined
|
||||
for (const serverRegistration of app.serverRegistry.getServerRegistrations()) {
|
||||
if (serverRegistration.lastRecordData) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
},
|
||||
highlightedValue: 'record'
|
||||
}
|
||||
]
|
||||
|
||||
const SORT_OPTION_INDEX_DEFAULT = 0
|
||||
const SORT_OPTION_INDEX_STORAGE_KEY = 'minetrack_sort_option_index'
|
||||
|
||||
export class SortController {
|
||||
constructor (app) {
|
||||
this._app = app
|
||||
this._buttonElement = document.getElementById('sort-by')
|
||||
this._textElement = document.getElementById('sort-by-text')
|
||||
this._sortOptionIndex = SORT_OPTION_INDEX_DEFAULT
|
||||
}
|
||||
|
||||
reset () {
|
||||
this._lastSortedServers = undefined
|
||||
|
||||
// Reset modified DOM structures
|
||||
this._buttonElement.style.display = 'none'
|
||||
this._textElement.innerText = '...'
|
||||
|
||||
// Remove bound DOM event listeners
|
||||
this._buttonElement.removeEventListener('click', this.handleSortByButtonClick)
|
||||
}
|
||||
|
||||
loadLocalStorage () {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
const sortOptionIndex = localStorage.getItem(SORT_OPTION_INDEX_STORAGE_KEY)
|
||||
if (sortOptionIndex) {
|
||||
this._sortOptionIndex = parseInt(sortOptionIndex)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateLocalStorage () {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
if (this._sortOptionIndex !== SORT_OPTION_INDEX_DEFAULT) {
|
||||
localStorage.setItem(SORT_OPTION_INDEX_STORAGE_KEY, this._sortOptionIndex)
|
||||
} else {
|
||||
localStorage.removeItem(SORT_OPTION_INDEX_STORAGE_KEY)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
show () {
|
||||
// Load the saved option selection, if any
|
||||
this.loadLocalStorage()
|
||||
|
||||
this.updateSortOption()
|
||||
|
||||
// Bind DOM event listeners
|
||||
// This is removed by #reset to avoid multiple listeners
|
||||
this._buttonElement.addEventListener('click', this.handleSortByButtonClick)
|
||||
|
||||
// Show #sort-by element
|
||||
this._buttonElement.style.display = 'inline-block'
|
||||
}
|
||||
|
||||
handleSortByButtonClick = () => {
|
||||
while (true) {
|
||||
// Increment to the next sort option, wrap around if needed
|
||||
this._sortOptionIndex = (this._sortOptionIndex + 1) % SORT_OPTIONS.length
|
||||
|
||||
// Only break if the sortOption is supported
|
||||
// This can technically cause an infinite loop, but never should assuming
|
||||
// at least one sortOption does not implement the test OR always returns true
|
||||
const sortOption = SORT_OPTIONS[this._sortOptionIndex]
|
||||
|
||||
if (!sortOption.testFunc || sortOption.testFunc(this._app)) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Redraw the button and sort the servers
|
||||
this.updateSortOption()
|
||||
|
||||
// Save the updated option selection
|
||||
this.updateLocalStorage()
|
||||
}
|
||||
|
||||
updateSortOption = () => {
|
||||
const sortOption = SORT_OPTIONS[this._sortOptionIndex]
|
||||
|
||||
// Pass app instance so sortOption names can be dynamically generated
|
||||
this._textElement.innerText = sortOption.getName(this._app)
|
||||
|
||||
// Update all servers highlighted values
|
||||
for (const serverRegistration of this._app.serverRegistry.getServerRegistrations()) {
|
||||
serverRegistration.updateHighlightedValue(sortOption.highlightedValue)
|
||||
}
|
||||
|
||||
this.sortServers()
|
||||
}
|
||||
|
||||
sortServers = () => {
|
||||
const sortOption = SORT_OPTIONS[this._sortOptionIndex]
|
||||
|
||||
const sortedServers = this._app.serverRegistry.getServerRegistrations().sort((a, b) => {
|
||||
if (a.isFavorite && !b.isFavorite) {
|
||||
return -1
|
||||
} else if (b.isFavorite && !a.isFavorite) {
|
||||
return 1
|
||||
}
|
||||
|
||||
return sortOption.sortFunc(a, b)
|
||||
})
|
||||
|
||||
// Test if sortedServers has changed from the previous listing
|
||||
// This avoids DOM updates and graphs being redrawn
|
||||
const sortedServerIds = sortedServers.map(server => server.serverId)
|
||||
|
||||
if (isArrayEqual(sortedServerIds, this._lastSortedServers)) {
|
||||
return
|
||||
}
|
||||
|
||||
this._lastSortedServers = sortedServerIds
|
||||
|
||||
// Sort a ServerRegistration list by the sortOption ONLY
|
||||
// This is used to determine the ServerRegistration's rankIndex without #isFavorite skewing values
|
||||
const rankIndexSort = this._app.serverRegistry.getServerRegistrations().sort(sortOption.sortFunc)
|
||||
|
||||
// Update the DOM structure
|
||||
sortedServers.forEach(function (serverRegistration) {
|
||||
$('#container_' + serverRegistration.serverId).appendTo('#server-list')
|
||||
|
||||
// Set the ServerRegistration's rankIndex to its indexOf the normal sort
|
||||
serverRegistration.updateServerRankIndex(rankIndexSort.indexOf(serverRegistration))
|
||||
})
|
||||
}
|
||||
}
|
@ -1,189 +1,160 @@
|
||||
var MISSING_FAVICON_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAJNUlEQVR4XtVbe4xU5RX/nXNnH+W1pS8iXaFVwRgaU7ph597ZxZKmD2maGtNSRNFiWyEQLSVaVKRStCgUqo2VVkiR0tiA0ldqW1IbEyzsfDOr2waJ1oYKQpcoWkFeBWe55zTfsEt3l537mLmzbO+/93zfOed3z+s737mEKj86fXodDh+eAse5UlQnkOplAowhohFQHV5kT3RSVU8wcEiJ9rDqHhDtwrFjL9BLLxWqKSJVY3Ntbv4oHOc6X+RqApqZub4cPgKcYqAdRNsAbKFsdn85+wStSQwAnTSpFg0NN/gicx0iN2lBBVD1feM4zjoUCpupo6MrCR4VA6BNTcNQU3OrqC5k5rFJCBW2hwCdrPowiH5CxpwKo6+aBajrzhTm1ax6cSVClLtWgNeY6A7KZn9V7h5lWYCm043KvImAT5XLOMl1CjxDqdQc2rHj9bj7xgZAXfeLQrSRgffFZVZNevH9t7imZg61tf0xDp9YAPiZzApWXRKHwWDS2kDJqsspl1selW8kAHTGDEcOHFjPzF+LunFvOgEOQuQvTGRA9DJ8fy+I3kY+fwLTpjFOnRoG5g8BGA+iSSIyBUTTGGgsk986NmYBARK2PhQAq7x2dm4l4NqwzfooLXIEjmNdZQuy2RcI0DjrLa163mQBrofvf5Ud54Mx129FY+Ms2rrVrygL+On0hjhfXoA3GFgBYEOlKapH8O4aY46I3MNE46ICIUQ/dbLZW8oGwM9kvseq90RhKCI+E61BV9d91NHxnyhr4tIUgRg16k4RWRK1uhTgQceYknGrpAtoJnMNVH8bQ8h7yZj7Y9CXTaotLZeLyFMMXBlpE6Ivl6oVBgTA5nlh3hUn1QnQxcBMMuY3kYSqkEg97z2iup6JZkfY6ih8fzK1t+/rTzsgAOJ5z5ZT5Aw6CACJ532fgTvCQPBFjJPPt/QPxucBYMtbEG0J27DU+8EGwcrhe97qKCAA+AYZs6G37H0AsAcbqat7pdLafrBBUGsJrvvzMHcQon8z80TaufNIDwh9AXDdxSBaVe7X71f8DH5MAHJhgVGAlY4xd58HgE0xMmrUawxcVNK8bapjXi7AdxioCQNq0C3BZoeurl3sOHUBLnqMC4Vx1NFx1NKcswDNZG6G6uOBSqmuolzuLvW8awV4ckiC4Hn3Agg7C9xNxqzsA8AZ1zVBnZxihVcoXNpT5AxVEIqWPHLknqCKUYB9jjGXnAOgu4e3N8SkbyNjHu1NM2RByGTmQnVdoD5EGcpmTdEF1PNsUHggwPePMPOHB6rthyIIRSsYPrwz6AAlwKOOMbcVATiTTj/nMF9VEgCih5xs9vZS74ciCP7ZAunbJXXy/Ved9vbLyPbt5fDhdwIPF0TNlM0+H2RSQw0Ee5QG8NcQt24kTadbwbwjIG0cZGMujnKeH2og+J73r5Cmyg2kmcwCqK4N8P/NTj5/fVjO73k/lEDwPe8XbBsqJX1XV5HveQ8z8K0Aom9SLvejqAB0B9UhUSeo5y0E8MOS+gO/tjX000T0hQAFP03GPBsHgKECgqbTnwHzMwHuvZvOuG67QzSlpIK+f8lA5+gogFxod9Dm5olwnH8EZIK3rAu8zMAVJRUSaaB8/lgUhQeiuZAgqOfZu4u3AyzgFPmuuz+w0WiME6W9PBRTpO0aAQjsT4YDUFdXQ9u3nynXAi5kdihe3NbWngySveou0Jv5YLuDptNjwPxGoAuEBkFgAhnzz0ot4EJYgmYyH4Pq7oAa582qpcGhEBPCWvsC7LYu8BADi0oKTLSQstlHkrKAwbQEdd0lILK3VAM+agshdd35IPpxSTNRfcLJ5W5MGoDBKJbCijzbHyT1vAyAtoBA0ekYU7UJkGoFRm1qqpFU6k1mfm+Adc+iYvNgxIijIXdtnyBj/lYNK6iWJYSVwUVdUqmxPR2h7QA+GWAFaxxjSjYXkgAmaUvwM5mNrDonQKc9jjETzwKQydwJ1WKXdKCnOH5y8mRjtYcWkwLBlsDFSTLAVoID66T6iJPLLewBYLyo7uNebfLzVhHNo2x2fRJfu9opUjOZpVANvqlWdSmXy5+7FzjT3NzmOI4NiKUQO8DHj0+othVUGhO0tXU0fN/eAjcEWHSxH2jf/+9ixPNuArAp5AsP3gxAmZcvvuuuZaIFIXosJmNW9wXApo3a2r1BPTQROc2p1Mepra3kGTtJF4kbEwDYOLY0yJVF5B1OpcZRW9vxPgB0B8PbobomSAkBXuS6ujRt3346SWVL7RUHhCjyCNEKJ5td2kPb93bYTl2I/J2ZxweCoPoE53I3RekURxEqjCYpEAQ4xIXC5T0Xo+dZQLcVfAmqvwwTSoCq1wa9ZUgEBNWbKZf7We99S43I/ImAz0YBgY1Z/P9gCT7RTiebvSp0RKZoBS0tY0V1F6t+IBQE6w5Ec5OaCQzjV44lCHCYHWcy7dx5oP/+pcfkWlo+LyK/DyyOunez52oGvkLGvBKmQBLvfc9bzoCdA4j2qF5DudzvBiIOHJVV110Gou9G4SK+/y6nUivR0PAgbdv2bpQ1cWk0nR4lRPeD6FYGOMp6Ae5zjFlWijZ0Vtj3vMcYmBeFmaXp/pvjAYwe/XhSQFjFwTxPgLtizi4+5hgzP0j2UADssDQ6OzcDmBEVhG4gbD/eTm5tLmdYWu0X9rypAlwHkdnMPCIWf5HNnM/PDmvphwJQDIp2XL6zc20cS+gtrACvQ/U5Bp4vjsur7gfRIRQKp1FbqyAaCZH3g+hSAFco4Knvt5YxIV5ka4cf2Bh70qt8XL5PLnbdZUK0LEpgjPO1kqLt/mFiKeVyJaddImeBUkKp604XkU3lfp2klO2/T7HKE7mR8vk/x+ERyQX6b6hTp16khcJGYv5cHGbVolXgDyTydcrnD8XlURYAPUy6ixLbVv9IXMZJ0Ivvv8rMiyiXe7rc/SoCoBggp02rx+nT84V5UaUzxlGVEBF7ofsDjB69vtJUWzEA56yhqakGtbWzfKJbSLUl6UBZ/HVWZIfDvA51dU8lcWFrZU8MgD7ZorV1HHx/pgJXK+AyMCzq1+2XPk8SYEh1G4ieJGMOlrNPRYVQpQztBQXq65ugevb3eWCCqI4hYATsL/RFP9ITqnq8+Ps8sx1z3QORF1Ff35HUly6lx38BC3SpK3sT2hIAAAAASUVORK5CYII=";
|
||||
export class Tooltip {
|
||||
constructor () {
|
||||
this._div = document.getElementById('tooltip')
|
||||
}
|
||||
|
||||
var tooltip = $('#tooltip');
|
||||
set (x, y, offsetX, offsetY, html) {
|
||||
this._div.innerHTML = html
|
||||
|
||||
var lastMojangServiceUpdate;
|
||||
var publicConfig;
|
||||
// Assign display: block so that the offsetWidth is valid
|
||||
this._div.style.display = 'block'
|
||||
|
||||
function showCaption(html) {
|
||||
var tagline = $('#tagline-text');
|
||||
tagline.stop(true, false);
|
||||
tagline.html(html);
|
||||
tagline.slideDown(100);
|
||||
}
|
||||
// Prevent the div from overflowing the page width
|
||||
const tooltipWidth = this._div.offsetWidth
|
||||
|
||||
function hideCaption() {
|
||||
var tagline = $('#tagline-text');
|
||||
tagline.stop(true, false);
|
||||
tagline.slideUp(100);
|
||||
}
|
||||
|
||||
function setPublicConfig(json) {
|
||||
publicConfig = json;
|
||||
$('#server-container-list').html('');
|
||||
}
|
||||
|
||||
function getServerByField(id, value) {
|
||||
for (var i = 0; i < publicConfig.servers.length; i++) {
|
||||
var entry = publicConfig.servers[i];
|
||||
|
||||
if (entry[id] === value) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getServerByIp(ip) {
|
||||
return getServerByField('ip', ip);
|
||||
}
|
||||
|
||||
function getServerByName(name) {
|
||||
return getServerByField('name', name);
|
||||
}
|
||||
|
||||
function getServerColor(name) {
|
||||
var server = getServerByName(name);
|
||||
|
||||
return server ? server.color : stringToColor(name);
|
||||
}
|
||||
|
||||
// Generate (and set) the HTML that displays Mojang status.
|
||||
// If nothing is passed, re-render the last update.
|
||||
// If something is passed, update and then re-render.
|
||||
function updateMojangServices(currentUpdate) {
|
||||
if (currentUpdate) {
|
||||
lastMojangServiceUpdate = currentUpdate;
|
||||
// 1.2 is a magic number used to pad the offset to ensure the tooltip
|
||||
// never gets close or surpasses the page's X width
|
||||
if (x + offsetX + (tooltipWidth * 1.2) > window.innerWidth) {
|
||||
x -= tooltipWidth
|
||||
offsetX *= -1
|
||||
}
|
||||
|
||||
if (!lastMojangServiceUpdate) {
|
||||
return;
|
||||
this._div.style.top = (y + offsetY) + 'px'
|
||||
this._div.style.left = (x + offsetX) + 'px'
|
||||
}
|
||||
|
||||
hide = () => {
|
||||
this._div.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
export class Caption {
|
||||
constructor () {
|
||||
this._div = document.getElementById('status-text')
|
||||
}
|
||||
|
||||
set (text) {
|
||||
this._div.innerText = text
|
||||
this._div.style.display = 'block'
|
||||
}
|
||||
|
||||
hide () {
|
||||
this._div.style.display = 'none'
|
||||
}
|
||||
}
|
||||
|
||||
// Minecraft Java Edition default server port: 25565
|
||||
// Minecraft Bedrock Edition default server port: 19132
|
||||
const MINECRAFT_DEFAULT_PORTS = [25565, 19132]
|
||||
|
||||
export function formatMinecraftServerAddress (ip, port) {
|
||||
if (port && !MINECRAFT_DEFAULT_PORTS.includes(port)) {
|
||||
return ip + ':' + port
|
||||
}
|
||||
return ip
|
||||
}
|
||||
|
||||
// Detect gaps in versions by matching their indexes to knownVersions
|
||||
export function formatMinecraftVersions (versions, knownVersions) {
|
||||
if (!versions || !versions.length || !knownVersions || !knownVersions.length) {
|
||||
return
|
||||
}
|
||||
|
||||
let currentVersionGroup = []
|
||||
const versionGroups = []
|
||||
|
||||
for (let i = 0; i < versions.length; i++) {
|
||||
const versionIndex = versions[i]
|
||||
|
||||
// Look for value mismatch between the previous index
|
||||
// Require i > 0 since lastVersionIndex is undefined for i === 0
|
||||
if (i > 0 && versions[i] - 1 !== versionIndex - 1) {
|
||||
versionGroups.push(currentVersionGroup)
|
||||
currentVersionGroup = []
|
||||
}
|
||||
|
||||
var keys = Object.keys(lastMojangServiceUpdate);
|
||||
currentVersionGroup.push(versionIndex)
|
||||
}
|
||||
|
||||
for (var i = 0; i < keys.length; i++) {
|
||||
var key = keys[i];
|
||||
var status = lastMojangServiceUpdate[key];
|
||||
// Ensure the last versionGroup is always pushed
|
||||
if (currentVersionGroup.length > 0) {
|
||||
versionGroups.push(currentVersionGroup)
|
||||
}
|
||||
|
||||
// hack: ensure mojang-status is added for alignment, replace existing class to swap status color
|
||||
$('#mojang-status_' + status.name).attr('class', 'mojang-status mojang-status-' + status.title.toLowerCase());
|
||||
$('#mojang-status-text_' + status.name).text(status.title);
|
||||
if (versionGroups.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Remap individual versionGroups values into named versions
|
||||
return versionGroups.map(versionGroup => {
|
||||
const startVersion = knownVersions[versionGroup[0]]
|
||||
|
||||
if (versionGroup.length === 1) {
|
||||
// A versionGroup may contain a single version, only return its name
|
||||
// This is a cosmetic catch to avoid version labels like 1.0-1.0
|
||||
return startVersion
|
||||
} else {
|
||||
const endVersion = knownVersions[versionGroup[versionGroup.length - 1]]
|
||||
|
||||
return startVersion + '-' + endVersion
|
||||
}
|
||||
}).join(', ')
|
||||
}
|
||||
|
||||
function findErrorMessage(error) {
|
||||
if (error.description) {
|
||||
return error.description;
|
||||
} else if (error.errno) {
|
||||
return error.errno;
|
||||
export function formatTimestamp (millis) {
|
||||
const date = new Date(0)
|
||||
date.setUTCSeconds(millis / 1000)
|
||||
return date.toLocaleTimeString()
|
||||
}
|
||||
|
||||
export function formatDate (millis) {
|
||||
const date = new Date(0)
|
||||
date.setUTCSeconds(millis / 1000)
|
||||
return date.toLocaleDateString()
|
||||
}
|
||||
|
||||
export function formatPercent (x, over) {
|
||||
const val = Math.round((x / over) * 100 * 10) / 10
|
||||
return val + '%'
|
||||
}
|
||||
|
||||
export function formatNumber (x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||||
}
|
||||
|
||||
export function isArrayEqual (a, b) {
|
||||
if (typeof a === 'undefined' || typeof a !== typeof b) {
|
||||
return false
|
||||
}
|
||||
if (a.length !== b.length) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
function getTimestamp(ms, timeOnly) {
|
||||
var date = new Date(0);
|
||||
export function isObjectEqual (a, b, props) {
|
||||
if (typeof a === 'undefined' || typeof a !== typeof b) {
|
||||
return false
|
||||
}
|
||||
for (let i = 0; i < props.length; i++) {
|
||||
const prop = props[i]
|
||||
|
||||
date.setUTCSeconds(ms);
|
||||
|
||||
return date.toLocaleTimeString();
|
||||
}
|
||||
|
||||
function safeName(name) {
|
||||
return name.replace(/ /g, '');
|
||||
}
|
||||
|
||||
function renderTooltip(x, y, html) {
|
||||
tooltip.html(html).css({
|
||||
top: y,
|
||||
left: x
|
||||
}).fadeIn(0);
|
||||
}
|
||||
|
||||
function hideTooltip() {
|
||||
tooltip.hide();
|
||||
}
|
||||
|
||||
function formatNumber(x) {
|
||||
return x.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',');
|
||||
if (typeof a[prop] === 'undefined' || typeof a[prop] !== typeof b[prop] || a[prop] !== b[prop]) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// From http://detectmobilebrowsers.com/
|
||||
function isMobileBrowser() {
|
||||
export function isMobileBrowser () {
|
||||
var check = false;
|
||||
(function(a){if(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0,4)))check = true})(navigator.userAgent||navigator.vendor||window.opera);
|
||||
return check;
|
||||
}
|
||||
|
||||
function trimOldPings(data, graphDuration) {
|
||||
var keys = Object.keys(data);
|
||||
|
||||
var timeMs = new Date().getTime();
|
||||
|
||||
for (var x = 0; x < keys.length; x++) {
|
||||
var listing = data[keys[x]];
|
||||
|
||||
var toSplice = [];
|
||||
|
||||
for (var i = 0; i < listing.length; i++) {
|
||||
var entry = listing[i];
|
||||
|
||||
if (timeMs - entry[0] > graphDuration) {
|
||||
toSplice.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < toSplice.length; i++) {
|
||||
listing.splice(toSplice[i], 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function stringToColor(base) {
|
||||
var hash;
|
||||
|
||||
for (var i = base.length - 1, hash = 0; i >= 0; i--) {
|
||||
hash = base.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
color = Math.floor(Math.abs((Math.sin(hash) * 10000) % 1 * 16777216)).toString(16);
|
||||
|
||||
return '#' + Array(6 - color.length + 1).join('0') + color;
|
||||
}
|
||||
|
||||
function roundToPoint(val, scale) {
|
||||
return Math.round(val * scale) / scale;
|
||||
}
|
||||
|
||||
function msToTime(timer) {
|
||||
var milliseconds = timer % 1000;
|
||||
timer = (timer - milliseconds) / 1000;
|
||||
|
||||
var seconds = timer % 60;
|
||||
timer = (timer - seconds) / 60;
|
||||
|
||||
var minutes = timer % 60;
|
||||
var hours = (timer - minutes) / 60;
|
||||
|
||||
var days = Math.floor(hours / 24);
|
||||
hours -= days * 24;
|
||||
|
||||
var string = '';
|
||||
|
||||
// hack: only format days if >1, if === 1 it will format as "24h" instead
|
||||
if (days > 1) {
|
||||
string += days + 'd';
|
||||
} else if (days === 1) {
|
||||
hours += 24;
|
||||
}
|
||||
|
||||
if (hours > 0) {
|
||||
string += hours + 'h';
|
||||
}
|
||||
if (minutes > 0) {
|
||||
string += minutes + 'm';
|
||||
}
|
||||
if (seconds > 0) {
|
||||
string += seconds + 's';
|
||||
}
|
||||
|
||||
return string;
|
||||
// eslint-disable-next-line no-useless-escape
|
||||
(function (a) { if (/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(a) || /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s\-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(a.substr(0, 4)))check = true })(navigator.userAgent || navigator.vendor || window.opera)
|
||||
return check
|
||||
}
|
||||
|
30
config.json
30
config.json
@ -1,14 +1,4 @@
|
||||
{
|
||||
"routes": {
|
||||
"/": "assets/html/index.html",
|
||||
"/images/compass.png": "assets/images/compass.png",
|
||||
"/js/site.js": "assets/js/site.js",
|
||||
"/js/util.js": "assets/js/util.js",
|
||||
"/js/graph.js": "assets/js/graph.js",
|
||||
"/css/main.css": "assets/css/main.css"
|
||||
},
|
||||
"faviconOverride": {
|
||||
},
|
||||
"site": {
|
||||
"port": 8080,
|
||||
"ip": "0.0.0.0"
|
||||
@ -20,23 +10,5 @@
|
||||
"connectTimeout": 2500
|
||||
},
|
||||
"logToDatabase": false,
|
||||
"graphDuration": 86400000,
|
||||
"versions": {
|
||||
"PC": [
|
||||
4,
|
||||
5,
|
||||
47,
|
||||
107,
|
||||
210,
|
||||
315,
|
||||
335,
|
||||
393,
|
||||
477,
|
||||
575
|
||||
],
|
||||
"PE": [
|
||||
0
|
||||
]
|
||||
},
|
||||
"serverTypesVisible": true
|
||||
"graphDuration": 86400000
|
||||
}
|
||||
|
@ -1,3 +1,38 @@
|
||||
**5** *(Apr 8 2020)*
|
||||
- New logo!
|
||||
- Completely rebuilt the frontend's Javascript (heavy optimizations and cleanup!)
|
||||
- Adds a button for mobile devices to manually request the historical graph
|
||||
- Adds timestamp to each server's player count record
|
||||
- Adds the ability to favorite servers so they'll always be sorted first
|
||||
- Adds "Sort By" option for controlling the server listing sort order
|
||||
- Adds "Only Favorites" button to graph controls
|
||||
- Adds ESLint configuration
|
||||
- New missing favicon icon
|
||||
- The versions section, and minecraft.json file, have been merged into minecraft_versions.json
|
||||
- Removes "routes" from config.json. The HTTP server will now serve static assets from dist/
|
||||
- Added Parcel bundler which bundles the assets/ directory into dist/
|
||||
- Custom favicons are now served from "favicons/" directory and their configuration moved into servers.json. Paths in servers.json should be updated to reflect their filename without the path.
|
||||
- Added finalhandler and serve-static dependencies
|
||||
- Add ```npm run dev``` and ```npm run build``` scripts to package.json
|
||||
- Added a distinct loading/connection status screen to simplify state management
|
||||
- publicConfig.json is now sent over the socket connection so the frontend can be safely reloaded on rebooted instances
|
||||
- Tooltips have been optimized and updated to a more readable design
|
||||
- Initial page loading has been optimized
|
||||
- MISSING_FAVICON_BASE64 has been moved to a file, images/missing_favicon.svg to improve caching behavior (and its customizable now!)
|
||||
- Peak player count labels are formatted using the graphDuration hours and now displays the timestamp seconds
|
||||
- Fixed favicon payloads being repeatedly sent.
|
||||
- Fixed the page being broken when connecting to a freshly booted instance
|
||||
- Fixed graphs starting at 0 player count when a server is initially pinged
|
||||
- Fixed status text ocassionally not being shown
|
||||
- Fixed some elements/frontend state not being completely reset on disconnect
|
||||
- Fixed Minecraft Bedrock Edition servers showing the default port of 19132
|
||||
- Fixed tooltips overflowing the page width
|
||||
- Fixed backend bug causing servers to skip some Minecraft versions
|
||||
- Minor connection blips have a grace period before the UI is updated, this prevents page reshuffle spam when experiencing minor connection issues
|
||||
- Moved localStorage keys to "minetrack_hidden_servers" since the data structure has been changed
|
||||
- Removed #validateBootTime loop and logic
|
||||
- Removed mime dependency
|
||||
|
||||
**4.0.5** *(Apr 1 2020)*
|
||||
- The frontend will now auto calculate the "24h Peak" label using your configured graphDuration in config.json
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
Compass icon by PixelBuddha: https://www.iconfinder.com/PixelBuddha
|
40
docs/MIGRATING.md
Normal file
40
docs/MIGRATING.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Migrating to Minetrack 5
|
||||
Minetrack 5 is the first of several upcoming updates designed to address several legacy bugs and design flaws in previous Minetrack versions. As part of this, it modifies some data structures and operational instructions that make the upgrade process more manual than seen previously. This guide covers the distinct differences, with upgrade instructions, to get you started.
|
||||
|
||||
## Upgrading Minetrack 5
|
||||
1. Stop any running instance of Minetrack.
|
||||
2. If you've cloned the repository, use `git clone https://github.com/Cryptkeeper/Minetrack Minetrack5`. If you've manually downloaded a release or an archive of the repository, download a fresh copy and extract it into a directory named "Minetrack5".
|
||||
3. Open the directory and execute `npm install --build-from-source`. This will install updated (and new) dependencies needed by the program.
|
||||
4. If you have `logToDatabase: true` in your `config.json`, make sure to copy your `database.sql` file into the new directory, otherwise you will lose historical server activity and records.
|
||||
5. Copy your existing `config.json` and `servers.json` files into the new directory.
|
||||
4. Build your copy of `dist/`.
|
||||
5. If you have previously configured any `faviconOverride` values within `config.json`, you will need to move them to the updated structure. Create a new directory within your Minetrack folder named `favicons/`.
|
||||
6. If you have previously configured any `minecraft.json` values not included in the new `minecraft_versions.json` file, you will need to update their structure copy them to the new file.
|
||||
7. Move your custom favicon images into the directory.
|
||||
8. Open `servers.json` in your favorite editor.
|
||||
9. For any server which you have a custom favicon, set the "favicon" field like so:
|
||||
|
||||
```
|
||||
{
|
||||
"name": "Hypixel",
|
||||
"ip": "mc.hypixel.net",
|
||||
"type": "PC",
|
||||
"favicon": "CustomHypixelFavicon.png"
|
||||
}
|
||||
```
|
||||
(Replacing "CustomHypixelFavicon.png" with your file's name.)
|
||||
|
||||
Do **NOT** include the `favicons/` path in the value. For example a file, "my-favicon.png" in the directory `favicons/` should be configured using simply "my-favicon.png".
|
||||
|
||||
You may delete the `faviconOverride`, `routes` and `versions` portions of your `config.json`, they are no longer supported features. You may delete the `minecraft.json` file, it has been merged into `minecraft_versions.json`.
|
||||
|
||||
You're done!
|
||||
|
||||
## Building `dist/`
|
||||
Minetrack now serves a "bundled" copy of the `assets/` directory, instead of the files directly from disk. This optimizes the delivery speed, but requires an additional step when installing Minetrack or when modifying files within `assets/`.
|
||||
|
||||
1. `cd` into your Minetrack directory (if not already there).
|
||||
2. Execute `npm run build` (`npm run dev` is also available, which skips the minimization step and makes active development easier).
|
||||
3. Run `ls` to ensure the `dist/` directory has been created.
|
||||
|
||||
Whenever modifying files within `assets/`, you will need to re-run the `npm run build` step to reflect the changes. Those expert few of you may wish to dig into Parcel's [watch and serve](https://parceljs.org/cli.html#watch) CLI commands.
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
var util = require('./util');
|
||||
|
||||
exports.setup = function() {
|
||||
@ -22,10 +27,10 @@ exports.setup = function() {
|
||||
};
|
||||
|
||||
exports.getTotalRecord = function(ip, callback) {
|
||||
db.all("SELECT MAX(playerCount) FROM pings WHERE ip = ?", [
|
||||
db.all("SELECT MAX(playerCount), timestamp FROM pings WHERE ip = ?", [
|
||||
ip
|
||||
], function(err, data) {
|
||||
callback(data[0]['MAX(playerCount)']);
|
||||
callback(data[0]['MAX(playerCount)'], data[0]['timestamp']);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
var winston = require('winston');
|
||||
|
||||
winston.remove(winston.transports.Console);
|
||||
|
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
var request = require('request');
|
||||
|
||||
var logger = require('./logger');
|
||||
|
10
lib/ping.js
10
lib/ping.js
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
var mcpe_ping = require('mcpe-ping-fixed');
|
||||
var mcpc_ping = require('mc-ping-updated');
|
||||
|
||||
@ -16,7 +21,7 @@ function pingMinecraftPC(host, port, timeout, callback, version) {
|
||||
var favicon;
|
||||
|
||||
// Ensure the returned favicon is a data URI
|
||||
if (res.favicon.indexOf('data:image/') === 0) {
|
||||
if (res.favicon && res.favicon.indexOf('data:image/') === 0) {
|
||||
favicon = res.favicon;
|
||||
}
|
||||
|
||||
@ -27,7 +32,7 @@ function pingMinecraftPC(host, port, timeout, callback, version) {
|
||||
},
|
||||
version: parseInt(res.version.protocol),
|
||||
latency: util.getCurrentTimeMs() - startTime,
|
||||
favicon
|
||||
favicon: favicon
|
||||
});
|
||||
}
|
||||
}, timeout, version);
|
||||
@ -47,7 +52,6 @@ function pingMinecraftPE(host, port, timeout, callback) {
|
||||
online: capPlayerCount(host, parseInt(res.currentPlayers)),
|
||||
max: parseInt(res.maxPlayers)
|
||||
},
|
||||
version: res.version,
|
||||
latency: util.getCurrentTimeMs() - startTime
|
||||
});
|
||||
}
|
||||
|
@ -1,74 +1,32 @@
|
||||
var http = require('http');
|
||||
var fs = require('fs');
|
||||
var url = require('url');
|
||||
var mime = require('mime');
|
||||
var io = require('socket.io');
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
const http = require('http')
|
||||
|
||||
var util = require('./util');
|
||||
var logger = require('./logger');
|
||||
const io = require('socket.io')
|
||||
const finalHandler = require('finalhandler')
|
||||
const serveStatic = require('serve-static')
|
||||
|
||||
var config = require('../config.json');
|
||||
var minecraft = require('../minecraft.json');
|
||||
var servers = require('../servers.json');
|
||||
const util = require('./util')
|
||||
const logger = require('./logger')
|
||||
|
||||
var urlMapping = [];
|
||||
const config = require('../config.json')
|
||||
|
||||
function setupRoutes() {
|
||||
var routeKeys = Object.keys(config.routes);
|
||||
const distHandler = serveStatic('dist/')
|
||||
const faviconsHandler = serveStatic('favicons/')
|
||||
|
||||
// Map the (static) routes from our config.
|
||||
for (var i = 0; i < routeKeys.length; i++) {
|
||||
urlMapping[routeKeys[i]] = config.routes[routeKeys[i]];
|
||||
}
|
||||
|
||||
logger.log('info', 'Routes: %s', routeKeys);
|
||||
function onRequest (req, res) {
|
||||
logger.log('info', '%s requested: %s', util.getRemoteAddr(req), req.url)
|
||||
distHandler(req, res, function () {
|
||||
faviconsHandler(req, res, finalHandler(req, res))
|
||||
})
|
||||
}
|
||||
|
||||
function handleRequest(req, res) {
|
||||
var requestUrl = url.parse(req.url).pathname;
|
||||
|
||||
logger.log('info', '%s requested: %s', util.getRemoteAddr(req), requestUrl);
|
||||
|
||||
if (requestUrl === '/publicConfig.json') {
|
||||
res.setHeader('Content-Type', 'application/javascript');
|
||||
|
||||
var publicConfig = {
|
||||
graphDuration: config.graphDuration,
|
||||
servers: servers,
|
||||
bootTime: util.getBootTime(),
|
||||
serverTypesVisible: config.serverTypesVisible || false,
|
||||
minecraftVersions: minecraft.versions
|
||||
};
|
||||
|
||||
res.write('setPublicConfig(' + JSON.stringify(publicConfig) + ');');
|
||||
|
||||
res.end();
|
||||
} else if (requestUrl in urlMapping) {
|
||||
var file = urlMapping[requestUrl];
|
||||
|
||||
res.setHeader('Content-Type', mime.getType(file));
|
||||
|
||||
fs.createReadStream(file).pipe(res);
|
||||
} else {
|
||||
res.statusCode = 404;
|
||||
|
||||
res.write('404');
|
||||
|
||||
res.end();
|
||||
}
|
||||
exports.start = function () {
|
||||
const server = http.createServer(onRequest)
|
||||
server.listen(config.site.port, config.site.ip)
|
||||
exports.io = io.listen(server)
|
||||
logger.log('info', 'Started on %s:%d', config.site.ip, config.site.port)
|
||||
}
|
||||
|
||||
exports.start = function() {
|
||||
setupRoutes();
|
||||
|
||||
// Create our tiny little HTTP server.
|
||||
var server = http.createServer(handleRequest);
|
||||
|
||||
server.listen(config.site.port, config.site.ip);
|
||||
|
||||
// I don't like this. But it works, I think.
|
||||
exports.io = io.listen(server);
|
||||
|
||||
// Since everything is loaded, let's celebrate!
|
||||
logger.log('info', 'Started on %s:%d', config.site.ip, config.site.port);
|
||||
};
|
||||
|
17
lib/util.js
17
lib/util.js
@ -1,3 +1,8 @@
|
||||
/**
|
||||
* THIS IS LEGACY, UNMAINTAINED CODE
|
||||
* IT MAY (AND LIKELY DOES) CONTAIN BUGS
|
||||
* USAGE IS NOT RECOMMENDED
|
||||
*/
|
||||
var logger = require('./logger');
|
||||
var dns = require('dns');
|
||||
|
||||
@ -6,8 +11,6 @@ var servers = require('../servers.json');
|
||||
|
||||
var serverNameLookup = [];
|
||||
|
||||
var bootTime;
|
||||
|
||||
// Finds a server in servers.json with a matching IP.
|
||||
// If it finds one, it caches the result for faster future lookups.
|
||||
function getServerNameByIp(ip) {
|
||||
@ -148,16 +151,6 @@ exports.convertServerHistory = function(sqlData) {
|
||||
return graphData;
|
||||
};
|
||||
|
||||
exports.getBootTime = function() {
|
||||
if (!bootTime) {
|
||||
bootTime = exports.getCurrentTimeMs();
|
||||
|
||||
logger.info('Selected %d as boot time.', bootTime);
|
||||
}
|
||||
|
||||
return bootTime;
|
||||
};
|
||||
|
||||
/**
|
||||
* Attempts to resolve Minecraft PC SRV records from DNS, otherwise falling back to the old hostname.
|
||||
*
|
||||
|
@ -1,16 +0,0 @@
|
||||
{
|
||||
"versions": {
|
||||
"PC": {
|
||||
"4": "1.7.2",
|
||||
"5": "1.7.10",
|
||||
"47": "1.8",
|
||||
"107": "1.9",
|
||||
"210": "1.10",
|
||||
"315": "1.11",
|
||||
"335": "1.12",
|
||||
"393": "1.13",
|
||||
"477": "1.14",
|
||||
"575": "1.15.1"
|
||||
}
|
||||
}
|
||||
}
|
44
minecraft_versions.json
Normal file
44
minecraft_versions.json
Normal file
@ -0,0 +1,44 @@
|
||||
{
|
||||
"PC": [
|
||||
{
|
||||
"name": "1.7.2",
|
||||
"protocolId": 4
|
||||
},
|
||||
{
|
||||
"name": "1.7.10",
|
||||
"protocolId": 5
|
||||
},
|
||||
{
|
||||
"name": "1.8",
|
||||
"protocolId": 47
|
||||
},
|
||||
{
|
||||
"name": "1.9",
|
||||
"protocolId": 107
|
||||
},
|
||||
{
|
||||
"name": "1.10",
|
||||
"protocolId": 210
|
||||
},
|
||||
{
|
||||
"name": "1.11",
|
||||
"protocolId": 315
|
||||
},
|
||||
{
|
||||
"name": "1.12",
|
||||
"protocolId": 335
|
||||
},
|
||||
{
|
||||
"name": "1.13",
|
||||
"protocolId": 393
|
||||
},
|
||||
{
|
||||
"name": "1.14",
|
||||
"protocolId": 477
|
||||
},
|
||||
{
|
||||
"name": "1.15.1",
|
||||
"protocolId": 575
|
||||
}
|
||||
]
|
||||
}
|
8010
package-lock.json
generated
8010
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
25
package.json
25
package.json
@ -1,13 +1,14 @@
|
||||
{
|
||||
"name": "minetrack",
|
||||
"version": "4.0.5",
|
||||
"version": "5.0.0",
|
||||
"description": "A Minecraft server tracker that lets you focus on the basics.",
|
||||
"main": "app.js",
|
||||
"dependencies": {
|
||||
"finalhandler": "^1.1.2",
|
||||
"mc-ping-updated": "0.1.1",
|
||||
"mcpe-ping-fixed": "0.0.3",
|
||||
"mime": "2.4.4",
|
||||
"request": "2.88.2",
|
||||
"serve-static": "^1.14.1",
|
||||
"socket.io": "2.3.0",
|
||||
"sqlite3": "4.1.1",
|
||||
"winston": "^2.0.0"
|
||||
@ -19,10 +20,26 @@
|
||||
"keywords": [
|
||||
"minetrack"
|
||||
],
|
||||
"author": "Cryptkeeper <hello@npm.nklow.com>",
|
||||
"author": "Nick Krecklow <hello@npm.nklow.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/Cryptkeeper/Minetrack/issues"
|
||||
},
|
||||
"homepage": "https://github.com/Cryptkeeper/Minetrack#README"
|
||||
"homepage": "https://github.com/Cryptkeeper/Minetrack#README",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.9.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"babel-eslint": "^10.1.0",
|
||||
"eslint": "^6.8.0",
|
||||
"eslint-config-standard": "^14.1.1",
|
||||
"eslint-plugin-import": "^2.20.2",
|
||||
"eslint-plugin-node": "^11.1.0",
|
||||
"eslint-plugin-promise": "^4.2.1",
|
||||
"eslint-plugin-standard": "^4.0.1",
|
||||
"parcel-bundler": "^1.12.4"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "eslint assets/js/*.js && parcel build assets/html/index.html",
|
||||
"dev": "parcel build assets/html/index.html --no-minify"
|
||||
}
|
||||
}
|
||||
|
@ -2,4 +2,4 @@ apt-get install git npm sqlite3
|
||||
git clone https://github.com/Cryptkeeper/Minetrack.git
|
||||
cd Minetrack/
|
||||
npm install --build-from-source
|
||||
sh scripts/start.sh
|
||||
npm run build
|
||||
|
10
servers.json
10
servers.json
@ -8,5 +8,15 @@
|
||||
"name": "HiveMC",
|
||||
"ip": "play.hivemc.com",
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "Mineplex",
|
||||
"ip": "us.mineplex.com",
|
||||
"type": "PC"
|
||||
},
|
||||
{
|
||||
"name": "CubeCraft",
|
||||
"ip": "play.cubecraft.net",
|
||||
"type": "PC"
|
||||
}
|
||||
]
|
||||
|
Loading…
x
Reference in New Issue
Block a user