Refactored website. Added historical stats.

This commit is contained in:
Matt 2014-04-10 19:33:41 -06:00
parent c487972f6b
commit 201d02d10b
18 changed files with 1003 additions and 182 deletions

View File

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

View File

@ -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": {

View File

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

View File

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

View File

@ -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(){

View File

@ -38,6 +38,8 @@
"mysql": "*",
"async": "*",
"express": "*",
"body-parser": "*",
"compression": "*",
"dot": "*",
"colors": "*",
"node-watch": "*"

View File

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

View File

@ -6,64 +6,57 @@
<link rel="icon" type="image/png" href="/static/favicon.png"/>
<link href='http://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.0.3/css/font-awesome.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/normalize/3.0.1/normalize.min.css">
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/pure/0.4.2/pure-min.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/Chart.js/0.2.0/Chart.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/zepto/1.1.3/zepto.min.js"></script>
<link rel="stylesheet" href="/static/style.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/d3/3.4.5/d3.min.js"></script>
<script src="/static/nvd3.js"></script>
<link rel="stylesheet" href="/static/nvd3.css">
<script src="/static/main.js"></script>
<link rel="stylesheet" href="/static/style.css">
<title>{{=it.portalConfig.website.siteTitle}}</title>
<title>NOMP</title>
</head>
<body>
<header class="header">
<div class="pure-menu pure-menu-open pure-menu-horizontal">
<header>
<div class="home-menu pure-menu pure-menu-open pure-menu-horizontal">
<a class="pure-menu-heading hot-swapper" href="/"><i class="fa fa-home"></i>&nbsp;NOMP</a>
<ul>
<li id="home" class="{{? it.selected === '' }}selected{{?}}">
<a class="hot-swapper" href="/"><i class="fa fa-home"></i>&nbsp;
Home
</a>
</li>
<li class="{{? it.selected === 'getting_started' }}selected{{?}}">
<li class="{{? it.selected === 'getting_started' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/getting_started">
<i class="fa fa-rocket"></i>&nbsp;
Getting Started
</a>
</li>
<li class="{{? it.selected === 'stats' }}selected{{?}}">
<li class="{{? it.selected === 'stats' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/stats">
<i class="fa fa-bar-chart-o"></i>&nbsp;
Stats
</a>
</li>
<li class="{{? it.selected === 'api' }}selected{{?}}">
<li class="{{? it.selected === 'api' }}pure-menu-selected{{?}}">
<a class="hot-swapper" href="/api">
<i class="fa fa-code"></i>&nbsp;
API
</a>
</li>
</ul>
{{ for(var algo in it.stats.algos) { }}
{{=algo}}: <div class="stats">
<div><i class="fa fa-users"></i>&nbsp;<span id="statsMiners{{=algo}}">{{=it.stats.algos[algo].workers}}</span> Miners</div>
<div><i class="fa fa-tachometer"></i>&nbsp;<span id="statsHashrate{{=algo}}">{{=it.stats.algos[algo].hashrateString}}</span></div>
</div>&nbsp;
{{ } }}
</div>
</header>
<main>
<div id="page">
{{=it.page}}
</div>
{{=it.page}}
</main>
@ -86,6 +79,5 @@
</footer>
</body>
</html>

50
website/pages/admin.html Normal file
View File

@ -0,0 +1,50 @@
<div>
<style>
#passwordForm, #adminCenter{
display: none;
}
#adminCenter{
display: flex;
flex-flow: row;
}
#leftMenu{
flex: 0 0 200px;
}
#editForm{
flex: 1 1 auto;
}
</style>
<form class="pure-form pure-form-stacked" id="passwordForm">
<fieldset>
<legend>Password</legend>
<input id="password" type="password" placeholder="Password">
<label for="remember" class="pure-checkbox">
<input id="remember" type="checkbox"> Stay Logged In
</label>
<button type="submit" class="pure-button pure-button-primary">Log In</button>
</fieldset>
</form>
<div id="adminCenter">
<div class="pure-menu pure-menu-open" id="leftMenu">
<a class="pure-menu-heading">Administration</a>
<ul>
<li id="addPool"><a href="#">Add Pool</a></li>
<li class="pure-menu-heading" id="poolList">Current Pools</li>
</ul>
</div>
<div id="editForm"></div>
</div>
<script src="/static/admin.js"></script>
</div>

View File

