Merge pull request #31 from Cryptkeeper/categories

Categories
This commit is contained in:
Cryptkeeper 2016-02-23 19:33:40 -06:00
commit 790227764a
14 changed files with 628 additions and 358 deletions

@ -3,3 +3,21 @@
- Servers now display their name on hover instead of their IP. - Servers now display their name on hover instead of their IP.
- Graph controls are now saved and loaded automatically. - Graph controls are now saved and loaded automatically.
- Moved server configuration into servers.json from config.json. - Moved server configuration into servers.json from config.json.
**2.1.0** *(Feb 23rd 2016)*
- You can now categorize servers. Add a "category tag" to their entry in ```servers.json```.
- Define the tags in ```config.json```, such as below:
```
"serverCategories": {
"major": "Major Networks",
"midsized": "Midsized Networks",
"small": "Small Networks"
}
```
- If you have no categories, it will create a (hidden) category named "default".
- You can control whether or not categories are visible by default using the "categoriesVisible" tag in ```config.json```.
- If true and there's >1 category, the browser will have an option to hide/show the categories. Otherwise the controls are always hidden.
- New endpoint (```publicConfig.json```) allows the browser to know system details before the socket connection is established.
- New header design to make it less annoying.
- Various bug fixes.

53
app.js

@ -142,22 +142,9 @@ function startServices() {
logger.log('info', '%s connected, total clients: %d', client.request.connection.remoteAddress, connectedClients); logger.log('info', '%s connected, total clients: %d', client.request.connection.remoteAddress, connectedClients);
setTimeout(function() { // We send the boot time (also sent in publicConfig.json) to the frontend to validate they have the same config.
client.emit('setGraphDuration', config.graphDuration); // If so, they'll send back "requestListing" event, otherwise they will pull the new config and retry.
client.emit('bootTime', util.getBootTime());
// Send them our previous data, so they have somewhere to start.
client.emit('updateMojangServices', mojang.toMessage());
// 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 < networkHistoryKeys.length; i++) {
client.emit('add', [networkHistory[networkHistoryKeys[i]]]);
}
}, 1);
// Attach our listeners. // Attach our listeners.
client.on('disconnect', function() { client.on('disconnect', function() {
@ -172,6 +159,40 @@ function startServices() {
client.emit('historyGraph', graphData); client.emit('historyGraph', graphData);
} }
}); });
client.on('requestListing', function() {
// Send them our previous data, so they have somewhere to start.
client.emit('updateMojangServices', mojang.toMessage());
// 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]]]);
}
}
});
}); });
startMainLoop(); startMainLoop();

