diff --git a/app.js b/app.js index d685adc..f1d0f4c 100644 --- a/app.js +++ b/app.js @@ -2,6 +2,7 @@ var server = require('./lib/server'); var ping = require('./lib/ping'); var logger = require('./lib/logger'); var mojang = require('./lib/mojang_services'); +var util = require('./lib/util'); var config = require('./config.json'); @@ -20,7 +21,14 @@ function pingAll() { logger.log('error', 'Failed to ping ' + network.ip + ': ' + JSON.stringify(err)); } - server.io.sockets.emit('update', res); + server.io.sockets.emit('update', { + result: res, + error: err, + info: { + name: network.name, + timestamp: util.getCurrentTimeMs() + } + }); // Log our response. if (!networkHistory[network.ip]) { @@ -29,18 +37,25 @@ function pingAll() { var _networkHistory = networkHistory[network.ip]; - // Remove our previous entrie's favicons, we don't need them, just the latest one. + // Remove our previous data that we don't need anymore. for (var i = 0; i < _networkHistory.length; i++) { - delete _networkHistory[i].favicon; + delete _networkHistory[i].info; } _networkHistory.push({ error: err, - result: res + result: res, + timestamp: util.getCurrentTimeMs(), + info: { + ip: network.ip, + port: network.port, + type: network.type, + name: network.name + } }); // Make sure we never log too much. - if (_networkHistory.length > 300) { + if (_networkHistory.length > 72) { // 60/2.5 = 24, so 24 is one minute _networkHistory.shift(); } }); @@ -68,6 +83,13 @@ function startMainLoop() { server.start(function() { // Track how many people are currently connected. server.io.on('connect', function(client) { + // If we haven't sent out at least one round of pings, disconnect them for now. + if (Object.keys(networkHistory) < config.servers.length) { + client.disconnect(); + + return; + } + // We're good to connect them! connectedClients += 1; diff --git a/assets/css/main.css b/assets/css/main.css index 6ef5823..c7924c1 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -1,4 +1,124 @@ +@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300,400); + * { margin: 0; padding: 0; +} + +body { + background: #3B3738; + color: #FFF; + font-family: "Open Sans", sans-serif; + font-size: 18px; + font-weight: 300 !important; +} + +/* Header */ +#header { + background: #EBEBEB; + color: #3B3738; + padding: 20px 0; + text-align: center; + border-top: 1px solid #DED3D6; + width: 840px; + margin: 0 auto; +} + +#header a { + text-decoration: none; + color: inherit; + border-bottom: 1px dashed #3B3738; +} + +#header a:hover { + border-bottom: 1px dashed transparent; +} + +/* Tagline */ +#tagline { + padding: 10px 0; + text-align: center; + width: 840px; + margin: 0 auto; + border-bottom-left-radius: 2px; + border-bottom-right-radius: 2px; +} + +/* Colors used by the Mojang service's status bar */ +.status-online { + background: #87D37C; + color: #3B3738; +} + +.status-unstable { + background: #E9E581; + color: #3B3738; +} + +.status-offline { + background: #e74c3c; +} + +.status-connecting { + background: #3498db; +} + +/* Server listing */ +#server-container { + width: 800px; + margin: 10px auto; +} + +#server-container .server:nth-child(2n) { + background: #4E4E4E; + border-radius: 2px; +} + +.server { + overflow: auto; + padding: 10px; +} + +.server > .column > img { + border-radius: 2px; +} + +.server > .column { + float: left; + display: inline-block; +} + +/* Charts */ +.chart { + height: 100px; + width: 400px; + margin-right: -3px; + margin-bottom: 5px; +} + +#tooltip { + display: none; + position: absolute; + padding: 5px; + border-radius: 3px; + background: rgba(0, 0, 0, 0.65); + z-index: 999; +} + +/* Existing elements */ +h3 { + text-transform: uppercase; +} + +/* Basic classes used randomly */ +.color-gray { + color: #C4C4C4; +} + +.color-red { + color: #c0392b; +} + +.text-uppercase { + text-transform: uppercase; } \ No newline at end of file diff --git a/assets/html/index.html b/assets/html/index.html index 176f662..9d43117 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -12,9 +12,34 @@ - - +
+ + +
+ + Connecting... + +
+ +
+ + + + + + + + + + diff --git a/assets/js/site.js b/assets/js/site.js index f07fee8..03aae66 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -1,15 +1,229 @@ +var lastMojangServiceUpdate; + +var smallChartOptions = { + series: { + shadowSize: 0 + }, + xaxis: { + font: { + color: "#E3E3E3" + }, + show: false + }, + yaxis: { + minTickSize: 100, + tickDecimals: 0, + show: true, + tickLength: 10, + tickFormatter: function(value) { + return formatNumber(value); + }, + font: { + color: "#E3E3E3" + }, + labelWidth: -10 + }, + grid: { + hoverable: true, + color: "#C4C4C4" + }, + colors: [ + "#E9E581" + ] +}; + +var graphs = {}; +var lastLatencyEntries = {}; +var lastPlayerEntries = {}; + +// Generate (and set) the HTML that displays Mojang status. +function updateMojangServices() { + var keys = Object.keys(lastMojangServiceUpdate); + var newStatus = 'Mojang Services: '; + var serviceCountByType = { + Online: 0, + Unstable: 0, + Offline: 0 + }; + + for (var i = 0; i < keys.length; i++) { + var entry = lastMojangServiceUpdate[keys[i]]; + + serviceCountByType[entry.title] += 1; + } + + if (serviceCountByType['Online'] === keys.length) { + $('#tagline').attr('class', 'status-online'); + + newStatus += 'All systems operational.'; + } else { + if (serviceCountByType['Unstable'] > serviceCountByType['Offline']) { + $('#tagline').attr('class', 'status-unstable'); + } else { + $('#tagline').attr('class', 'status-offline'); + } + + for (var i = 0; i < keys.length; i++) { + var entry = lastMojangServiceUpdate[keys[i]]; + + if (entry.startTime) { + newStatus += entry.name + ' ' + entry.title.toLowerCase() + ' for ' + msToTime((new Date()).getTime() - entry.startTime); + } + } + } + + $('#tagline-text').text(newStatus); +} + +function updateServerStatus(lastEntry) { + var info = lastEntry.info; + var div = $('#status_' + safeName(info.name)); + + if (lastEntry.result) { + var result = lastEntry.result; + var newStatus = formatNumber(result.players.online) + '/' + formatNumber(result.players.max); + + if (lastPlayerEntries[info.name]) { + newStatus += ' ('; + + var playerDifference = lastPlayerEntries[info.name] - result.players.online; + + if (playerDifference >= 0) { + newStatus += '+'; + } + + newStatus += playerDifference + ')'; + } + + if (lastLatencyEntries[info.name]) { + newStatus += '
'; + + var latencyDifference = lastLatencyEntries[info.name] - result.latency; + + if (latencyDifference >= 0) { + newStatus += '+'; + } + + newStatus += latencyDifference + 'ms'; + } + + lastPlayerEntries[info.name] = result.players.online; + lastLatencyEntries[info.name] = result.latency; + + div.html(newStatus); + } +} + +function safeName(name) { + return name.replace(' ', ''); +} + $(document).ready(function() { var socket = io.connect(); + var mojangServicesUpdater; socket.on('connect', function() { - + $('#tagline-text').text('Loading...'); }); + socket.on('disconnect', function() { + if (mojangServicesUpdater) { + clearInterval(mojangServicesUpdater); + } + + $('#tagline').attr('class', 'status-connecting'); + $('#tagline-text').text('Attempting reconnnect...'); + + lastPlayerEntries = {}; + lastLatencyEntries = {}; + graphs = {}; + + $('#server-container').html(''); + }); + 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; + + $('
', { + id: safeName(info.name), + class: 'server', + html: '
\ + \ +
\ +