@ -1,5 +1,278 @@
<div>
<style>
#holder{
display: flex;
flex-direction: row;
}
To get started simply....
.glow{
box-shadow: inset 0 0 12px 4px #ff6c00;
}
</div>
.hidden{
display: none !important;
}
#menu{
background-color: #3d3d3d;
min-width: 170px;
}
#menu > div:first-child{
color: #e3f7ff;
border-bottom: 1px solid #7f878b;
font-size: 1.2em;
padding: 16px 0 4px 15px;
}
#coinList{
padding: 20px;
transition-duration: 200ms;
}
#coinList > a{
display: block;
color: #e3f7ff;
text-decoration: none;
padding: 5px;
}
#coinList > a:hover{
color: #f69b3a;
}
#main{
flex: 1 1 auto;
display: flex;
flex-direction: column;
margin: 18px;
}
.miningOption{
color: white;
display: flex;
flex: 1 1 auto;
flex-direction: row;
flex-wrap: wrap;
min-height: 215px;
justify-content: center;
align-items: center;
text-decoration: none;
}
a.miningOption:hover{
color: #f69b3a;
}
.miningOption:first-child{
background-color: #0eafc7;
}
.miningOption:last-child{
background-color: #b064e1;
}
.miningOptionNum{
font-size: 6em;
padding-right: 20px;
width: 140px;
text-align: center;
}
.miningOptionInstructions{
flex: 1 1 auto;
}
.miningOptionInstructions > div:first-child{
font-size: 2.4em;
}
.miningOptionInstructions > div:last-child{
margin-top: 20px;
font-size: 1.3em;
}
#orHolder{
height: 37px;
text-align: center;
}
#orLine{
border-bottom: 1px solid #c2cacf;
height: 19px;
margin-bottom: -13px;
}
#orText{
background-color: #ebf4fa;
color: #5c5c5c;
display: inline-block;
width: 35px;
font-style: italic;
}
#coinInfoBackground{
transition-duration: 400ms;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: black;
opacity: 0.0;
}
#coinInfo{
display: flex;
flex-direction: column;
color: white;
width: 750px;
min-height: 400px;
top: 50px;
left: 50%;
margin-left: -375px;
position: absolute;
background-color: #f06350;
}
#coinInfo > div:first-of-type{
font-size: 1.8em;
text-align: center;
margin-top: 40px;
margin-bottom: 35px;
}
#coinInfoRows{
display: flex;
flex-direction: row;
justify-content: center;
flex: 1 1 auto;
margin-bottom: 70px;
}
#coinInfoRows > div{
display: flex;
flex-direction: column;
justify-content: center;
}
#coinInfoRows > div > div{
padding: 3px;
}
#coinInfoRowKeys{
font-weight: bold;
padding-right: 30px;
}
#coinInfoRowKeys .coinInfoSubtle{
font-weight: normal;
}
#coinInfoRowValues{
}
#coinInfoClose{
position: absolute;
font-size: 3em;
top: 0;
right: 0;
width: 60px;
height: 60px;
text-align: center;
color: white;
text-decoration: none;
}
#coinInfoClose:hover{
color: #50f0e3;
}
</style>
<div id="holder">
<div id="menu">
<div>Coins</div>
<div id="coinList">
{{ for(var pool in it.poolsConfigs) {
var info = JSON.stringify({
coin: it.poolsConfigs[pool].coin,
ports: it.poolsConfigs[pool].ports,
host: it.portalConfig.website.stratumHost
}).replace(/"/g, '&quot;');
}}
<a href="#" class="poolOption" data-info="{{=info}}">{{=pool}}</a>
{{ } }}
</div>
</div>
<div id="main">
<a href="#" class="miningOption" id="nompAppDownload">
<div class="miningOptionNum">1.</div>
<div class="miningOptionInstructions">
<div>Download NOMP App</div>
<div>Our preconfigured app makes mining that easy</div>
</div>
</a>
<div id="orHolder">
<div id="orLine"></div>
<div id="orText">or</div>
</div>
<a href="#" class="miningOption" id="coinGlowTrigger">
<div class="miningOptionNum">2.</div>
<div class="miningOptionInstructions">
<div>Select a coin for connection details</div>
<div>Configurations for each coin are available for advanced miners</div>
</div>
</a>
</div>
</div>
<a href="#" id="coinInfoBackground" class="hidden"></a>
<div id="coinInfo" class="hidden">
<a href="#" id="coinInfoClose">×</a>
<div><span class="coinInfoName"></span> Configuration:</div>
<div id="coinInfoRows">
<div id="coinInfoRowKeys">
<div>Username:</div>
<div>Password:</div>
</div>
<div id="coinInfoRowValues">
<div>your <span class="coinInfoName"></span> wallet address</div>
<div>anything</div>
</div>
</div>
</div>
<script>
function showCoinConfig(info){
var htmlKeys = '<div class="coinInfoData">Algorithm:</div>';
var htmlValues = '<div class="coinInfoData">' + info.coin.algorithm + '</div>';
for (var port in info.ports){
htmlKeys += '<div class="coinInfoData">URL <span class="coinInfoSubtle">(difficulty ' + info.ports[port].diff + ')</span>:</div>';
htmlValues += '<div class="coinInfoData">stratum+tcp://' + info.host + ':' + port + '</div>';
}
$('.coinInfoName').text(info.coin.name);
$('.coinInfoData').remove();
$('#coinInfoRowKeys').append(htmlKeys);
$('#coinInfoRowValues').append(htmlValues);
}
$('#coinGlowTrigger').click(function(event){
event.preventDefault();
$('#coinList').addClass('glow');
setTimeout(function(){
$('#coinList').removeClass('glow');
}, 200);
return false;
});
$('.poolOption').click(function(event){
event.preventDefault();
showCoinConfig($(this).data('info'));
$('#coinInfoBackground,#coinInfo').removeClass('hidden');
$('#coinInfoBackground').css('opacity', 0.7);
return false;
});
$('#coinInfoBackground,#coinInfoClose').click(function(event){
event.preventDefault();
$('#coinInfoBackground,#coinInfo').addClass('hidden');
$('#coinInfoBackground').css('opacity', 0.0);
return false;
});
$('#nompAppDownload').click(function(event){
event.preventDefault();
alert('NOMP App development still in progress...');
return false;
});
</script>

View File

@ -1 +1,119 @@
<div>Welcome to {{=it.portalConfig.website.siteTitle}}! - The first multi-pool running on NOMP!</div>
<style>
#boxWelcome{
background-color: #0eafc7;
color: white;
margin: 18px;
}
#logoImg{
height: 285px;
margin: 55px;
}
#welcomeText{
font-size: 2.7em;
margin: 50px 18px 10px 18px;
}
#welcomeItems{
list-style-type: none;
font-size: 1.3em;
padding: 0 !important;
margin: 0 0 0 18px !important;
}
#welcomeItems > li{
margin: 30px !important;
}
#boxesLower {
margin: 0 9px;
}
#boxesLower > div > div {
flex: 1 1 auto;
margin: 0 9px 18px 9px;
padding: 10px;
display: flex;
flex-direction: column;
}
.boxLowerHeader{
font-size: 1.3em;
margin: 0 0 5px 10px;
}
#boxStatsLeft{
background-color: #b064e1;
}
#boxStatsRight{
background-color: #10bb9c;
}
.boxStats{
color: white;
}
.boxStatsList{
display: flex;
flex-flow: row wrap;
justify-content: space-around;
opacity: 0.77;
margin-bottom: 5px;
}
.boxStatsList i.fa{
height: 15px;
width: 33px;
text-align: center;
}
.boxStatsList > div{
padding: 5px 20px;
}
.boxStatsList > div > div{
padding: 3px;
}
</style>
<div class="pure-g-r" id="boxWelcome">
<div class="pure-u-1-3">
<img id="logoImg" src="/static/logo.svg">
</div>
<div class="pure-u-2-3">
<div id="welcomeText">Welcome to the future of mining</div>
<ul id="welcomeItems">
<li>Low fees</li>
<li>High performance Node.js backend</li>
<li>User friendly mining client</li>
<li>Multi-coin / multi-pool</li>
</ul>
</div>
</div>
<div class="pure-g-r" id="boxesLower">
<div class="pure-u-1-2">
<div class="boxStats" id="boxStatsLeft">
<div class="boxLowerHeader">Global Stats</div>
<div class="boxStatsList">
{{ for(var algo in it.stats.algos) { }}
<div>
<div><i class="fa fa-flask"></i>{{=algo}}</div>
<div><i class="fa fa-users"></i><span id="statsMiners{{=algo}}">{{=it.stats.algos[algo].workers}}</span> Miners</div>
<div><i class="fa fa-tachometer"></i><span id="statsHashrate{{=algo}}">{{=it.stats.algos[algo].hashrateString}}</span></div>
</div>
{{ } }}
</div>
</div>
</div>
<div class="pure-u-1-2">
<div class="boxStats" id="boxStatsRight">
<div class="boxLowerHeader">Pools / Coins</div>
<div class="boxStatsList">
{{ for(var pool in it.stats.pools) { }}
<div>
<div><i class="fa fa-dot-circle-o"></i>{{=pool}}</div>
<div><i class="fa fa-users"></i><span id="statsMiners{{=pool}}">{{=Object.keys(it.stats.pools[pool].workers).length}}</span> Miners</div>
<div><i class="fa fa-tachometer"></i><span id="statsHashrate{{=pool}}">{{=it.stats.pools[pool].hashrateString}}</span></div>
</div>
{{ } }}
</div>
</div>
</div>
</div>

