httpd: add batch API endpoint and worker improvements
- /api/dashboard: single endpoint returning stats + workers + countries - dashboard.js: use batch endpoint (2 requests -> 1 per poll cycle) - _get_workers_data: refactored from /api/workers for code reuse - worker verification: trust scoring based on result accuracy - fair distribution: dynamic batch sizing based on queue and workers - queue tracking: session progress, due/claimed/pending counts
This commit is contained in:
@@ -608,19 +608,24 @@ function update(d) {
|
||||
}
|
||||
|
||||
function fetchStats() {
|
||||
fetch('/api/stats')
|
||||
// Use batch endpoint for reduced RTT (single request instead of multiple)
|
||||
fetch('/api/dashboard')
|
||||
.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); });
|
||||
.then(function(data) {
|
||||
// Extract stats (same structure as /api/stats)
|
||||
if (data.stats) {
|
||||
update(data.stats);
|
||||
}
|
||||
// Extract workers data (same structure as /api/workers)
|
||||
if (data.workers) {
|
||||
updateWorkers(data.workers);
|
||||
}
|
||||
})
|
||||
.catch(function(e) {
|
||||
$('dot').className = 'dot err';
|
||||
$('statusTxt').textContent = 'Error';
|
||||
console.error('Failed to fetch dashboard:', e);
|
||||
});
|
||||
}
|
||||
|
||||
function updateWorkers(data) {
|
||||
@@ -646,46 +651,125 @@ function updateWorkers(data) {
|
||||
|
||||
// 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);
|
||||
var q = data.queue;
|
||||
var pct = q.session_pct || 0;
|
||||
|
||||
if ($('queueTotal')) $('queueTotal').textContent = fmt(q.total || 0);
|
||||
if ($('queueUntested')) $('queueUntested').textContent = fmt(q.untested || 0);
|
||||
if ($('queueClaimed')) $('queueClaimed').textContent = fmt(q.claimed || 0);
|
||||
if ($('queueDue')) $('queueDue').textContent = fmt(q.due || 0);
|
||||
if ($('queueSessionTested')) $('queueSessionTested').textContent = fmt(q.session_tested || 0);
|
||||
if ($('queuePending')) $('queuePending').textContent = fmt(q.pending || 0);
|
||||
if ($('queueTotal2')) $('queueTotal2').textContent = fmt(q.total || 0);
|
||||
if ($('queueSessionPct')) $('queueSessionPct').textContent = pct + '%';
|
||||
if ($('queueProgressBar')) $('queueProgressBar').style.width = Math.min(pct, 100) + '%';
|
||||
}
|
||||
|
||||
// Update worker cards
|
||||
var container = $('workerCards');
|
||||
if (!container) return;
|
||||
|
||||
var html = '';
|
||||
|
||||
// Helper to build a unified worker/manager card
|
||||
function buildCard(opts) {
|
||||
var rate = opts.rate || 0;
|
||||
var successRate = opts.successRate || 0;
|
||||
var queue = opts.queue || 0;
|
||||
var rateClass = successRate >= 50 ? 'grn' : (successRate >= 20 ? 'yel' : 'red');
|
||||
var barColor = successRate >= 50 ? 'var(--green)' : (successRate >= 20 ? 'var(--yellow)' : 'var(--red)');
|
||||
var borderStyle = opts.isManager ? 'border:1px solid var(--cyan);box-shadow:0 0 12px rgba(56,189,248,0.15)' : '';
|
||||
var badges = '<span class="tag tag-' + (opts.active ? 'ok' : 'err') + '">' + (opts.active ? 'ACTIVE' : 'OFFLINE') + '</span>';
|
||||
if (opts.profiling) badges += '<span class="tag tag-warn" style="margin-left:4px;font-size:9px">PROF</span>';
|
||||
// Trust indicator for workers with verified results
|
||||
if (opts.trustScore !== undefined && opts.trustScore < 0.8 && !opts.isManager) {
|
||||
var trustClass = opts.trustScore < 0.5 ? 'tag-err' : 'tag-warn';
|
||||
badges += '<span class="tag ' + trustClass + '" style="margin-left:4px;font-size:9px">LOW TRUST</span>';
|
||||
}
|
||||
// Calculate ETA based on queue and rate
|
||||
var eta = '-';
|
||||
if (rate > 0 && queue > 0) {
|
||||
var secs = Math.round(queue / rate);
|
||||
if (secs < 60) eta = secs + 's';
|
||||
else if (secs < 3600) eta = Math.round(secs / 60) + 'm';
|
||||
else if (secs < 86400) eta = Math.round(secs / 3600) + 'h';
|
||||
else eta = Math.round(secs / 86400) + 'd';
|
||||
}
|
||||
|
||||
return '<div class="c" style="' + borderStyle + '">' +
|
||||
'<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(--cyan)">' + opts.name + '</span>' +
|
||||
'<span>' + badges + '</span>' +
|
||||
'</div>' +
|
||||
'<div class="stats-wrap" style="margin:0">' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Rate</span><span class="stat-val cyn">' + (rate > 0 ? rate.toFixed(1) + '/s' : '-') + '</span></div>' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Tested</span><span class="stat-val">' + fmt(opts.tested) + '</span></div>' +
|
||||
'<div class="stat-row"><span class="stat-lbl">Working</span><span class="stat-val grn">' + fmt(opts.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">Queue</span><span class="stat-val">' + fmt(queue) + '</span></div>' +
|
||||
'<div class="stat-row"><span class="stat-lbl">ETA</span><span class="stat-val">' + eta + '</span></div>' +
|
||||
'</div>' +
|
||||
'<div style="margin-top:10px">' +
|
||||
'<div style="font-size:0.75em;color:var(--dim);margin-bottom:4px">' + opts.barLabel + '</div>' +
|
||||
'<div style="background:var(--border);border-radius:4px;height:6px;overflow:hidden">' +
|
||||
'<div style="background:' + barColor + ';height:100%;width:' + Math.min(successRate, 100) + '%;transition:width 0.5s"></div>' +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
'<div style="font-size:0.75em;color:var(--dim);margin-top:8px">' + opts.footer + '</div>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
// Global queue info for all cards
|
||||
var globalQueue = data.queue ? data.queue.due : 0;
|
||||
|
||||
// Manager card (if manager has local testing enabled)
|
||||
if (data.manager) {
|
||||
var m = data.manager;
|
||||
html += buildCard({
|
||||
name: 'Manager',
|
||||
isManager: true,
|
||||
active: true,
|
||||
rate: m.rate,
|
||||
tested: m.tested,
|
||||
working: m.passed,
|
||||
successRate: m.success_rate,
|
||||
queue: globalQueue,
|
||||
barLabel: m.threads + ' threads | Success Rate',
|
||||
footer: 'Uptime: ' + formatAge(m.uptime).replace(' ago', ''),
|
||||
profiling: false
|
||||
});
|
||||
}
|
||||
|
||||
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>';
|
||||
if (!data.manager) {
|
||||
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;
|
||||
}
|
||||
container.innerHTML = html;
|
||||
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 testRate = w.test_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">Rate</span><span class="stat-val cyn">' + (testRate > 0 ? testRate.toFixed(1) + '/s' : '-') + '</span></div>' +
|
||||
'<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>' +
|
||||
'<div style="font-size:0.75em;color:var(--dim);margin-top:8px">' +
|
||||
'IP: ' + w.ip + ' | Last: ' + formatAge(w.age) +
|
||||
'</div>' +
|
||||
'</div>';
|
||||
var threadLabel = w.threads > 0 ? (w.threads + ' threads | ') : '';
|
||||
var trustLabel = w.verifications > 0 ? ('Trust: ' + (w.trust_score * 100).toFixed(0) + '%') : '';
|
||||
var footerInfo = 'Last seen: ' + formatAge(w.age);
|
||||
if (trustLabel) footerInfo += ' | ' + trustLabel;
|
||||
html += buildCard({
|
||||
name: w.name,
|
||||
isManager: false,
|
||||
active: w.active,
|
||||
rate: w.test_rate,
|
||||
tested: w.proxies_tested,
|
||||
working: w.proxies_working,
|
||||
successRate: w.success_rate,
|
||||
queue: globalQueue,
|
||||
barLabel: threadLabel + 'Success Rate',
|
||||
footer: footerInfo,
|
||||
profiling: w.profiling,
|
||||
trustScore: w.trust_score
|
||||
});
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user