dashboard: refactor layout and add worker stats
This commit is contained in:
@@ -9,10 +9,6 @@ var uplotCharts = {};
|
||||
// Chart.js instances (persistent)
|
||||
var chartJsInstances = {};
|
||||
|
||||
// Network rate tracking (for real-time speed calculation)
|
||||
var prevNet = null;
|
||||
var prevNetTime = null;
|
||||
|
||||
// Tab switching
|
||||
function initTabs() {
|
||||
$$('.tab-btn').forEach(function(btn) {
|
||||
@@ -86,13 +82,6 @@ function fmtBytes(b) {
|
||||
return b.toFixed(i > 0 ? 1 : 0) + ' ' + units[i];
|
||||
}
|
||||
|
||||
function fmtRate(bps) {
|
||||
if (bps < 1) return '0';
|
||||
if (bps < 1024) return bps.toFixed(0) + 'B';
|
||||
if (bps < 1024 * 1024) return (bps / 1024).toFixed(1) + 'K';
|
||||
return (bps / (1024 * 1024)).toFixed(1) + 'M';
|
||||
}
|
||||
|
||||
function fmtTime(s) {
|
||||
if (!s) return '-';
|
||||
var d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60);
|
||||
@@ -301,46 +290,32 @@ function update(d) {
|
||||
var memGrowthStr = memGrowth > 0 ? ' (+' + fmtBytes(memGrowth) + ')' : '';
|
||||
$('sysProcMem').textContent = fmtBytes(sys.proc_rss) + memGrowthStr;
|
||||
|
||||
// Network speed (current rate, not average)
|
||||
var net = d.network || {};
|
||||
var now = Date.now();
|
||||
if (prevNet && prevNetTime) {
|
||||
var dt = (now - prevNetTime) / 1000;
|
||||
if (dt > 0) {
|
||||
var s = net.scraper || {}, ps = prevNet.scraper || {};
|
||||
var p = net.proxy || {}, pp = prevNet.proxy || {};
|
||||
$('netScrapeTx').textContent = fmtRate((s.bytes_tx - (ps.bytes_tx || 0)) / dt);
|
||||
$('netScrapeRx').textContent = fmtRate((s.bytes_rx - (ps.bytes_rx || 0)) / dt);
|
||||
$('netProxyTx').textContent = fmtRate((p.bytes_tx - (pp.bytes_tx || 0)) / dt);
|
||||
$('netProxyRx').textContent = fmtRate((p.bytes_rx - (pp.bytes_rx || 0)) / dt);
|
||||
}
|
||||
}
|
||||
prevNet = net;
|
||||
prevNetTime = now;
|
||||
|
||||
// Main stats - db stats are nested under d.db
|
||||
var db = d.db || {};
|
||||
$('working').textContent = fmt(db.working);
|
||||
$('total').textContent = fmt(db.total);
|
||||
$('tested').textContent = fmt(d.tested);
|
||||
$('passed').textContent = fmt(d.passed);
|
||||
$('failed').textContent = fmt(d.failed);
|
||||
|
||||
// Success rate
|
||||
// Success rate (displayed in Worker Testing section)
|
||||
var sr = d.success_rate || 0;
|
||||
$('successRate').textContent = fmtDec(sr, 1) + '%';
|
||||
$('successRate').className = 'val-md ' + (sr < 20 ? 'red' : sr < 50 ? 'yel' : 'grn');
|
||||
setBar('srBar', sr, 100, sr < 20 ? 'red' : sr < 50 ? 'yel' : 'grn');
|
||||
$('successRate').className = 'stat-val ' + (sr < 20 ? 'red' : sr < 50 ? 'yel' : 'grn');
|
||||
|
||||
var rsr = d.recent_success_rate || 0;
|
||||
$('recentSuccessRate').textContent = fmtDec(rsr, 1) + '%';
|
||||
$('recentSuccessRate').className = 'stat-val ' + (rsr < 20 ? 'red' : rsr < 50 ? 'yel' : 'grn');
|
||||
if ($('recentSuccessRate')) {
|
||||
$('recentSuccessRate').textContent = fmtDec(rsr, 1) + '%';
|
||||
$('recentSuccessRate').className = 'stat-val ' + (rsr < 20 ? 'red' : rsr < 50 ? 'yel' : 'grn');
|
||||
}
|
||||
|
||||
// Rates
|
||||
$('rate').textContent = fmtDec(d.recent_rate, 2);
|
||||
$('recentRate').textContent = fmtDec(d.recent_rate, 2) + '/s';
|
||||
$('peakRate').textContent = fmtDec(d.peak_rate, 2) + '/s';
|
||||
$('passRate').textContent = fmtDec(d.pass_rate, 3);
|
||||
// Rates (Performance tab)
|
||||
if ($('recentRate')) $('recentRate').textContent = fmtDec(d.recent_rate, 2) + '/s';
|
||||
if ($('peakRate')) $('peakRate').textContent = fmtDec(d.peak_rate, 2) + '/s';
|
||||
if ($('passRate')) $('passRate').textContent = fmtDec(d.pass_rate, 3);
|
||||
|
||||
// Scraper stats
|
||||
var engAvail = d.engines_available || 0;
|
||||
var engTotal = d.engines_total || 0;
|
||||
if ($('engActive')) $('engActive').textContent = engAvail + '/' + engTotal;
|
||||
if ($('proxiesAdded')) $('proxiesAdded').textContent = fmt(db.added_last_day || d.proxies_added || 0);
|
||||
|
||||
// Latency
|
||||
var lat = d.avg_latency || 0;
|
||||
@@ -354,18 +329,6 @@ function update(d) {
|
||||
$('p99').textContent = fmtMs(pctl.p99);
|
||||
|
||||
// System
|
||||
$('threads').textContent = d.threads + '/' + d.max_threads;
|
||||
setBar('threadBar', d.threads, d.max_threads, 'blu');
|
||||
$('queue').textContent = fmt(d.queue_size);
|
||||
// Calculate queue ETA: queue_size / tests_per_second
|
||||
var queueEta = '-';
|
||||
if (d.queue_size > 0 && d.recent_rate > 0.01) {
|
||||
var etaSecs = d.queue_size / d.recent_rate;
|
||||
queueEta = fmtTime(etaSecs);
|
||||
} else if (d.queue_size === 0) {
|
||||
queueEta = 'empty';
|
||||
}
|
||||
$('queueEta').textContent = queueEta;
|
||||
$('uptime').textContent = fmtTime(d.uptime_seconds);
|
||||
|
||||
// Charts
|
||||
@@ -621,37 +584,6 @@ function update(d) {
|
||||
$('mitmRecent').innerHTML = recentHtml;
|
||||
}
|
||||
|
||||
// Network usage stats
|
||||
if (d.network) {
|
||||
var net = d.network;
|
||||
$('netRx').textContent = fmtBytes(net.bytes_rx || 0);
|
||||
$('netTx').textContent = fmtBytes(net.bytes_tx || 0);
|
||||
$('netTotal').textContent = fmtBytes(net.bytes_total || 0);
|
||||
$('netRxRate').textContent = fmtBytes(net.rx_rate || 0) + '/s';
|
||||
$('netTxRate').textContent = fmtBytes(net.tx_rate || 0) + '/s';
|
||||
if (net.proxy) {
|
||||
$('netProxy').textContent = fmtBytes(net.proxy.bytes_total || 0);
|
||||
}
|
||||
if (net.scraper) {
|
||||
$('netScraper').textContent = fmtBytes(net.scraper.bytes_total || 0);
|
||||
}
|
||||
// Per-tor-node stats
|
||||
var torContainer = $('netTorNodes');
|
||||
if (torContainer && net.tor_nodes) {
|
||||
var html = '';
|
||||
var nodes = Object.keys(net.tor_nodes).sort();
|
||||
nodes.forEach(function(node) {
|
||||
var s = net.tor_nodes[node];
|
||||
html += '<div class="c c-sm">';
|
||||
html += '<div class="lbl" style="font-size:10px">' + node + '</div>';
|
||||
html += '<div class="val-sm cyn">' + fmtBytes(s.rx + s.tx) + '</div>';
|
||||
html += '<div class="sub" style="font-size:9px">' + fmt(s.requests || 0) + ' req</div>';
|
||||
html += '</div>';
|
||||
});
|
||||
torContainer.innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
$('lastUpdate').textContent = new Date().toLocaleTimeString();
|
||||
}
|
||||
|
||||
@@ -660,6 +592,93 @@ function fetchStats() {
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(update)
|
||||
.catch(function(e) { $('dot').className = 'dot err'; $('statusTxt').textContent = 'Error'; });
|
||||
// Also fetch worker stats
|
||||
fetchWorkers();
|
||||
}
|
||||
|
||||
function fetchWorkers() {
|
||||
fetch('/api/workers')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(updateWorkers)
|
||||
.catch(function(e) { console.error('Failed to fetch workers:', e); });
|
||||
}
|
||||
|
||||
function updateWorkers(data) {
|
||||
if (!data) return;
|
||||
|
||||
// Update summary stats (both main panel and Workers tab)
|
||||
var active = data.active || 0;
|
||||
var total = data.total || 0;
|
||||
if ($('wkActive')) $('wkActive').textContent = active + '/' + total;
|
||||
if ($('dwActive')) $('dwActive').textContent = active + '/' + total;
|
||||
|
||||
if (data.summary) {
|
||||
if ($('wkTested')) $('wkTested').textContent = fmt(data.summary.total_tested);
|
||||
if ($('wkWorking')) $('wkWorking').textContent = fmt(data.summary.total_working);
|
||||
if ($('wkSuccessRate')) $('wkSuccessRate').textContent = data.summary.overall_success_rate.toFixed(1) + '%';
|
||||
// Main panel distributed workers
|
||||
if ($('dwTested')) $('dwTested').textContent = fmt(data.summary.total_tested);
|
||||
if ($('dwWorking')) $('dwWorking').textContent = fmt(data.summary.total_working);
|
||||
// Calculate combined rate from worker stats
|
||||
var combinedRate = 0;
|
||||
if (data.workers) {
|
||||
data.workers.forEach(function(w) {
|
||||
if (w.active && w.test_rate) combinedRate += w.test_rate;
|
||||
});
|
||||
}
|
||||
if ($('dwRate')) $('dwRate').textContent = combinedRate > 0 ? combinedRate.toFixed(1) + '/s' : '-';
|
||||
}
|
||||
|
||||
// Queue status
|
||||
if (data.queue) {
|
||||
if ($('queuePending')) $('queuePending').textContent = fmt(data.queue.pending || 0);
|
||||
if ($('queueClaimed')) $('queueClaimed').textContent = fmt(data.queue.claimed || 0);
|
||||
if ($('queueDue')) $('queueDue').textContent = fmt(data.queue.due || 0);
|
||||
}
|
||||
|
||||
// Update worker cards
|
||||
var container = $('workerCards');
|
||||
if (!container) return;
|
||||
|
||||
if (!data.workers || data.workers.length === 0) {
|
||||
container.innerHTML = '<div class="c" style="text-align:center;color:var(--dim);padding:40px">' +
|
||||
'<div style="font-size:24px;margin-bottom:8px">No workers connected</div>' +
|
||||
'<div style="font-size:12px">Add workers with: <code>python ppf.py --register --server URL</code></div></div>';
|
||||
return;
|
||||
}
|
||||
|
||||
var html = '';
|
||||
data.workers.forEach(function(w) {
|
||||
var statusClass = w.active ? 'grn' : 'red';
|
||||
var statusText = w.active ? 'ACTIVE' : 'OFFLINE';
|
||||
var successRate = w.success_rate || 0;
|
||||
var rateClass = successRate >= 50 ? 'grn' : (successRate >= 20 ? 'yel' : 'red');
|
||||
var profBadge = w.profiling ? '<span class="tag tag-warn" style="margin-left:4px;font-size:9px">PROF</span>' : '';
|
||||
|
||||
html += '<div class="c">' +
|
||||
'<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px">' +
|
||||
'<span style="font-size:1.1em;font-weight:600;color:var(--cyn)">' + w.name + '</span>' +
|
||||
'<span><span class="tag tag-' + (w.active ? 'ok' : 'err') + '">' + statusText + '</span>' + profBadge + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="stats-wrap" style="margin:0">' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Tested</span><span class="stat-val">' + fmt(w.proxies_tested) + '</span></div>' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Working</span><span class="stat-val grn">' + fmt(w.proxies_working) + '</span></div>' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Success</span><span class="stat-val ' + rateClass + '">' + successRate.toFixed(1) + '%</span></div>' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Jobs</span><span class="stat-val">' + fmt(w.jobs_completed) + '</span></div>' +
|
||||
'</div>' +
|
||||
'<div style="font-size:0.75em;color:var(--dim);margin-top:8px">' +
|
||||
'IP: ' + w.ip + ' | Last: ' + formatAge(w.age) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function formatAge(seconds) {
|
||||
if (seconds < 60) return seconds + 's ago';
|
||||
if (seconds < 3600) return Math.floor(seconds / 60) + 'm ago';
|
||||
if (seconds < 86400) return Math.floor(seconds / 3600) + 'h ago';
|
||||
return Math.floor(seconds / 86400) + 'd ago';
|
||||
}
|
||||
|
||||
// Visibility-aware polling - pause when tab is hidden
|
||||
|
||||
Reference in New Issue
Block a user