Skip to content

Commit 92ec67f

Browse files
committedAug 7, 2014
Added new statistics charts & new admin panel
1 parent c2a9aae commit 92ec67f

17 files changed

+1474
-194
lines changed
 

‎README.md

+57
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,63 @@ Explanation for each field:
282282
"host": "127.0.0.1",
283283
"port": 6379
284284
}
285+
286+
/* Monitoring RPC services. Statistics will be displayed in Admin panel */
287+
"monitoring": {
288+
"daemon": {
289+
"checkInterval": 60, //interval of sending rpcMethod request
290+
"rpcMethod": "getblockcount" //RPC method name
291+
},
292+
"wallet": {
293+
"checkInterval": 60,
294+
"rpcMethod": "getbalance"
295+
}
296+
297+
/* Collect pool statistics to display in frontend charts */
298+
"charts": {
299+
"pool": {
300+
"hashrate": {
301+
"enabled": true, //enable data collection and chart displaying in frontend
302+
"updateInterval": 60, //how often to get current value
303+
"stepInterval": 1800, //chart step interval calculated as average of all updated values
304+
"maximumPeriod": 86400 //chart maximum periods (chart points number = maximumPeriod / stepInterval = 48)
305+
},
306+
"workers": {
307+
"enabled": true,
308+
"updateInterval": 60,
309+
"stepInterval": 1800, //chart step interval calculated as maximum of all updated values
310+
"maximumPeriod": 86400
311+
},
312+
"difficulty": {
313+
"enabled": true,
314+
"updateInterval": 1800,
315+
"stepInterval": 10800,
316+
"maximumPeriod": 604800
317+
},
318+
"price": { //USD price of one currency coin received from cryptonator.com/api
319+
"enabled": true,
320+
"updateInterval": 1800,
321+
"stepInterval": 10800,
322+
"maximumPeriod": 604800
323+
},
324+
"profit": { //Reward * Rate / Difficulty
325+
"enabled": true,
326+
"updateInterval": 1800,
327+
"stepInterval": 10800,
328+
"maximumPeriod": 604800
329+
}
330+
},
331+
"user": { //chart data displayed in user stats block
332+
"hashrate": {
333+
"enabled": true,
334+
"updateInterval": 180,
335+
"stepInterval": 1800,
336+
"maximumPeriod": 86400
337+
},
338+
"payments": { //payment chart uses all user payments data stored in DB
339+
"enabled": true
340+
}
341+
}
285342
```
286343
287344
#### 3) [Optional] Configure cryptonote-easy-miner for your pool

‎config.json

+58
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"coin": "monero",
33
"symbol": "XMR",
4+
"coinUnits": 1000000000000,
45

56
"logging": {
67
"files": {
@@ -113,5 +114,62 @@
113114
"redis": {
114115
"host": "127.0.0.1",
115116
"port": 6379
117+
},
118+
119+
"monitoring": {
120+
"daemon": {
121+
"checkInterval": 60,
122+
"rpcMethod": "getblockcount"
123+
},
124+
"wallet": {
125+
"checkInterval": 60,
126+
"rpcMethod": "getbalance"
127+
}
128+
},
129+
130+
"charts": {
131+
"pool": {
132+
"hashrate": {
133+
"enabled": true,
134+
"updateInterval": 60,
135+
"stepInterval": 1800,
136+
"maximumPeriod": 86400
137+
},
138+
"workers": {
139+
"enabled": true,
140+
"updateInterval": 60,
141+
"stepInterval": 1800,
142+
"maximumPeriod": 86400
143+
},
144+
"difficulty": {
145+
"enabled": true,
146+
"updateInterval": 1800,
147+
"stepInterval": 10800,
148+
"maximumPeriod": 604800
149+
},
150+
"price": {
151+
"enabled": true,
152+
"updateInterval": 1800,
153+
"stepInterval": 10800,
154+
"maximumPeriod": 604800
155+
},
156+
"profit": {
157+
"enabled": true,
158+
"updateInterval": 1800,
159+
"stepInterval": 10800,
160+
"maximumPeriod": 604800
161+
}
162+
},
163+
"user": {
164+
"hashrate": {
165+
"enabled": true,
166+
"updateInterval": 180,
167+
"stepInterval": 1800,
168+
"maximumPeriod": 86400
169+
},
170+
"payments": {
171+
"enabled": true
172+
}
173+
}
116174
}
117175
}

‎init.js

+24-2
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ if (cluster.isWorker){
3030
case 'cli':
3131
require('./lib/cli.js');
3232
break
33+
case 'chartsDataCollector':
34+
require('./lib/chartsDataCollector.js');
35+
break
36+
3337
}
3438
return;
3539
}
@@ -40,7 +44,7 @@ require('./lib/exceptionWriter.js')(logSystem);
4044

4145
var singleModule = (function(){
4246

43-
var validModules = ['pool', 'api', 'unlocker', 'payments'];
47+
var validModules = ['pool', 'api', 'unlocker', 'payments', 'chartsDataCollector'];
4448

4549
for (var i = 0; i < process.argv.length; i++){
4650
if (process.argv[i].indexOf('-module=') === 0){
@@ -75,13 +79,17 @@ var singleModule = (function(){
7579
case 'api':
7680
spawnApi();
7781
break;
82+
case 'chartsDataCollector':
83+
spawnChartsDataCollector();
84+
break;
7885
}
7986
}
8087
else{
8188
spawnPoolWorkers();
8289
spawnBlockUnlocker();
8390
spawnPaymentProcessor();
8491
spawnApi();
92+
spawnChartsDataCollector();
8593
}
8694

8795
spawnCli();
@@ -228,4 +236,18 @@ function spawnApi(){
228236

229237
function spawnCli(){
230238

231-
}
239+
}
240+
241+
function spawnChartsDataCollector(){
242+
if (!config.charts) return;
243+
244+
var worker = cluster.fork({
245+
workerType: 'chartsDataCollector'
246+
});
247+
worker.on('exit', function(code, signal){
248+
log('error', logSystem, 'chartsDataCollector died, spawning replacement...');
249+
setTimeout(function(){
250+
spawnChartsDataCollector();
251+
}, 2000);
252+
});
253+
}

‎lib/api.js

+227-13
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ var zlib = require('zlib');
66
var async = require('async');
77

88
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
9+
var charts = require('./charts.js');
10+
var authSid = Math.round(Math.random() * 10000000000) + '' + Math.round(Math.random() * 10000000000);
911

1012
var logSystem = 'api';
1113
require('./exceptionWriter.js')(logSystem);
@@ -28,6 +30,7 @@ var currentStats = "";
2830
var currentStatsCompressed = "";
2931

3032
var minerStats = {};
33+
var minersHashrate = {};
3134

3235
var liveConnections = {};
3336
var addressConnections = {};
@@ -67,18 +70,20 @@ function collectStats(){
6770
var hashrates = replies[1];
6871

6972
minerStats = {};
73+
minersHashrate = {};
7074

7175
for (var i = 0; i < hashrates.length; i++){
7276
var hashParts = hashrates[i].split(':');
73-
minerStats[hashParts[1]] = (minerStats[hashParts[1]] || 0) + parseInt(hashParts[0]);
77+
minersHashrate[hashParts[1]] = (minerStats[hashParts[1]] || 0) + parseInt(hashParts[0]);
7478
}
7579

7680
var totalShares = 0;
7781

78-
for (var miner in minerStats){
79-
var shares = minerStats[miner];
82+
for (var miner in minersHashrate){
83+
var shares = minersHashrate[miner];
8084
totalShares += shares;
81-
minerStats[miner] = getReadableHashRateString(shares / config.api.hashrateWindow);
85+
minersHashrate[miner] = Math.round(shares / config.api.hashrateWindow);
86+
minerStats[miner] = getReadableHashRateString(minersHashrate[miner]);
8287
}
8388

8489
data.miners = Object.keys(minerStats).length;
@@ -133,7 +138,8 @@ function collectStats(){
133138
minPaymentThreshold: config.payments.minPayment,
134139
denominationUnit: config.payments.denomination
135140
});
136-
}
141+
},
142+
charts: charts.getPoolChartsData
137143
}, function(error, results){
138144

139145
log('info', logSystem, 'Stat collection finished: %d ms redis, %d ms daemon', [redisFinished - startTime, daemonFinished - startTime]);
@@ -237,7 +243,13 @@ function handleMinerStats(urlParts, response){
237243
}
238244
var stats = replies[0];
239245
stats.hashrate = minerStats[address];
240-
response.end(JSON.stringify({stats: stats, payments: replies[1]}));
246+
charts.getUserChartsData(address, replies[1], function(error, chartsData) {
247+
response.end(JSON.stringify({
248+
stats: stats,
249+
payments: replies[1],
250+
charts: chartsData
251+
}));
252+
});
241253
});
242254
}
243255
}
@@ -307,25 +319,59 @@ function handleGetBlocks(urlParts, response){
307319
});
308320
}
309321

322+
function handleGetMinersHashrate(response) {
323+
var reply = JSON.stringify({
324+
minersHashrate: minersHashrate
325+
});
326+
response.writeHead("200", {
327+
'Access-Control-Allow-Origin': '*',
328+
'Cache-Control': 'no-cache',
329+
'Content-Type': 'application/json',
330+
'Content-Length': reply.length
331+
});
332+
response.end(reply);
333+
}
310334

311-
collectStats();
335+
function parseCookies(request) {
336+
var list = {},
337+
rc = request.headers.cookie;
338+
rc && rc.split(';').forEach(function(cookie) {
339+
var parts = cookie.split('=');
340+
list[parts.shift().trim()] = unescape(parts.join('='));
341+
});
342+
return list;
343+
}
312344

313345
function authorize(request, response){
346+
if(request.connection.remoteAddress == '127.0.0.1') {
347+
return true;
348+
}
314349

315350
response.setHeader('Access-Control-Allow-Origin', '*');
316351

352+
var cookies = parseCookies(request);
353+
if(cookies.sid && cookies.sid == authSid) {
354+
return true;
355+
}
356+
317357
var sentPass = url.parse(request.url, true).query.password;
318358

359+
319360
if (sentPass !== config.api.password){
320361
response.statusCode = 401;
321362
response.end('invalid password');
322363
return;
323364
}
324365

366+
log('warn', logSystem, 'Admin authorized');
325367
response.statusCode = 200;
368+
369+
var cookieExpire = new Date( new Date().getTime() + 60*60*24*1000);
370+
response.setHeader('Set-Cookie', 'sid=' + authSid + '; path=/; expires=' + cookieExpire.toUTCString());
326371
response.setHeader('Cache-Control', 'no-cache');
327372
response.setHeader('Content-Type', 'application/json');
328373

374+
329375
return true;
330376
}
331377

@@ -406,6 +452,149 @@ function handleAdminStats(response){
406452

407453
}
408454

455+
456+
function handleAdminUsers(response){
457+
async.waterfall([
458+
// get workers Redis keys
459+
function(callback) {
460+
redisClient.keys(config.coin + ':workers:*', callback);
461+
},
462+
// get workers data
463+
function(workerKeys, callback) {
464+
var redisCommands = workerKeys.map(function(k) {
465+
return ['hmget', k, 'balance', 'paid', 'lastShare', 'hashes'];
466+
});
467+
redisClient.multi(redisCommands).exec(function(error, redisData) {
468+
var workersData = {};
469+
var addressLength = config.poolServer.poolAddress.length;
470+
for(var i in redisData) {
471+
var address = workerKeys[i].substr(-addressLength);
472+
var data = redisData[i];
473+
workersData[address] = {
474+
pending: data[0] / config.coinUnits,
475+
paid: data[1] / config.coinUnits,
476+
lastShare: data[2],
477+
hashes: data[3],
478+
hashrate: minersHashrate[address] ? minersHashrate[address] : 0
479+
};
480+
}
481+
callback(null, workersData);
482+
});
483+
}
484+
], function(error, workersData) {
485+
if(error) {
486+
response.end(JSON.stringify({error: 'error collecting users stats'}));
487+
return;
488+
}
489+
response.end(JSON.stringify(workersData));
490+
}
491+
);
492+
}
493+
494+
495+
function handleAdminMonitoring(response) {
496+
response.writeHead("200", {
497+
'Access-Control-Allow-Origin': '*',
498+
'Cache-Control': 'no-cache',
499+
'Content-Type': 'application/json'
500+
});
501+
async.parallel({
502+
monitoring: getMonitoringData,
503+
logs: getLogFiles
504+
}, function(error, result) {
505+
response.end(JSON.stringify(result));
506+
});
507+
}
508+
509+
function handleAdminLog(urlParts, response){
510+
var file = urlParts.query.file;
511+
var filePath = config.logging.files.directory + '/' + file;
512+
if(!file.match(/^\w+\.log$/)) {
513+
response.end('wrong log file');
514+
}
515+
response.writeHead(200, {
516+
'Content-Type': 'text/plain',
517+
'Cache-Control': 'no-cache',
518+
'Content-Length': fs.statSync(filePath).size
519+
});
520+
fs.createReadStream(filePath).pipe(response)
521+
}
522+
523+
524+
function startRpcMonitoring(rpc, module, method, interval) {
525+
setInterval(function() {
526+
rpc(method, {}, function(error, response) {
527+
var stat = {
528+
lastCheck: new Date() / 1000 | 0,
529+
lastStatus: error ? 'fail' : 'ok',
530+
lastResponse: JSON.stringify(error ? error : response)
531+
};
532+
if(error) {
533+
stat.lastFail = stat.lastCheck;
534+
stat.lastFailResponse = stat.lastResponse;
535+
}
536+
var key = getMonitoringDataKey(module);
537+
var redisCommands = [];
538+
for(var property in stat) {
539+
redisCommands.push(['hset', key, property, stat[property]]);
540+
}
541+
redisClient.multi(redisCommands).exec();
542+
});
543+
}, interval * 1000);
544+
}
545+
546+
function getMonitoringDataKey(module) {
547+
return config.coin + ':status:' + module;
548+
}
549+
550+
function initMonitoring() {
551+
var modulesRpc = {
552+
daemon: apiInterfaces.rpcDaemon,
553+
wallet: apiInterfaces.rpcWallet
554+
};
555+
for(var module in config.monitoring) {
556+
var settings = config.monitoring[module];
557+
if(settings.checkInterval) {
558+
startRpcMonitoring(modulesRpc[module], module, settings.rpcMethod, settings.checkInterval);
559+
}
560+
}
561+
}
562+
563+
564+
565+
function getMonitoringData(callback) {
566+
var modules = Object.keys(config.monitoring);
567+
var redisCommands = [];
568+
for(var i in modules) {
569+
redisCommands.push(['hgetall', getMonitoringDataKey(modules[i])])
570+
}
571+
redisClient.multi(redisCommands).exec(function(error, results) {
572+
var stats = {};
573+
for(var i in modules) {
574+
if(results[i]) {
575+
stats[modules[i]] = results[i];
576+
}
577+
}
578+
callback(error, stats);
579+
});
580+
}
581+
582+
function getLogFiles(callback) {
583+
var dir = config.logging.files.directory;
584+
fs.readdir(dir, function(error, files) {
585+
var logs = {};
586+
for(var i in files) {
587+
var file = files[i];
588+
var stats = fs.statSync(dir + '/' + file);
589+
logs[file] = {
590+
size: stats.size,
591+
changed: Date.parse(stats.mtime) / 1000 | 0
592+
}
593+
}
594+
callback(error, logs);
595+
});
596+
}
597+
409598
var server = http.createServer(function(request, response){
410599

411600
if (request.method.toUpperCase() === "OPTIONS"){
@@ -426,12 +615,13 @@ var server = http.createServer(function(request, response){
426615

427616
switch(urlParts.pathname){
428617
case '/stats':
429-
var reply = currentStatsCompressed;
618+
var deflate = request.headers['accept-encoding'] && request.headers['accept-encoding'].indexOf('deflate') != -1;
619+
var reply = deflate ? currentStatsCompressed : currentStats;
430620
response.writeHead("200", {
431621
'Access-Control-Allow-Origin': '*',
432622
'Cache-Control': 'no-cache',
433623
'Content-Type': 'application/json',
434-
'Content-Encoding': 'deflate',
624+
'Content-Encoding': deflate ? 'deflate' : '',
435625
'Content-Length': reply.length
436626
});
437627
response.end(reply);
@@ -462,21 +652,45 @@ var server = http.createServer(function(request, response){
462652
case '/admin_stats':
463653
if (!authorize(request, response))
464654
return;
465-
log('warn', logSystem, 'Admin authorized');
466655
handleAdminStats(response);
467656
break;
657+
case '/admin_monitoring':
658+
if(!authorize(request, response)) {
659+
return;
660+
}
661+
handleAdminMonitoring(response);
662+
break;
663+
case '/admin_log':
664+
if(!authorize(request, response)) {
665+
return;
666+
}
667+
handleAdminLog(urlParts, response);
668+
break;
669+
case '/admin_users':
670+
if(!authorize(request, response)) {
671+
return;
672+
}
673+
handleAdminUsers(response);
674+
break;
675+
676+
case '/miners_hashrate':
677+
if (!authorize(request, response))
678+
return;
679+
handleGetMinersHashrate(response);
680+
break;
681+
468682
default:
469683
response.writeHead(404, {
470684
'Access-Control-Allow-Origin': '*'
471685
});
472686
response.end('Invalid API call');
473687
break;
474688
}
475-
476-
477689
});
478690

691+
collectStats();
692+
initMonitoring();
479693

480694
server.listen(config.api.port, function(){
481695
log('info', logSystem, 'API started & listening on port %d', [config.api.port]);
482-
});
696+
});

‎lib/apiInterfaces.js

+13-7
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
var http = require('http');
2+
var https = require('https');
23

3-
function jsonHttpRequest(host, port, data, callback){
4+
function jsonHttpRequest(host, port, data, callback, path){
5+
path = path || '/json_rpc';
46

57
var options = {
68
hostname: host,
79
port: port,
8-
path: '/json_rpc',
9-
method: 'POST',
10+
path: path,
11+
method: data ? 'POST' : 'GET',
1012
headers: {
1113
'Content-Length': data.length,
1214
'Content-Type': 'application/json',
1315
'Accept': 'application/json'
1416
}
1517
};
1618

17-
var req = http.request(options, function(res){
19+
var req = (port == 443 ? https : http).request(options, function(res){
1820
var replyData = '';
1921
res.setEncoding('utf8');
2022
res.on('data', function(chunk){
@@ -72,7 +74,7 @@ function batchRpc(host, port, array, callback){
7274
}
7375

7476

75-
module.exports = function(daemonConfig, walletConfig){
77+
module.exports = function(daemonConfig, walletConfig, poolApiConfig){
7678
return {
7779
batchRpcDaemon: function(batchArray, callback){
7880
batchRpc(daemonConfig.host, daemonConfig.port, batchArray, callback);
@@ -82,6 +84,10 @@ module.exports = function(daemonConfig, walletConfig){
8284
},
8385
rpcWallet: function(method, params, callback){
8486
rpc(walletConfig.host, walletConfig.port, method, params, callback);
85-
}
87+
},
88+
pool: function(method, callback){
89+
jsonHttpRequest('127.0.0.1', poolApiConfig.port, '', callback, method);
90+
},
91+
jsonHttpRequest: jsonHttpRequest
8692
}
87-
};
93+
};

‎lib/blockUnlocker.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
var async = require('async');
22

3-
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
3+
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
44

55
var logSystem = 'unlocker';
66
require('./exceptionWriter.js')(logSystem);

‎lib/charts.js

+240
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
var fs = require('fs');
2+
var async = require('async');
3+
var http = require('http');
4+
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
5+
6+
var logSystem = 'charts';
7+
require('./exceptionWriter.js')(logSystem);
8+
9+
log('info', logSystem, 'Started');
10+
11+
function startDataCollectors() {
12+
async.each(Object.keys(config.charts.pool), function(chartName) {
13+
var settings = config.charts.pool[chartName];
14+
if(settings.enabled) {
15+
setInterval(function() {
16+
collectPoolStatWithInterval(chartName, settings);
17+
}, settings.updateInterval * 1000);
18+
}
19+
});
20+
21+
var settings = config.charts.user.hashrate;
22+
if(settings.enabled) {
23+
setInterval(function() {
24+
collectUsersHashrate('hashrate', settings);
25+
}, settings.updateInterval * 1000)
26+
}
27+
}
28+
29+
function getChartDataFromRedis(chartName, callback) {
30+
redisClient.get(getStatsRedisKey(chartName), function(error, data) {
31+
callback(data ? JSON.parse(data) : []);
32+
});
33+
}
34+
35+
function getUserHashrateChartData(address, callback) {
36+
getChartDataFromRedis('hashrate:' + address, callback);
37+
}
38+
39+
function convertPaymentsDataToChart(paymentsData) {
40+
var data = [];
41+
if(paymentsData && paymentsData.length) {
42+
for(var i = 0; paymentsData[i]; i += 2) {
43+
data.push([+paymentsData[i + 1], paymentsData[i].split(':')[1] / config.coinUnits]);
44+
}
45+
}
46+
return data;
47+
}
48+
49+
function getUserChartsData(address, paymentsData, callback) {
50+
var stats = {};
51+
var chartsFuncs = {
52+
hashrate: function(callback) {
53+
getUserHashrateChartData(address, function(data) {
54+
callback(null, data);
55+
});
56+
},
57+
58+
payments: function(callback) {
59+
callback(null, convertPaymentsDataToChart(paymentsData));
60+
}
61+
};
62+
for(var chartName in chartsFuncs) {
63+
if(!config.charts.user[chartName].enabled) {
64+
delete chartsFuncs[chartName];
65+
}
66+
}
67+
async.parallel(chartsFuncs, callback);
68+
}
69+
70+
function getStatsRedisKey(chartName) {
71+
return config.coin + ':charts:' + chartName;
72+
}
73+
74+
var chartStatFuncs = {
75+
hashrate: getPoolHashrate,
76+
workers: getPoolWorkers,
77+
difficulty: getNetworkDifficulty,
78+
price: getCoinPrice,
79+
profit: getCoinProfit
80+
};
81+
82+
var statValueHandler = {
83+
avg: function(set, value) {
84+
set[1] = (set[1] * set[2] + value) / (set[2] + 1);
85+
},
86+
round: function(set, value) {
87+
statValueHandler.avg(set, value);
88+
set[1] = Math.round(set[1]);
89+
},
90+
max: function(set, value) {
91+
if(value > set[1]) {
92+
set[1] = value;
93+
}
94+
}
95+
};
96+
97+
var preSaveFunctions = {
98+
hashrate: statValueHandler.avg,
99+
workers: statValueHandler.max,
100+
difficulty: statValueHandler.round,
101+
price: statValueHandler.avg,
102+
profit: statValueHandler.avg
103+
};
104+
105+
function storeCollectedValues(chartName, values, settings) {
106+
for(var i in values) {
107+
storeCollectedValue(chartName + ':' + i, values[i], settings);
108+
}
109+
}
110+
111+
function storeCollectedValue(chartName, value, settings) {
112+
var now = new Date() / 1000 | 0;
113+
getChartDataFromRedis(chartName, function(sets) {
114+
var lastSet = sets[sets.length - 1]; // [time, avgValue, updatesCount]
115+
if(!lastSet || now - lastSet[0] > settings.stepInterval) {
116+
lastSet = [now, value, 1];
117+
sets.push(lastSet);
118+
while(now - sets[0][0] > settings.maximumPeriod) { // clear old sets
119+
sets.shift();
120+
}
121+
}
122+
else {
123+
preSaveFunctions[chartName]
124+
? preSaveFunctions[chartName](lastSet, value)
125+
: statValueHandler.round(lastSet, value);
126+
lastSet[2]++;
127+
}
128+
redisClient.set(getStatsRedisKey(chartName), JSON.stringify(sets));
129+
log('info', logSystem, chartName + ' chart collected value ' + value + '. Total sets count ' + sets.length);
130+
log('info', logSystem, chartName + ' data: ' + JSON.stringify(sets));
131+
});
132+
}
133+
134+
function collectPoolStatWithInterval(chartName, settings) {
135+
async.waterfall([
136+
chartStatFuncs[chartName],
137+
function(value, callback) {
138+
storeCollectedValue(chartName, value, settings, callback);
139+
}
140+
]);
141+
}
142+
143+
function getPoolStats(callback) {
144+
apiInterfaces.pool('/stats', callback);
145+
}
146+
147+
function getPoolHashrate(callback) {
148+
getPoolStats(function(error, stats) {
149+
callback(error, stats.pool ? Math.round(stats.pool.hashrate) : null);
150+
});
151+
}
152+
153+
function getPoolWorkers(callback) {
154+
getPoolStats(function(error, stats) {
155+
callback(error, stats.pool ? stats.pool.miners : null);
156+
});
157+
}
158+
159+
function getNetworkDifficulty(callback) {
160+
getPoolStats(function(error, stats) {
161+
callback(error, stats.pool ? stats.network.difficulty : null);
162+
});
163+
}
164+
165+
function getUsersHashrates(callback) {
166+
apiInterfaces.pool('/miners_hashrate', function(error, data) {
167+
callback(data.minersHashrate);
168+
});
169+
}
170+
171+
function collectUsersHashrate(chartName, settings) {
172+
var redisBaseKey = getStatsRedisKey(chartName) + ':';
173+
redisClient.keys(redisBaseKey + '*', function(keys) {
174+
var hashrates = {};
175+
for(var i in keys) {
176+
hashrates[keys[i].substr(keys[i].length)] = 0;
177+
}
178+
getUsersHashrates(function(newHashrates) {
179+
for(var address in newHashrates) {
180+
hashrates[address] = newHashrates[address];
181+
}
182+
storeCollectedValues(chartName, hashrates, settings);
183+
});
184+
});
185+
}
186+
187+
function getCoinPrice(callback) {
188+
apiInterfaces.jsonHttpRequest('www.cryptonator.com', 443, '', function(error, response) {
189+
callback(response.error ? response.error : error, response.success ? +response.ticker.price : null);
190+
}, '/api/ticker/' + config.symbol.toLowerCase() + '-usd');
191+
}
192+
193+
function getCoinProfit(callback) {
194+
getCoinPrice(function(error, price) {
195+
if(error) {
196+
callback(error);
197+
return;
198+
}
199+
getPoolStats(function(error, stats) {
200+
if(error) {
201+
callback(error);
202+
return;
203+
}
204+
callback(null, stats.network.reward * price / stats.network.difficulty / config.coinUnits);
205+
});
206+
});
207+
}
208+
209+
function getPoolChartsData(callback) {
210+
var chartsNames = [];
211+
var redisKeys = [];
212+
for(var chartName in config.charts.pool) {
213+
if(config.charts.pool[chartName].enabled) {
214+
chartsNames.push(chartName);
215+
redisKeys.push(getStatsRedisKey(chartName));
216+
}
217+
}
218+
if(redisKeys.length) {
219+
redisClient.mget(redisKeys, function(error, data) {
220+
var stats = {};
221+
if(data) {
222+
for(var i in data) {
223+
if(data[i]) {
224+
stats[chartsNames[i]] = JSON.parse(data[i]);
225+
}
226+
}
227+
}
228+
callback(error, stats);
229+
});
230+
}
231+
else {
232+
callback(null, {});
233+
}
234+
}
235+
236+
module.exports = {
237+
startDataCollectors: startDataCollectors,
238+
getUserChartsData: getUserChartsData,
239+
getPoolChartsData: getPoolChartsData
240+
};

‎lib/chartsDataCollector.js

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
var fs = require('fs');
2+
var async = require('async');
3+
var http = require('http');
4+
var charts = require('./charts.js');
5+
6+
var logSystem = 'chartsDataCollector';
7+
require('./exceptionWriter.js')(logSystem);
8+
9+
log('info', logSystem, 'Started');
10+
11+
charts.startDataCollectors();

‎lib/paymentProcessor.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ var fs = require('fs');
22

33
var async = require('async');
44

5-
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
5+
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
66

77

88
var logSystem = 'payments';

‎lib/pool.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var threadId = '(Thread ' + process.env.forkId + ') ';
1313
var logSystem = 'pool';
1414
require('./exceptionWriter.js')(logSystem);
1515

16-
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet);
16+
var apiInterfaces = require('./apiInterfaces.js')(config.daemon, config.wallet, config.api);
1717
var utils = require('./utils.js');
1818

1919
var log = function(severity, system, text, data){

‎website/admin.html

+248-144
Large diffs are not rendered by default.

‎website/custom.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,7 @@ a.list-group-item:last-child {
387387
cursor: pointer;
388388
}
389389
.usersList tr > th:first-child {
390-
width: 50%;
390+
width: 40%;
391391
}
392392
strong,
393393
b {

‎website/index.html

+6-3
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,8 @@
99

1010

1111
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
12-
1312
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery-timeago/1.4.0/jquery.timeago.min.js"></script>
14-
13+
<script src="http://cdnjs.cloudflare.com/ajax/libs/jquery-sparklines/2.1.2/jquery.sparkline.min.js"></script>
1514
<link href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css" rel="stylesheet">
1615
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
1716

@@ -27,7 +26,7 @@
2726
text-transform: capitalize;
2827
}
2928
body {
30-
padding-top: 90px;
29+
padding-top: 65px;
3130
padding-bottom: 80px;
3231
overflow-y: scroll;
3332
}
@@ -120,6 +119,10 @@
120119
}
121120
};
122121

122+
function getTransactionUrl(id) {
123+
return transactionExplorer.replace('{symbol}', lastStats.config.symbol.toLowerCase()).replace('{id}', id);
124+
}
125+
123126
$.fn.update = function(txt){
124127
var el = this[0];
125128
if (el.textContent !== txt)

‎website/pages/admin/monitoring.html

+130
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
<!-- /// define Handlebars template /// -->
2+
<script id="monitoringInfo" type="text/x-handlebars-template">
3+
<div class="tab-pane active" id="rpcLog">
4+
<div class="row">
5+
<div class="col-sm-6">
6+
<h3>Daemon</h3>
7+
<ul class="list-unstyled">
8+
<li><strong>Last check:</strong> {{monitoringDaemon.lastCheck}}</li>
9+
<li><strong>Last status:</strong>
10+
<span id="daemonStatus">{{monitoringDaemon.lastStatus}}</span>
11+
</li>
12+
<li><strong>Last response:</strong>
13+
<pre>{{monitoringDaemon.lastResponse}}</pre>
14+
</li>
15+
<li><strong>Last fail:</strong>
16+
<span>{{monitoringDaemon.lastFail}}</span>
17+
</li>
18+
<li><strong>Last fail response:</strong>
19+
<pre>{{monitoringDaemon.lastFailResponse}}</pre>
20+
</li>
21+
</ul>
22+
</div>
23+
<div class="col-sm-6">
24+
<h3>Wallet</h3>
25+
<ul class="list-unstyled">
26+
<li><strong>Last check:</strong> {{monitoringWallet.lastCheck}}</li>
27+
<li><strong>Last status:</strong>
28+
<span id="walletStatus">{{monitoringWallet.lastStatus}}</span>
29+
</li>
30+
<li><strong>Last response:</strong>
31+
<pre>{{monitoringWallet.lastResponse}}</pre>
32+
</li>
33+
<li><strong>Last fail:</strong>
34+
<span>{{monitoringWallet.lastFail}}</span>
35+
</li>
36+
<li><strong>Last fail response:</strong>
37+
<pre>{{monitoringWallet.lastFailResponse}}</span></pre>
38+
</ul>
39+
</div>
40+
</div>
41+
42+
<h3>Logs</h3>
43+
44+
<table class="table table-hover table-striped logList" id="logTable">
45+
<thead>
46+
<tr>
47+
<th class="sort">Name <i class="fa fa-sort"></i></th>
48+
<th class="sort">Modified <i class="fa fa-sort"></i></th>
49+
<th class="sort">Size <i class="fa fa-sort"></i></th>
50+
</tr>
51+
</thead>
52+
<tbody>
53+
{{#each logs}}
54+
<tr>
55+
<td data-sort="{{@key}}"><a href="{{this.link}}" target="_blank">{{@key}}</a></td>
56+
<td data-sort="{{this.changed}}">{{this.changed}}</td>
57+
<td data-sort="{{this.size}}">{{this.size}} bytes</td>
58+
</tr>
59+
{{/each}}
60+
</tbody>
61+
</table>
62+
</div>
63+
</script>
64+
65+
66+
<script>
67+
function getCheckTime(timestamp) {
68+
return timestamp ? $.timeago(new Date(timestamp * 1000).toISOString()) : null;
69+
}
70+
71+
function monitoringInfoParse(data) {
72+
var monitoringDaemon = {
73+
lastCheck: getCheckTime(data['monitoring'].daemon.lastCheck) || 'never',
74+
lastStatus: data['monitoring'].daemon.lastStatus || '',
75+
lastFail: getCheckTime(data['monitoring'].daemon.lastFail) || 'never',
76+
lastFailResponse: data['monitoring'].daemon.lastFailResponse || ' ',
77+
lastResponse: data['monitoring'].daemon.lastResponse || ' '
78+
};
79+
var monitoringWallet = {
80+
lastCheck: getCheckTime(data['monitoring'].wallet.lastCheck) || 'never',
81+
lastStatus: data['monitoring'].wallet.lastStatus || '',
82+
lastFail: getCheckTime(data['monitoring'].wallet.lastFail) || 'never',
83+
lastFailResponse: data['monitoring'].wallet.lastFailResponse || ' ',
84+
lastResponse: data['monitoring'].wallet.lastResponse || ' '
85+
};
86+
var properData = {};
87+
88+
for(var prop in data) {
89+
if(data.hasOwnProperty('logs')) {
90+
properData['logs'] = data['logs'];
91+
for(var log in data['logs']) {
92+
properData['logs'][log].changed = Date(data['logs'][log].changed * 1000);
93+
data['logs'][log].link = api + '/admin_log?file=' + log + '&password=' + docCookies.getItem('password');
94+
}
95+
}
96+
}
97+
properData['monitoringDaemon'] = monitoringDaemon;
98+
properData['monitoringWallet'] = monitoringWallet;
99+
100+
return properData;
101+
}
102+
103+
function renderLogInfo() {
104+
$.ajax({
105+
url: api + '/admin_monitoring',
106+
data: {password: docCookies.getItem('password')},
107+
cache: false,
108+
dataType: 'json',
109+
success: function(data) {
110+
renderTemplate(monitoringInfoParse(data), '#monitoringInfo', '#monitoringInfoView');
111+
112+
$('#daemonStatus').addClass(data['monitoring'].daemon.lastStatus == 'ok' ? 'text-success' : 'text-danger');
113+
$('#walletStatus').addClass(data['monitoring'].wallet.lastStatus == 'ok' ? 'text-success' : 'text-danger');
114+
115+
$('#logTable th.sort').on('click', sortTable);
116+
}
117+
});
118+
}
119+
120+
$(function() {
121+
renderLogInfo();
122+
});
123+
</script>
124+
125+
<div class="adminMonitor">
126+
<!-- Tab panes -->
127+
<div class="tab-content">
128+
<div id="monitoringInfoView"></div>
129+
</div>
130+
</div>

‎website/pages/admin/statistics.html

+90
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<script>
2+
var formatLuck = function(difficulty, shares) {
3+
if(difficulty <= shares) {
4+
var percent = (100 - Math.round(difficulty / shares * 100)) * -1;
5+
return '<span class="luckBad">' + percent + '%</span>';
6+
}
7+
else {
8+
var percent = 100 - Math.round(shares / difficulty * 100);
9+
return '<span class="luckGood">' + percent + '%</span>';
10+
}
11+
};
12+
13+
function getStats(promptPassword) {
14+
15+
var password = docCookies.getItem('password');
16+
17+
if(!password || promptPassword) {
18+
password = prompt('Enter admin password');
19+
}
20+
21+
$('#loading').show();
22+
$.ajax({
23+
url: api + '/admin_stats',
24+
data: {password: password},
25+
success: function(data) {
26+
docCookies.setItem('password', password, Infinity);
27+
$('#loading').hide();
28+
renderData(data);
29+
},
30+
error: function(e) {
31+
docCookies.removeItem('password');
32+
getStats(true);
33+
}
34+
});
35+
}
36+
37+
function renderData(data) {
38+
$('#totalOwed').text(getReadableCoins(data.totalOwed));
39+
$('#totalPaid').text(getReadableCoins(data.totalPaid));
40+
$('#totalMined').text(getReadableCoins(data.totalRevenue));
41+
$('#profit').text(getReadableCoins(data.totalRevenue - data.totalOwed - data.totalPaid));
42+
$('#averageLuck').html(formatLuck(data.totalDiff, data.totalShares));
43+
$('#orphanPercent').text((data.blocksOrphaned / data.blocksUnlocked * 100).toFixed(2));
44+
$('#registeredAddresses').text(data.totalWorkers);
45+
}
46+
47+
$(function() {
48+
getStats();
49+
});
50+
</script>
51+
<!-- <h4>Stats</h4>
52+
<dl class="dl-horizontal" id="statsHolder">
53+
<dt>Total Owed</dt><dd id="totalOwed">...</dd>
54+
<dt>Total Paid</dt><dd id="totalPaid">...</dd>
55+
<dt>Total Mined</dt><dd id="totalMined">...</dd>
56+
<dt>Profit (before tx fees)</dt><dd id="profit">...</dd>
57+
<dt>Average Luck</dt><dd id="averageLuck">...</dd>
58+
<dt>Orphan Percent</dt><dd id="orphanPercent">...</dd>
59+
<dt>Registered Addresses</dt><dd id="registeredAddresses">...</dd>
60+
</dl> -->
61+
<div class="row adminStats">
62+
<div class="col-sm-3 color1">
63+
<h4>Total Owed</h4>
64+
<span class="statValue" id="totalOwed">...</span>
65+
</div>
66+
<div class="col-sm-3 color2">
67+
<h4>Total Paid</h4>
68+
<span class="statValue" id="totalPaid">...</span>
69+
</div>
70+
<div class="col-sm-3 color3">
71+
<h4>Total Mined</h4>
72+
<span class="statValue" id="totalMined">...</span>
73+
</div>
74+
<div class="col-sm-3 color4">
75+
<h4>Profit (before tx fees)</h4>
76+
<span class="statValue" id="profit">...</span>
77+
</div>
78+
<div class="col-sm-4 color5">
79+
<h4>Average Luck</h4>
80+
<span class="statValue lead" id="averageLuck">...</span>
81+
</div>
82+
<div class="col-sm-4 color6">
83+
<h4>Orphan Percent</h4>
84+
<span class="statValue lead" id="orphanPercent">...</span>
85+
</div>
86+
<div class="col-sm-4 color7">
87+
<h4>Registered Addresses</h4>
88+
<span class="statValue lead" id="registeredAddresses">...</span>
89+
</div>
90+
</div>

‎website/pages/admin/userslist.html

+77
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<style>
2+
td {
3+
text-align: center
4+
}
5+
</style>
6+
<!-- /// define Handlebars template /// -->
7+
<script id="usersListTable" type="text/x-handlebars-template">
8+
{{#each users}}
9+
<tr>
10+
<td><a href="./?wallet={{this.number}}">{{this.number}}</a></td>
11+
<td data-sort="{{this.wallet.hashrate}}">{{this.readableHashrate}}</td>
12+
<td data-sort="{{this.wallet.hashes}}">{{this.readableHashes}}</td>
13+
<td data-sort="{{this.wallet.pending}}">{{this.wallet.pending}}</td>
14+
<td data-sort="{{this.wallet.paid}}">{{this.wallet.paid}}</td>
15+
<td data-sort="{{this.wallet.lastShare}}">{{this.timeago}}</td>
16+
</tr>
17+
{{/each}}
18+
</script>
19+
20+
<script>
21+
function parseUsers(wallets) {
22+
var walletsArray = [],
23+
properObject = {};
24+
for(var wallet in wallets) {
25+
if(wallets.hasOwnProperty(wallet)) {
26+
walletsArray.push({
27+
number: wallet,
28+
wallet: wallets[wallet],
29+
timeago: $.timeago(new Date(wallets[wallet].lastShare * 1000).toISOString()),
30+
readableHashrate: getReadableHashRateString(wallets[wallet].hashrate) + '/s',
31+
readableHashes: getReadableHashRateString(wallets[wallet].hashes)
32+
});
33+
}
34+
}
35+
properObject['users'] = walletsArray.sort(function(a, b) {
36+
return a.wallet.hashrate - b.wallet.hashrate
37+
}).reverse();
38+
39+
return properObject;
40+
}
41+
42+
function cretaUserTable() {
43+
$.ajax({
44+
url: api + '/admin_users',
45+
data: {password: docCookies.getItem('password')},
46+
cache: false,
47+
dataType: 'json',
48+
success: function(data) {
49+
renderTemplate(parseUsers(data), '#usersListTable', '#template');
50+
}
51+
});
52+
}
53+
54+
$(function() {
55+
$('[data-toggle="tooltip"]').tooltip();
56+
$('.usersList th.sort').on('click', sortTable);
57+
cretaUserTable();
58+
});
59+
60+
</script>
61+
<div class="table-responsive">
62+
<table class="table table-hover table-striped usersList">
63+
<thead>
64+
<tr>
65+
<th>Wallet</th>
66+
<th class="sort" style="width:10%;">Hashrate <i class="fa fa-sort"></i></th>
67+
<th class="sort" style="width:10%;">Hashes <i class="fa fa-sort"></i></th>
68+
<th class="sort" style="width:16%;">Pending <i class="fa fa-sort"></i></th>
69+
<th class="sort" style="width:10%;">Paid <i class="fa fa-sort"></i></th>
70+
<th class="sort" style="width:14%;">Last share <i class="fa fa-sort"></i></th>
71+
</tr>
72+
</thead>
73+
<tbody id="template">
74+
75+
</tbody>
76+
</table>
77+
</div>

‎website/pages/home.html

+289-21
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.