Begin work on frontend!

This commit is contained in:
Cryptkeeper 2015-11-08 18:34:17 -06:00
parent e213561436
commit d64252d35d
11 changed files with 505 additions and 57 deletions

32
app.js

@ -2,6 +2,7 @@ var server = require('./lib/server');
var ping = require('./lib/ping'); var ping = require('./lib/ping');
var logger = require('./lib/logger'); var logger = require('./lib/logger');
var mojang = require('./lib/mojang_services'); var mojang = require('./lib/mojang_services');
var util = require('./lib/util');
var config = require('./config.json'); var config = require('./config.json');
@ -20,7 +21,14 @@ function pingAll() {
logger.log('error', 'Failed to ping ' + network.ip + ': ' + JSON.stringify(err)); 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. // Log our response.
if (!networkHistory[network.ip]) { if (!networkHistory[network.ip]) {
@ -29,18 +37,25 @@ function pingAll() {
var _networkHistory = networkHistory[network.ip]; 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++) { for (var i = 0; i < _networkHistory.length; i++) {
delete _networkHistory[i].favicon; delete _networkHistory[i].info;
} }
_networkHistory.push({ _networkHistory.push({
error: err, 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. // 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(); _networkHistory.shift();
} }
}); });
@ -68,6 +83,13 @@ function startMainLoop() {
server.start(function() { server.start(function() {
// Track how many people are currently connected. // Track how many people are currently connected.
server.io.on('connect', function(client) { 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! // We're good to connect them!
connectedClients += 1; connectedClients += 1;

@ -1,4 +1,124 @@
@import url(https://fonts.googleapis.com/css?family=Open+Sans:700,300,400);
* { * {
margin: 0; margin: 0;
padding: 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;
}

@ -12,9 +12,34 @@
<body> <body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.7/socket.io.min.js"></script> <div id="tooltip"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<div id="header">
<h1 class="text-uppercase">Minetrack</h1>
<p class="text-uppercase">All your favorite Minecraft servers, right now.</p>
<p class="text-uppercase">Made by <a href="http://cryptkpr.me">Cryptkeeper</a> | Source code on <a href="https://github.com/Cryptkeeper/Minetrack">Github</a></p>
</div>
<div id="tagline" class="status-connecting">
<span id="tagline-text" class="text-uppercase">Connecting...</span>
</div>
<div id="server-container" class="container"></div>
<!-- External JS assets -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.3.7/socket.io.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/flot/0.8.3/jquery.flot.min.js"></script>
<!-- Internal JS assets -->
<script src="js/util.js"></script>
<script src="js/site.js"></script> <script src="js/site.js"></script>
</body> </body>

@ -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 += '<span class="color-gray"> (';
var playerDifference = lastPlayerEntries[info.name] - result.players.online;
if (playerDifference >= 0) {
newStatus += '+';
}
newStatus += playerDifference + ')</span>';
}
if (lastLatencyEntries[info.name]) {
newStatus += '<br />';
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() { $(document).ready(function() {
var socket = io.connect(); var socket = io.connect();
var mojangServicesUpdater;
socket.on('connect', function() { 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) { 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;
$('<div/>', {
id: safeName(info.name),
class: 'server',
html: '<div class="column" style="width: 80px;">\
<img style="padding-top: 5px;" id="favicon_' + safeName(info.name) + '">\
</div>\
<div class="column" style="width: 280px;"><h3>' + info.name + '</h3>\
<span class="color-gray">' + info.ip + '</span>\
<br />\
<span id="status_' + safeName(info.name) + '">Waiting</span>\
</div>\
<div class="column" style="float: right;">\
<div class="chart" id="chart_' + safeName(info.name) + '"></div>\
</div>'
}).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) + '\
<br />\
' + 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);
}); });

49
assets/js/util.js Normal file

@ -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;
}

@ -6,8 +6,13 @@
"type": "PC" "type": "PC"
}, },
{ {
"name": "Mineplex", "name": "HiveMC",
"ip": "us.mineplex.com", "ip": "play.hivemc.com",
"type": "PC"
},
{
"name": "CCGN",
"ip": "play.cubecraftgames.net",
"type": "PC" "type": "PC"
}, },
{ {
@ -16,15 +21,56 @@
"type": "PC" "type": "PC"
}, },
{ {
"name": "Hypixel PE", "name": "Shotbow",
"ip": "pe.hypixel.net", "ip": "us.shotbow.net",
"type": "PE" "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": { "routes": {
"/": "assets/html/index.html", "/": "assets/html/index.html",
"/images/compass.png": "assets/images/compass.png", "/images/compass.png": "assets/images/compass.png",
"/js/site.js": "assets/js/site.js", "/js/site.js": "assets/js/site.js",
"/js/util.js": "assets/js/util.js",
"/css/main.css": "assets/css/main.css" "/css/main.css": "assets/css/main.css"
}, },
"site": { "site": {
@ -35,6 +81,6 @@
"upateMojangStatus": 5000, "upateMojangStatus": 5000,
"mojangStatusTimeout": 3500, "mojangStatusTimeout": 3500,
"pingAll": 2500, "pingAll": 2500,
"connectTimeout": 2500 "connectTimeout": 2000
} }
} }

@ -1,10 +1,9 @@
var request = require('request'); var request = require('request');
var logger = require('./logger'); var logger = require('./logger');
var profiler = require('./profiler'); var util = require('./util');
var serviceNameLookup = { var serviceNameLookup = {
'minecraft.net': 'Website',
'sessionserver.mojang.com': 'Sessions', 'sessionserver.mojang.com': 'Sessions',
'authserver.mojang.com': 'Auth', 'authserver.mojang.com': 'Auth',
'textures.minecraft.net': 'Skins', 'textures.minecraft.net': 'Skins',
@ -25,7 +24,7 @@ function updateService(name, status) {
// If it's an outage, track when it started. // If it's an outage, track when it started.
if (status === 'yellow'|| status === 'red') { if (status === 'yellow'|| status === 'red') {
newEntry.startTime = profiler.getCurrentTimeMs(); newEntry.startTime = util.getCurrentTimeMs();
} }
// Generate a nice title from the color. // Generate a nice title from the color.

@ -1,11 +1,11 @@
var mcpe_ping = require('mcpe-ping'); var mcpe_ping = require('mcpe-ping');
var mcpc_ping = require('mc-ping-updated'); 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. // 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) { 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 catch incase the down stream module is bad at handling exceptions.
try { try {
@ -20,7 +20,8 @@ function pingMinecraftPC(host, port, timeout, callback) {
max: res.players.max max: res.players.max
}, },
version: res.version.protocol, version: res.version.protocol,
latency: profiler.untrack(host) latency: util.getCurrentTimeMs() - startTime,
favicon: res.favicon
}); });
} }
}, timeout); }, 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. // This is a wrapper function for mcpe-ping, mainly used to convert the data structure of the result.
function pingMinecraftPE(host, port, timeout, callback) { 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 catch incase the down stream module is bad at handling exceptions.
try { try {
@ -42,11 +43,11 @@ function pingMinecraftPE(host, port, timeout, callback) {
// Remap our JSON into our custom structure. // Remap our JSON into our custom structure.
callback(err, { callback(err, {
players: { players: {
online: res.currentPlayers, online: parseInt(res.currentPlayers),
max: res.maxPlayers max: parseInt(res.maxPlayers)
}, },
version: res.version, version: res.version,
latency: profiler.untrack(host) latency: util.getCurrentTimeMs() - startTime
}); });
} }
}, timeout); }, timeout);

@ -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;

3
lib/util.js Normal file

@ -0,0 +1,3 @@
exports.getCurrentTimeMs = function() {
return new Date().getTime();
};

@ -4,7 +4,7 @@
"description": "A Minecraft server tracker that lets you focus on the basics.", "description": "A Minecraft server tracker that lets you focus on the basics.",
"main": "app.js", "main": "app.js",
"dependencies": { "dependencies": {
"mc-ping-updated": "0.0.6", "mc-ping-updated": "0.0.7",
"mcpe-ping": "0.0.3", "mcpe-ping": "0.0.3",
"mime": "^1.3.4", "mime": "^1.3.4",
"request": "^2.65.0", "request": "^2.65.0",