@ -18,19 +18,34 @@ html, body {
height: 100%; height: 100%;
} }
a {
cursor: pointer;
}
#push { #push {
position: relative; position: relative;
min-height: 100%; min-height: 100%;
} }
/* Header, footer */ /* Header */
#header, #footer { #header, #tagline {
width: 1540px;
margin: 0 auto;
}
#header {
background: #EBEBEB; background: #EBEBEB;
color: #3B3738; color: #3B3738;
padding: 20px 0; padding: 20px 0;
text-align: center; overflow: auto;
width: 1540px; }
margin: 0 auto;
#header .slogan {
font-size: 20px;
}
#header .subslogan {
font-size: 19px;
} }
#header a { #header a {
@ -43,34 +58,46 @@ html, body {
border-bottom: 1px dashed transparent; border-bottom: 1px dashed transparent;
} }
#header > .slogan {
letter-spacing: 2px;
font-size: 20px;
}
#header > .info {
font-size: 17px;
}
#header > h1 { #header > h1 {
font-size: 42px; font-size: 42px;
} }
#header > #column-center {
width: 1480px;
margin: 0 auto;
}
#header > #column-center > .column {
width: 740px;
display: inline-block;
float: left;
}
/* Category controls */
#category-controller {
display: none;
}
/* Footer */
#footer { #footer {
border-top-right-radius: 2px; width: 1540px;
border-top-left-radius: 2px;
font-size: 16px; font-size: 16px;
text-transform: uppercase; text-transform: uppercase;
background: #EBEBEB;
color: #3B3738;
padding: 15px 0;
margin: 0 auto;
text-align: center;
border-top-right-radius: 2px;
border-top-left-radius: 2px;
} }
/* Tagline */ /* Tagline */
#tagline { #tagline {
padding: 10px 0; padding: 10px 0;
text-align: center; text-align: center;
width: 1540px;
margin: 0 auto;
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px; border-bottom-right-radius: 2px;
border-bottom-left-radius: 2px;
} }
/* Colors used by the Mojang service's status bar */ /* Colors used by the Mojang service's status bar */
@ -93,7 +120,7 @@ html, body {
} }
/* Server listing */ /* Server listing */
#server-container { .server-container {
width: 1540px; width: 1540px;
margin: 10px auto; margin: 10px auto;
overflow: auto; overflow: auto;
@ -130,6 +157,12 @@ html, body {
margin-bottom: 2px; margin-bottom: 2px;
} }
.category-header {
margin-bottom: 10px;
margin-left: 15px;
text-align: left;
}
/* Charts */ /* Charts */
.chart { .chart {
height: 100px; height: 100px;
@ -169,30 +202,6 @@ h3 {
text-align: center; text-align: center;
} }
/* Icon Quick jump */
#quick-jump-container {
position: fixed;
top: 10px;
left: 10px;
overflow: auto;
display: none;
}
.quick-jump-icon {
height: 24px;
width: 24px;
display: block;
margin-bottom: 2px;
border-radius: 2px;
transition: all 0.1s;
}
.quick-jump-icon:hover {
cursor: pointer;
height: 42px;
width: 42px;
}
/* The big graph */ /* The big graph */
#big-graph, #big-graph-controls, #big-graph-checkboxes { #big-graph, #big-graph-controls, #big-graph-checkboxes {
width: 1540px; width: 1540px;
@ -221,7 +230,6 @@ h3 {
#big-graph-controls a:hover { #big-graph-controls a:hover {
border-bottom: 1px dashed transparent; border-bottom: 1px dashed transparent;
cursor: pointer;
} }
/* Basic elements */ /* Basic elements */
@ -238,5 +246,4 @@ h3 {
.button:hover { .button:hover {
background: #ecf0f1; background: #ecf0f1;
color: #3498db; color: #3498db;
cursor: pointer;
} }

@ -20,13 +20,32 @@
<div id="header"> <div id="header">
<div id="column-center">
<div class="column">
<h1 class="text-uppercase">Minetrack</h1> <h1 class="text-uppercase">Minetrack</h1>
<p class="slogan text-uppercase">All your favorite Minecraft servers, right now.</p> <p class="slogan text-uppercase">All your favorite Minecraft servers, right now.</p>
<p class="info text-uppercase">Made with <span style="color: #e74c3c;">&#9829;</span> by <a href="http://cryptkpr.me">Cryptkeeper</a> &middot; Open source'd on <a href="https://github.com/Cryptkeeper/Minetrack">Github</a></p> <p class="subslogan text-uppercase">Watching <span id="stat_totalPlayers">0</span> players on <span id="stat_networks">0</span> networks.</p>
</div>
<div class="column">
<p class="text-uppercase">Source code available on <a href="https://github.com/Cryptkeeper/Minetrack">Github</a></p>
<p class="text-uppercase">Made with <span style="color: #e74c3c;">&#9829;</span> by <a href="http://cryptkpr.me">Cryptkeeper</a></p>
<br /> <br />
<p class="info text-uppercase">Watching <span id="stat_totalPlayers">0</span> players on <span id="stat_networks">0</span> networks.</p> <div id="category-controller">
<a class="text-uppercase" onclick="setCategoriesVisible(true);">Show Categories</a> // <a class="text-uppercase" onclick="setCategoriesVisible(false);">Hide Categories</a>
</div>
</div>
</div>
</div> </div>
@ -63,7 +82,7 @@
</div> </div>
<div id="server-container" class="container"></div> <div id="server-container-list"></div>
</div> </div>
@ -73,8 +92,6 @@
</div> </div>
<div id="quick-jump-container"></div>
<!-- External JS assets --> <!-- 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/jquery/2.1.4/jquery.min.js"></script>
@ -84,8 +101,11 @@
<!-- Internal JS assets --> <!-- Internal JS assets -->
<script src="js/util.js"></script> <script src="js/util.js"></script>
<script src="js/graph.js"></script>
<script src="js/site.js"></script> <script src="js/site.js"></script>
<script src="publicConfig.json"></script>
<script> <script>
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){

Binary file not shown.

Before

Width:  |  Height:  |  Size: 585 B

After

Width:  |  Height:  |  Size: 20 KiB

BIN
assets/images/logo_2014.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

130
assets/js/graph.js Normal file

@ -0,0 +1,130 @@
// 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"
]
};
// Used by the one chart to rule them all
var bigChartOptions = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: "#E3E3E3"
},
show: false
},
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
}
};
function toggleControlsDrawer() {
var div = $('#big-graph-controls-drawer');
div.css('display', div.css('display') !== 'none' ? 'none' : 'block');
}
function saveGraphControls(displayedServers) {
if (typeof(localStorage)) {
var json = JSON.stringify(displayedServers);
localStorage.setItem('displayedServers', json);
}
}
function loadGraphControls() {
if (typeof(localStorage)) {
var item = localStorage.getItem('displayedServers');
if (item) {
return JSON.parse(item);
}
}
}
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;
}
renderTooltip(item.pageX + 5, item.pageY + 5, text);
} else {
hideTooltip();
}
}
// 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: stringToColor(keys[i])
});
}
return data;
}

