Added new statistics charts & new admin panel
parent
c2a9aae907
commit
92ec67f866
57
README.md
57
README.md
|
@ -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
|
||||
|
|
58
config.json
58
config.json
|
@ -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
26
init.js
|
@ -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);
|
||||
});
|
||||
}
|
||||
|
|
240
lib/api.js
240
lib/api.js
|
@ -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]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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
|
||||
}
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
};
|
|
@ -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();
|
|
@ -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';
|
||||
|
|
|
@ -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){
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -387,7 +387,7 @@ a.list-group-item:last-child {
|
|||
cursor: pointer;
|
||||
}
|
||||
.usersList tr > th:first-child {
|
||||
width: 50%;
|
||||
width: 40%;
|
||||
}
|
||||
strong,
|
||||
b {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue