Added new statistics charts & new admin panel

pull/3/head
fancoder 2014-08-08 00:45:33 +04:00
parent c2a9aae907
commit 92ec67f866
17 changed files with 1462 additions and 182 deletions

View File

@ -282,6 +282,63 @@ Explanation for each field:
"host": "127.0.0.1",
"port": 6379
}
/* Monitoring RPC services. Statistics will be displayed in Admin panel */
"monitoring": {
"daemon": {
"checkInterval": 60, //interval of sending rpcMethod request
"rpcMethod": "getblockcount" //RPC method name
},
"wallet": {
"checkInterval": 60,
"rpcMethod": "getbalance"
}
/* Collect pool statistics to display in frontend charts */
"charts": {
"pool": {
"hashrate": {
"enabled": true, //enable data collection and chart displaying in frontend
"updateInterval": 60, //how often to get current value
"stepInterval": 1800, //chart step interval calculated as average of all updated values
"maximumPeriod": 86400 //chart maximum periods (chart points number = maximumPeriod / stepInterval = 48)
},
"workers": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800, //chart step interval calculated as maximum of all updated values
"maximumPeriod": 86400
},
"difficulty": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
},
"price": { //USD price of one currency coin received from cryptonator.com/api
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
},
"profit": { //Reward * Rate / Difficulty
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
}
},
"user": { //chart data displayed in user stats block
"hashrate": {
"enabled": true,
"updateInterval": 180,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"payments": { //payment chart uses all user payments data stored in DB
"enabled": true
}
}
```
#### 3) [Optional] Configure cryptonote-easy-miner for your pool

View File

@ -1,6 +1,7 @@
{
"coin": "monero",
"symbol": "XMR",
"coinUnits": 1000000000000,
"logging": {
"files": {
@ -113,5 +114,62 @@
"redis": {
"host": "127.0.0.1",
"port": 6379
},
"monitoring": {
"daemon": {
"checkInterval": 60,
"rpcMethod": "getblockcount"
},
"wallet": {
"checkInterval": 60,
"rpcMethod": "getbalance"
}
},
"charts": {
"pool": {
"hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"workers": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"difficulty": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
},
"price": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
},
"profit": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
}
},
"user": {
"hashrate": {
"enabled": true,
"updateInterval": 180,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"payments": {
"enabled": true
}
}
}
}

26
init.js
View File

@ -30,6 +30,10 @@ if (cluster.isWorker){
case 'cli':
require('./lib/cli.js');
break
case 'chartsDataCollector':
require('./lib/chartsDataCollector.js');
break
}
return;
}
@ -40,7 +44,7 @@ require('./lib/exceptionWriter.js')(logSystem);
var singleModule = (function(){
var validModules = ['pool', 'api', 'unlocker', 'payments'];
var validModules = ['pool', 'api', 'unlocker', 'payments', 'chartsDataCollector'];
for (var i = 0; i < process.argv.length; i++){
if (process.argv[i].indexOf('-module=') === 0){
@ -75,6 +79,9 @@ var singleModule = (function(){
case 'api':
spawnApi();
break;
case 'chartsDataCollector':
spawnChartsDataCollector();
break;
}
}
else{
@ -82,6 +89,7 @@ var singleModule = (function(){
spawnBlockUnlocker();
spawnPaymentProcessor();
spawnApi();
spawnChartsDataCollector();
}
spawnCli();
@ -228,4 +236,18 @@ function spawnApi(){
function spawnCli(){
}
}
function spawnChartsDataCollector(){
if (!config.charts) return;
var worker = cluster.fork({
workerType: 'chartsDataCollector'
});
worker.on('exit', function(code, signal){
log('error', logSystem, 'chartsDataCollector died, spawning replacement...');
setTimeout(function(){
spawnChartsDataCollector();
}, 2000);
});
}

View File

@ -6,6 +6,8 @@ var zlib = require('zlib');
var async = require('async');
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
var charts = require('./charts.js');
var authSid = Math.round(Math.random() * 10000000000) + '' + Math.round(Math.random() * 10000000000);
var logSystem = 'api';
require('./exceptionWriter.js')(logSystem);
@ -28,6 +30,7 @@ var currentStats = "";
var currentStatsCompressed = "";
var minerStats = {};
var minersHashrate = {};
var liveConnections = {};
var addressConnections = {};
@ -67,18 +70,20 @@ function collectStats(){
var hashrates = replies[1];
minerStats = {};
minersHashrate = {};
for (var i = 0; i < hashrates.length; i++){
var hashParts = hashrates[i].split(':');
minerStats[hashParts[1]] = (minerStats[hashParts[1]] || 0) + parseInt(hashParts[0]);
minersHashrate[hashParts[1]] = (minerStats[hashParts[1]] || 0) + parseInt(hashParts[0]);
}
var totalShares = 0;
for (var miner in minerStats){
var shares = minerStats[miner];
for (var miner in minersHashrate){
var shares = minersHashrate[miner];
totalShares += shares;
minerStats[miner] = getReadableHashRateString(shares / config.api.hashrateWindow);
minersHashrate[miner] = Math.round(shares / config.api.hashrateWindow);
minerStats[miner] = getReadableHashRateString(minersHashrate[miner]);
}
data.miners = Object.keys(minerStats).length;
@ -133,7 +138,8 @@ function collectStats(){
minPaymentThreshold: config.payments.minPayment,
denominationUnit: config.payments.denomination
});
}
},
charts: charts.getPoolChartsData
}, function(error, results){
log('info', logSystem, 'Stat collection finished: %d ms redis, %d ms daemon', [redisFinished - startTime, daemonFinished - startTime]);
@ -237,7 +243,13 @@ function handleMinerStats(urlParts, response){
}
var stats = replies[0];
stats.hashrate = minerStats[address];
response.end(JSON.stringify({stats: stats, payments: replies[1]}));
charts.getUserChartsData(address, replies[1], function(error, chartsData) {
response.end(JSON.stringify({
stats: stats,
payments: replies[1],
charts: chartsData
}));
});
});
}
}
@ -307,25 +319,59 @@ function handleGetBlocks(urlParts, response){
});
}
function handleGetMinersHashrate(response) {
var reply = JSON.stringify({
minersHashrate: minersHashrate
});
response.writeHead("200", {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
'Content-Length': reply.length
});
response.end(reply);
}
collectStats();
function parseCookies(request) {
var list = {},
rc = request.headers.cookie;
rc && rc.split(';').forEach(function(cookie) {
var parts = cookie.split('=');
list[parts.shift().trim()] = unescape(parts.join('='));
});
return list;
}
function authorize(request, response){
if(request.connection.remoteAddress == '127.0.0.1') {
return true;
}
response.setHeader('Access-Control-Allow-Origin', '*');
var cookies = parseCookies(request);
if(cookies.sid && cookies.sid == authSid) {
return true;
}
var sentPass = url.parse(request.url, true).query.password;
if (sentPass !== config.api.password){
response.statusCode = 401;
response.end('invalid password');
return;
}
log('warn', logSystem, 'Admin authorized');
response.statusCode = 200;
var cookieExpire = new Date( new Date().getTime() + 60*60*24*1000);
response.setHeader('Set-Cookie', 'sid=' + authSid + '; path=/; expires=' + cookieExpire.toUTCString());
response.setHeader('Cache-Control', 'no-cache');
response.setHeader('Content-Type', 'application/json');
return true;
}
@ -406,6 +452,149 @@ function handleAdminStats(response){
}
function handleAdminUsers(response){
async.waterfall([
// get workers Redis keys
function(callback) {
redisClient.keys(config.coin + ':workers:*', callback);
},
// get workers data
function(workerKeys, callback) {
var redisCommands = workerKeys.map(function(k) {
return ['hmget', k, 'balance', 'paid', 'lastShare', 'hashes'];
});
redisClient.multi(redisCommands).exec(function(error, redisData) {
var workersData = {};
var addressLength = config.poolServer.poolAddress.length;
for(var i in redisData) {
var address = workerKeys[i].substr(-addressLength);
var data = redisData[i];
workersData[address] = {
pending: data[0] / config.coinUnits,
paid: data[1] / config.coinUnits,
lastShare: data[2],
hashes: data[3],
hashrate: minersHashrate[address] ? minersHashrate[address] : 0
};
}
callback(null, workersData);
});
}
], function(error, workersData) {
if(error) {
response.end(JSON.stringify({error: 'error collecting users stats'}));
return;
}
response.end(JSON.stringify(workersData));
}
);
}
function handleAdminMonitoring(response) {
response.writeHead("200", {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json'
});
async.parallel({
monitoring: getMonitoringData,
logs: getLogFiles
}, function(error, result) {
response.end(JSON.stringify(result));
});
}
function handleAdminLog(urlParts, response){
var file = urlParts.query.file;
var filePath = config.logging.files.directory + '/' + file;
if(!file.match(/^\w+\.log$/)) {
response.end('wrong log file');
}
response.writeHead(200, {
'Content-Type': 'text/plain',
'Cache-Control': 'no-cache',
'Content-Length': fs.statSync(filePath).size
});
fs.createReadStream(filePath).pipe(response)
}
function startRpcMonitoring(rpc, module, method, interval) {
setInterval(function() {
rpc(method, {}, function(error, response) {
var stat = {
lastCheck: new Date() / 1000 | 0,
lastStatus: error ? 'fail' : 'ok',
lastResponse: JSON.stringify(error ? error : response)
};
if(error) {
stat.lastFail = stat.lastCheck;
stat.lastFailResponse = stat.lastResponse;
}
var key = getMonitoringDataKey(module);
var redisCommands = [];
for(var property in stat) {
redisCommands.push(['hset', key, property, stat[property]]);
}
redisClient.multi(redisCommands).exec();
});
}, interval * 1000);
}
function getMonitoringDataKey(module) {
return config.coin + ':status:' + module;
}
function initMonitoring() {
var modulesRpc = {
daemon: apiInterfaces.rpcDaemon,
wallet: apiInterfaces.rpcWallet
};
for(var module in config.monitoring) {
var settings = config.monitoring[module];
if(settings.checkInterval) {
startRpcMonitoring(modulesRpc[module], module, settings.rpcMethod, settings.checkInterval);
}
}
}
function getMonitoringData(callback) {
var modules = Object.keys(config.monitoring);
var redisCommands = [];
for(var i in modules) {
redisCommands.push(['hgetall', getMonitoringDataKey(modules[i])])
}
redisClient.multi(redisCommands).exec(function(error, results) {
var stats = {};
for(var i in modules) {
if(results[i]) {
stats[modules[i]] = results[i];
}
}
callback(error, stats);
});
}
function getLogFiles(callback) {
var dir = config.logging.files.directory;
fs.readdir(dir, function(error, files) {
var logs = {};
for(var i in files) {
var file = files[i];
var stats = fs.statSync(dir + '/' + file);
logs[file] = {
size: stats.size,
changed: Date.parse(stats.mtime) / 1000 | 0
}
}
callback(error, logs);
});
}
var server = http.createServer(function(request, response){
if (request.method.toUpperCase() === "OPTIONS"){
@ -426,12 +615,13 @@ var server = http.createServer(function(request, response){
switch(urlParts.pathname){
case '/stats':
var reply = currentStatsCompressed;
var deflate = request.headers['accept-encoding'] && request.headers['accept-encoding'].indexOf('deflate') != -1;
var reply = deflate ? currentStatsCompressed : currentStats;
response.writeHead("200", {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
'Content-Encoding': 'deflate',
'Content-Encoding': deflate ? 'deflate' : '',
'Content-Length': reply.length
});
response.end(reply);
@ -462,9 +652,33 @@ var server = http.createServer(function(request, response){
case '/admin_stats':
if (!authorize(request, response))
return;
log('warn', logSystem, 'Admin authorized');
handleAdminStats(response);
break;
case '/admin_monitoring':
if(!authorize(request, response)) {
return;
}
handleAdminMonitoring(response);
break;
case '/admin_log':
if(!authorize(request, response)) {
return;
}
handleAdminLog(urlParts, response);
break;
case '/admin_users':
if(!authorize(request, response)) {
return;
}
handleAdminUsers(response);
break;
case '/miners_hashrate':
if (!authorize(request, response))
return;
handleGetMinersHashrate(response);
break;
default:
response.writeHead(404, {
'Access-Control-Allow-Origin': '*'
@ -472,11 +686,11 @@ var server = http.createServer(function(request, response){
response.end('Invalid API call');
break;
}
});
collectStats();
initMonitoring();
server.listen(config.api.port, function(){
log('info', logSystem, 'API started & listening on port %d', [config.api.port]);
});
});

View File

@ -1,12 +1,14 @@
var http = require('http');
var https = require('https');
function jsonHttpRequest(host, port, data, callback){
function jsonHttpRequest(host, port, data, callback, path){
path = path || '/json_rpc';
var options = {
hostname: host,
port: port,
path: '/json_rpc',
method: 'POST',
path: path,
method: data ? 'POST' : 'GET',
headers: {
'Content-Length': data.length,
'Content-Type': 'application/json',
@ -14,7 +16,7 @@ function jsonHttpRequest(host, port, data, callback){
}
};
var req = http.request(options, function(res){
var req = (port == 443 ? https : http).request(options, function(res){
var replyData = '';
res.setEncoding('utf8');
res.on('data', function(chunk){
@ -72,7 +74,7 @@ function batchRpc(host, port, array, callback){
}
module.exports = function(daemonConfig, walletConfig){
module.exports = function(daemonConfig, walletConfig, poolApiConfig){
return {
batchRpcDaemon: function(batchArray, callback){
batchRpc(daemonConfig.host, daemonConfig.port, batchArray, callback);
@ -82,6 +84,10 @@ module.exports = function(daemonConfig, walletConfig){
},
rpcWallet: function(method, params, callback){
rpc(walletConfig.host, walletConfig.port, method, params, callback);
}
},
pool: function(method, callback){
jsonHttpRequest('127.0.0.1', poolApiConfig.port, '', callback, method);
},
jsonHttpRequest: jsonHttpRequest
}
};
};

View File

@ -1,6 +1,6 @@
var async = require('async');
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
var logSystem = 'unlocker';
require('./exceptionWriter.js')(logSystem);

240
lib/charts.js 100644
View File

@ -0,0 +1,240 @@
var fs = require('fs');
var async = require('async');
var http = require('http');
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
var logSystem = 'charts';
require('./exceptionWriter.js')(logSystem);
log('info', logSystem, 'Started');
function startDataCollectors() {
async.each(Object.keys(config.charts.pool), function(chartName) {
var settings = config.charts.pool[chartName];
if(settings.enabled) {
setInterval(function() {
collectPoolStatWithInterval(chartName, settings);
}, settings.updateInterval * 1000);
}
});
var settings = config.charts.user.hashrate;
if(settings.enabled) {
setInterval(function() {
collectUsersHashrate('hashrate', settings);
}, settings.updateInterval * 1000)
}
}
function getChartDataFromRedis(chartName, callback) {
redisClient.get(getStatsRedisKey(chartName), function(error, data) {
callback(data ? JSON.parse(data) : []);
});
}
function getUserHashrateChartData(address, callback) {
getChartDataFromRedis('hashrate:' + address, callback);
}
function convertPaymentsDataToChart(paymentsData) {
var data = [];
if(paymentsData && paymentsData.length) {
for(var i = 0; paymentsData[i]; i += 2) {
data.push([+paymentsData[i + 1], paymentsData[i].split(':')[1] / config.coinUnits]);
}
}
return data;
}
function getUserChartsData(address, paymentsData, callback) {
var stats = {};
var chartsFuncs = {
hashrate: function(callback) {
getUserHashrateChartData(address, function(data) {
callback(null, data);
});
},
payments: function(callback) {
callback(null, convertPaymentsDataToChart(paymentsData));
}
};
for(var chartName in chartsFuncs) {
if(!config.charts.user[chartName].enabled) {
delete chartsFuncs[chartName];
}
}
async.parallel(chartsFuncs, callback);
}
function getStatsRedisKey(chartName) {
return config.coin + ':charts:' + chartName;
}
var chartStatFuncs = {
hashrate: getPoolHashrate,
workers: getPoolWorkers,
difficulty: getNetworkDifficulty,
price: getCoinPrice,
profit: getCoinProfit
};
var statValueHandler = {
avg: function(set, value) {
set[1] = (set[1] * set[2] + value) / (set[2] + 1);
},
round: function(set, value) {
statValueHandler.avg(set, value);
set[1] = Math.round(set[1]);
},
max: function(set, value) {
if(value > set[1]) {
set[1] = value;
}
}
};
var preSaveFunctions = {
hashrate: statValueHandler.avg,
workers: statValueHandler.max,
difficulty: statValueHandler.round,
price: statValueHandler.avg,
profit: statValueHandler.avg
};
function storeCollectedValues(chartName, values, settings) {
for(var i in values) {
storeCollectedValue(chartName + ':' + i, values[i], settings);
}
}
function storeCollectedValue(chartName, value, settings) {
var now = new Date() / 1000 | 0;
getChartDataFromRedis(chartName, function(sets) {
var lastSet = sets[sets.length - 1]; // [time, avgValue, updatesCount]
if(!lastSet || now - lastSet[0] > settings.stepInterval) {
lastSet = [now, value, 1];
sets.push(lastSet);
while(now - sets[0][0] > settings.maximumPeriod) { // clear old sets
sets.shift();
}
}
else {
preSaveFunctions[chartName]
? preSaveFunctions[chartName](lastSet, value)
: statValueHandler.round(lastSet, value);
lastSet[2]++;
}
redisClient.set(getStatsRedisKey(chartName), JSON.stringify(sets));
log('info', logSystem, chartName + ' chart collected value ' + value + '. Total sets count ' + sets.length);
log('info', logSystem, chartName + ' data: ' + JSON.stringify(sets));
});
}
function collectPoolStatWithInterval(chartName, settings) {
async.waterfall([
chartStatFuncs[chartName],
function(value, callback) {
storeCollectedValue(chartName, value, settings, callback);
}
]);
}
function getPoolStats(callback) {
apiInterfaces.pool('/stats', callback);
}
function getPoolHashrate(callback) {
getPoolStats(function(error, stats) {
callback(error, stats.pool ? Math.round(stats.pool.hashrate) : null);
});
}
function getPoolWorkers(callback) {
getPoolStats(function(error, stats) {
callback(error, stats.pool ? stats.pool.miners : null);
});
}
function getNetworkDifficulty(callback) {
getPoolStats(function(error, stats) {
callback(error, stats.pool ? stats.network.difficulty : null);
});
}
function getUsersHashrates(callback) {
apiInterfaces.pool('/miners_hashrate', function(error, data) {
callback(data.minersHashrate);
});
}
function collectUsersHashrate(chartName, settings) {
var redisBaseKey = getStatsRedisKey(chartName) + ':';
redisClient.keys(redisBaseKey + '*', function(keys) {
var hashrates = {};
for(var i in keys) {
hashrates[keys[i].substr(keys[i].length)] = 0;
}
getUsersHashrates(function(newHashrates) {
for(var address in newHashrates) {
hashrates[address] = newHashrates[address];
}
storeCollectedValues(chartName, hashrates, settings);
});
});
}
function getCoinPrice(callback) {
apiInterfaces.jsonHttpRequest('www.cryptonator.com', 443, '', function(error, response) {
callback(response.error ? response.error : error, response.success ? +response.ticker.price : null);
}, '/api/ticker/' + config.symbol.toLowerCase() + '-usd');
}
function getCoinProfit(callback) {
getCoinPrice(function(error, price) {
if(error) {
callback(error);
return;
}
getPoolStats(function(error, stats) {
if(error) {
callback(error);
return;
}
callback(null, stats.network.reward * price / stats.network.difficulty / config.coinUnits);
});
});
}
function getPoolChartsData(callback) {
var chartsNames = [];
var redisKeys = [];
for(var chartName in config.charts.pool) {
if(config.charts.pool[chartName].enabled) {
chartsNames.push(chartName);
redisKeys.push(getStatsRedisKey(chartName));
}
}
if(redisKeys.length) {
redisClient.mget(redisKeys, function(error, data) {
var stats = {};
if(data) {
for(var i in data) {
if(data[i]) {
stats[chartsNames[i]] = JSON.parse(data[i]);
}
}
}
callback(error, stats);
});
}
else {
callback(null, {});
}
}
module.exports = {
startDataCollectors: startDataCollectors,
getUserChartsData: getUserChartsData,
getPoolChartsData: getPoolChartsData
};

View File

@ -0,0 +1,11 @@
var fs = require('fs');
var async = require('async');
var http = require('http');
var charts = require('./charts.js');
var logSystem = 'chartsDataCollector';
require('./exceptionWriter.js')(logSystem);
log('info', logSystem, 'Started');
charts.startDataCollectors();

View File

@ -2,7 +2,7 @@ var fs = require('fs');
var async = require('async');
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
var logSystem = 'payments';

View File

@ -13,7 +13,7 @@ var threadId = '(Thread ' + process.env.forkId + ') ';
var logSystem = 'pool';
require('./exceptionWriter.js')(logSystem);
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
var utils = require('./utils.js');
var log = function(severity, system, text, data){

View File

@ -1,155 +1,259 @@
<!DOCTYPE html>
<html>
<head lang="en">
<meta charset="UTF-8">
<title></title>
<meta charset="UTF-8">
<title></title>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.0/jquery.timeago.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.0/jquery.timeago.min.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/handlebars.js/1.3.0/handlebars.min.js"></script>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<style>
#statsHolder{
margin-bottom: 0;
}
.luckGood{
color: darkgreen;
}
.luckBad{
color: darkred;
}
</style>
<link href="//netdna.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css" rel="stylesheet">
<script src="config.js"></script>
<link href="custom.css" rel="stylesheet">
<script src="custom.js"></script>
<script>
<style>
#statsHolder {
margin-bottom: 0;
}
.luckGood {
color: #03a678;
}
.luckBad {
color: #e66b6b;
}
body {
padding-top: 65px;
padding-bottom: 80px;
overflow-y: scroll;
}
.navbar {
font-size: 1.2em;
}
.layout {
position: relative;
}
#loading {
font-size: 2em;
position: absolute;
width: 100%;
text-align: center;
top: 10%;
}
footer {
position: fixed;
bottom: 0;
width: 100%;
background-color: #f5f5f5;
}
footer > div {
margin: 10px auto;
text-align: center;
}
</style>
<script src="config.js"></script>
<script>
$(function(){
getStats();
});
$(function() {
$.get(api + '/stats', function(data) {
routePage();
});
});
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);
}
};
function getReadableCoins(coins) {
return (parseInt(coins || 0) / coinUnits).toFixed(coinUnits.toString().length - 1);
}
function getReadableHashRateString(hashrate) {
var i = 0;
var byteUnits = [' H', ' KH', ' MH', ' GH', ' TH', ' PH' ];
while(hashrate > 1024) {
hashrate = hashrate / 1024;
i++;
}
return hashrate.toFixed(2) + byteUnits[i];
}
window.onhashchange = function() {
routePage();
};
function fetchLiveStats() {
$.ajax({
url: api + '/live_stats',
dataType: 'json',
cache: 'false'
}).done(function(data) {
//pulseLiveUpdate();
//lastStats = data;
//updateIndex();
if(currentPage.update) {
currentPage.update();
}
}).always(function() {
fetchLiveStats();
});
}
// init Handlebars template
function renderTemplate(usersData, templateId, view) {
var source = $(templateId).html(),
template = Handlebars.compile(source),
context = usersData,
html = template(context);
$(view).html(html);
}
function sortTable() {
var table = $(this).parents('table').eq(0),
rows = table.find('tr:gt(0)').toArray().sort(comparer($(this).index()));
this.asc = !this.asc;
if(!this.asc) {
rows = rows.reverse()
}
for(var i = 0; i < rows.length; i++) {
table.append(rows[i])
}
}
function comparer(index) {
return function(a, b) {
var valA = getCellValue(a, index), valB = getCellValue(b, index);
return $.isNumeric(valA) && $.isNumeric(valB) ? valA - valB : valA.localeCompare(valB)
}
}
function getCellValue(row, index) {
return $(row).children('td').eq(index).data("sort")
}
var currentPage;
var xhrPageLoading;
function routePage(loadedCallback) {
if(currentPage && currentPage.destroy) {
currentPage.destroy();
}
$('#page').html('');
$('#loading').show();
if(xhrPageLoading) {
xhrPageLoading.abort();
}
$('.hot_link').removeClass('active');
var $link = $('a.hot_link[href="' + (window.location.hash || '#') + '"]');
$link.addClass('active');
var page = $link.data('page');
xhrPageLoading = $.ajax({
url: 'pages/' + page,
cache: false,
success: function(data) {
$('#loading').hide();
$('#page').show().html(data);
currentPage && currentPage.update && currentPage.update();
if(loadedCallback) {
loadedCallback();
}
}
});
}
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);
}
};
function getReadableCoins(coins){
return (parseInt(coins || 0) / coinUnits).toFixed(coinUnits.toString().length - 1);
}
function getStats(promptPassword){
var password = docCookies.getItem('password');
if (!password || promptPassword)
password = prompt('Enter admin password');
$('#loading').show();
$.ajax({
url: api + '/admin_stats',
data: {password: password},
success: function(data){
docCookies.setItem('password', password, Infinity);
$('#loading').hide();
renderData(data);
},
error: function(e){
docCookies.removeItem('password');
getStats(true);
}
});
}
var formatLuck = function(difficulty, shares){
if (difficulty > shares){
var percent = 100 - Math.round(shares / difficulty * 100);
return '<span class="luckGood">' + percent + '%</span>';
}
else{
var percent = (100 - Math.round(difficulty / shares * 100)) * -1;
return '<span class="luckBad">' + percent + '%</span>';
}
};
function renderData(data){
$('#totalOwed').text(getReadableCoins(data.totalOwed));
$('#totalPaid').text(getReadableCoins(data.totalPaid));
$('#totalMined').text(getReadableCoins(data.totalRevenue));
$('#profit').text(getReadableCoins(data.totalRevenue - data.totalOwed - data.totalPaid));
$('#averageLuck').html(formatLuck(data.totalDiff, data.totalShares));
$('#orphanPercent').text((data.blocksOrphaned / data.blocksUnlocked * 100).toFixed(2));
$('#registeredAddresses').text(data.totalWorkers);
}
</script>
</script>
</head>
<body>
<div class="navbar navbar-inverse navbar-fixed-top" role="navigation">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle" data-toggle="collapse" data-target=".navbar-collapse">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="#">Admin Center</a>
</div>
<div class="collapse navbar-collapse">
<!-- <div class="navbar-form navbar-right">
<a href="" class="btn btn-primary"><i class="fa fa-sign-out"></i> Sign out</a>
</div> -->
</div>
</div>
</div>
<div class="container">
<div class="row">
<div class="col-sm-3">
<div class="list-group">
<a data-page="admin/statistics.html" href="#" class="list-group-item hot_link active"> <i class="fa fa-bar-chart-o"></i> Statistics </a> <a data-page="admin/monitoring.html" href="#monitoring" class="hot_link list-group-item"> <i class="fa fa-eye"></i> Monitoring </a> <a data-page="admin/userslist.html" href="#users_list" class=" hot_link list-group-item"> <i class="fa fa-users"></i> Users List </a>
</div>
</div>
<div class="col-sm-9 layout">
<p id="loading" class="text-center"><i class="fa fa-circle-o-notch fa-spin"></i></p>
<h3>Admin Center <i id="loading" class="fa fa-circle-o-notch fa-spin"></i></h3>
<hr>
<h4>Stats</h4>
<dl class="dl-horizontal" id="statsHolder">
<dt>Total Owed</dt><dd id="totalOwed">...</dd>
<dt>Total Paid</dt><dd id="totalPaid">...</dd>
<dt>Total Mined</dt><dd id="totalMined">...</dd>
<dt>Profit (before tx fees)</dt><dd id="profit">...</dd>
<dt>Average Luck</dt><dd id="averageLuck">...</dd>
<dt>Orphan Percent</dt><dd id="orphanPercent">...</dd>
<dt>Registered Addresses</dt><dd id="registeredAddresses">...</dd>
</dl>
<br>
<hr>
<h4>Miner Lookup</h4>
<div id="page"></div>
</div>
</div>
</div>
</body>
</html>
</html>

View File

@ -387,7 +387,7 @@ a.list-group-item:last-child {
cursor: pointer;
}
.usersList tr > th:first-child {
width: 50%;
width: 40%;
}
strong,
b {

View File

@ -9,9 +9,8 @@
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.0/jquery.timeago.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
@ -27,7 +26,7 @@
text-transform: capitalize;
}
body {
padding-top: 90px;
padding-top: 65px;
padding-bottom: 80px;
overflow-y: scroll;
}
@ -120,6 +119,10 @@
}
};
function getTransactionUrl(id) {
return transactionExplorer.replace('{symbol}', lastStats.config.symbol.toLowerCase()).replace('{id}', id);
}
$.fn.update = function(txt){
var el = this[0];
if (el.textContent !== txt)

View File

@ -0,0 +1,130 @@
<!-- /// define Handlebars template /// -->
<script id="monitoringInfo" type="text/x-handlebars-template">
<div class="tab-pane active" id="rpcLog">
<div class="row">
<div class="col-sm-6">
<h3>Daemon</h3>
<ul class="list-unstyled">
<li><strong>Last check:</strong> {{monitoringDaemon.lastCheck}}</li>
<li><strong>Last status:</strong>
<span id="daemonStatus">{{monitoringDaemon.lastStatus}}</span>
</li>
<li><strong>Last response:</strong>
<pre>{{monitoringDaemon.lastResponse}}</pre>
</li>
<li><strong>Last fail:</strong>
<span>{{monitoringDaemon.lastFail}}</span>
</li>
<li><strong>Last fail response:</strong>
<pre>{{monitoringDaemon.lastFailResponse}}</pre>
</li>
</ul>
</div>
<div class="col-sm-6">
<h3>Wallet</h3>
<ul class="list-unstyled">
<li><strong>Last check:</strong> {{monitoringWallet.lastCheck}}</li>
<li><strong>Last status:</strong>
<span id="walletStatus">{{monitoringWallet.lastStatus}}</span>
</li>
<li><strong>Last response:</strong>
<pre>{{monitoringWallet.lastResponse}}</pre>
</li>
<li><strong>Last fail:</strong>
<span>{{monitoringWallet.lastFail}}</span>
</li>
<li><strong>Last fail response:</strong>
<pre>{{monitoringWallet.lastFailResponse}}</span></pre>
</ul>
</div>
</div>
<h3>Logs</h3>
<table class="table table-hover table-striped logList" id="logTable">
<thead>
<tr>
<th class="sort">Name <i class="fa fa-sort"></i></th>
<th class="sort">Modified <i class="fa fa-sort"></i></th>
<th class="sort">Size <i class="fa fa-sort"></i></th>
</tr>
</thead>
<tbody>
{{#each logs}}
<tr>
<td data-sort="{{@key}}"><a href="{{this.link}}" target="_blank">{{@key}}</a></td>
<td data-sort="{{this.changed}}">{{this.changed}}</td>
<td data-sort="{{this.size}}">{{this.size}} bytes</td>
</tr>
{{/each}}
</tbody>
</table>
</div>
</script>
<script>
function getCheckTime(timestamp) {
return timestamp ? $.timeago(new Date(timestamp * 1000).toISOString()) : null;
}
function monitoringInfoParse(data) {
var monitoringDaemon = {
lastCheck: getCheckTime(data['monitoring'].daemon.lastCheck) || 'never',
lastStatus: data['monitoring'].daemon.lastStatus || '',
lastFail: getCheckTime(data['monitoring'].daemon.lastFail) || 'never',
lastFailResponse: data['monitoring'].daemon.lastFailResponse || ' ',
lastResponse: data['monitoring'].daemon.lastResponse || ' '
};
var monitoringWallet = {
lastCheck: getCheckTime(data['monitoring'].wallet.lastCheck) || 'never',
lastStatus: data['monitoring'].wallet.lastStatus || '',
lastFail: getCheckTime(data['monitoring'].wallet.lastFail) || 'never',
lastFailResponse: data['monitoring'].wallet.lastFailResponse || ' ',
lastResponse: data['monitoring'].wallet.lastResponse || ' '
};
var properData = {};
for(var prop in data) {
if(data.hasOwnProperty('logs')) {
properData['logs'] = data['logs'];
for(var log in data['logs']) {
properData['logs'][log].changed = Date(data['logs'][log].changed * 1000);
data['logs'][log].link = api + '/admin_log?file=' + log + '&password=' + docCookies.getItem('password');
}
}
}
properData['monitoringDaemon'] = monitoringDaemon;
properData['monitoringWallet'] = monitoringWallet;
return properData;
}
function renderLogInfo() {
$.ajax({
url: api + '/admin_monitoring',
data: {password: docCookies.getItem('password')},
cache: false,
dataType: 'json',
success: function(data) {
renderTemplate(monitoringInfoParse(data), '#monitoringInfo', '#monitoringInfoView');
$('#daemonStatus').addClass(data['monitoring'].daemon.lastStatus == 'ok' ? 'text-success' : 'text-danger');
$('#walletStatus').addClass(data['monitoring'].wallet.lastStatus == 'ok' ? 'text-success' : 'text-danger');
$('#logTable th.sort').on('click', sortTable);
}
});
}
$(function() {
renderLogInfo();
});
</script>
<div class="adminMonitor">
<!-- Tab panes -->
<div class="tab-content">
<div id="monitoringInfoView"></div>
</div>
</div>

View File

@ -0,0 +1,90 @@
<script>
var formatLuck = function(difficulty, shares) {
if(difficulty <= shares) {
var percent = (100 - Math.round(difficulty / shares * 100)) * -1;
return '<span class="luckBad">' + percent + '%</span>';
}
else {
var percent = 100 - Math.round(shares / difficulty * 100);
return '<span class="luckGood">' + percent + '%</span>';
}
};
function getStats(promptPassword) {
var password = docCookies.getItem('password');
if(!password || promptPassword) {
password = prompt('Enter admin password');
}
$('#loading').show();
$.ajax({
url: api + '/admin_stats',
data: {password: password},
success: function(data) {
docCookies.setItem('password', password, Infinity);
$('#loading').hide();
renderData(data);
},
error: function(e) {
docCookies.removeItem('password');
getStats(true);
}
});
}
function renderData(data) {
$('#totalOwed').text(getReadableCoins(data.totalOwed));
$('#totalPaid').text(getReadableCoins(data.totalPaid));
$('#totalMined').text(getReadableCoins(data.totalRevenue));
$('#profit').text(getReadableCoins(data.totalRevenue - data.totalOwed - data.totalPaid));
$('#averageLuck').html(formatLuck(data.totalDiff, data.totalShares));
$('#orphanPercent').text((data.blocksOrphaned / data.blocksUnlocked * 100).toFixed(2));
$('#registeredAddresses').text(data.totalWorkers);
}
$(function() {
getStats();
});
</script>
<!-- <h4>Stats</h4>
<dl class="dl-horizontal" id="statsHolder">
<dt>Total Owed</dt><dd id="totalOwed">...</dd>
<dt>Total Paid</dt><dd id="totalPaid">...</dd>
<dt>Total Mined</dt><dd id="totalMined">...</dd>
<dt>Profit (before tx fees)</dt><dd id="profit">...</dd>
<dt>Average Luck</dt><dd id="averageLuck">...</dd>
<dt>Orphan Percent</dt><dd id="orphanPercent">...</dd>
<dt>Registered Addresses</dt><dd id="registeredAddresses">...</dd>
</dl> -->
<div class="row adminStats">
<div class="col-sm-3 color1">
<h4>Total Owed</h4>
<span class="statValue" id="totalOwed">...</span>
</div>
<div class="col-sm-3 color2">
<h4>Total Paid</h4>
<span class="statValue" id="totalPaid">...</span>
</div>
<div class="col-sm-3 color3">
<h4>Total Mined</h4>
<span class="statValue" id="totalMined">...</span>
</div>
<div class="col-sm-3 color4">
<h4>Profit (before tx fees)</h4>
<span class="statValue" id="profit">...</span>
</div>
<div class="col-sm-4 color5">
<h4>Average Luck</h4>
<span class="statValue lead" id="averageLuck">...</span>
</div>
<div class="col-sm-4 color6">
<h4>Orphan Percent</h4>
<span class="statValue lead" id="orphanPercent">...</span>
</div>
<div class="col-sm-4 color7">
<h4>Registered Addresses</h4>
<span class="statValue lead" id="registeredAddresses">...</span>
</div>
</div>

View File

@ -0,0 +1,77 @@
<style>
td {
text-align: center
}
</style>
<!-- /// define Handlebars template /// -->
<script id="usersListTable" type="text/x-handlebars-template">
{{#each users}}
<tr>
<td><a href="./?wallet={{this.number}}">{{this.number}}</a></td>
<td data-sort="{{this.wallet.hashrate}}">{{this.readableHashrate}}</td>
<td data-sort="{{this.wallet.hashes}}">{{this.readableHashes}}</td>
<td data-sort="{{this.wallet.pending}}">{{this.wallet.pending}}</td>
<td data-sort="{{this.wallet.paid}}">{{this.wallet.paid}}</td>
<td data-sort="{{this.wallet.lastShare}}">{{this.timeago}}</td>
</tr>
{{/each}}
</script>
<script>
function parseUsers(wallets) {
var walletsArray = [],
properObject = {};
for(var wallet in wallets) {
if(wallets.hasOwnProperty(wallet)) {
walletsArray.push({
number: wallet,
wallet: wallets[wallet],
timeago: $.timeago(new Date(wallets[wallet].lastShare * 1000).toISOString()),
readableHashrate: getReadableHashRateString(wallets[wallet].hashrate) + '/s',
readableHashes: getReadableHashRateString(wallets[wallet].hashes)
});
}
}
properObject['users'] = walletsArray.sort(function(a, b) {
return a.wallet.hashrate - b.wallet.hashrate
}).reverse();
return properObject;
}
function cretaUserTable() {
$.ajax({
url: api + '/admin_users',
data: {password: docCookies.getItem('password')},
cache: false,
dataType: 'json',
success: function(data) {
renderTemplate(parseUsers(data), '#usersListTable', '#template');
}
});
}
$(function() {
$('[data-toggle="tooltip"]').tooltip();
$('.usersList th.sort').on('click', sortTable);
cretaUserTable();
});
</script>
<div class="table-responsive">
<table class="table table-hover table-striped usersList">
<thead>
<tr>
<th>Wallet</th>
<th class="sort" style="width:10%;">Hashrate <i class="fa fa-sort"></i></th>
<th class="sort" style="width:10%;">Hashes <i class="fa fa-sort"></i></th>
<th class="sort" style="width:16%;">Pending <i class="fa fa-sort"></i></th>
<th class="sort" style="width:10%;">Paid <i class="fa fa-sort"></i></th>
<th class="sort" style="width:14%;">Last share <i class="fa fa-sort"></i></th>
</tr>
</thead>
<tbody id="template">
</tbody>
</table>
</div>

View File

@ -99,19 +99,127 @@
<div><i class="fa fa-tachometer"></i> Hash Rate: <span id="poolHashrate"></span></div>
<div><i class="fa fa-clock-o"></i> Block Found: <span id="poolLastBlockFound"></span></div>
<div><i class="fa fa-users"></i> Connected Miners: <span id="poolMiners"></span></div>
<div><i class="fa fa-gift"></i> Donations: <span id="poolDonations"></span></div>
<div id="donations"><i class="fa fa-gift"></i> Donations: <span id="poolDonations"></span></div>
<div><i class="fa fa-money"></i> Total Pool Fee: <span id="poolFee"></span></div>
<div><i class="fa fa-history"></i> Block Found Every: <span id="blockSolvedTime"></span> (est.)</div>
</div>
<div class="col-md-4 stats">
<div class="col-md-4 stats marketRate">
<h3 id="marketHeader">Market</h3>
<div class="marketFooter">Updated: <span id="marketLastUpdated"></span></div>
<div class="marketFooter">Powered by <a href="https://www.cryptonator.com/">Cryptonator</a></div>
</div>
</div>
<hr>
<!-- <hr> -->
<div class="row chartsPoolStat">
<div class="col-sm-2 chartWrap">
<h4>Hash/USD <span data-toggle="tooltip" data-placement="top" data-original-title="Reward * Rate / Difficulty"><i class="fa fa-question-circle"></i></span></h4>
<div id="chartHashUsd" data-chart="profit">
<div class="chart"></div>
<!-- <p class="text-center" id="cur_profit">-</p> -->
</div>
</div>
<div class="col-sm-20 chartWrap">
<h4>Price in USD</h4>
<div id="chartPriceUsd" data-chart="price">
<div class="chart"></div>
<!-- <p class="text-center" id="cur_price">-</p> -->
</div>
</div>
<div class="col-sm-20 chartWrap">
<h4>Difficulty</h4>
<div id="chartDifficulty" data-chart="diff">
<div class="chart"></div>
<!-- <p class="text-center" id="cur_diff">-</p> -->
</div>
</div>
<div class="col-sm-20 chartWrap">
<h4>Hashrate</h4>
<div id="chartHashrate" data-chart="hashrate">
<div class="chart"></div>
<!-- <p class="text-center" id="cur_hashrate">-</p> -->
</div>
</div>
<div class="col-sm-20 chartWrap">
<h4>Workers</h4>
<div id="chartWorkers" data-chart="workers">
<div class="chart"></div>
<!-- <p class="text-center" id="cur_workers">-</p> -->
</div>
</div>
<script>
var currencyGraphStat = {
type: 'line',
width: '100%',
height: '75',
lineColor: '#03a678',
fillColor: 'rgba(3, 166, 120, .3)',
spotColor: '#00bf00',
minSpotColor: '#00bf00',
maxSpotColor: '#00bf00',
highlightLineColor: '#236d26',
spotRadius: 3,
// chartRangeMin: 0,
minSpotColor: null,
maxSpotColor: null,
drawNormalOnTop: false,
tooltipFormat: '<b>{{y}}</b>, {{offset:names}}',
tooltipValueLookups: {
names: null
}
}
var userGraphStat = {
type: 'line',
width: '100%',
height: '180',
lineColor: '#03a678',
fillColor: 'rgba(3, 166, 120, .3)',
spotColor: '#00bf00',
minSpotColor: '#00bf00',
maxSpotColor: '#00bf00',
highlightLineColor: '#236d26',
spotRadius: 3,
minSpotColor: null,
maxSpotColor: null,
drawNormalOnTop: false,
chartRangeMin: 0,
tooltipFormat: '<b>{{y}}</b>, {{offset:names}}',
tooltipValueLookups: {
names: null
}
}, userGraphStat2 = {
type: 'line',
width: '100%',
height: '180',
lineColor: '#03a678',
fillColor: 'rgba(3, 166, 120, .3)',
spotColor: '#00bf00',
minSpotColor: '#00bf00',
maxSpotColor: '#00bf00',
highlightLineColor: '#236d26',
spotRadius: 3,
minSpotColor: null,
maxSpotColor: null,
drawNormalOnTop: false,
chartRangeMin: 0,
tooltipFormat: '<b>{{y}}</b>, {{offset:names}}',
tooltipValueLookups: {
names: null
}
}
$(function() {
$('[data-toggle="tooltip"]').tooltip();
});
</script>
</div>
<!-- <hr> -->
<div id="miningProfitCalc">
<h3>Estimate Mining Profits</h3>
@ -147,13 +255,34 @@
</button></span>
</div>
<div id="addressError"></div>
<div class="yourStats"><i class="fa fa-key"></i> Address: <span id="yourAddressDisplay"></span></div>
<div class="yourStats"><i class="fa fa-bank"></i> Pending Balance: <span id="yourPendingBalance"></span></div>
<div class="yourStats"><i class="fa fa-money"></i> Total Paid: <span id="yourPaid"></span></div>
<div class="yourStats"><i class="fa fa-clock-o"></i> Last Share Submitted: <span id="yourLastShare"></span></div>
<div class="yourStats"><i class="fa fa-tachometer"></i> Hash Rate: <span id="yourHashrateHolder"></span></div>
<div class="yourStats"><i class="fa fa-cloud-upload"></i> Total Hashes Submitted: <span id="yourHashes"></span></div>
<div class="row">
<div class="col-sm-4 stats">
<div id="addressError"></div>
<!-- <div class="yourStats"><i class="fa fa-key"></i> Address: <span id="yourAddressDisplay"></span></div> -->
<div class="yourStats"><i class="fa fa-bank"></i> Pending Balance: <span id="yourPendingBalance"></span></div>
<div class="yourStats"><i class="fa fa-money"></i> Total Paid: <span id="yourPaid"></span></div>
<div class="yourStats"><i class="fa fa-clock-o"></i> Last Share Submitted: <span id="yourLastShare"></span></div>
<div class="yourStats"><i class="fa fa-tachometer"></i> Hash Rate: <span id="yourHashrateHolder"></span></div>
<div class="yourStats"><i class="fa fa-cloud-upload"></i> Total Hashes Submitted: <span id="yourHashes"></span></div>
</div>
<div class="col-sm-4">
<div class="userChart" data-chart="user_hashrate">
<h4>Hash Rate</h4>
<div class="chart">
</div>
</div>
</div>
<div class="col-sm-4">
<div class="userChart" data-chart="user_payments">
<h4>Payments</h4>
<div class="chart">
</div>
</div>
</div>
</div>
<br class="yourStats">
@ -221,12 +350,12 @@
totalFee += lastStats.config.donation;
totalFee += lastStats.config.coreDonation;
var feeText = [];
if (lastStats.config.donation > 0) feeText.push(lastStats.config.donation + '% to pool dev');
if (lastStats.config.donation > 0) feeText.push(lastStats.config.donation + '% to pool dev');
if (lastStats.config.coreDonation > 0) feeText.push(lastStats.config.coreDonation + '% to core devs');
updateText('poolDonations', feeText.join(', '));
updateText('poolDonations', feeText.join(', '));
}
else{
updateText('poolDonations', '');
$('#donations').hide()
}
updateText('poolFee', totalFee + '%');
@ -290,6 +419,11 @@
for (var i = 0; i < cryptonatorWidget.length; i++){
(function(i){
xhrMarketGets[cryptonatorWidget[i]] = $.get('https://www.cryptonator.com/api/ticker/' + cryptonatorWidget[i], function(data){
if(data.error) {
return;
}
$('.marketRate').show();
marketsData[i] = data;
completedFetches++;
if (completedFetches !== cryptonatorWidget.length) return;
@ -382,13 +516,13 @@
},
dataType: 'json',
cache: 'false',
success: function(data){
success: function(data){
$('#lookUp > span:last-child').hide();
$('#lookUp > span:first-child').show();
if (!data.stats){
$('.yourStats').hide();
$('.yourStats, .userChart').hide();
$('#addressError').text(data.error).show();
if (addressTimeout) clearTimeout(addressTimeout);
@ -401,7 +535,6 @@
$('#addressError').hide();
updateText('yourAddressDisplay', address);
if (data.stats.lastShare)
$('#yourLastShare').timeago('update', new Date(parseInt(data.stats.lastShare) * 1000).toISOString());
@ -415,29 +548,68 @@
renderPayments(data.payments);
$('.yourStats').show();
$('.yourStats, .userChart').show(function(){
xhrRenderUserCharts = $.ajax({
url: api + '/stats_address?address=' + address + '&longpoll=false',
cache: false,
dataType: 'json',
success: function(data) {
createUserCharts(data);
}
});
});
docCookies.setItem('mining_address', address, Infinity);
fetchAddressStats(true);
},
error: function(e){
if (e.statusText === 'abort') return;
$('#lookUp').html(lookupBtnHtml);
$('#addressError').text('Connection error').show();
if (addressTimeout) clearTimeout(addressTimeout);
addressTimeout = setTimeout(function(){
fetchAddressStats(false);
}, 2000);
}
});
}
fetchAddressStats(false);
});
var urlWalletAddress = location.search.split('wallet=')[1] || 0;
var address = urlWalletAddress || docCookies.getItem('mining_address');
var xhrRenderUserCharts;
function createUserCharts(data) {
if (data.hasOwnProperty("charts")) {
var graphData = {
hashrate: getGraphData(data["charts"].hashrate),
payments: getGraphData(data["charts"].payments)
};
for (var graphType in graphData) {
if (graphData.hasOwnProperty("hashrate")) {
$('[data-chart=user_hashrate] .chart').sparkline(graphData["hashrate"].values, userGraphStat);
userGraphStat.tooltipValueLookups.names = graphData["hashrate"].names;
}
if (graphData.hasOwnProperty("payments")) {
$('[data-chart=user_payments] .chart').sparkline(graphData["payments"].values, userGraphStat2);
userGraphStat2.tooltipValueLookups.names = graphData["payments"].names;
}
}
}
}
var address = docCookies.getItem('mining_address');
if (address){
$('#yourStatsInput').val(address);
@ -466,4 +638,100 @@
});
});
</script>
/* Show stats of the currency */
function getGraphData (rawData) {
var graphData = {
names: [],
values: []
};
if(rawData) {
for (var i = 0, xy; xy = rawData[i]; i++) {
graphData.names.push(new Date(xy[0]*1000).toUTCString());
graphData.values.push(xy[1]);
}
}
return graphData;
}
function createCharts(data) {
if (data.hasOwnProperty("charts")) {
var graphData = {
profit: getGraphData(data.charts.profit),
diff: getGraphData(data.charts.difficulty),
hashrate: getGraphData(data.charts.hashrate),
price: getGraphData(data.charts.price),
workers: getGraphData(data.charts.workers)
};
for(var graphType in graphData) {
if(graphData.hasOwnProperty(graphType)) {
currencyGraphStat.tooltipValueLookups.names = graphData[graphType].names;
$('[data-chart=' + graphType + '] .chart').sparkline(graphData[graphType].values, currencyGraphStat);
}
}
}
}
function loadStatistics () {
$.get(api + '/stats', function (stats) {
if (stats) {
showStats(stats)
}
});
}
function showStats (stats) {
$('#cur_diff').text(stats.network.difficulty);
$('#cur_hashrate').text(getReadableHashRateString(stats.pool.hashrate) + '/s');
$('#cur_workers').text(stats.pool.miners);
// Some values aren't available in stats.
// Get the values from charts data.
if (stats.hasOwnProperty('charts')) {
var priceData = stats.charts.price;
$('#cur_price').text(priceData ? priceData[priceData.length-1][1] : '---');
}
if (stats.hasOwnProperty('charts')) {
var profitValue;
var profitData = stats.charts.profit;
if (profitData) {
profitValue = profitData[profitData.length-1][1];
if (profitValue) {
profitValue = profitValue.toPrecision(3).toString().replace(/(.*?)e(\+|\-)(\d+)/, '$1<sup>10<sup>$2$3</sup></sup>');
}
else {
profitValue = '---';
}
}
else {
profitValue = '---';
}
$('#cur_profit').html(profitValue);
}
}
var xhrRenderCharts;
$(function(){
xhrRenderCharts = $.ajax({
url: api + '/stats',
cache: false,
success: function(data) {
createCharts(data);
}
});
});
</script>