@ -1,126 +1,14 @@
var smallChartOptions = { var graphs = [];
series: { var lastPlayerEntries = [];
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"
]
};
var bigChartOptions = {
series: {
shadowSize: 0
},
xaxis: {
font: {
color: "#E3E3E3"
},
show: false
},
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
}
};
var lastMojangServiceUpdate;
var graphs = {};
var lastPlayerEntries = {};
var historyPlot; var historyPlot;
var displayedGraphData; var displayedGraphData;
var hiddenGraphData = []; var hiddenGraphData = [];
// Generate (and set) the HTML that displays Mojang status. var isConnected = false;
function updateMojangServices() {
if (!lastMojangServiceUpdate) {
return;
}
var keys = Object.keys(lastMojangServiceUpdate); var mojangServicesUpdater;
var newStatus = 'Mojang Services: '; var sortServersTask;
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 findErrorMessage(error) {
if (error.description) {
return error.description;
} else if (error.errno) {
return error.errno;
}
}
function updateServerStatus(lastEntry) { function updateServerStatus(lastEntry) {
var info = lastEntry.info; var info = lastEntry.info;
@ -171,19 +59,50 @@ function updateServerStatus(lastEntry) {
} }
function sortServers() { function sortServers() {
var keys = Object.keys(lastPlayerEntries); if (categoriesVisible) {
var nameList = []; var byCategories = getServersByCategory();
var categories = Object.keys(byCategories);
for (var i = 0; i < categories.length; i++) {
var relevantPlayers = [];
for (var x = 0; x < byCategories[categories[i]].length; x++) {
var server = byCategories[categories[i]][x];
relevantPlayers[server.name] = lastPlayerEntries[server.name];
}
var keys = Object.keys(relevantPlayers);
keys.sort(function(a, b) { keys.sort(function(a, b) {
return lastPlayerEntries[b] - lastPlayerEntries[a]; return relevantPlayers[b] - relevantPlayers[a];
}); });
keys.reverse(); for (var x = 0; x < keys.length; x++) {
$('#' + safeName(keys[x])).appendTo('#server-container-' + categories[i]);
$('#ranking_' + safeName(keys[x])).text('#' + (x + 1));
}
}
} else {
var serverNames = [];
var keys = Object.keys(lastPlayerEntries);
for (var i = 0; i < keys.length; i++) { for (var i = 0; i < keys.length; i++) {
$('#' + safeName(keys[i])).prependTo('#server-container'); serverNames.push(keys[i]);
}
$('#ranking_' + safeName(keys[i])).text('#' + (keys.length - i)); serverNames.sort(function(a, b) {
return (lastPlayerEntries[b] || 0) - (lastPlayerEntries[a] || 0);
});
for (var i = 0; i < serverNames.length; i++) {
$('#' + safeName(serverNames[i])).appendTo('#server-container-all');
$('#ranking_' + safeName(serverNames[i])).text('#' + (i + 1));
}
} }
} }
@ -223,10 +142,35 @@ function setAllGraphVisibility(visible) {
} }
} }
function toggleControlsDrawer() { function validateBootTime(bootTime, socket) {
var div = $('#big-graph-controls-drawer'); $('#tagline-text').text('Validating...');
div.css('display', div.css('display') !== 'none' ? 'none' : 'block'); 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 {
$('#tagline').attr('class', 'status-offline');
$('#tagline-text').text('Failed to update! Refresh?');
}
});
}
} }
$(document).ready(function() { $(document).ready(function() {
@ -236,27 +180,13 @@ $(document).ready(function() {
reconnectionAttempts: 10 reconnectionAttempts: 10
}); });
var mojangServicesUpdater; socket.on('bootTime', function(bootTime) {
var sortServersTask; validateBootTime(bootTime, socket);
var graphDuration;
socket.on('connect', function() {
$('#tagline-text').text('Loading...');
if (!isMobileBrowser()) {
socket.emit('requestHistoryGraph');
}
}); });
socket.on('disconnect', function() { socket.on('disconnect', function() {
if (mojangServicesUpdater) { if (mojangServicesUpdater) clearInterval(mojangServicesUpdater);
clearInterval(mojangServicesUpdater); if (sortServersTask) clearInterval(sortServersTask);
}
if (sortServersTask) {
clearInterval(sortServersTask);
}
$('#tagline').attr('class', 'status-offline'); $('#tagline').attr('class', 'status-offline');
$('#tagline-text').text('Disconnected! Refresh?'); $('#tagline-text').text('Disconnected! Refresh?');
@ -264,16 +194,20 @@ $(document).ready(function() {
lastPlayerEntries = {}; lastPlayerEntries = {};
graphs = {}; graphs = {};
$('#server-container').html(''); $('#server-container-list').html('');
$('#quick-jump-container').html('');
createdCategories = false;
$('#big-graph').html(''); $('#big-graph').html('');
$('#big-graph-checkboxes').html(''); $('#big-graph-checkboxes').html('');
$('#big-graph-controls').css('display', 'none'); $('#big-graph-controls').css('display', 'none');
});
socket.on('setGraphDuration', function(value) { $('#category-controller').css('display', 'none');
graphDuration = value;
$("#stat_totalPlayers").text(0);
$("#stat_networks").text(0);
isConnected = false;
}); });
socket.on('historyGraph', function(rawData) { socket.on('historyGraph', function(rawData) {
@ -335,14 +269,14 @@ $(document).ready(function() {
socket.on('updateHistoryGraph', function(rawData) { socket.on('updateHistoryGraph', function(rawData) {
// Prevent race conditions. // Prevent race conditions.
if (!graphDuration || !displayedGraphData || !hiddenGraphData) { if (!displayedGraphData || !hiddenGraphData) {
return; return;
} }
// If it's not in our display group, use the hidden group instead. // If it's not in our display group, use the hidden group instead.
var targetGraphData = displayedGraphData[rawData.name] ? displayedGraphData : hiddenGraphData; var targetGraphData = displayedGraphData[rawData.name] ? displayedGraphData : hiddenGraphData;
trimOldPings(targetGraphData, graphDuration); trimOldPings(targetGraphData, publicConfig.graphDuration);
targetGraphData[rawData.name].push([rawData.timestamp, rawData.players]); targetGraphData[rawData.name].push([rawData.timestamp, rawData.players]);
@ -356,6 +290,10 @@ $(document).ready(function() {
}); });
socket.on('add', function(servers) { socket.on('add', function(servers) {
if (Object.keys(publicConfig.categories).length > 1) {
$('#category-controller').css('display', 'block');
}
for (var i = 0; i < servers.length; i++) { for (var i = 0; i < servers.length; i++) {
var history = servers[i]; var history = servers[i];
var listing = []; var listing = [];
@ -396,7 +334,7 @@ $(document).ready(function() {
<div class="column" style="float: right;">\ <div class="column" style="float: right;">\
<div class="chart" id="chart_' + safeName(info.name) + '"></div>\ <div class="chart" id="chart_' + safeName(info.name) + '"></div>\
</div>' </div>'
}).appendTo("#server-container"); }).appendTo("#server-container-" + getServerByIp(info.ip).category);
var favicon = MISSING_FAVICON_BASE64; var favicon = MISSING_FAVICON_BASE64;
@ -406,8 +344,6 @@ $(document).ready(function() {
$('#favicon_' + safeName(info.name)).attr('src', favicon); $('#favicon_' + safeName(info.name)).attr('src', favicon);
$('#quick-jump-container').append('<img id="quick-jump-' + safeName(info.name) + '" data-target-network="' + safeName(info.name) + '" title="' + info.name + '" alt="' + info.name + '" class="quick-jump-icon" src="' + favicon + '">');
graphs[lastEntry.info.name] = { graphs[lastEntry.info.name] = {
listing: listing, listing: listing,
plot: $.plot('#chart_' + safeName(info.name), [listing], smallChartOptions) plot: $.plot('#chart_' + safeName(info.name), [listing], smallChartOptions)
@ -430,7 +366,6 @@ $(document).ready(function() {
// We have a new favicon, update the old one. // We have a new favicon, update the old one.
if (update.result && update.result.favicon) { if (update.result && update.result.favicon) {
$('#favicon_' + safeName(update.info.name)).attr('src', update.result.favicon); $('#favicon_' + safeName(update.info.name)).attr('src', update.result.favicon);
$('#quick-jump-' + safeName(update.info.name)).attr('src', update.result.favicon);
} }
var graph = graphs[update.info.name]; var graph = graphs[update.info.name];
@ -452,29 +387,9 @@ $(document).ready(function() {
}); });
socket.on('updateMojangServices', function(data) { socket.on('updateMojangServices', function(data) {
// Store the update and force an update. if (isConnected) {
lastMojangServiceUpdate = data; updateMojangServices(data);
}
updateMojangServices();
});
// Start any special updating tasks.
mojangServicesUpdater = setInterval(function() {
updateMojangServices();
}, 1000);
sortServersTask = setInterval(function() {
sortServers();
}, 10 * 1000);
// Our super fancy scrolly thing!
$(document).on('click', '.quick-jump-icon', function(e) {
var serverName = $(this).attr('data-target-network');
var target = $('#server-' + serverName);
$('html, body').animate({
scrollTop: target.offset().top
}, 100);
}); });
$(document).on('click', '.graph-control', function(e) { $(document).on('click', '.graph-control', function(e) {

@ -2,27 +2,129 @@ var MISSING_FAVICON_BASE64 = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAA
var tooltip = $('#tooltip'); var tooltip = $('#tooltip');
function saveGraphControls(displayedServers) { var lastMojangServiceUpdate;
if (typeof(localStorage) !== undefined) { var publicConfig;
var json = JSON.stringify(displayedServers);
localStorage.setItem('displayedServers', json); var createdCategories = false;
var categoriesVisible;
function setPublicConfig(json) {
publicConfig = json;
$('#server-container-list').html('');
createdCategories = false;
createCategories();
setCategoriesVisible(publicConfig.categoriesVisible);
}
function setCategoriesVisible(newCategoriesVisible) {
categoriesVisible = newCategoriesVisible;
$('.category-header').css('display', (categoriesVisible ? 'block' : 'none'));
$('.server-container').css('margin', (categoriesVisible ? '10px auto' : '0 auto'));
sortServers();
}
function createCategories() {
if (!createdCategories) {
createdCategories = true;
var keys = Object.keys(publicConfig.categories);
for (var i = 0; i < keys.length; i++) {
var title = publicConfig.categories[keys[i]];
$('#server-container-list').append('<div id="server-container-' + keys[i] + '" class="container server-container"><h3 class="category-header">' + title + '</h3></div>');
}
$('#server-container-list').append('<div id="server-container-all" class="container server-container"></div>');
} }
} }
function loadGraphControls() { function getServersByCategory() {
if (typeof(localStorage) !== undefined) { var byCategory = {};
var item = localStorage.getItem('displayedServers');
if (item) { for (var i = 0; i < publicConfig.servers.length; i++) {
return JSON.parse(item); var entry = publicConfig.servers[i];
if (!byCategory[entry.category]) {
byCategory[entry.category] = [];
}
byCategory[entry.category].push(entry);
}
return byCategory;
}
function getServerByIp(ip) {
for (var i = 0; i < publicConfig.servers.length; i++) {
var entry = publicConfig.servers[i];
if (entry.ip === ip) {
return entry;
} }
} }
} }
function resetGraphControls() { // Generate (and set) the HTML that displays Mojang status.
if (typeof(localStorage) !== undefined) { // If nothing is passed, re-render the last update.
localStorage.removeItem('displayedServers'); // If something is passed, update and then re-render.
function updateMojangServices(currentUpdate) {
if (currentUpdate) {
lastMojangServiceUpdate = currentUpdate;
}
if (!lastMojangServiceUpdate) {
return;
}
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 findErrorMessage(error) {
if (error.description) {
return error.description;
} else if (error.errno) {
return error.errno;
} }
} }
@ -84,39 +186,6 @@ function trimOldPings(data, graphDuration) {
} }
} }
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;
}
renderTooltip(item.pageX + 5, item.pageY + 5, text);
} else {
hideTooltip();
}
}
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: stringToColor(keys[i])
});
}
return data;
}
function stringToColor(base) { function stringToColor(base) {
var hash; var hash;

@ -4,6 +4,7 @@
"/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", "/js/util.js": "assets/js/util.js",
"/js/graph.js": "assets/js/graph.js",
"/css/main.css": "assets/css/main.css", "/css/main.css": "assets/css/main.css",
"/favicons/hypixelpe.png": "assets/images/favicons/hypixelpe.png", "/favicons/hypixelpe.png": "assets/images/favicons/hypixelpe.png",
"/favicons/mineplex.png": "assets/images/favicons/mineplex.png", "/favicons/mineplex.png": "assets/images/favicons/mineplex.png",
@ -15,7 +16,7 @@
"Mobcrush Land": "/favicons/mobcrush.png" "Mobcrush Land": "/favicons/mobcrush.png"
}, },
"site": { "site": {
"port": 80, "port": 8080,
"ip": "0.0.0.0" "ip": "0.0.0.0"
}, },
"rates": { "rates": {
@ -25,5 +26,11 @@
"connectTimeout": 2500 "connectTimeout": 2500
}, },
"logToDatabase": false, "logToDatabase": false,
"graphDuration": 86400000 "graphDuration": 86400000,
"serverCategories": {
"major": "Major Networks",
"midsized": "Midsized Networks",
"small": "Small Networks"
},
"categoriesVisible": true
} }

@ -4,9 +4,11 @@ var url = require('url');
var mime = require('mime'); var mime = require('mime');
var io = require('socket.io'); var io = require('socket.io');
var util = require('./util');
var logger = require('./logger'); var logger = require('./logger');
var config = require('../config.json'); var config = require('../config.json');
var servers = require('../servers.json');
var urlMapping = []; var urlMapping = [];
@ -34,6 +36,43 @@ function handleRequest(req, res) {
message: 'API deprecated.' message: 'API deprecated.'
})); }));
res.end();
} else if (requestUrl === '/publicConfig.json') {
res.setHeader('Content-Type', 'application/javascript');
var categories = config.serverCategories;
// Legacy support for people without categories configured.
if (!categories) {
categories = {
'default': 'All Networks'
};
}
for (var i = 0; i < servers.length; i++) {
var entry = servers[i];
if (!entry.category) {
entry.category = 'default';
logger.warn('%s has no category, defaulting!', entry.name);
} else if (!categories[entry.category]) {
logger.warn('%s has an unknown category (%s), defaulting!', entry.name, entry.category);
entry.category = 'default';
}
}
var publicConfig = {
categories: categories,
graphDuration: config.graphDuration,
servers: servers,
bootTime: util.getBootTime(),
categoriesVisible: config.categoriesVisible || false
};
res.write('setPublicConfig(' + JSON.stringify(publicConfig) + ');');
res.end(); res.end();
} else if (requestUrl in urlMapping) { } else if (requestUrl in urlMapping) {
var file = urlMapping[requestUrl]; var file = urlMapping[requestUrl];

@ -3,7 +3,9 @@ var logger = require('./logger');
var config = require('../config.json'); var config = require('../config.json');
var servers = require('../servers.json'); var servers = require('../servers.json');
var serverNameLookup = {}; var serverNameLookup = [];
var bootTime;
// Finds a server in servers.json with a matching IP. // Finds a server in servers.json with a matching IP.
// If it finds one, it caches the result for faster future lookups. // If it finds one, it caches the result for faster future lookups.
@ -137,3 +139,13 @@ exports.convertPingsToGraph = function(sqlData) {
return graphData; return graphData;
}; };
exports.getBootTime = function() {
if (!bootTime) {
bootTime = exports.getCurrentTimeMs();
logger.info('Selected %d as boot time.', bootTime);
}
return bootTime;
};

@ -1,6 +1,6 @@
{ {
"name": "minetrack", "name": "minetrack",
"version": "2.0.0", "version": "2.1.0",
"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": {

@ -2,161 +2,193 @@
{ {
"name": "Hypixel", "name": "Hypixel",
"ip": "mc.hypixel.net", "ip": "mc.hypixel.net",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "HiveMC", "name": "HiveMC",
"ip": "play.hivemc.com", "ip": "play.hivemc.com",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "CubeCraft", "name": "CubeCraft",
"ip": "play.cubecraft.net", "ip": "play.cubecraft.net",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "Overcast", "name": "Overcast",
"ip": "us.oc.tc", "ip": "us.oc.tc",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "Shotbow", "name": "Shotbow",
"ip": "us.shotbow.net", "ip": "us.shotbow.net",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Badlion", "name": "Badlion",
"ip": "na.badlion.net", "ip": "na.badlion.net",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "MCGamer", "name": "MCGamer",
"ip": "play.mcgamer.net", "ip": "play.mcgamer.net",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Olimpocraft", "name": "Olimpocraft",
"ip": "olimpo.me", "ip": "olimpo.me",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Minecade", "name": "Minecade",
"ip": "mineca.de", "ip": "mineca.de",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "The Nexus", "name": "The Nexus",
"ip": "hub.thenexusmc.com", "ip": "hub.thenexusmc.com",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Kohi", "name": "Kohi",
"ip": "kohi.us", "ip": "kohi.us",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "Wynncraft", "name": "Wynncraft",
"ip": "play.wynncraft.com", "ip": "play.wynncraft.com",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "Mineplex", "name": "Mineplex",
"ip": "us.mineplex.com", "ip": "us.mineplex.com",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "Hypixel PE", "name": "Hypixel PE",
"ip": "pe.hypixel.net", "ip": "pe.hypixel.net",
"type": "PE" "type": "PE",
"category": "major"
}, },
{ {
"name": "Cosmic PVP", "name": "Cosmic PVP",
"ip": "cosmicpvp.me", "ip": "cosmicpvp.me",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "PartyZone", "name": "PartyZone",
"ip": "play.mcpz.net", "ip": "play.mcpz.net",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "Brawl", "name": "Brawl",
"ip": "brawl.com", "ip": "brawl.com",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "SaicoPvP", "name": "SaicoPvP",
"ip": "saicopvp.com", "ip": "saicopvp.com",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "GotPvP", "name": "GotPvP",
"ip": "play.gotpvp.com", "ip": "play.gotpvp.com",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Rewinside", "name": "Rewinside",
"ip": "mc.rewinside.tv", "ip": "mc.rewinside.tv",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Woodycraft", "name": "Woodycraft",
"ip": "woodycraft.net", "ip": "woodycraft.net",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Battlemine", "name": "Battlemine",
"ip": "mc.battlemine.net", "ip": "mc.battlemine.net",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "Arkham", "name": "Arkham",
"ip": "mc.arkhamnetwork.org", "ip": "mc.arkhamnetwork.org",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "MCMagic", "name": "MCMagic",
"ip": "mcmagic.us", "ip": "mcmagic.us",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "Island Clash", "name": "Island Clash",
"ip": "play.islandclash.com", "ip": "play.islandclash.com",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "Playminity", "name": "Playminity",
"ip": "mc.playminity.com", "ip": "mc.playminity.com",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Mobcrush Land", "name": "Mobcrush Land",
"ip": "play.mobcrush.com", "ip": "play.mobcrush.com",
"type": "PE" "type": "PE",
"category": "small"
}, },
{ {
"name": "Epicube", "name": "Epicube",
"ip": "play.epicube.fr", "ip": "play.epicube.fr",
"type": "PC" "type": "PC",
"category": "major"
}, },
{ {
"name": "BeanBlockz", "name": "BeanBlockz",
"ip": "mc.beanblockz.com", "ip": "mc.beanblockz.com",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "GuildCraft", "name": "GuildCraft",
"ip": "play.guildcraft.org", "ip": "play.guildcraft.org",
"type": "PC" "type": "PC",
"category": "midsized"
}, },
{ {
"name": "Empire MC", "name": "Empire MC",
"ip": "play.emc.gs", "ip": "play.emc.gs",
"type": "PC" "type": "PC",
"category": "small"
}, },
{ {
"name": "Timolia", "name": "Timolia",
"ip": "play.timolia.de", "ip": "play.timolia.de",
"type": "PC" "type": "PC",
"category": "midsized"
} }
] ]