httpd: extract static files to separate directory
This commit is contained in:
651
static/dashboard.js
Normal file
651
static/dashboard.js
Normal file
@@ -0,0 +1,651 @@
|
||||
/* 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 = {};
|
||||
|
||||
// Network rate tracking (for real-time speed calculation)
|
||||
var prevNet = null;
|
||||
var prevNetTime = null;
|
||||
|
||||
// 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 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);
|
||||
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 += '<div class="histo-bar" style="height:' + h + '%;background:' + c + '" data-label="' + d.range + '" title="' + d.range + 'ms: ' + d.count + ' (' + d.pct + '%)"></div>';
|
||||
});
|
||||
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 = '<div style="color:var(--dim);font-size:11px;padding:8px 0">No data</div>'; 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 += '<div class="lb-item"><div class="lb-rank' + (i === 0 ? ' top' : '') + '">' + (i + 1) + '</div>';
|
||||
html += '<span class="lb-name">' + name + '</span><span class="lb-val grn">' + fmt(val) + '</span></div>';
|
||||
});
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
function update(d) {
|
||||
$('dot').className = 'dot'; $('statusTxt').textContent = 'Live';
|
||||
|
||||
// SSL badge (main test mode when enabled)
|
||||
var sslBadge = $('sslBadge');
|
||||
if (sslBadge) {
|
||||
sslBadge.style.display = d.use_ssl ? 'inline-block' : 'none';
|
||||
}
|
||||
// Check type badge (fallback/secondary indicator)
|
||||
var ct = d.checktype || 'unknown';
|
||||
var ctBadge = $('checktypeBadge');
|
||||
if (ctBadge) {
|
||||
ctBadge.textContent = ct.toUpperCase();
|
||||
ctBadge.className = 'mode-badge mode-' + ct;
|
||||
}
|
||||
// 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;
|
||||
|
||||
// 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
|
||||
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');
|
||||
|
||||
var rsr = d.recent_success_rate || 0;
|
||||
$('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);
|
||||
|
||||
// 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
|
||||
$('threads').textContent = d.threads + '/' + d.max_threads;
|
||||
setBar('threadBar', d.threads, d.max_threads, 'blu');
|
||||
$('queue').textContent = fmt(d.queue_size);
|
||||
$('uptime').textContent = fmtTime(d.uptime_seconds);
|
||||
|
||||
// Charts
|
||||
renderLineChart('rateChart', d.rate_history, '#58a6ff', d.peak_rate * 1.1);
|
||||
renderLineChart('srChart', d.success_rate_history, '#3fb950', 100);
|
||||
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)
|
||||
var passed = d.passed || 0, failed = d.failed || 0, total = passed + failed;
|
||||
if (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)
|
||||
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]; });
|
||||
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 += '<div class="legend-item"><div class="legend-dot" style="background:' + col + '"></div>';
|
||||
fhtml += '<span class="legend-name">' + cat + '</span><span class="legend-val">' + n + '</span></div>';
|
||||
});
|
||||
} else {
|
||||
fhtml = '<div style="color:var(--dim);font-size:11px">No failures yet</div>';
|
||||
}
|
||||
$('failLegend').innerHTML = fhtml;
|
||||
|
||||
// Leaderboards (session data)
|
||||
renderLeaderboard('topAsns', d.top_asns_session, 'asn', 'count');
|
||||
|
||||
// Country pie chart (Chart.js doughnut)
|
||||
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 += '<div class="legend-item"><div class="legend-dot" style="background:' + col + '"></div>';
|
||||
chtml += '<span class="legend-name">' + code + '</span><span class="legend-val">' + fmt(cnt) + '</span>';
|
||||
chtml += '<span class="sub" style="margin-left:4px">' + pctVal + '%</span></div>';
|
||||
});
|
||||
renderDoughnutChart('countryPie', cLabels, cValues, cColors);
|
||||
$('countryLegend').innerHTML = chtml;
|
||||
} else {
|
||||
$('countryLegend').innerHTML = '<div style="color:var(--dim);font-size:11px">No data</div>';
|
||||
}
|
||||
|
||||
// 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 += '<div class="tor-card"><div class="host-card">';
|
||||
thtml += '<span class="host-addr">' + h.address + '</span>';
|
||||
thtml += '<span class="tag ' + statusCls + '">' + statusTxt + '</span>';
|
||||
thtml += '</div><div class="host-stats">' + fmtMs(h.latency_ms) + ' / ' + fmtDec(h.success_rate, 0) + '% success</div></div>';
|
||||
});
|
||||
}
|
||||
$('torPool').innerHTML = thtml || '<div class="tor-card" style="color:var(--dim)">No Tor hosts</div>';
|
||||
|
||||
// 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 += '<div class="judge-item"><span class="judge-name">' + j.judge + '</span>';
|
||||
jhtml += '<div class="judge-stats"><span class="grn">' + j.success + '/' + j.tests + '</span>';
|
||||
jhtml += '<span style="color:var(--dim)">' + fmtDec(j.rate, 0) + '%</span></div></div>';
|
||||
});
|
||||
}
|
||||
$('topJudges').innerHTML = jhtml || '<div style="color:var(--dim)">No data</div>';
|
||||
}
|
||||
|
||||
// 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 '<div class="stat-row"><span class="stat-lbl">' + p.toUpperCase() + '</span><span class="stat-val">' + fmt(c) + '</span></div>';
|
||||
}).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 += '<div class="lb-item"><div class="lb-rank' + (i === 0 ? ' top' : '') + '">' + (i + 1) + '</div>';
|
||||
ehtml += '<span class="lb-name">' + e.name + '</span>';
|
||||
ehtml += '<span class="tag ' + statusCls + '">' + statusTxt + '</span>';
|
||||
ehtml += '<span class="lb-val grn">' + fmt(e.successes) + '</span></div>';
|
||||
});
|
||||
$('topEngines').innerHTML = ehtml || '<div style="color:var(--dim)">No engines</div>';
|
||||
} else {
|
||||
$('topEngines').innerHTML = '<div style="color:var(--dim)">Scraper disabled</div>';
|
||||
}
|
||||
|
||||
// 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 += '<div class="stat-row"><span class="stat-lbl">' + level.charAt(0).toUpperCase() + level.slice(1) + '</span>';
|
||||
anonHtml += '<span class="stat-val ' + anonColors[level] + '">' + fmt(count) + '</span></div>';
|
||||
}
|
||||
});
|
||||
$('anonBreakdown').innerHTML = anonHtml || '<div style="color:var(--dim)">No data</div>';
|
||||
}
|
||||
|
||||
// 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 += '<div class="lb-item"><div class="lb-rank' + (i === 0 ? ' top' : '') + '">' + (i + 1) + '</div>';
|
||||
orgsHtml += '<span class="lb-name">' + (org.name || 'Unknown') + '</span>';
|
||||
orgsHtml += '<span class="lb-val red">' + fmt(org.count) + '</span></div>';
|
||||
});
|
||||
} else {
|
||||
orgsHtml = '<div style="color:var(--dim);font-size:11px;padding:8px 0">No MITM certs detected</div>';
|
||||
}
|
||||
$('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 += '<div class="lb-item"><div class="lb-rank' + (i === 0 ? ' top' : '') + '">' + (i + 1) + '</div>';
|
||||
issHtml += '<span class="lb-name">' + (iss.name || 'Unknown') + '</span>';
|
||||
issHtml += '<span class="lb-val yel">' + fmt(iss.count) + '</span></div>';
|
||||
});
|
||||
} else {
|
||||
issHtml = '<div style="color:var(--dim);font-size:11px;padding:8px 0">No MITM certs detected</div>';
|
||||
}
|
||||
$('mitmIssuers').innerHTML = issHtml;
|
||||
|
||||
// Certificate details
|
||||
var certHtml = '';
|
||||
if (mitm.certificates && mitm.certificates.length > 0) {
|
||||
mitm.certificates.slice(0, 10).forEach(function(cert) {
|
||||
certHtml += '<div class="stat-row" style="flex-wrap:wrap;padding:6px 0;border-bottom:1px solid var(--border)">';
|
||||
certHtml += '<div style="width:100%;margin-bottom:4px">';
|
||||
certHtml += '<span class="stat-lbl" style="font-weight:500">CN:</span> <span class="cyn">' + (cert.subject_cn || '-') + '</span>';
|
||||
certHtml += '</div>';
|
||||
certHtml += '<div style="width:50%"><span class="stat-lbl">Org:</span> ' + (cert.subject_o || '-') + '</div>';
|
||||
certHtml += '<div style="width:50%"><span class="stat-lbl">Issuer:</span> ' + (cert.issuer_cn || '-') + '</div>';
|
||||
certHtml += '<div style="width:50%"><span class="stat-lbl">Count:</span> <span class="red">' + fmt(cert.count || 1) + '</span></div>';
|
||||
certHtml += '<div style="width:50%"><span class="stat-lbl">Proxies:</span> ' + (cert.proxies ? cert.proxies.length : 0) + '</div>';
|
||||
certHtml += '<div style="width:100%;font-size:9px;color:var(--dim)">FP: ' + (cert.fingerprint || '-') + '</div>';
|
||||
certHtml += '</div>';
|
||||
});
|
||||
} else {
|
||||
certHtml = '<div style="color:var(--dim);font-size:11px;padding:12px 0">No MITM certificates captured yet</div>';
|
||||
}
|
||||
$('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 += '<div class="stat-row" style="padding:4px 0;border-bottom:1px solid var(--border)">';
|
||||
recentHtml += '<span class="stat-lbl" style="color:var(--dim);width:60px">' + ts + '</span>';
|
||||
recentHtml += '<span style="flex:1">' + (r.proxy || '-') + '</span>';
|
||||
recentHtml += '<span class="cyn" style="width:120px;overflow:hidden;text-overflow:ellipsis">' + (r.subject_cn || '-') + '</span>';
|
||||
recentHtml += '</div>';
|
||||
});
|
||||
} else {
|
||||
recentHtml = '<div style="color:var(--dim);font-size:11px;padding:12px 0">No recent MITM detections</div>';
|
||||
}
|
||||
$('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();
|
||||
}
|
||||
|
||||
function fetchStats() {
|
||||
fetch('/api/stats')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(update)
|
||||
.catch(function(e) { $('dot').className = 'dot err'; $('statusTxt').textContent = 'Error'; });
|
||||
}
|
||||
|
||||
fetchStats();
|
||||
setInterval(fetchStats, 3000);
|
||||
Reference in New Issue
Block a user