/* PPF Dashboard JavaScript */ var $ = function(id) { return document.getElementById(id); }; var $$ = function(sel) { return document.querySelectorAll(sel); }; var fmt = function(n) { return n == null ? '-' : n.toLocaleString(); }; // uPlot chart instances (persistent) var uplotCharts = {}; // Chart.js instances (persistent) var chartJsInstances = {}; // Get currently active tab ID function getActiveTab() { var activeBtn = document.querySelector('.tab-btn.active'); return activeBtn ? activeBtn.dataset.tab : 'overview'; } // Check if a specific tab is currently active function isTabActive(tabId) { return getActiveTab() === tabId; } // Tab switching function initTabs() { $$('.tab-btn').forEach(function(btn) { btn.addEventListener('click', function() { var tabId = this.dataset.tab; // Update buttons $$('.tab-btn').forEach(function(b) { b.classList.remove('active'); }); this.classList.add('active'); // Update content $$('.tab-content').forEach(function(c) { c.classList.remove('active'); }); var content = $('tab-' + tabId); if (content) content.classList.add('active'); // Save preference try { localStorage.setItem('ppf-tab', tabId); } catch(e) {} }); }); // Restore saved tab try { var saved = localStorage.getItem('ppf-tab'); if (saved) { var btn = document.querySelector('.tab-btn[data-tab="' + saved + '"]'); if (btn) btn.click(); } } catch(e) {} } document.addEventListener('DOMContentLoaded', initTabs); // Theme toggle (cycles: dark -> muted-dark -> light -> dark) var themes = ['dark', 'muted-dark', 'light']; function getTheme() { if (document.documentElement.classList.contains('light')) return 'light'; if (document.documentElement.classList.contains('muted-dark')) return 'muted-dark'; return 'dark'; } function setTheme(theme) { document.documentElement.classList.remove('light', 'muted-dark'); if (theme === 'light') document.documentElement.classList.add('light'); else if (theme === 'muted-dark') document.documentElement.classList.add('muted-dark'); try { localStorage.setItem('ppf-theme', theme); } catch(e) {} } function initTheme() { // Check saved preference or system preference var saved = null; try { saved = localStorage.getItem('ppf-theme'); } catch(e) {} if (saved && themes.indexOf(saved) !== -1) { setTheme(saved); } else if (window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches) { setTheme('light'); } // Setup toggle button var btn = document.getElementById('themeToggle'); if (btn) { btn.addEventListener('click', function() { var current = getTheme(); var idx = themes.indexOf(current); var next = themes[(idx + 1) % themes.length]; setTheme(next); }); } } document.addEventListener('DOMContentLoaded', initTheme); var fmtDec = function(n, d) { return n == null ? '-' : n.toFixed(d || 1); }; var pct = function(n, t) { return t > 0 ? ((n / t) * 100).toFixed(1) : '0.0'; }; function fmtBytes(b) { if (!b || b <= 0) return '-'; var units = ['B', 'KB', 'MB', 'GB', 'TB']; var i = 0; while (b >= 1024 && i < units.length - 1) { b /= 1024; i++; } return b.toFixed(i > 0 ? 1 : 0) + ' ' + units[i]; } function fmtTime(s) { if (!s) return '-'; var d = Math.floor(s / 86400), h = Math.floor((s % 86400) / 3600), m = Math.floor((s % 3600) / 60); if (d > 0) return d + 'd ' + h + 'h'; if (h > 0) return h + 'h ' + m + 'm'; return m + 'm ' + Math.floor(s % 60) + 's'; } function setBarColor(el, pct) { if (pct > 90) return 'background:var(--red)'; if (pct > 70) return 'background:var(--yellow)'; return 'background:var(--green)'; } function fmtMs(ms) { if (!ms || ms <= 0) return '-'; if (ms < 1000) return Math.round(ms) + 'ms'; return (ms / 1000).toFixed(1) + 's'; } function setBar(id, val, max, cls) { var el = $(id); if (el) { el.style.width = Math.min(val / max * 100, 100) + '%'; el.className = 'bar ' + (cls || 'grn'); } } // uPlot-based line chart with electric cyan theme function renderLineChart(id, data, color, maxVal) { var el = $(id); if (!el || !data || data.length < 2) return; var w = el.clientWidth || 300, h = el.clientHeight || 80; var max = maxVal || Math.max.apply(null, data) || 1; // Generate time indices (mock timestamps, 3s apart) var now = Date.now() / 1000; var times = data.map(function(_, i) { return now - (data.length - 1 - i) * 3; }); var opts = { width: w, height: h, padding: [4, 4, 4, 4], cursor: { show: false }, legend: { show: false }, axes: [ { show: false }, // x-axis hidden { show: false } // y-axis hidden ], scales: { x: { time: false }, y: { range: [0, max * 1.1] } }, series: [ {}, // x series (timestamps) { stroke: color, width: 2, fill: function(u, seriesIdx) { var grad = u.ctx.createLinearGradient(0, 0, 0, h); grad.addColorStop(0, color.replace(')', ',0.4)').replace('rgb', 'rgba')); grad.addColorStop(1, color.replace(')', ',0.05)').replace('rgb', 'rgba')); return grad; } } ] }; // Destroy existing chart if any if (uplotCharts[id]) { uplotCharts[id].destroy(); } el.innerHTML = ''; uplotCharts[id] = new uPlot(opts, [times, data], el); } // Chart.js doughnut chart with electric cyan theme function renderDoughnutChart(id, labels, values, colors, cutout) { var el = $(id); if (!el) return; cutout = cutout || '65%'; // Convert div to canvas if needed var canvas; if (el.tagName !== 'CANVAS') { canvas = el.querySelector('canvas'); if (!canvas) { canvas = document.createElement('canvas'); el.innerHTML = ''; el.appendChild(canvas); } } else { canvas = el; } var ctx = canvas.getContext('2d'); var total = values.reduce(function(a, b) { return a + b; }, 0); if (total === 0) return; // Destroy existing chart if (chartJsInstances[id]) { chartJsInstances[id].destroy(); } chartJsInstances[id] = new Chart(ctx, { type: 'doughnut', data: { labels: labels, datasets: [{ data: values, backgroundColor: colors, borderColor: 'rgba(24,31,42,0.8)', borderWidth: 2, hoverBorderColor: '#38bdf8', hoverBorderWidth: 3 }] }, options: { responsive: true, maintainAspectRatio: true, cutout: cutout, plugins: { legend: { display: false }, tooltip: { backgroundColor: 'rgba(24,31,42,0.95)', titleColor: '#38bdf8', bodyColor: '#e6edf3', borderColor: 'rgba(56,189,248,0.3)', borderWidth: 1, padding: 12, displayColors: true, callbacks: { label: function(ctx) { var pct = ((ctx.raw / total) * 100).toFixed(1); return ctx.label + ': ' + fmt(ctx.raw) + ' (' + pct + '%)'; } } } }, animation: { duration: 400, easing: 'easeOutQuart' } } }); } function renderHistogram(id, data) { var el = $(id); if (!el || !data || !data.length) return; var max = Math.max.apply(null, data.map(function(d) { return d.pct; })) || 1; var colors = ['#3fb950', '#3fb950', '#58a6ff', '#58a6ff', '#d29922', '#f85149', '#f85149']; var html = ''; data.forEach(function(d, i) { var h = Math.max(4, (d.pct / max) * 100); var c = colors[Math.min(i, colors.length - 1)]; html += '
'; }); el.innerHTML = html; } function renderLeaderboard(id, data, nameKey, valKey, limit) { var el = $(id); if (!el) return; limit = limit || 8; if (!data || !data.length) { el.innerHTML = '
No data
'; return; } var html = ''; data.slice(0, limit).forEach(function(item, i) { var name = Array.isArray(item) ? item[0] : item[nameKey]; var val = Array.isArray(item) ? item[1] : item[valKey]; html += '
' + (i + 1) + '
'; html += '' + name + '' + fmt(val) + '
'; }); el.innerHTML = html; } function update(d) { $('dot').className = 'dot'; $('statusTxt').textContent = 'Live'; // SSL badge (always visible - PPF's signature feature) var sslBadge = $('sslBadge'); if (sslBadge) { sslBadge.style.display = 'inline-block'; } // Check type badges (multiple supported) var ctContainer = $('checktypeBadges'); if (ctContainer) { var checktypes = (d.checktype || 'unknown').split(','); var badgeColors = {judges: 'blu', head: 'grn', ssl: 'cyn', irc: 'pur', tor: 'mag', unknown: 'dim'}; var html = ''; checktypes.forEach(function(ct) { ct = ct.trim(); var color = badgeColors[ct] || 'dim'; html += '' + ct.toUpperCase() + ''; }); ctContainer.innerHTML = html; } // Profiling badge var profBadge = $('profileBadge'); if (profBadge) { profBadge.style.display = d.profiling ? 'inline-block' : 'none'; } // System monitor bar var sys = d.system || {}; $('sysLoad').textContent = (sys.load_1m || 0) + ' / ' + (sys.cpu_count || 1); $('sysMemVal').textContent = fmtBytes(sys.mem_used) + ' / ' + fmtBytes(sys.mem_total); $('sysMemPct').textContent = (sys.mem_pct || 0) + '%'; var memFill = $('sysMemFill'); if (memFill) { memFill.style.width = (sys.mem_pct || 0) + '%'; memFill.style.cssText = 'width:' + (sys.mem_pct || 0) + '%;' + setBarColor(memFill, sys.mem_pct || 0); } $('sysDiskVal').textContent = fmtBytes(sys.disk_used) + ' / ' + fmtBytes(sys.disk_total); $('sysDiskPct').textContent = (sys.disk_pct || 0) + '%'; var diskFill = $('sysDiskFill'); if (diskFill) { diskFill.style.width = (sys.disk_pct || 0) + '%'; diskFill.style.cssText = 'width:' + (sys.disk_pct || 0) + '%;' + setBarColor(diskFill, sys.disk_pct || 0); } var memGrowth = sys.proc_rss_growth || 0; var memGrowthStr = memGrowth > 0 ? ' (+' + fmtBytes(memGrowth) + ')' : ''; $('sysProcMem').textContent = fmtBytes(sys.proc_rss) + memGrowthStr; // Main stats - db stats are nested under d.db var db = d.db || {}; $('working').textContent = fmt(db.working); $('total').textContent = fmt(db.total); // Success rate (displayed in Worker Testing section) var sr = d.success_rate || 0; $('successRate').textContent = fmtDec(sr, 1) + '%'; $('successRate').className = 'stat-val ' + (sr < 20 ? 'red' : sr < 50 ? 'yel' : 'grn'); var rsr = d.recent_success_rate || 0; if ($('recentSuccessRate')) { $('recentSuccessRate').textContent = fmtDec(rsr, 1) + '%'; $('recentSuccessRate').className = 'stat-val ' + (rsr < 20 ? 'red' : rsr < 50 ? 'yel' : 'grn'); } // 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; $('avgLatency').textContent = fmtMs(lat); $('minLatency').textContent = fmtMs(d.min_latency); $('maxLatency').textContent = fmtMs(d.max_latency); var pctl = d.latency_percentiles || {}; $('p50').textContent = fmtMs(pctl.p50); $('p90').textContent = fmtMs(pctl.p90); $('p99').textContent = fmtMs(pctl.p99); // System $('uptime').textContent = fmtTime(d.uptime_seconds); // Charts - only render for active tabs to reduce CPU usage var activeTab = getActiveTab(); if (activeTab === 'overview' || activeTab === 'perf') { renderLineChart('rateChart', d.rate_history, '#58a6ff', d.peak_rate * 1.1); renderLineChart('srChart', d.success_rate_history, '#3fb950', 100); } if (activeTab === 'perf') { renderHistogram('latencyHisto', d.latency_histogram); } // Protocol breakdown var ps = d.proto_stats || {}; ['http', 'socks4', 'socks5'].forEach(function(p) { var s = ps[p] || {passed: 0, tested: 0, success_rate: 0}; $(p + 'Passed').textContent = fmt(s.passed); $(p + 'Tested').textContent = fmt(s.tested); var rateEl = $(p + 'Rate'); rateEl.textContent = fmtDec(s.success_rate, 0) + '%'; rateEl.className = 'proto-rate ' + (s.success_rate < 20 ? 'tag-err' : s.success_rate < 50 ? 'tag-warn' : 'tag-ok'); }); // Results pie (Chart.js doughnut) - only on overview tab var passed = d.passed || 0, failed = d.failed || 0, total = passed + failed; if (activeTab === 'overview' && total > 0) { renderDoughnutChart('resultsPie', ['Passed', 'Failed'], [passed, failed], ['#3fb950', '#f85149']); } $('passedLeg').textContent = fmt(passed); $('passedPct').textContent = pct(passed, total) + '%'; $('failedLeg').textContent = fmt(failed); $('failedPct').textContent = pct(failed, total) + '%'; // Failures breakdown (Chart.js doughnut) - only on overview tab var fhtml = '', failColors = ['#f85149','#db6d28','#d29922','#58a6ff','#a371f7','#39c5cf','#db61a2','#7d8590']; if (d.failures && Object.keys(d.failures).length > 0) { var cats = Object.keys(d.failures).sort(function(a,b) { return d.failures[b] - d.failures[a]; }); if (activeTab === 'overview') { var failVals = cats.map(function(c) { return d.failures[c]; }); var failCols = cats.map(function(_, i) { return failColors[i % failColors.length]; }); renderDoughnutChart('failPie', cats, failVals, failCols); } cats.forEach(function(cat, i) { var n = d.failures[cat], col = failColors[i % failColors.length]; fhtml += '
'; fhtml += '' + cat + '' + n + '
'; }); } else { fhtml = '
No failures yet
'; } $('failLegend').innerHTML = fhtml; // Leaderboards (session data) renderLeaderboard('topAsns', d.top_asns_session, 'asn', 'count'); // Country pie chart (Chart.js doughnut) - only on overview/db tabs var countryColors = ['#58a6ff','#3fb950','#d29922','#f85149','#a371f7','#39c5cf','#db61a2','#db6d28','#7ee787','#7d8590']; if (d.db && d.db.top_countries && d.db.top_countries.length > 0) { var countries = d.db.top_countries.slice(0, 8); var countryTotal = countries.reduce(function(s, c) { return s + (c.count || c[1] || 0); }, 0); var cLabels = [], cValues = [], cColors = [], chtml = ''; countries.forEach(function(c, i) { var code = c.code || c[0], cnt = c.count || c[1] || 0; var col = countryColors[i % countryColors.length]; var pctVal = countryTotal > 0 ? ((cnt / countryTotal) * 100).toFixed(1) : '0'; cLabels.push(code); cValues.push(cnt); cColors.push(col); chtml += '
'; chtml += '' + code + '' + fmt(cnt) + ''; chtml += '' + pctVal + '%
'; }); if (activeTab === 'overview' || activeTab === 'db') { renderDoughnutChart('countryPie', cLabels, cValues, cColors); } $('countryLegend').innerHTML = chtml; } else { $('countryLegend').innerHTML = '
No data
'; } // Tor pool var thtml = ''; if (d.tor_pool && d.tor_pool.hosts) { d.tor_pool.hosts.forEach(function(h) { // Status: OK only if available AND has successes, WARN if available but 0%, DOWN if in backoff var statusCls = !h.healthy ? 'tag-err' : (h.success_rate > 0 ? 'tag-ok' : 'tag-warn'); var statusTxt = !h.healthy ? 'DOWN' : (h.success_rate > 0 ? 'OK' : 'IDLE'); thtml += '
'; thtml += '' + h.address + ''; thtml += '' + statusTxt + ''; thtml += '
' + fmtMs(h.latency_ms) + ' / ' + fmtDec(h.success_rate, 0) + '% success
'; }); } $('torPool').innerHTML = thtml || '
No Tor hosts
'; // Judges if (d.judges) { $('judgesAvail').textContent = d.judges.available + '/' + d.judges.total; $('judgesCooldown').textContent = d.judges.in_cooldown; var jhtml = ''; if (d.judges.top_judges) { d.judges.top_judges.slice(0, 6).forEach(function(j) { jhtml += '
' + j.judge + ''; jhtml += '
' + j.success + '/' + j.tests + ''; jhtml += '' + fmtDec(j.rate, 0) + '%
'; }); } $('topJudges').innerHTML = jhtml || '
No data
'; } // Database stats if (d.db) { var dbs = d.db; $('dbByProto').innerHTML = ['http', 'socks4', 'socks5'].map(function(p) { var c = dbs.by_proto && dbs.by_proto[p] || 0; return '
' + p.toUpperCase() + '' + fmt(c) + '
'; }).join(''); } // Scraper/Engine stats $('engAvail').textContent = fmt(d.engines_available); $('engBackoff').textContent = fmt(d.engines_backoff); $('engTotal').textContent = fmt(d.engines_total); if (d.scraper && d.scraper.engines) { var ehtml = ''; d.scraper.engines.slice(0, 5).forEach(function(e, i) { var statusCls = e.available ? 'tag-ok' : 'tag-warn'; var statusTxt = e.available ? 'OK' : (e.backoff_remaining > 0 ? e.backoff_remaining + 's' : 'OFF'); ehtml += '
' + (i + 1) + '
'; ehtml += '' + e.name + ''; ehtml += '' + statusTxt + ''; ehtml += '' + fmt(e.successes) + '
'; }); $('topEngines').innerHTML = ehtml || '
No engines
'; } else { $('topEngines').innerHTML = '
Scraper disabled
'; } // SSL/TLS stats if (d.ssl) { var ssl = d.ssl; $('sslTested').textContent = fmt(ssl.tested); $('sslPassed').textContent = fmt(ssl.passed); $('sslFailed').textContent = fmt(ssl.failed); var sslRate = ssl.success_rate || 0; setBar('sslBar', sslRate, 100, sslRate < 50 ? 'red' : sslRate < 80 ? 'yel' : 'grn'); $('mitmDetected').textContent = fmt(ssl.mitm_detected); $('mitmDetected').className = 'stat-val ' + (ssl.mitm_detected > 0 ? 'red' : 'grn'); $('certErrors').textContent = fmt(ssl.cert_errors); $('certErrors').className = 'stat-val ' + (ssl.cert_errors > 0 ? 'yel' : 'grn'); } // Anonymity breakdown var dbh = d.db_health || {}; if (dbh.anonymity) { var anonHtml = ''; var anonColors = {elite: 'grn', anonymous: 'blu', transparent: 'yel', unknown: 'dim'}; var anonOrder = ['elite', 'anonymous', 'transparent', 'unknown']; anonOrder.forEach(function(level) { var count = dbh.anonymity[level] || 0; if (count > 0) { anonHtml += '
' + level.charAt(0).toUpperCase() + level.slice(1) + ''; anonHtml += '' + fmt(count) + '
'; } }); $('anonBreakdown').innerHTML = anonHtml || '
No data
'; } // Database health if (dbh.db_size) { $('dbSize').textContent = fmtBytes(dbh.db_size); $('dbTestedHour').textContent = fmt(dbh.tested_last_hour); $('dbAddedDay').textContent = fmt(dbh.added_last_day); $('dbDead').textContent = fmt(dbh.dead_count); $('dbFailing').textContent = fmt(dbh.failing_count); $('dbFreelist').textContent = fmt(dbh.freelist_count); $('dbAvgLat').textContent = fmtMs(dbh.db_avg_latency); $('dbMinLat').textContent = fmtMs(dbh.db_min_latency); $('dbMaxLat').textContent = fmtMs(dbh.db_max_latency); } // Tor pool enhanced stats if (d.tor_pool) { var tp = d.tor_pool; $('torTotal').textContent = fmt(tp.total_requests || 0); $('torSuccess').textContent = fmtDec(tp.success_rate || 0, 1) + '%'; $('torHealthy').textContent = (tp.healthy_count || 0) + '/' + (tp.total_count || 0); if (tp.avg_latency) { $('torLatency').textContent = fmtMs(tp.avg_latency); } } // MITM certificate stats if (d.mitm) { var mitm = d.mitm; $('mitmTotal').textContent = fmt(mitm.total_detections || 0); $('mitmUniqueCerts').textContent = fmt(mitm.unique_certs || 0); $('mitmUniqueProxies').textContent = fmt(mitm.unique_proxies || 0); $('mitmSslTests').textContent = d.ssl ? fmt(d.ssl.tested || 0) : '-'; // Top organizations var orgsHtml = ''; if (mitm.top_organizations && mitm.top_organizations.length > 0) { mitm.top_organizations.slice(0, 8).forEach(function(org, i) { orgsHtml += '
' + (i + 1) + '
'; orgsHtml += '' + (org.name || 'Unknown') + ''; orgsHtml += '' + fmt(org.count) + '
'; }); } else { orgsHtml = '
No MITM certs detected
'; } $('mitmOrgs').innerHTML = orgsHtml; // Top issuers var issHtml = ''; if (mitm.top_issuers && mitm.top_issuers.length > 0) { mitm.top_issuers.slice(0, 8).forEach(function(iss, i) { issHtml += '
' + (i + 1) + '
'; issHtml += '' + (iss.name || 'Unknown') + ''; issHtml += '' + fmt(iss.count) + '
'; }); } else { issHtml = '
No MITM certs detected
'; } $('mitmIssuers').innerHTML = issHtml; // Certificate details var certHtml = ''; if (mitm.certificates && mitm.certificates.length > 0) { mitm.certificates.slice(0, 10).forEach(function(cert) { certHtml += '
'; certHtml += '
'; certHtml += 'CN: ' + (cert.subject_cn || '-') + ''; certHtml += '
'; certHtml += '
Org: ' + (cert.subject_o || '-') + '
'; certHtml += '
Issuer: ' + (cert.issuer_cn || '-') + '
'; certHtml += '
Count: ' + fmt(cert.count || 1) + '
'; certHtml += '
Proxies: ' + (cert.proxies ? cert.proxies.length : 0) + '
'; certHtml += '
FP: ' + (cert.fingerprint || '-') + '
'; certHtml += '
'; }); } else { certHtml = '
No MITM certificates captured yet
'; } $('mitmCerts').innerHTML = certHtml; // Recent detections var recentHtml = ''; if (mitm.recent && mitm.recent.length > 0) { mitm.recent.slice(-10).reverse().forEach(function(r) { var ts = r.timestamp ? new Date(r.timestamp * 1000).toLocaleTimeString() : '-'; recentHtml += '
'; recentHtml += '' + ts + ''; recentHtml += '' + (r.proxy || '-') + ''; recentHtml += '' + (r.subject_cn || '-') + ''; recentHtml += '
'; }); } else { recentHtml = '
No recent MITM detections
'; } $('mitmRecent').innerHTML = recentHtml; } $('lastUpdate').textContent = new Date().toLocaleTimeString(); } function fetchStats() { // Use batch endpoint for reduced RTT (single request instead of multiple) fetch('/api/dashboard') .then(function(r) { return r.json(); }) .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) { 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); // Combined rate from summary var combinedRate = data.summary.combined_rate || 0; if ($('dwRate')) $('dwRate').textContent = combinedRate > 0 ? combinedRate.toFixed(1) + '/s' : '-'; } // Queue status if (data.queue) { 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 = '' + (opts.active ? 'ACTIVE' : 'OFFLINE') + ''; if (opts.profiling) badges += 'PROF'; // 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 += 'LOW TRUST'; } // 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 '
' + '
' + '' + opts.name + '' + '' + badges + '' + '
' + '
' + '
Rate' + (rate > 0 ? rate.toFixed(1) + '/s' : '-') + '
' + '
Tested' + fmt(opts.tested) + '
' + '
Working' + fmt(opts.working) + '
' + '
Success' + successRate.toFixed(1) + '%
' + '
Queue' + fmt(queue) + '
' + '
ETA' + eta + '
' + '
' + '
' + '
' + opts.barLabel + '
' + '
' + '
' + '
' + '
' + '
' + opts.footer + '
' + '
'; } // 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) { if (!data.manager) { container.innerHTML = '
' + '
No workers connected
' + '
Add workers with: python ppf.py --register --server URL
'; return; } container.innerHTML = html; return; } data.workers.forEach(function(w) { 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; } 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 var pollInterval = null; var pollDelay = 3000; function startPolling() { if (!pollInterval) { fetchStats(); // Fetch immediately pollInterval = setInterval(fetchStats, pollDelay); } } function stopPolling() { if (pollInterval) { clearInterval(pollInterval); pollInterval = null; $('dot').className = 'dot warn'; $('statusTxt').textContent = 'Paused'; } } function handleVisibilityChange() { if (document.hidden) { stopPolling(); } else { startPolling(); } } document.addEventListener('visibilitychange', handleVisibilityChange); // Keyboard shortcuts document.addEventListener('keydown', function(e) { // Ignore if typing in input/textarea if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; var key = e.key.toLowerCase(); // r = refresh if (key === 'r' && !e.ctrlKey && !e.metaKey) { fetchStats(); return; } // 1-9 = switch tabs if (key >= '1' && key <= '9') { var tabs = document.querySelectorAll('.tab-btn'); var idx = parseInt(key) - 1; if (tabs[idx]) { tabs[idx].click(); } return; } // t = cycle theme if (key === 't' && !e.ctrlKey && !e.metaKey) { var btn = document.getElementById('themeToggle'); if (btn) btn.click(); return; } // p = toggle polling pause if (key === 'p' && !e.ctrlKey && !e.metaKey) { if (pollInterval) { stopPolling(); } else { startPolling(); } return; } }); // Initial start if (!document.hidden) { startPolling(); }