' + info.name + '

\ + ' + info.ip + '\ +
\ + Waiting\ +
\ +
\ +
\ +
' + }).appendTo("#server-container"); + + if (lastEntry.result && lastEntry.result.favicon) { + $('#favicon_' + safeName(info.name)).attr('src', lastEntry.result.favicon); + } + + updateServerStatus(lastEntry); + + graphs[lastEntry.info.name] = { + listing: listing, + plot: $.plot('#chart_' + safeName(info.name), [listing], smallChartOptions) + }; + + $('#chart_' + safeName(info.name)).bind('plothover', function(event, pos, item) { + if (item) { + renderTooltip(item.pageX + 5, item.pageY + 5, getTimestamp(item.datapoint[0] / 1000) + '\ +
\ + ' + formatNumber(item.datapoint[1]) + ' Players'); + } else { + hideTooltip(); + } + }); + } }); - socket.on('update', function(server) { + socket.on('update', function(update) { + var graph = graphs[update.info.name]; + updateServerStatus(update); + + 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) { + // Store the update and force an update. + lastMojangServiceUpdate = data; + + updateMojangServices(); + }); + + // Start any special updating tasks. + mojangServicesUpdater = setInterval(function() { + updateMojangServices(); + }, 1000); }); \ No newline at end of file diff --git a/assets/js/util.js b/assets/js/util.js new file mode 100644 index 0000000..4273505 --- /dev/null +++ b/assets/js/util.js @@ -0,0 +1,49 @@ +var tooltip = $('#tooltip'); + + function getTimestamp(ms, timeOnly) { + var date = new Date(0); + + date.setUTCSeconds(ms); + + return date.toLocaleTimeString(); +} + +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, ','); +} + +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 string = ''; + + if (hours > 0) { + string += hours + 'h'; + } + if (minutes > 0) { + string += minutes + 'm'; + } + if (seconds > 0) { + string += seconds + 's'; + } + + return string; +} \ No newline at end of file diff --git a/config.json b/config.json index 009a7a2..96fd8ca 100644 --- a/config.json +++ b/config.json @@ -6,8 +6,13 @@ "type": "PC" }, { - "name": "Mineplex", - "ip": "us.mineplex.com", + "name": "HiveMC", + "ip": "play.hivemc.com", + "type": "PC" + }, + { + "name": "CCGN", + "ip": "play.cubecraftgames.net", "type": "PC" }, { @@ -16,15 +21,56 @@ "type": "PC" }, { - "name": "Hypixel PE", - "ip": "pe.hypixel.net", - "type": "PE" + "name": "Shotbow", + "ip": "us.shotbow.net", + "type": "PC" + }, + { + "name": "Badlion", + "ip": "na.badlion.net", + "type": "PC" + }, + { + "name": "MCGamer", + "ip": "play.mcgamer.net", + "type": "PC" + }, + { + "name": "Olimpocraft", + "ip": "olimpo.me", + "type": "PC" + }, + { + "name": "Minecade", + "ip": "mineca.de", + "type": "PC" + }, + { + "name": "The Nexus", + "ip": "hub.thenexusmc.com", + "type": "PC" + }, + { + "name": "Kohi", + "ip": "kohi.us", + "type": "PC" + }, + { + "name": "Wynncraft", + "ip": "play.wynncraft.com", + "type": "PC" + }, + { + "name": "Mineplex", + "ip": "us.mineplex.com", + "type": "PC" } ], "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", "/css/main.css": "assets/css/main.css" }, "site": { @@ -35,6 +81,6 @@ "upateMojangStatus": 5000, "mojangStatusTimeout": 3500, "pingAll": 2500, - "connectTimeout": 2500 + "connectTimeout": 2000 } } diff --git a/lib/mojang_services.js b/lib/mojang_services.js index c4272d8..6b7e2ed 100644 --- a/lib/mojang_services.js +++ b/lib/mojang_services.js @@ -1,10 +1,9 @@ var request = require('request'); var logger = require('./logger'); -var profiler = require('./profiler'); +var util = require('./util'); var serviceNameLookup = { - 'minecraft.net': 'Website', 'sessionserver.mojang.com': 'Sessions', 'authserver.mojang.com': 'Auth', 'textures.minecraft.net': 'Skins', @@ -25,7 +24,7 @@ function updateService(name, status) { // If it's an outage, track when it started. if (status === 'yellow'|| status === 'red') { - newEntry.startTime = profiler.getCurrentTimeMs(); + newEntry.startTime = util.getCurrentTimeMs(); } // Generate a nice title from the color. diff --git a/lib/ping.js b/lib/ping.js index 0e390fa..fdfddda 100644 --- a/lib/ping.js +++ b/lib/ping.js @@ -1,11 +1,11 @@ var mcpe_ping = require('mcpe-ping'); var mcpc_ping = require('mc-ping-updated'); -var profiler = require('./profiler'); +var util = require('./util'); // This is a wrapper function for mc-ping-updated, mainly used to convert the data structure of the result. function pingMinecraftPC(host, port, timeout, callback) { - profiler.track(host); + var startTime = util.getCurrentTimeMs(); // Try catch incase the down stream module is bad at handling exceptions. try { @@ -20,7 +20,8 @@ function pingMinecraftPC(host, port, timeout, callback) { max: res.players.max }, version: res.version.protocol, - latency: profiler.untrack(host) + latency: util.getCurrentTimeMs() - startTime, + favicon: res.favicon }); } }, timeout); @@ -31,7 +32,7 @@ function pingMinecraftPC(host, port, timeout, callback) { // This is a wrapper function for mcpe-ping, mainly used to convert the data structure of the result. function pingMinecraftPE(host, port, timeout, callback) { - profiler.track(host); + var startTime = util.getCurrentTimeMs(); // Try catch incase the down stream module is bad at handling exceptions. try { @@ -42,11 +43,11 @@ function pingMinecraftPE(host, port, timeout, callback) { // Remap our JSON into our custom structure. callback(err, { players: { - online: res.currentPlayers, - max: res.maxPlayers + online: parseInt(res.currentPlayers), + max: parseInt(res.maxPlayers) }, version: res.version, - latency: profiler.untrack(host) + latency: util.getCurrentTimeMs() - startTime }); } }, timeout); diff --git a/lib/profiler.js b/lib/profiler.js deleted file mode 100644 index 45da209..0000000 --- a/lib/profiler.js +++ /dev/null @@ -1,31 +0,0 @@ -var logger = require('./logger'); - -var timestamps = {}; - -function getCurrentTimeMs() { - return (new Date).getTime(); -}; - -exports.track = function(name) { - if (timestamps[name]) { - throw new Error(name + ' is already being profiled!'); - } - - timestamps[name] = getCurrentTimeMs(); -}; - -exports.untrack = function(name) { - if (!timestamps[name]) { - throw new Error(name + ' isn\'t being profiled!'); - } - - var timestamp = getCurrentTimeMs() - timestamps[name]; - - delete timestamps[name]; - - logger.log('debug', name + ' took ' + timestamp + 'ms'); - - return timestamp; -}; - -exports.getCurrentTimeMs = getCurrentTimeMs; \ No newline at end of file diff --git a/lib/util.js b/lib/util.js new file mode 100644 index 0000000..667854d --- /dev/null +++ b/lib/util.js @@ -0,0 +1,3 @@ +exports.getCurrentTimeMs = function() { + return new Date().getTime(); +}; \ No newline at end of file diff --git a/package.json b/package.json index 2caff3b..7488536 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "A Minecraft server tracker that lets you focus on the basics.", "main": "app.js", "dependencies": { - "mc-ping-updated": "0.0.6", + "mc-ping-updated": "0.0.7", "mcpe-ping": "0.0.3", "mime": "^1.3.4", "request": "^2.65.0",