diff --git a/app.js b/app.js index 53f8886..c3bafe9 100644 --- a/app.js +++ b/app.js @@ -10,6 +10,9 @@ var config = require('./config.json'); var networkHistory = []; var connectedClients = 0; +var graphData = []; +var lastGraphPush = []; + function pingAll() { var servers = config.servers; @@ -79,6 +82,27 @@ function pingAll() { if (config.logToDatabase) { db.log(network.ip, util.getCurrentTimeMs(), res ? res.players.online : 0); } + + // Push it to our graphs. + var timeMs = util.getCurrentTimeMs(); + + if (!lastGraphPush[network.ip] || timeMs - lastGraphPush[network.ip] >= 60 * 1000) { + lastGraphPush[network.ip] = timeMs; + + // Don't have too much data! + if (graphData[network.ip].length >= 24 * 60) { + graphData[network.ip].shift(); + } + + graphData[network.ip].push([timeMs, res ? res.players.online : 0]); + + // Send the update. + server.io.sockets.emit('updateHistoryGraph', { + ip: network.ip, + players: (res ? res.players.online : 0), + timestamp: timeMs + }); + } }); })(servers[i]); } @@ -98,6 +122,14 @@ function startMainLoop() { if (config.logToDatabase) { // Setup our database. db.setup(); + + var timestamp = util.getCurrentTimeMs(); + + db.queryPings(24 * 60 * 60 * 1000, function(data) { + graphData = util.convertPingsToGraph(data); + + logger.log('info', 'Queried and parsed ping history in %sms', util.getCurrentTimeMs() - timestamp); + }); } else { logger.warn('Database logging is not enabled. You can enable it by setting "logToDatabase" to true in config.json. This requires sqlite3 to be installed.'); } @@ -130,6 +162,9 @@ server.start(function() { for (var i = 0; i < networkHistoryKeys.length; i++) { client.emit('add', [networkHistory[networkHistoryKeys[i]]]); } + + // Send them the big 24h graph. + client.emit('historyGraph', graphData); }, 1); // Attach our listeners. diff --git a/assets/css/main.css b/assets/css/main.css index ea8706e..627a599 100644 --- a/assets/css/main.css +++ b/assets/css/main.css @@ -177,3 +177,11 @@ h3 { height: 42px; width: 42px; } + +/* The big graph */ +#big-graph { + height: 500px; + width: 800px; + margin: 15px auto 0 auto; + padding-left: 500px; +} \ No newline at end of file diff --git a/assets/html/index.html b/assets/html/index.html index abf0fb3..6a20306 100644 --- a/assets/html/index.html +++ b/assets/html/index.html @@ -34,6 +34,8 @@ +
+
diff --git a/assets/js/site.js b/assets/js/site.js index 344d918..749bf54 100644 --- a/assets/js/site.js +++ b/assets/js/site.js @@ -30,6 +30,36 @@ var smallChartOptions = { ] }; +var bigChartOptions = { + series: { + shadowSize: 0 + }, + xaxis: { + font: { + color: "#E3E3E3" + }, + show: false + }, + yaxis: { + show: true, + tickLength: 10, + tickFormatter: function(value) { + return formatNumber(value); + }, + font: { + color: "#E3E3E3" + }, + labelWidth: -5 + }, + grid: { + hoverable: true, + color: "#696969" + }, + legend: { + show: false + } +}; + var lastMojangServiceUpdate; var graphs = {}; @@ -169,6 +199,38 @@ function safeName(name) { return name.replace(/ /g, ''); } +function handlePlotHover(event, pos, item) { + if (item) { + var text = getTimestamp(item.datapoint[0] / 1000) + '\ +
\ + ' + formatNumber(item.datapoint[1]) + ' Players'; + + if (item.series && item.series.label) { + text = item.series.label + '
' + 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] + }); + } + + return data; +} + $(document).ready(function() { var socket = io.connect({ reconnect: true, @@ -179,6 +241,9 @@ $(document).ready(function() { var mojangServicesUpdater; var sortServersTask; + var historyPlot; + var historyData; + socket.on('connect', function() { $('#tagline-text').text('Loading...'); }); @@ -203,6 +268,27 @@ $(document).ready(function() { $('#quick-jump-container').html(''); }); + socket.on('historyGraph', function(rawData) { + historyData = rawData; + + historyPlot = $.plot('#big-graph', convertGraphData(rawData), bigChartOptions); + + $('#big-graph').bind('plothover', handlePlotHover); + }); + + socket.on('updateHistoryGraph', function(rawData) { + if (historyData[rawData.ip].length > 24 * 60) { + historyData[rawData.ip].shift(); + } + + historyData[rawData.ip].push([rawData.timestamp, rawData.players]); + + historyPlot.setData(convertGraphData(historyData)); + historyPlot.setupGrid(); + + historyPlot.draw(); + }); + socket.on('add', function(servers) { for (var i = 0; i < servers.length; i++) { var history = servers[i]; @@ -265,15 +351,7 @@ $(document).ready(function() { updateServerStatus(lastEntry); - $('#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(); - } - }); + $('#chart_' + safeName(info.name)).bind('plothover', handlePlotHover); } sortServers(); @@ -323,7 +401,7 @@ $(document).ready(function() { sortServersTask = setInterval(function() { sortServers(); - }, 30 * 1000); + }, 10 * 1000); // Our super fancy scrolly thing! $(document).on('click', '.quick-jump-icon', function(e) { diff --git a/config.json b/config.json index 07b3f14..a17715b 100644 --- a/config.json +++ b/config.json @@ -190,8 +190,8 @@ "rates": { "upateMojangStatus": 5000, "mojangStatusTimeout": 3500, - "pingAll": 3000, - "connectTimeout": 2500 + "pingAll": 2000, + "connectTimeout": 1500 }, - "logToDatabase": false + "logToDatabase": true } diff --git a/lib/database.js b/lib/database.js index 90bc86b..08da012 100644 --- a/lib/database.js +++ b/lib/database.js @@ -1,3 +1,5 @@ +var util = require('./util'); + exports.setup = function() { var sqlite = require('sqlite3'); @@ -10,6 +12,21 @@ exports.setup = function() { exports.log = function(ip, timestamp, playerCount) { var insertStatement = db.prepare('INSERT INTO pings (timestamp, ip, playerCount) VALUES (?, ?, ?)'); - insertStatement.run(timestamp, ip, playerCount); + db.serialize(function() { + insertStatement.run(timestamp, ip, playerCount); + }); + + insertStatement.finalize(); + }; + + exports.queryPings = function(duration, callback) { + var currentTime = util.getCurrentTimeMs(); + + db.all("SELECT * FROM pings WHERE timestamp >= ? AND timestamp <= ?", [ + currentTime - duration, + currentTime + ], function(err, data) { + callback(data); + }); }; }; \ No newline at end of file diff --git a/lib/util.js b/lib/util.js index c8aa50a..bab3dfc 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1,3 +1,31 @@ +// This method is a monstrosity. +// Since we loaded ALL pings from the database, we need to filter out the pings so each entry is a minute apart. +// This is done by iterating over the list, since the time between each ping can be completely arbitrary. +function trimUselessPings(data) { + var keys = Object.keys(data); + + for (var i = 0; i < keys.length; i++) { + var listing = data[keys[i]]; + var lastTimestamp = 0; + + var filteredListing = []; + + for (var x = 0; x < listing.length; x++) { + var entry = listing[x]; + + // 0 is the index of the timestamp. + // See the convertPingsToGraph method. + if (entry[0] - lastTimestamp >= 60 * 1000) { + filteredListing.push(entry); + + lastTimestamp = entry[0]; + } + } + + data[keys[i]] = filteredListing; + } +} + exports.getCurrentTimeMs = function() { return new Date().getTime(); }; @@ -8,4 +36,23 @@ exports.setIntervalNoDelay = function(func, delay) { func(); return task; +}; + +exports.convertPingsToGraph = function(sqlData) { + var graphData = {}; + + for (var i = 0; i < sqlData.length; i++) { + var entry = sqlData[i]; + + if (!graphData[entry.ip]) { + graphData[entry.ip] = []; + } + + graphData[entry.ip].push([entry.timestamp, entry.playerCount]); + } + + // Break it into minutes. + trimUselessPings(graphData); + + return graphData; }; \ No newline at end of file