View File

@ -1,27 +1,44 @@
<div>
<table class="pure-table">
<thead>
<tr>
<th>Pool</th>
<th>Algo</th>
<th>Workers</th>
<th>Valid Shares</th>
<th>Invalid Shares</th>
<th>Blocks</th>
<th>Hashrate</th>
</tr>
</tr>
</thead>
{{ for(var pool in it.stats.pools) { }}
<tr class="pure-table-odd">
<td>{{=it.stats.pools[pool].name}}</td>
<td>{{=it.stats.pools[pool].algorithm}}</td>
<td>{{=Object.keys(it.stats.pools[pool].workers).length}}</td>
<td>{{=it.stats.pools[pool].poolStats.validShares}}</td>
<td>{{=it.stats.pools[pool].poolStats.invalidShares}}</td>
<td>{{=it.stats.pools[pool].poolStats.validBlocks}}</td>
<td>{{=it.stats.pools[pool].hashrateString}}</div>
</tr>
{{ } }}
</table>
<style>
#topCharts{
padding: 18px;
}
#topCharts > div > svg{
display: block;
height: 280px;
}
.chartLabel{
font-size: 1.2em;
text-align: center;
padding: 4px;
}
.chartHolder{
border: solid 1px #c7c7c7;
border-radius: 5px;
padding: 5px;
margin-bottom: 18px;
}
</style>
<div id="topCharts">
<div class="chartLabel">Workers Per Pool</div>
<div class="chartHolder"><svg id="poolWorkers"/></div>
<div class="chartLabel">Hashrate Per Pool</div>
<div class="chartHolder"><svg id="poolHashrate"/></div>
<div class="chartLabel">Blocks Pending Per Pool</div>
<div class="chartHolder"><svg id="poolBlocks"/></div>
</div>
<script>
document.querySelector('main').appendChild(document.createElement('script')).src = '/static/stats.js';
</script>

