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",