diff --git a/README.md b/README.md index de00abb..baa047b 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,27 @@ Explanation for each field: "website": { "enabled": true, "port": 80, - "liveStats": true + /* Used for displaying stratum connection data on the Getting Started page. */ + "stratumHost": "cryppit.com", + "stats": { + /* Gather stats to broadcast to page viewers and store in redis for historical stats + every this many seconds. */ + "updateInterval": 15, + /* How many seconds to hold onto historical stats. Currently set to 24 hours. */ + "historicalRetention": 43200, + /* How many seconds worth of shares should be gathered to generate hashrate. */ + "hashrateWindow": 300, + /* Redis instance of where to store historical stats. */ + "redis": { + "host": "localhost", + "port": 6379 + } + }, + /* Not done yet. */ + "adminCenter": { + "enabled": true, + "password": "password" + } }, /* With this enabled, the master process listen on the configured port for messages from the @@ -463,7 +483,7 @@ When updating NOMP to the latest code its important to not only `git pull` the l * Inside your NOMP directory (where the init.js script is) do `git pull` to get the latest NOMP code. * Remove the dependenices by deleting the `node_modules` directory with `rm -r node_modules`. * Run `npm update` to force updating/reinstalling of the dependencies. -* Compare your `config.json` and `pool_configs/coin.json` configurations to the lateset example ones in this repo or the ones in the setup instructions where each config field is explained. You may need to modify or add any new changes. +* Compare your `config.json` and `pool_configs/coin.json` configurations to the latest example ones in this repo or the ones in the setup instructions where each config field is explained. You may need to modify or add any new changes. Donations --------- @@ -481,7 +501,7 @@ To support development of this project feel free to donate :) Credits ------- * [Jerry Brady / mintyfresh68](https://github.com/bluecircle) - got coin-switching fully working and developed proxy-per-algo feature -* [Tony Dobbs](http://anthonydobbs.com) - graphical help with logo and front-end design +* [Tony Dobbs](http://anthonydobbs.com) - designs for front-end and created the NOMP logo * [vekexasia](//github.com/vekexasia) - co-developer & great tester * [TheSeven](//github.com/TheSeven) - answering an absurd amount of my questions and being a very helpful gentleman * [UdjinM6](//github.com/UdjinM6) - helped implement fee withdrawal in payment processing diff --git a/config_example.json b/config_example.json index af3774b..8759fcf 100644 --- a/config_example.json +++ b/config_example.json @@ -8,10 +8,21 @@ "website": { "enabled": true, - "siteTitle": "Cryppit", "port": 80, - "statUpdateInterval": 1.5, - "hashrateWindow": 300 + "stratumHost": "cryppit.com", + "stats": { + "updateInterval": 15, + "historicalRetention": 43200, + "hashrateWindow": 300, + "redis": { + "host": "localhost", + "port": 6379 + } + }, + "adminCenter": { + "enabled": true, + "password": "password" + } }, "blockNotifyListener": { diff --git a/libs/api.js b/libs/api.js index 092d9b8..53ebd70 100644 --- a/libs/api.js +++ b/libs/api.js @@ -17,6 +17,10 @@ module.exports = function(logger, portalConfig, poolConfigs){ case 'stats': res.end(portalStats.statsString); return; + case 'pool_stats': + res.writeHead(200, {'content-encoding': 'gzip'}); + res.end(portalStats.statPoolHistoryBuffer); + return; case 'live_stats': res.writeHead(200, { 'Content-Type': 'text/event-stream', @@ -36,4 +40,16 @@ module.exports = function(logger, portalConfig, poolConfigs){ } }; + + this.handleAdminApiRequest = function(req, res, next){ + switch(req.params.method){ + case 'pools': { + res.end(JSON.stringify({result: poolConfigs})); + return; + } + default: + next(); + } + }; + }; \ No newline at end of file diff --git a/libs/stats.js b/libs/stats.js index 7f2e126..459ff14 100644 --- a/libs/stats.js +++ b/libs/stats.js @@ -1,6 +1,9 @@ +var zlib = require('zlib'); + var redis = require('redis'); var async = require('async'); + var os = require('os'); var algos = require('stratum-pool/lib/algoProperties.js'); @@ -13,6 +16,17 @@ module.exports = function(logger, portalConfig, poolConfigs){ var logSystem = 'Stats'; var redisClients = []; + var redisStats; + + this.statHistory = []; + this.statPoolHistory = []; + this.statPoolHistoryBuffer; + + this.stats = {}; + this.statsString = ''; + + setupStatsRedis(); + gatherStatHistory(); var canDoStats = true; @@ -45,15 +59,65 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); - this.stats = {}; - this.statsString = ''; + function setupStatsRedis(){ + redisStats = redis.createClient(portalConfig.website.stats.redis.port, portalConfig.website.stats.redis.host); + redisStats.on('error', function(err){ + logger.error(logSystem, 'Historics', 'Redis for stats had an error ' + JSON.stringify(err)); + }); + } + + function gatherStatHistory(){ + + var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0).toString(); + + redisStats.zrangebyscore(['statHistory', retentionTime, '+inf'], function(err, replies){ + if (err) { + logger.error(logSystem, 'Historics', 'Error when trying to grab historical stats ' + JSON.stringify(err)); + return; + } + for (var i = 0; i < replies.length; i++){ + _this.statHistory.push(JSON.parse(replies[i])); + } + _this.statHistory = _this.statHistory.sort(function(a, b){ + return a.time - b.time; + }); + _this.statHistory.forEach(function(stats){ + addStatPoolHistory(stats); + }); + deflateStatPoolHistory(); + }); + } + + function addStatPoolHistory(stats){ + var data = { + time: stats.time, + pools: {} + }; + for (var pool in stats.pools){ + data.pools[pool] = { + hashrate: stats.pools[pool].hashrate, + workers: stats.pools[pool].workerCount, + blocks: stats.pools[pool].blocks + } + } + _this.statPoolHistory.push(data); + } + + + function deflateStatPoolHistory(){ + zlib.gzip(JSON.stringify(_this.statPoolHistory), function(err, buffer){ + _this.statPoolHistoryBuffer = buffer; + }); + } this.getGlobalStats = function(callback){ + var statGatherTime = Date.now() / 1000 | 0; + var allCoinStats = {}; async.each(redisClients, function(client, callback){ - var windowTime = (((Date.now() / 1000) - portalConfig.website.hashrateWindow) | 0).toString(); + var windowTime = (((Date.now() / 1000) - portalConfig.website.stats.hashrateWindow) | 0).toString(); var redisCommands = []; @@ -68,11 +132,10 @@ module.exports = function(logger, portalConfig, poolConfigs){ var commandsPerCoin = redisComamndTemplates.length; - client.coins.map(function(coin){ redisComamndTemplates.map(function(t){ var clonedTemplates = t.slice(0); - clonedTemplates[1] = coin + clonedTemplates [1]; + clonedTemplates[1] = coin + clonedTemplates[1]; redisCommands.push(clonedTemplates); }); }); @@ -80,7 +143,7 @@ module.exports = function(logger, portalConfig, poolConfigs){ client.client.multi(redisCommands).exec(function(err, replies){ if (err){ - console.log('error with getting hashrate stats ' + JSON.stringify(err)); + logger.error(logSystem, 'Global', 'error with getting global stats ' + JSON.stringify(err)); callback(err); } else{ @@ -105,12 +168,13 @@ module.exports = function(logger, portalConfig, poolConfigs){ }); }, function(err){ if (err){ - console.log('error getting all stats' + JSON.stringify(err)); + logger.error(logSystem, 'Global', 'error getting all stats' + JSON.stringify(err)); callback(); return; } var portalStats = { + time: statGatherTime, global:{ workers: 0, hashrate: 0 @@ -129,24 +193,25 @@ module.exports = function(logger, portalConfig, poolConfigs){ coinStats.shares += workerShares; var worker = parts[1]; if (worker in coinStats.workers) - coinStats.workers[worker] += workerShares + coinStats.workers[worker] += workerShares; else - coinStats.workers[worker] = workerShares + coinStats.workers[worker] = workerShares; }); var shareMultiplier = algos[coinStats.algorithm].multiplier || 0; - var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.hashrateWindow; + var hashratePre = shareMultiplier * coinStats.shares / portalConfig.website.stats.hashrateWindow; coinStats.hashrate = hashratePre | 0; - portalStats.global.workers += Object.keys(coinStats.workers).length; + coinStats.workerCount = Object.keys(coinStats.workers).length; + portalStats.global.workers += coinStats.workerCount; /* algorithm specific global stats */ - var algo = coinStats.algorithm; + var algo = coinStats.algorithm; if (!portalStats.algos.hasOwnProperty(algo)){ portalStats.algos[algo] = { workers: 0, hashrate: 0, hashrateString: null }; - } + } portalStats.algos[algo].hashrate += coinStats.hashrate; portalStats.algos[algo].workers += Object.keys(coinStats.workers).length; @@ -162,6 +227,33 @@ module.exports = function(logger, portalConfig, poolConfigs){ _this.stats = portalStats; _this.statsString = JSON.stringify(portalStats); + + + + _this.statHistory.push(portalStats); + addStatPoolHistory(portalStats); + + var retentionTime = (((Date.now() / 1000) - portalConfig.website.stats.historicalRetention) | 0); + + for (var i = 0; i < _this.statHistory.length; i++){ + if (retentionTime < _this.statHistory[i].time){ + if (i > 0) { + _this.statHistory = _this.statHistory.slice(i); + _this.statPoolHistory = _this.statPoolHistory.slice(i); + } + break; + } + } + + deflateStatPoolHistory(); + + redisStats.multi([ + ['zadd', 'statHistory', statGatherTime, _this.statsString], + ['zremrangebyscore', 'statHistory', '-inf', '(' + retentionTime] + ]).exec(function(err, replies){ + if (err) + logger.error(logSystem, 'Historics', 'Error adding stats to historics ' + JSON.stringify(err)); + }); callback(); }); diff --git a/libs/website.js b/libs/website.js index d4ea491..44d9b55 100644 --- a/libs/website.js +++ b/libs/website.js @@ -5,6 +5,8 @@ var path = require('path'); var async = require('async'); var dot = require('dot'); var express = require('express'); +var bodyParser = require('body-parser'); +var compress = require('compression'); var watch = require('node-watch'); @@ -29,7 +31,8 @@ module.exports = function(logger){ 'home.html': '', 'getting_started.html': 'getting_started', 'stats.html': 'stats', - 'api.html': 'api' + 'api.html': 'api', + 'admin.html': 'admin' }; var pageTemplates = {}; @@ -60,8 +63,9 @@ module.exports = function(logger){ }; - var readPageFiles = function(){ - async.each(Object.keys(pageFiles), function(fileName, callback){ + + var readPageFiles = function(files){ + async.each(files, function(fileName, callback){ var filePath = 'website/' + (fileName === 'index.html' ? '' : 'pages/') + fileName; fs.readFile(filePath, 'utf8', function(err, data){ var pTemp = dot.template(data); @@ -78,12 +82,14 @@ module.exports = function(logger){ }; - + //If an html file was changed reload it watch('website', function(filename){ - //if (event === 'change' && filename in pageFiles) - //READ ALL THE FILEZ BLAHHH - readPageFiles(); - + var basename = path.basename(filename); + if (basename in pageFiles){ + console.log(filename); + readPageFiles([basename]); + logger.debug(logSystem, 'Server', 'Reloaded file ' + basename); + } }); portalStats.getGlobalStats(function(){ @@ -103,10 +109,9 @@ module.exports = function(logger){ }); }; - setInterval(buildUpdatedWebsite, websiteConfig.statUpdateInterval * 1000); + setInterval(buildUpdatedWebsite, websiteConfig.stats.updateInterval * 1000); - var app = express(); var getPage = function(pageId){ if (pageId in pageProcessed){ @@ -127,6 +132,11 @@ module.exports = function(logger){ + var app = express(); + + + app.use(bodyParser.json()); + app.get('/get_page', function(req, res, next){ var requestedPage = getPage(req.query.id); if (requestedPage){ @@ -143,11 +153,27 @@ module.exports = function(logger){ portalApi.handleApiRequest(req, res, next); }); + app.post('/api/admin/:method', function(req, res, next){ + if (portalConfig.website + && portalConfig.website.adminCenter + && portalConfig.website.adminCenter.enabled){ + if (portalConfig.website.adminCenter.password === req.body.password) + portalApi.handleAdminApiRequest(req, res, next); + else + res.send(401, JSON.stringify({error: 'Incorrect Password'})); + + } + else + next(); + + }); + + app.use(compress()); app.use('/static', express.static('website/static')); app.use(function(err, req, res, next){ console.error(err.stack); - res.end(500, 'Something broke!'); + res.send(500, 'Something broke!'); }); app.listen(portalConfig.website.port, function(){ diff --git a/package.json b/package.json index 24e73b1..c1a28a3 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,8 @@ "mysql": "*", "async": "*", "express": "*", + "body-parser": "*", + "compression": "*", "dot": "*", "colors": "*", "node-watch": "*" diff --git a/pool_configs/litecoin_example.json b/pool_configs/litecoin_example.json index 0e7536e..a753c31 100644 --- a/pool_configs/litecoin_example.json +++ b/pool_configs/litecoin_example.json @@ -53,7 +53,10 @@ "ports": { "3008": { - "diff": 8, + "diff": 8 + }, + "3032": { + "diff": 32, "varDiff": { "minDiff": 8, "maxDiff": 512, @@ -62,9 +65,6 @@ "variancePercent": 30 } }, - "3032": { - "diff": 8 - }, "3256": { "diff": 256 } diff --git a/website/index.html b/website/index.html index e68fdb4..e613e65 100644 --- a/website/index.html +++ b/website/index.html @@ -6,64 +6,57 @@ + + + - + - + + + + + - -