100
website/static/admin.js Normal file
View File

@ -0,0 +1,100 @@
var docCookies = {
getItem: function (sKey) {
return decodeURIComponent(document.cookie.replace(new RegExp("(?:(?:^|.*;)\\s*" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=\\s*([^;]*).*$)|^.*$"), "$1")) || null;
},
setItem: function (sKey, sValue, vEnd, sPath, sDomain, bSecure) {
if (!sKey || /^(?:expires|max\-age|path|domain|secure)$/i.test(sKey)) { return false; }
var sExpires = "";
if (vEnd) {
switch (vEnd.constructor) {
case Number:
sExpires = vEnd === Infinity ? "; expires=Fri, 31 Dec 9999 23:59:59 GMT" : "; max-age=" + vEnd;
break;
case String:
sExpires = "; expires=" + vEnd;
break;
case Date:
sExpires = "; expires=" + vEnd.toUTCString();
break;
}
}
document.cookie = encodeURIComponent(sKey) + "=" + encodeURIComponent(sValue) + sExpires + (sDomain ? "; domain=" + sDomain : "") + (sPath ? "; path=" + sPath : "") + (bSecure ? "; secure" : "");
return true;
},
removeItem: function (sKey, sPath, sDomain) {
if (!sKey || !this.hasItem(sKey)) { return false; }
document.cookie = encodeURIComponent(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT" + ( sDomain ? "; domain=" + sDomain : "") + ( sPath ? "; path=" + sPath : "");
return true;
},
hasItem: function (sKey) {
return (new RegExp("(?:^|;\\s*)" + encodeURIComponent(sKey).replace(/[\-\.\+\*]/g, "\\$&") + "\\s*\\=")).test(document.cookie);
}
};
var password = docCookies.getItem('password');
function showLogin(){
$('#adminCenter').hide();
$('#passwordForm').show();
}
function showAdminCenter(){
$('#passwordForm').hide();
$('#adminCenter').show();
}
function tryLogin(){
apiRequest('pools', {}, function(response){
showAdminCenter();
displayMenu(response.result)
});
}
function displayMenu(pools){
$('#poolList').after(Object.keys(pools).map(function(poolName){
return '<li class="poolMenuItem"><a href="#">' + poolName + '</a></li>';
}).join(''));
}
function apiRequest(func, data, callback){
var httpRequest = new XMLHttpRequest();
httpRequest.onreadystatechange = function(){
if (httpRequest.readyState === 4 && httpRequest.responseText){
if (httpRequest.status === 401){
docCookies.removeItem('password');
$('#password').val('');
showLogin();
alert('Incorrect Password');
}
else{
var response = JSON.parse(httpRequest.responseText);
callback(response);
}
}
};
httpRequest.open('POST', '/api/admin/' + func);
data.password = password;
httpRequest.setRequestHeader('Content-Type', 'application/json');
httpRequest.send(JSON.stringify(data));
}
if (password){
tryLogin();
}
else{
showLogin();
}
$('#passwordForm').submit(function(event){
event.preventDefault();
password = $('#password').val();
if (password){
if ($('#remember').is(':checked'))
docCookies.setItem('password', password, Infinity);
else
docCookies.setItem('password', password);
tryLogin();
}
return false;
});

View File

@ -1,11 +1,12 @@
$(function(){
var hotSwap = function(page, pushSate){
if (pushSate) history.pushState(null, null, '/' + page);
$('.selected').removeClass('selected');
$('a[href="/' + page + '"]').parent().addClass('selected')
$('.pure-menu-selected').removeClass('pure-menu-selected');
$('a[href="/' + page + '"]').parent().addClass('pure-menu-selected');
$.get("/get_page", {id: page}, function(data){
$('#page').html(data);
$('main').html(data);
}, 'html')
};
@ -25,7 +26,7 @@ $(function(){
}, 0);
});
var statsSource = new EventSource("/api/live_stats");
window.statsSource = new EventSource("/api/live_stats");
statsSource.addEventListener('message', function(e){
var stats = JSON.parse(e.data);
for (algo in stats.algos) {

1
website/static/nvd3.css Normal file

File diff suppressed because one or more lines are too long

6
website/static/nvd3.js Normal file

File diff suppressed because one or more lines are too long

150
website/static/stats.js Normal file
View File

@ -0,0 +1,150 @@
var poolWorkerData = [];
var poolHashrateData = [];
var poolBlockData = [];
var poolWorkerChart;
var poolHashrateChart;
var poolBlockChart;
function buildChartData(data){
var pools = {};
for (var i = 0; i < data.length; i++){
var time = data[i].time * 1000;
for (var pool in data[i].pools){
var a = pools[pool] = (pools[pool] || {
hashrate: [],
workers: [],
blocks: []
});
a.hashrate.push([time, data[i].pools[pool].hashrate || 0]);
a.workers.push([time, data[i].pools[pool].workers || 0]);
a.blocks.push([time, data[i].pools[pool].blocks.pending])
}
}
for (var pool in pools){
poolWorkerData.push({
key: pool,
values: pools[pool].workers
});
poolHashrateData.push({
key: pool,
values: pools[pool].hashrate
});
poolBlockData.push({
key: pool,
values: pools[pool].blocks
})
}
}
function getReadableHashRateString(hashrate){
var i = -1;
var byteUnits = [ ' KH', ' MH', ' GH', ' TH', ' PH' ];
do {
hashrate = hashrate / 1024;
i++;
} while (hashrate > 1024);
return hashrate.toFixed(2) + byteUnits[i];
}
function displayCharts(){
nv.addGraph(function() {
poolWorkerChart = nv.models.stackedAreaChart()
.x(function(d){ return d[0] })
.y(function(d){ return d[1] })
.useInteractiveGuideline(true)
.clipEdge(true);
poolWorkerChart.xAxis.tickFormat(function(d) {
return d3.time.format('%X')(new Date(d))
});
poolWorkerChart.yAxis.tickFormat(d3.format('d'));
d3.select('#poolWorkers').datum(poolWorkerData).call(poolWorkerChart);
return poolWorkerChart;
});
nv.addGraph(function() {
poolHashrateChart = nv.models.lineChart()
.x(function(d){ return d[0] })
.y(function(d){ return d[1] })
.useInteractiveGuideline(true);
poolHashrateChart.xAxis.tickFormat(function(d) {
return d3.time.format('%X')(new Date(d))
});
poolHashrateChart.yAxis.tickFormat(function(d){
return getReadableHashRateString(d);
});
d3.select('#poolHashrate').datum(poolHashrateData).call(poolHashrateChart);
return poolHashrateChart;
});
nv.addGraph(function() {
poolBlockChart = nv.models.multiBarChart()
.x(function(d){ return d[0] })
.y(function(d){ return d[1] });
poolBlockChart.xAxis.tickFormat(function(d) {
return d3.time.format('%X')(new Date(d))
});
d3.select('#poolBlocks').datum(poolBlockData).call(poolBlockChart);
return poolBlockChart;
});
}
function TriggerChartUpdates(){
poolWorkerChart.update();
poolHashrateChart.update();
poolBlockChart.update();
}
nv.utils.windowResize(TriggerChartUpdates);
$.getJSON('/api/pool_stats', function(data){
buildChartData(data);
displayCharts();
});
statsSource.addEventListener('message', function(e){
var stats = JSON.parse(e.data);
var time = stats.time * 1000;
for (var pool in stats.pools){
for (var i = 0; i < poolWorkerData.length; i++){
if (poolWorkerData[i].key === pool){
poolWorkerData[i].values.shift();
poolWorkerData[i].values.push([time, stats.pools[pool].workerCount]);
break;
}
}
for (var i = 0; i < poolHashrateData.length; i++){
if (poolHashrateData[i].key === pool){
poolHashrateData[i].values.shift();
poolHashrateData[i].values.push([time, stats.pools[pool].hashrate]);
break;
}
}
for (var i = 0; i < poolBlockData.length; i++){
if (poolBlockData[i].key === pool){
poolBlockData[i].values.shift();
poolBlockData[i].values.push([time, stats.pools[pool].blocks.pending]);
break;
}
}
}
TriggerChartUpdates();
});

View File

@ -1,121 +1,67 @@
html, button, input, select, textarea, .pure-g [class *= "pure-u"], .pure-g-r [class *= "pure-u"]{
font-family: 'Open Sans', sans-serif;
}
html{
height: 100%;
background: #2d2d2d;
}
body{
min-height: 100vh;
overflow: hidden;
display: -moz-box;
display: -webkit-flexbox;
display: -ms-flexbox;
display: -webkit-flex;
display: -moz-flex;
display: flex;
-webkit-flex-direction: column;
-moz-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
}
header{
background-color: #f3f2ef;
}
header > div{
display: -moz-box;
display: -webkit-flexbox;
display: -ms-flexbox;
display: -webkit-flex;
display: -moz-flex;
display: flex;
}
header > div > ul{
flex: 1;
}
header .selected, header a:active, header a:focus{
background-color: #404040 !important;
}
.selected > a {
color: white !important;
}
header a {
line-height: 2.5em !important;
font-size: 1.1em !important;
}
header a:hover {
background-color: #4d4d4d !important;
color: white !important;
}
.stats{
display: flex;
flex-direction: column;
max-width: 1160px;
margin: 0 auto;
}
header > .home-menu{
background: inherit !important;
height: 54px;
display: flex;
}
header > .home-menu > a.pure-menu-heading, header > .home-menu > ul, header > .home-menu > ul > li{
display: flex !important;
align-items: center;
justify-content: center;
line-height: normal !important;
}
.stats > div{
padding: 2px 15px;
border-radius: 4px;
color: #f2f2f2;
font-size: 0.95em;
width: 120px;
text-align: center;
header > .home-menu > a.pure-menu-heading{
color: white;
font-size: 1.5em;
}
.stats > div:nth-child(1){
background-color: #71a380;
margin-bottom: 4px;
}
.stats > div:nth-child(2){
background-color: #7196a3;
header > .home-menu > ul > li > a{
color: #ced4d9;
}
.pure-menu-heading{
padding-left: 0 !important;
header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li > a:focus{
background: inherit !important;
}
header > div{
background-color: transparent !important;
margin-left: auto;
margin-right: auto;
header > .home-menu > ul > li > a:hover, header > .home-menu > ul > li.pure-menu-selected > a{
color: white;
}
main > div, header > div{
max-width: 800px;
main{
background-color: #ebf4fa;
position: relative;
}
main {
flex: 1 1 auto;
padding: 20px 0;
}
main > div{
margin-left: auto;
margin-right: auto;
}
footer{
text-align: center;
color: #b3b3b3;
background-color: #404040;
text-decoration: none;
font-size: 0.8em;
padding: 15px;
line-height: 24px;
}
footer a{
color: #fff;
text-decoration: none;
}
footer iframe{
vertical-align: middle;
}