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:
Username
2026-01-08 09:02:56 +01:00
parent 44604f1ce3
commit 6cc903c924
2 changed files with 568 additions and 178 deletions

View File

@@ -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;
}