httpd: extract static files to separate directory

This commit is contained in:
Username
2025-12-25 02:51:30 +01:00
parent 630ed96aa2
commit 9429d24fd5
17 changed files with 4176 additions and 877 deletions

1122
httpd.py

File diff suppressed because it is too large Load Diff

431
static/dashboard.html Normal file
View File

@@ -0,0 +1,431 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PPF Dashboard</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/lib/uPlot.min.css">
</head>
<body>
<div class="container">
<div class="hdr">
<h1>PPF Dashboard <a href="/map" style="font-size:12px;font-weight:normal;color:var(--dim);margin-left:12px">Map</a> <a href="/mitm" style="font-size:12px;font-weight:normal;color:var(--dim);margin-left:8px">MITM Search</a></h1>
<div class="status">
<span class="mode-badge mode-ssl" id="sslBadge" style="display:none">SSL</span>
<span class="mode-badge mode-judges" id="checktypeBadge">-</span>
<span class="mode-badge mode-profile" id="profileBadge" style="display:none">PROFILING</span>
<div class="status-item"><div class="dot" id="dot"></div><span id="statusTxt">Connecting</span></div>
<div class="status-item">Updated: <span id="lastUpdate">-</span></div>
<button class="theme-toggle" id="themeToggle" title="Toggle light/dark theme">
<span class="theme-toggle-icon">&#9790;</span>
</button>
</div>
</div>
<!-- System Monitor Bar -->
<div class="sysbar">
<div class="sysbar-item"><span class="sysbar-lbl">Load:</span><span class="sysbar-val" id="sysLoad">-</span></div>
<div class="sysbar-item"><span class="sysbar-lbl">Memory:</span><span class="sysbar-val" id="sysMemVal">-</span>
<div class="sysbar-bar"><div class="sysbar-fill" id="sysMemFill" style="width:0"></div></div>
<span class="sysbar-val" id="sysMemPct">-</span></div>
<div class="sysbar-item"><span class="sysbar-lbl">Disk:</span><span class="sysbar-val" id="sysDiskVal">-</span>
<div class="sysbar-bar"><div class="sysbar-fill" id="sysDiskFill" style="width:0"></div></div>
<span class="sysbar-val" id="sysDiskPct">-</span></div>
<div class="sysbar-item"><span class="sysbar-lbl">Process:</span><span class="sysbar-val" id="sysProcMem">-</span></div>
<div class="sysbar-item sysbar-net"><span class="sysbar-lbl">Scrape:</span><span class="sysbar-val net-tx" id="netScrapeTx">-</span><span class="sysbar-val net-rx" id="netScrapeRx">-</span></div>
<div class="sysbar-item sysbar-net"><span class="sysbar-lbl">Proxy:</span><span class="sysbar-val net-tx" id="netProxyTx">-</span><span class="sysbar-val net-rx" id="netProxyRx">-</span></div>
</div>
<!-- Primary Stats Row -->
<div class="g g5">
<div class="c">
<div class="lbl">Working Proxies</div>
<div class="val grn" id="working">-</div>
<div class="sub">of <span id="total">-</span> in database</div>
</div>
<div class="c">
<div class="lbl">Tests (Cumulative)</div>
<div class="val" id="tested">-</div>
<div class="sub"><span class="grn" id="passed">-</span> passed / <span class="red" id="failed">-</span> failed</div>
</div>
<div class="c">
<div class="lbl">Success Rate</div>
<div class="val-md grn" id="successRate">-</div>
<div class="bar-wrap"><div class="bar grn" id="srBar" style="width:0"></div></div>
</div>
<div class="c">
<div class="lbl">Test Rate</div>
<div class="val-md blu" id="rate">-</div>
<div class="sub">tests/sec (60s)</div>
</div>
<div class="c">
<div class="lbl">Uptime</div>
<div class="val-md" id="uptime">-</div>
<div class="sub">session duration</div>
</div>
</div>
<!-- Worker Pool (always visible) -->
<div class="g g2" style="margin-bottom:16px">
<div class="c">
<div class="sec-hdr" style="margin-top:0">Worker Pool</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Active Threads</span><span class="stat-val" id="threads">-</span></div>
<div class="bar-wrap"><div class="bar blu" id="threadBar" style="width:0"></div></div>
<div class="stat-row" style="margin-top:8px"><span class="stat-lbl">Job Queue</span><span class="stat-val yel" id="queue">-</span></div>
</div>
</div>
<div class="c">
<div class="sec-hdr" style="margin-top:0">Tor Pool</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Total Requests</span><span class="stat-val" id="torTotal">-</span></div>
<div class="stat-row"><span class="stat-lbl">Success Rate</span><span class="stat-val grn" id="torSuccess">-</span></div>
<div class="stat-row"><span class="stat-lbl">Healthy Nodes</span><span class="stat-val" id="torHealthy">-</span></div>
<div class="stat-row"><span class="stat-lbl">Avg Latency</span><span class="stat-val cyn" id="torLatency">-</span></div>
</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="tabs">
<div class="tab-nav">
<button class="tab-btn active" data-tab="perf">Performance</button>
<button class="tab-btn" data-tab="proto">Protocols</button>
<button class="tab-btn" data-tab="geo">Geography</button>
<button class="tab-btn" data-tab="infra">Infrastructure</button>
<button class="tab-btn" data-tab="mitm">MITM</button>
<button class="tab-btn" data-tab="db">Database</button>
</div>
</div>
<!-- Performance Tab -->
<div id="tab-perf" class="tab-content active">
<!-- Rate & Success Charts -->
<div class="g g2">
<div class="c c-lg">
<div class="lbl">Test Rate History (10 min)</div>
<div class="mini">
<div class="mini-item"><span class="mini-val blu" id="recentRate">-</span><span class="mini-lbl">current</span></div>
<div class="mini-item"><span class="mini-val yel" id="peakRate">-</span><span class="mini-lbl">peak</span></div>
<div class="mini-item"><span class="mini-val grn" id="passRate">-</span><span class="mini-lbl">pass/s</span></div>
</div>
<div class="chart-wrap">
<div class="chart chart-lg" id="rateChart"></div>
</div>
</div>
<div class="c c-lg">
<div class="lbl">Success Rate History</div>
<div class="mini">
<div class="mini-item"><span class="mini-val" id="recentSuccessRate">-</span><span class="mini-lbl">recent</span></div>
</div>
<div class="chart-wrap">
<div class="chart chart-lg" id="srChart"></div>
</div>
</div>
</div>
<!-- Latency Section -->
<div class="sec">
<div class="sec-hdr">Latency Analysis</div>
<div class="g g2">
<div class="c">
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Average</span><span class="stat-val cyn" id="avgLatency">-</span></div>
<div class="stat-row"><span class="stat-lbl">Min</span><span class="stat-val grn" id="minLatency">-</span></div>
<div class="stat-row"><span class="stat-lbl">Max</span><span class="stat-val red" id="maxLatency">-</span></div>
</div>
<div class="pct-badges">
<div class="pct-badge"><div class="pct-label">P50</div><div class="pct-value cyn" id="p50">-</div></div>
<div class="pct-badge"><div class="pct-label">P90</div><div class="pct-value yel" id="p90">-</div></div>
<div class="pct-badge"><div class="pct-label">P99</div><div class="pct-value org" id="p99">-</div></div>
</div>
</div>
<div class="c">
<div class="lbl">Response Time Distribution</div>
<div class="histo-wrap">
<div class="histo" id="latencyHisto"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Protocols Tab -->
<div id="tab-proto" class="tab-content">
<!-- Protocol Breakdown -->
<div class="sec">
<div class="sec-hdr">Protocol Performance</div>
<div class="g g3">
<div class="c proto-card">
<div class="proto-icon">&#x1F310;</div>
<div class="proto-name">HTTP</div>
<div class="proto-val grn" id="httpPassed">-</div>
<div class="sub">of <span id="httpTested">-</span> tested</div>
<div class="proto-rate tag-ok" id="httpRate">-</div>
</div>
<div class="c proto-card">
<div class="proto-icon">&#x1F50C;</div>
<div class="proto-name">SOCKS4</div>
<div class="proto-val blu" id="socks4Passed">-</div>
<div class="sub">of <span id="socks4Tested">-</span> tested</div>
<div class="proto-rate tag-ok" id="socks4Rate">-</div>
</div>
<div class="c proto-card">
<div class="proto-icon">&#x1F512;</div>
<div class="proto-name">SOCKS5</div>
<div class="proto-val pur" id="socks5Passed">-</div>
<div class="sub">of <span id="socks5Tested">-</span> tested</div>
<div class="proto-rate tag-ok" id="socks5Rate">-</div>
</div>
</div>
</div>
<!-- Results & Failures -->
<div class="g g2">
<div class="c">
<div class="sec-hdr" style="margin-top:0">Test Results</div>
<div class="pie-wrap">
<div class="pie" id="resultsPie"></div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:#3fb950"></div><span class="legend-name">Passed</span><span class="legend-val grn" id="passedLeg">-</span><span class="sub" id="passedPct">-</span></div>
<div class="legend-item"><div class="legend-dot" style="background:#f85149"></div><span class="legend-name">Failed</span><span class="legend-val red" id="failedLeg">-</span><span class="sub" id="failedPct">-</span></div>
</div>
</div>
</div>
<div class="c">
<div class="sec-hdr" style="margin-top:0">Failure Breakdown</div>
<div class="pie-wrap">
<div class="pie" id="failPie"></div>
<div class="legend" id="failLegend"></div>
</div>
</div>
</div>
</div>
<!-- Geography Tab -->
<div id="tab-geo" class="tab-content">
<!-- Geographic Distribution -->
<div class="sec">
<div class="sec-hdr">Geographic Distribution <a href="/map" style="font-size:10px;font-weight:normal;margin-left:8px">View Map &rarr;</a></div>
<div class="g g2">
<div class="c">
<div class="lbl">Proxies by Country (Database)</div>
<div class="pie-wrap">
<div class="pie" id="countryPie"></div>
<div class="legend" id="countryLegend"></div>
</div>
</div>
<div class="c">
<div class="lbl">Top ASNs (Session)</div>
<div class="lb-wrap">
<div class="lb" id="topAsns"></div>
</div>
</div>
</div>
</div>
</div>
<!-- Infrastructure Tab -->
<div id="tab-infra" class="tab-content">
<!-- Judge Services & Anonymity -->
<div class="g g2">
<div class="c">
<div class="sec-hdr" style="margin-top:0">Judge Services</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Available</span><span class="stat-val grn" id="judgesAvail">-</span></div>
<div class="stat-row"><span class="stat-lbl">In Cooldown</span><span class="stat-val yel" id="judgesCooldown">-</span></div>
</div>
<div class="lbl" style="margin-top:10px">Top Performers</div>
<div class="lb-wrap">
<div id="topJudges"></div>
</div>
</div>
<div class="c">
<div class="sec-hdr" style="margin-top:0">Anonymity Levels</div>
<div class="stats-wrap">
<div id="anonBreakdown"></div>
</div>
<div class="sub" style="margin-top:8px;font-size:10px">Elite = no headers, Anonymous = adds headers, Transparent = reveals IP</div>
</div>
</div>
<!-- Tor Exit Nodes -->
<div class="sec">
<div class="sec-hdr">Tor Exit Nodes</div>
<div class="g g3" id="torPool"></div>
</div>
<!-- Network Usage -->
<div class="sec">
<div class="sec-hdr">Network Usage</div>
<div class="g g3">
<div class="c c-sm">
<div class="lbl">Total RX</div>
<div class="val-sm grn" id="netRx">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Total TX</div>
<div class="val-sm blu" id="netTx">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Total</div>
<div class="val-sm cyn" id="netTotal">-</div>
</div>
</div>
<div class="g g2" style="margin-top:12px">
<div class="c">
<div class="lbl" style="text-align:center;margin-bottom:8px">By Category</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Proxy Testing</span><span class="stat-val cyn" id="netProxy">-</span></div>
<div class="stat-row"><span class="stat-lbl">Scraping</span><span class="stat-val yel" id="netScraper">-</span></div>
</div>
</div>
<div class="c">
<div class="lbl" style="text-align:center;margin-bottom:8px">Rates (avg)</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">RX Rate</span><span class="stat-val grn" id="netRxRate">-</span></div>
<div class="stat-row"><span class="stat-lbl">TX Rate</span><span class="stat-val blu" id="netTxRate">-</span></div>
</div>
</div>
</div>
<div class="lbl" style="margin-top:12px">Per Tor Node</div>
<div class="g g3" id="netTorNodes"></div>
</div>
<!-- Scraper & SSL Stats -->
<div class="g g2">
<div class="c">
<div class="sec-hdr" style="margin-top:0">Search Engines</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Available</span><span class="stat-val grn" id="engAvail">-</span></div>
<div class="stat-row"><span class="stat-lbl">In Backoff</span><span class="stat-val yel" id="engBackoff">-</span></div>
<div class="stat-row"><span class="stat-lbl">Total</span><span class="stat-val" id="engTotal">-</span></div>
</div>
<div class="lbl" style="margin-top:10px">Top Engines</div>
<div class="lb-wrap">
<div class="lb" id="topEngines"></div>
</div>
</div>
<div class="c">
<div class="sec-hdr" style="margin-top:0">SSL/TLS Security</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">SSL Tests</span><span class="stat-val" id="sslTested">-</span></div>
<div class="stat-row"><span class="stat-lbl">Passed</span><span class="stat-val grn" id="sslPassed">-</span></div>
<div class="stat-row"><span class="stat-lbl">Failed</span><span class="stat-val red" id="sslFailed">-</span></div>
<div class="bar-wrap" style="margin:8px 0"><div class="bar grn" id="sslBar" style="width:0"></div></div>
<div class="stat-row"><span class="stat-lbl">MITM Detected</span><span class="stat-val red" id="mitmDetected">-</span></div>
<div class="stat-row"><span class="stat-lbl">Cert Errors</span><span class="stat-val yel" id="certErrors">-</span></div>
</div>
</div>
</div>
</div>
<!-- MITM Tab -->
<div id="tab-mitm" class="tab-content">
<!-- MITM Certificate Statistics -->
<div class="sec">
<div class="sec-hdr">MITM Detection Summary</div>
<div class="g g4">
<div class="c c-sm">
<div class="lbl">Total Detections</div>
<div class="val-sm red" id="mitmTotal">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Unique Certs</div>
<div class="val-sm yel" id="mitmUniqueCerts">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Unique Proxies</div>
<div class="val-sm org" id="mitmUniqueProxies">-</div>
</div>
<div class="c c-sm">
<div class="lbl">SSL Tests</div>
<div class="val-sm cyn" id="mitmSslTests">-</div>
</div>
</div>
</div>
<div class="g g2">
<div class="c">
<div class="sec-hdr" style="margin-top:0">Top Organizations</div>
<div class="lb-wrap">
<div class="lb" id="mitmOrgs"></div>
</div>
</div>
<div class="c">
<div class="sec-hdr" style="margin-top:0">Top Issuers</div>
<div class="lb-wrap">
<div class="lb" id="mitmIssuers"></div>
</div>
</div>
</div>
<div class="sec">
<div class="sec-hdr">Certificate Details</div>
<div class="stats-wrap" id="mitmCerts" style="max-height:300px;overflow-y:auto"></div>
</div>
<div class="sec">
<div class="sec-hdr">Recent Detections</div>
<div class="stats-wrap" id="mitmRecent" style="max-height:200px;overflow-y:auto"></div>
</div>
</div>
<!-- Database Tab -->
<div id="tab-db" class="tab-content">
<!-- Database Stats -->
<div class="sec">
<div class="sec-hdr">Database Overview</div>
<div class="g g4">
<div class="c c-sm">
<div class="lbl">Database Size</div>
<div class="val-sm cyn" id="dbSize">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Tested (1h)</div>
<div class="val-sm blu" id="dbTestedHour">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Added (24h)</div>
<div class="val-sm grn" id="dbAddedDay">-</div>
</div>
<div class="c c-sm">
<div class="lbl">Dead Proxies</div>
<div class="val-sm red" id="dbDead">-</div>
</div>
</div>
<div class="g g3">
<div class="c">
<div class="lbl">Working by Protocol</div>
<div class="stats-wrap">
<div id="dbByProto"></div>
</div>
</div>
<div class="c">
<div class="lbl">Latency Stats</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Average</span><span class="stat-val cyn" id="dbAvgLat">-</span></div>
<div class="stat-row"><span class="stat-lbl">Min</span><span class="stat-val grn" id="dbMinLat">-</span></div>
<div class="stat-row"><span class="stat-lbl">Max</span><span class="stat-val red" id="dbMaxLat">-</span></div>
</div>
</div>
<div class="c">
<div class="lbl">Activity</div>
<div class="stats-wrap">
<div class="stat-row"><span class="stat-lbl">Failing</span><span class="stat-val yel" id="dbFailing">-</span></div>
<div class="stat-row"><span class="stat-lbl">Freelist</span><span class="stat-val" id="dbFreelist">-</span></div>
</div>
</div>
</div>
</div>
</div>
<div class="ftr">PPF Python Proxy Finder</div>
</div>
<script src="/static/lib/uPlot.min.js"></script>
<script src="/static/lib/chart.min.js"></script>
<script src="/static/dashboard.js"></script>
</body>
</html>

651
static/dashboard.js Normal file
View 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);

View File

@@ -0,0 +1,60 @@
.marker-cluster-small {
background-color: rgba(181, 226, 140, 0.6);
}
.marker-cluster-small div {
background-color: rgba(110, 204, 57, 0.6);
}
.marker-cluster-medium {
background-color: rgba(241, 211, 87, 0.6);
}
.marker-cluster-medium div {
background-color: rgba(240, 194, 12, 0.6);
}
.marker-cluster-large {
background-color: rgba(253, 156, 115, 0.6);
}
.marker-cluster-large div {
background-color: rgba(241, 128, 23, 0.6);
}
/* IE 6-8 fallback colors */
.leaflet-oldie .marker-cluster-small {
background-color: rgb(181, 226, 140);
}
.leaflet-oldie .marker-cluster-small div {
background-color: rgb(110, 204, 57);
}
.leaflet-oldie .marker-cluster-medium {
background-color: rgb(241, 211, 87);
}
.leaflet-oldie .marker-cluster-medium div {
background-color: rgb(240, 194, 12);
}
.leaflet-oldie .marker-cluster-large {
background-color: rgb(253, 156, 115);
}
.leaflet-oldie .marker-cluster-large div {
background-color: rgb(241, 128, 23);
}
.marker-cluster {
background-clip: padding-box;
border-radius: 20px;
}
.marker-cluster div {
width: 30px;
height: 30px;
margin-left: 5px;
margin-top: 5px;
text-align: center;
border-radius: 15px;
font: 12px "Helvetica Neue", Arial, Helvetica, sans-serif;
}
.marker-cluster span {
line-height: 30px;
}

View File

@@ -0,0 +1,14 @@
.leaflet-cluster-anim .leaflet-marker-icon, .leaflet-cluster-anim .leaflet-marker-shadow {
-webkit-transition: -webkit-transform 0.3s ease-out, opacity 0.3s ease-in;
-moz-transition: -moz-transform 0.3s ease-out, opacity 0.3s ease-in;
-o-transition: -o-transform 0.3s ease-out, opacity 0.3s ease-in;
transition: transform 0.3s ease-out, opacity 0.3s ease-in;
}
.leaflet-cluster-spider-leg {
/* stroke-dashoffset (duration and function) should match with leaflet-marker-icon transform in order to track it exactly */
-webkit-transition: -webkit-stroke-dashoffset 0.3s ease-out, -webkit-stroke-opacity 0.3s ease-in;
-moz-transition: -moz-stroke-dashoffset 0.3s ease-out, -moz-stroke-opacity 0.3s ease-in;
-o-transition: -o-stroke-dashoffset 0.3s ease-out, -o-stroke-opacity 0.3s ease-in;
transition: stroke-dashoffset 0.3s ease-out, stroke-opacity 0.3s ease-in;
}

20
static/lib/chart.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,11 @@
/*
(c) 2014, Vladimir Agafonkin
simpleheat, a tiny JavaScript library for drawing heatmaps with Canvas
https://github.com/mourner/simpleheat
*/
!function(){"use strict";function t(i){return this instanceof t?(this._canvas=i="string"==typeof i?document.getElementById(i):i,this._ctx=i.getContext("2d"),this._width=i.width,this._height=i.height,this._max=1,void this.clear()):new t(i)}t.prototype={defaultRadius:25,defaultGradient:{.4:"blue",.6:"cyan",.7:"lime",.8:"yellow",1:"red"},data:function(t,i){return this._data=t,this},max:function(t){return this._max=t,this},add:function(t){return this._data.push(t),this},clear:function(){return this._data=[],this},radius:function(t,i){i=i||15;var a=this._circle=document.createElement("canvas"),s=a.getContext("2d"),e=this._r=t+i;return a.width=a.height=2*e,s.shadowOffsetX=s.shadowOffsetY=200,s.shadowBlur=i,s.shadowColor="black",s.beginPath(),s.arc(e-200,e-200,t,0,2*Math.PI,!0),s.closePath(),s.fill(),this},gradient:function(t){var i=document.createElement("canvas"),a=i.getContext("2d"),s=a.createLinearGradient(0,0,0,256);i.width=1,i.height=256;for(var e in t)s.addColorStop(e,t[e]);return a.fillStyle=s,a.fillRect(0,0,1,256),this._grad=a.getImageData(0,0,1,256).data,this},draw:function(t){this._circle||this.radius(this.defaultRadius),this._grad||this.gradient(this.defaultGradient);var i=this._ctx;i.clearRect(0,0,this._width,this._height);for(var a,s=0,e=this._data.length;e>s;s++)a=this._data[s],i.globalAlpha=Math.max(a[2]/this._max,t||.05),i.drawImage(this._circle,a[0]-this._r,a[1]-this._r);var n=i.getImageData(0,0,this._width,this._height);return this._colorize(n.data,this._grad),i.putImageData(n,0,0),this},_colorize:function(t,i){for(var a,s=3,e=t.length;e>s;s+=4)a=4*t[s],a&&(t[s-3]=i[a],t[s-2]=i[a+1],t[s-1]=i[a+2])}},window.simpleheat=t}(),/*
(c) 2014, Vladimir Agafonkin
Leaflet.heat, a tiny and fast heatmap plugin for Leaflet.
https://github.com/Leaflet/Leaflet.heat
*/
L.HeatLayer=(L.Layer?L.Layer:L.Class).extend({initialize:function(t,i){this._latlngs=t,L.setOptions(this,i)},setLatLngs:function(t){return this._latlngs=t,this.redraw()},addLatLng:function(t){return this._latlngs.push(t),this.redraw()},setOptions:function(t){return L.setOptions(this,t),this._heat&&this._updateOptions(),this.redraw()},redraw:function(){return!this._heat||this._frame||this._map._animating||(this._frame=L.Util.requestAnimFrame(this._redraw,this)),this},onAdd:function(t){this._map=t,this._canvas||this._initCanvas(),t._panes.overlayPane.appendChild(this._canvas),t.on("moveend",this._reset,this),t.options.zoomAnimation&&L.Browser.any3d&&t.on("zoomanim",this._animateZoom,this),this._reset()},onRemove:function(t){t.getPanes().overlayPane.removeChild(this._canvas),t.off("moveend",this._reset,this),t.options.zoomAnimation&&t.off("zoomanim",this._animateZoom,this)},addTo:function(t){return t.addLayer(this),this},_initCanvas:function(){var t=this._canvas=L.DomUtil.create("canvas","leaflet-heatmap-layer leaflet-layer"),i=L.DomUtil.testProp(["transformOrigin","WebkitTransformOrigin","msTransformOrigin"]);t.style[i]="50% 50%";var a=this._map.getSize();t.width=a.x,t.height=a.y;var s=this._map.options.zoomAnimation&&L.Browser.any3d;L.DomUtil.addClass(t,"leaflet-zoom-"+(s?"animated":"hide")),this._heat=simpleheat(t),this._updateOptions()},_updateOptions:function(){this._heat.radius(this.options.radius||this._heat.defaultRadius,this.options.blur),this.options.gradient&&this._heat.gradient(this.options.gradient),this.options.max&&this._heat.max(this.options.max)},_reset:function(){var t=this._map.containerPointToLayerPoint([0,0]);L.DomUtil.setPosition(this._canvas,t);var i=this._map.getSize();this._heat._width!==i.x&&(this._canvas.width=this._heat._width=i.x),this._heat._height!==i.y&&(this._canvas.height=this._heat._height=i.y),this._redraw()},_redraw:function(){var t,i,a,s,e,n,h,o,r,d=[],_=this._heat._r,l=this._map.getSize(),m=new L.Bounds(L.point([-_,-_]),l.add([_,_])),c=void 0===this.options.max?1:this.options.max,u=void 0===this.options.maxZoom?this._map.getMaxZoom():this.options.maxZoom,f=1/Math.pow(2,Math.max(0,Math.min(u-this._map.getZoom(),12))),g=_/2,p=[],v=this._map._getMapPanePos(),w=v.x%g,y=v.y%g;for(t=0,i=this._latlngs.length;i>t;t++)if(a=this._map.latLngToContainerPoint(this._latlngs[t]),m.contains(a)){e=Math.floor((a.x-w)/g)+2,n=Math.floor((a.y-y)/g)+2;var x=void 0!==this._latlngs[t].alt?this._latlngs[t].alt:void 0!==this._latlngs[t][2]?+this._latlngs[t][2]:1;r=x*f,p[n]=p[n]||[],s=p[n][e],s?(s[0]=(s[0]*s[2]+a.x*r)/(s[2]+r),s[1]=(s[1]*s[2]+a.y*r)/(s[2]+r),s[2]+=r):p[n][e]=[a.x,a.y,r]}for(t=0,i=p.length;i>t;t++)if(p[t])for(h=0,o=p[t].length;o>h;h++)s=p[t][h],s&&d.push([Math.round(s[0]),Math.round(s[1]),Math.min(s[2],c)]);this._heat.data(d).draw(this.options.minOpacity),this._frame=null},_animateZoom:function(t){var i=this._map.getZoomScale(t.zoom),a=this._map._getCenterOffset(t.center)._multiplyBy(-i).subtract(this._map._getMapPanePos());L.DomUtil.setTransform?L.DomUtil.setTransform(this._canvas,a,i):this._canvas.style[L.DomUtil.TRANSFORM]=L.DomUtil.getTranslateString(a)+" scale("+i+")"}}),L.heatLayer=function(t,i){return new L.HeatLayer(t,i)};

661
static/lib/leaflet.css Normal file
View File

@@ -0,0 +1,661 @@
/* required styles */
.leaflet-pane,
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-tile-container,
.leaflet-pane > svg,
.leaflet-pane > canvas,
.leaflet-zoom-box,
.leaflet-image-layer,
.leaflet-layer {
position: absolute;
left: 0;
top: 0;
}
.leaflet-container {
overflow: hidden;
}
.leaflet-tile,
.leaflet-marker-icon,
.leaflet-marker-shadow {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
-webkit-user-drag: none;
}
/* Prevents IE11 from highlighting tiles in blue */
.leaflet-tile::selection {
background: transparent;
}
/* Safari renders non-retina tile on retina better with this, but Chrome is worse */
.leaflet-safari .leaflet-tile {
image-rendering: -webkit-optimize-contrast;
}
/* hack that prevents hw layers "stretching" when loading new tiles */
.leaflet-safari .leaflet-tile-container {
width: 1600px;
height: 1600px;
-webkit-transform-origin: 0 0;
}
.leaflet-marker-icon,
.leaflet-marker-shadow {
display: block;
}
/* .leaflet-container svg: reset svg max-width decleration shipped in Joomla! (joomla.org) 3.x */
/* .leaflet-container img: map is broken in FF if you have max-width: 100% on tiles */
.leaflet-container .leaflet-overlay-pane svg {
max-width: none !important;
max-height: none !important;
}
.leaflet-container .leaflet-marker-pane img,
.leaflet-container .leaflet-shadow-pane img,
.leaflet-container .leaflet-tile-pane img,
.leaflet-container img.leaflet-image-layer,
.leaflet-container .leaflet-tile {
max-width: none !important;
max-height: none !important;
width: auto;
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
}
.leaflet-container.leaflet-touch-drag {
-ms-touch-action: pinch-zoom;
/* Fallback for FF which doesn't support pinch-zoom */
touch-action: none;
touch-action: pinch-zoom;
}
.leaflet-container.leaflet-touch-drag.leaflet-touch-zoom {
-ms-touch-action: none;
touch-action: none;
}
.leaflet-container {
-webkit-tap-highlight-color: transparent;
}
.leaflet-container a {
-webkit-tap-highlight-color: rgba(51, 181, 229, 0.4);
}
.leaflet-tile {
filter: inherit;
visibility: hidden;
}
.leaflet-tile-loaded {
visibility: inherit;
}
.leaflet-zoom-box {
width: 0;
height: 0;
-moz-box-sizing: border-box;
box-sizing: border-box;
z-index: 800;
}
/* workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=888319 */
.leaflet-overlay-pane svg {
-moz-user-select: none;
}
.leaflet-pane { z-index: 400; }
.leaflet-tile-pane { z-index: 200; }
.leaflet-overlay-pane { z-index: 400; }
.leaflet-shadow-pane { z-index: 500; }
.leaflet-marker-pane { z-index: 600; }
.leaflet-tooltip-pane { z-index: 650; }
.leaflet-popup-pane { z-index: 700; }
.leaflet-map-pane canvas { z-index: 100; }
.leaflet-map-pane svg { z-index: 200; }
.leaflet-vml-shape {
width: 1px;
height: 1px;
}
.lvml {
behavior: url(#default#VML);
display: inline-block;
position: absolute;
}
/* control positioning */
.leaflet-control {
position: relative;
z-index: 800;
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
.leaflet-top,
.leaflet-bottom {
position: absolute;
z-index: 1000;
pointer-events: none;
}
.leaflet-top {
top: 0;
}
.leaflet-right {
right: 0;
}
.leaflet-bottom {
bottom: 0;
}
.leaflet-left {
left: 0;
}
.leaflet-control {
float: left;
clear: both;
}
.leaflet-right .leaflet-control {
float: right;
}
.leaflet-top .leaflet-control {
margin-top: 10px;
}
.leaflet-bottom .leaflet-control {
margin-bottom: 10px;
}
.leaflet-left .leaflet-control {
margin-left: 10px;
}
.leaflet-right .leaflet-control {
margin-right: 10px;
}
/* zoom and fade animations */
.leaflet-fade-anim .leaflet-popup {
opacity: 0;
-webkit-transition: opacity 0.2s linear;
-moz-transition: opacity 0.2s linear;
transition: opacity 0.2s linear;
}
.leaflet-fade-anim .leaflet-map-pane .leaflet-popup {
opacity: 1;
}
.leaflet-zoom-animated {
-webkit-transform-origin: 0 0;
-ms-transform-origin: 0 0;
transform-origin: 0 0;
}
svg.leaflet-zoom-animated {
will-change: transform;
}
.leaflet-zoom-anim .leaflet-zoom-animated {
-webkit-transition: -webkit-transform 0.25s cubic-bezier(0,0,0.25,1);
-moz-transition: -moz-transform 0.25s cubic-bezier(0,0,0.25,1);
transition: transform 0.25s cubic-bezier(0,0,0.25,1);
}
.leaflet-zoom-anim .leaflet-tile,
.leaflet-pan-anim .leaflet-tile {
-webkit-transition: none;
-moz-transition: none;
transition: none;
}
.leaflet-zoom-anim .leaflet-zoom-hide {
visibility: hidden;
}
/* cursors */
.leaflet-interactive {
cursor: pointer;
}
.leaflet-grab {
cursor: -webkit-grab;
cursor: -moz-grab;
cursor: grab;
}
.leaflet-crosshair,
.leaflet-crosshair .leaflet-interactive {
cursor: crosshair;
}
.leaflet-popup-pane,
.leaflet-control {
cursor: auto;
}
.leaflet-dragging .leaflet-grab,
.leaflet-dragging .leaflet-grab .leaflet-interactive,
.leaflet-dragging .leaflet-marker-draggable {
cursor: move;
cursor: -webkit-grabbing;
cursor: -moz-grabbing;
cursor: grabbing;
}
/* marker & overlays interactivity */
.leaflet-marker-icon,
.leaflet-marker-shadow,
.leaflet-image-layer,
.leaflet-pane > svg path,
.leaflet-tile-container {
pointer-events: none;
}
.leaflet-marker-icon.leaflet-interactive,
.leaflet-image-layer.leaflet-interactive,
.leaflet-pane > svg path.leaflet-interactive,
svg.leaflet-image-layer.leaflet-interactive path {
pointer-events: visiblePainted; /* IE 9-10 doesn't have auto */
pointer-events: auto;
}
/* visual tweaks */
.leaflet-container {
background: #ddd;
outline-offset: 1px;
}
.leaflet-container a {
color: #0078A8;
}
.leaflet-zoom-box {
border: 2px dotted #38f;
background: rgba(255,255,255,0.5);
}
/* general typography */
.leaflet-container {
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
font-size: 12px;
font-size: 0.75rem;
line-height: 1.5;
}
/* general toolbar styles */
.leaflet-bar {
box-shadow: 0 1px 5px rgba(0,0,0,0.65);
border-radius: 4px;
}
.leaflet-bar a {
background-color: #fff;
border-bottom: 1px solid #ccc;
width: 26px;
height: 26px;
line-height: 26px;
display: block;
text-align: center;
text-decoration: none;
color: black;
}
.leaflet-bar a,
.leaflet-control-layers-toggle {
background-position: 50% 50%;
background-repeat: no-repeat;
display: block;
}
.leaflet-bar a:hover,
.leaflet-bar a:focus {
background-color: #f4f4f4;
}
.leaflet-bar a:first-child {
border-top-left-radius: 4px;
border-top-right-radius: 4px;
}
.leaflet-bar a:last-child {
border-bottom-left-radius: 4px;
border-bottom-right-radius: 4px;
border-bottom: none;
}
.leaflet-bar a.leaflet-disabled {
cursor: default;
background-color: #f4f4f4;
color: #bbb;
}
.leaflet-touch .leaflet-bar a {
width: 30px;
height: 30px;
line-height: 30px;
}
.leaflet-touch .leaflet-bar a:first-child {
border-top-left-radius: 2px;
border-top-right-radius: 2px;
}
.leaflet-touch .leaflet-bar a:last-child {
border-bottom-left-radius: 2px;
border-bottom-right-radius: 2px;
}
/* zoom control */
.leaflet-control-zoom-in,
.leaflet-control-zoom-out {
font: bold 18px 'Lucida Console', Monaco, monospace;
text-indent: 1px;
}
.leaflet-touch .leaflet-control-zoom-in, .leaflet-touch .leaflet-control-zoom-out {
font-size: 22px;
}
/* layers control */
.leaflet-control-layers {
box-shadow: 0 1px 5px rgba(0,0,0,0.4);
background: #fff;
border-radius: 5px;
}
.leaflet-control-layers-toggle {
background-image: url(images/layers.png);
width: 36px;
height: 36px;
}
.leaflet-retina .leaflet-control-layers-toggle {
background-image: url(images/layers-2x.png);
background-size: 26px 26px;
}
.leaflet-touch .leaflet-control-layers-toggle {
width: 44px;
height: 44px;
}
.leaflet-control-layers .leaflet-control-layers-list,
.leaflet-control-layers-expanded .leaflet-control-layers-toggle {
display: none;
}
.leaflet-control-layers-expanded .leaflet-control-layers-list {
display: block;
position: relative;
}
.leaflet-control-layers-expanded {
padding: 6px 10px 6px 6px;
color: #333;
background: #fff;
}
.leaflet-control-layers-scrollbar {
overflow-y: scroll;
overflow-x: hidden;
padding-right: 5px;
}
.leaflet-control-layers-selector {
margin-top: 2px;
position: relative;
top: 1px;
}
.leaflet-control-layers label {
display: block;
font-size: 13px;
font-size: 1.08333em;
}
.leaflet-control-layers-separator {
height: 0;
border-top: 1px solid #ddd;
margin: 5px -10px 5px -6px;
}
/* Default icon URLs */
.leaflet-default-icon-path { /* used only in path-guessing heuristic, see L.Icon.Default */
background-image: url(images/marker-icon.png);
}
/* attribution and scale controls */
.leaflet-container .leaflet-control-attribution {
background: #fff;
background: rgba(255, 255, 255, 0.8);
margin: 0;
}
.leaflet-control-attribution,
.leaflet-control-scale-line {
padding: 0 5px;
color: #333;
line-height: 1.4;
}
.leaflet-control-attribution a {
text-decoration: none;
}
.leaflet-control-attribution a:hover,
.leaflet-control-attribution a:focus {
text-decoration: underline;
}
.leaflet-attribution-flag {
display: inline !important;
vertical-align: baseline !important;
width: 1em;
height: 0.6669em;
}
.leaflet-left .leaflet-control-scale {
margin-left: 5px;
}
.leaflet-bottom .leaflet-control-scale {
margin-bottom: 5px;
}
.leaflet-control-scale-line {
border: 2px solid #777;
border-top: none;
line-height: 1.1;
padding: 2px 5px 1px;
white-space: nowrap;
-moz-box-sizing: border-box;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.8);
text-shadow: 1px 1px #fff;
}
.leaflet-control-scale-line:not(:first-child) {
border-top: 2px solid #777;
border-bottom: none;
margin-top: -2px;
}
.leaflet-control-scale-line:not(:first-child):not(:last-child) {
border-bottom: 2px solid #777;
}
.leaflet-touch .leaflet-control-attribution,
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
box-shadow: none;
}
.leaflet-touch .leaflet-control-layers,
.leaflet-touch .leaflet-bar {
border: 2px solid rgba(0,0,0,0.2);
background-clip: padding-box;
}
/* popup */
.leaflet-popup {
position: absolute;
text-align: center;
margin-bottom: 20px;
}
.leaflet-popup-content-wrapper {
padding: 1px;
text-align: left;
border-radius: 12px;
}
.leaflet-popup-content {
margin: 13px 24px 13px 20px;
line-height: 1.3;
font-size: 13px;
font-size: 1.08333em;
min-height: 1px;
}
.leaflet-popup-content p {
margin: 17px 0;
margin: 1.3em 0;
}
.leaflet-popup-tip-container {
width: 40px;
height: 20px;
position: absolute;
left: 50%;
margin-top: -1px;
margin-left: -20px;
overflow: hidden;
pointer-events: none;
}
.leaflet-popup-tip {
width: 17px;
height: 17px;
padding: 1px;
margin: -10px auto 0;
pointer-events: auto;
-webkit-transform: rotate(45deg);
-moz-transform: rotate(45deg);
-ms-transform: rotate(45deg);
transform: rotate(45deg);
}
.leaflet-popup-content-wrapper,
.leaflet-popup-tip {
background: white;
color: #333;
box-shadow: 0 3px 14px rgba(0,0,0,0.4);
}
.leaflet-container a.leaflet-popup-close-button {
position: absolute;
top: 0;
right: 0;
border: none;
text-align: center;
width: 24px;
height: 24px;
font: 16px/24px Tahoma, Verdana, sans-serif;
color: #757575;
text-decoration: none;
background: transparent;
}
.leaflet-container a.leaflet-popup-close-button:hover,
.leaflet-container a.leaflet-popup-close-button:focus {
color: #585858;
}
.leaflet-popup-scrolled {
overflow: auto;
}
.leaflet-oldie .leaflet-popup-content-wrapper {
-ms-zoom: 1;
}
.leaflet-oldie .leaflet-popup-tip {
width: 24px;
margin: 0 auto;
-ms-filter: "progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678)";
filter: progid:DXImageTransform.Microsoft.Matrix(M11=0.70710678, M12=0.70710678, M21=-0.70710678, M22=0.70710678);
}
.leaflet-oldie .leaflet-control-zoom,
.leaflet-oldie .leaflet-control-layers,
.leaflet-oldie .leaflet-popup-content-wrapper,
.leaflet-oldie .leaflet-popup-tip {
border: 1px solid #999;
}
/* div icon */
.leaflet-div-icon {
background: #fff;
border: 1px solid #666;
}
/* Tooltip */
/* Base styles for the element that has a tooltip */
.leaflet-tooltip {
position: absolute;
padding: 6px;
background-color: #fff;
border: 1px solid #fff;
border-radius: 3px;
color: #222;
white-space: nowrap;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
pointer-events: none;
box-shadow: 0 1px 3px rgba(0,0,0,0.4);
}
.leaflet-tooltip.leaflet-interactive {
cursor: pointer;
pointer-events: auto;
}
.leaflet-tooltip-top:before,
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
position: absolute;
pointer-events: none;
border: 6px solid transparent;
background: transparent;
content: "";
}
/* Directions */
.leaflet-tooltip-bottom {
margin-top: 6px;
}
.leaflet-tooltip-top {
margin-top: -6px;
}
.leaflet-tooltip-bottom:before,
.leaflet-tooltip-top:before {
left: 50%;
margin-left: -6px;
}
.leaflet-tooltip-top:before {
bottom: 0;
margin-bottom: -12px;
border-top-color: #fff;
}
.leaflet-tooltip-bottom:before {
top: 0;
margin-top: -12px;
margin-left: -6px;
border-bottom-color: #fff;
}
.leaflet-tooltip-left {
margin-left: -6px;
}
.leaflet-tooltip-right {
margin-left: 6px;
}
.leaflet-tooltip-left:before,
.leaflet-tooltip-right:before {
top: 50%;
margin-top: -6px;
}
.leaflet-tooltip-left:before {
right: 0;
margin-right: -12px;
border-left-color: #fff;
}
.leaflet-tooltip-right:before {
left: 0;
margin-left: -12px;
border-right-color: #fff;
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {
-webkit-print-color-adjust: exact;
print-color-adjust: exact;
}
}

6
static/lib/leaflet.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

1
static/lib/uPlot.min.css vendored Normal file
View File

@@ -0,0 +1 @@
.uplot, .uplot *, .uplot *::before, .uplot *::after {box-sizing: border-box;}.uplot {font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";line-height: 1.5;width: min-content;}.u-title {text-align: center;font-size: 18px;font-weight: bold;}.u-wrap {position: relative;user-select: none;}.u-over, .u-under {position: absolute;}.u-under {overflow: hidden;}.uplot canvas {display: block;position: relative;width: 100%;height: 100%;}.u-axis {position: absolute;}.u-legend {font-size: 14px;margin: auto;text-align: center;}.u-inline {display: block;}.u-inline * {display: inline-block;}.u-inline tr {margin-right: 16px;}.u-legend th {font-weight: 600;}.u-legend th > * {vertical-align: middle;display: inline-block;}.u-legend .u-marker {width: 1em;height: 1em;margin-right: 4px;background-clip: padding-box !important;}.u-inline.u-live th::after {content: ":";vertical-align: middle;}.u-inline:not(.u-live) .u-value {display: none;}.u-series > * {padding: 4px;}.u-series th {cursor: pointer;}.u-legend .u-off > * {opacity: 0.3;}.u-select {background: rgba(0,0,0,0.07);position: absolute;pointer-events: none;}.u-cursor-x, .u-cursor-y {position: absolute;left: 0;top: 0;pointer-events: none;will-change: transform;}.u-hz .u-cursor-x, .u-vt .u-cursor-y {height: 100%;border-right: 1px dashed #607D8B;}.u-hz .u-cursor-y, .u-vt .u-cursor-x {width: 100%;border-bottom: 1px dashed #607D8B;}.u-cursor-pt {position: absolute;top: 0;left: 0;border-radius: 50%;border: 0 solid;pointer-events: none;will-change: transform;/*this has to be !important since we set inline "background" shorthand */background-clip: padding-box !important;}.u-axis.u-off, .u-select.u-off, .u-cursor-x.u-off, .u-cursor-y.u-off, .u-cursor-pt.u-off {display: none;}

2
static/lib/uPlot.min.js vendored Normal file

File diff suppressed because one or more lines are too long

31
static/map.html Normal file
View File

@@ -0,0 +1,31 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>PPF Proxy Map</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
<link rel="stylesheet" href="/static/lib/leaflet.css">
<link rel="stylesheet" href="/static/lib/MarkerCluster.css">
<link rel="stylesheet" href="/static/lib/MarkerCluster.Default.css">
</head>
<body class="map-page">
<div class="map-nav glass" style="display:flex;align-items:center;gap:12px"><a href="/dashboard">&larr; Dashboard</a><button class="theme-toggle" id="themeToggle" title="Toggle theme"><span class="theme-toggle-icon">&#9790;</span></button></div>
<div class="map-stats-panel glass">
<div class="map-stat"><span class="map-stat-val" id="countryCount">-</span><span class="map-stat-lbl">Countries</span></div>
<div class="map-stat"><span class="map-stat-val" id="proxyCount">-</span><span class="map-stat-lbl">Proxies</span></div>
</div>
<div id="map"></div>
<div class="map-legend-panel glass">
<div class="map-legend-title">Anonymity Level</div>
<div class="map-legend-row"><div class="map-legend-dot elite"></div> Elite</div>
<div class="map-legend-row"><div class="map-legend-dot anonymous"></div> Anonymous</div>
<div class="map-legend-row"><div class="map-legend-dot transparent"></div> Transparent</div>
</div>
<div class="map-footer">PPF uses the IP2Location LITE database for <a href="https://lite.ip2location.com">IP geolocation</a>.</div>
<script src="/static/lib/leaflet.js"></script>
<script src="/static/lib/leaflet-heat.js"></script>
<script src="/static/lib/leaflet.markercluster.js"></script>
<script src="/static/map.js"></script>
</body>
</html>

406
static/map.js Normal file
View File

@@ -0,0 +1,406 @@
/**
* PPF Proxy Map - Interactive visualization
*/
(function() {
'use strict';
// Theme toggle (shared with dashboard)
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() {
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');
}
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);
// Country coordinates (ISO 3166-1 alpha-2 -> [lat, lon])
var COORDS = {
"AD":[42.5,1.5],"AE":[24,54],"AF":[33,65],"AG":[17.05,-61.8],"AL":[41,20],
"AM":[40,45],"AO":[-12.5,18.5],"AR":[-34,-64],"AT":[47.33,13.33],"AU":[-27,133],
"AZ":[40.5,47.5],"BA":[44,18],"BB":[13.17,-59.53],"BD":[24,90],"BE":[50.83,4],
"BF":[13,-2],"BG":[43,25],"BH":[26,50.55],"BI":[-3.5,30],"BJ":[9.5,2.25],
"BN":[4.5,114.67],"BO":[-17,-65],"BR":[-10,-55],"BS":[24.25,-76],"BT":[27.5,90.5],
"BW":[-22,24],"BY":[53,28],"BZ":[17.25,-88.75],"CA":[60,-95],"CD":[-2.5,23.5],
"CF":[7,21],"CG":[-1,15],"CH":[47,8],"CI":[8,-5],"CL":[-30,-71],"CM":[6,12],
"CN":[35,105],"CO":[4,-72],"CR":[10,-84],"CU":[21.5,-80],"CV":[16,-24],
"CY":[35,33],"CZ":[49.75,15.5],"DE":[51,9],"DJ":[11.5,43],"DK":[56,10],
"DO":[19,-70],"DZ":[28,3],"EC":[-2,-77.5],"EE":[59,26],"EG":[27,30],
"ER":[15,39],"ES":[40,-4],"ET":[8,38],"FI":[64,26],"FJ":[-18,175],"FR":[46,2],
"GA":[-1,11.75],"GB":[54,-2],"GE":[42,43.5],"GH":[8,-2],"GM":[13.47,-16.57],
"GN":[11,-10],"GQ":[2,10],"GR":[39,22],"GT":[15.5,-90.25],"GW":[12,-15],
"GY":[5,-59],"HK":[22.25,114.17],"HN":[15,-86.5],"HR":[45.17,15.5],
"HT":[19,-72.42],"HU":[47,20],"ID":[-5,120],"IE":[53,-8],"IL":[31.5,34.75],
"IN":[20,77],"IQ":[33,44],"IR":[32,53],"IS":[65,-18],"IT":[42.83,12.83],
"JM":[18.25,-77.5],"JO":[31,36],"JP":[36,138],"KE":[-1,38],"KG":[41,75],
"KH":[13,105],"KM":[-12.17,44.25],"KP":[40,127],"KR":[37,127.5],"KW":[29.5,45.75],
"KZ":[48,68],"LA":[18,105],"LB":[33.83,35.83],"LK":[7,81],"LR":[6.5,-9.5],
"LS":[-29.5,28.5],"LT":[56,24],"LU":[49.75,6.17],"LV":[57,25],"LY":[25,17],
"MA":[32,-5],"MC":[43.73,7.42],"MD":[47,29],"ME":[42.5,19.3],"MG":[-20,47],
"MK":[41.83,22],"ML":[17,-4],"MM":[22,98],"MN":[46,105],"MO":[22.17,113.55],
"MR":[20,-12],"MT":[35.83,14.58],"MU":[-20.28,57.55],"MV":[3.25,73],
"MW":[-13.5,34],"MX":[23,-102],"MY":[2.5,112.5],"MZ":[-18.25,35],"NA":[-22,17],
"NE":[16,8],"NG":[10,8],"NI":[13,-85],"NL":[52.5,5.75],"NO":[62,10],
"NP":[28,84],"NZ":[-41,174],"OM":[21,57],"PA":[9,-80],"PE":[-10,-76],
"PG":[-6,147],"PH":[13,122],"PK":[30,70],"PL":[52,20],"PR":[18.25,-66.5],
"PS":[32,35.25],"PT":[39.5,-8],"PY":[-23,-58],"QA":[25.5,51.25],"RO":[46,25],
"RS":[44,21],"RU":[60,100],"RW":[-2,30],"SA":[25,45],"SC":[-4.58,55.67],
"SD":[15,30],"SE":[62,15],"SG":[1.37,103.8],"SI":[46.12,14.82],"SK":[48.67,19.5],
"SL":[8.5,-11.5],"SN":[14,-14],"SO":[10,49],"SR":[4,-56],"SS":[7,30],
"SV":[13.83,-88.92],"SY":[35,38],"SZ":[-26.5,31.5],"TD":[15,19],"TG":[8,1.17],
"TH":[15,100],"TJ":[39,71],"TM":[40,60],"TN":[34,9],"TO":[-20,-175],
"TR":[39,35],"TT":[11,-61],"TW":[23.5,121],"TZ":[-6,35],"UA":[49,32],
"UG":[1,32],"US":[38,-97],"UY":[-33,-56],"UZ":[41,64],"VE":[8,-66],
"VN":[16,106],"YE":[15,48],"ZA":[-29,24],"ZM":[-15,30],"ZW":[-20,30],"XK":[42.6,20.9]
};
// Country names
var NAMES = {
"AD":"Andorra","AE":"UAE","AF":"Afghanistan","AG":"Antigua","AL":"Albania",
"AM":"Armenia","AO":"Angola","AR":"Argentina","AT":"Austria","AU":"Australia",
"AZ":"Azerbaijan","BA":"Bosnia","BB":"Barbados","BD":"Bangladesh","BE":"Belgium",
"BF":"Burkina Faso","BG":"Bulgaria","BH":"Bahrain","BI":"Burundi","BJ":"Benin",
"BN":"Brunei","BO":"Bolivia","BR":"Brazil","BS":"Bahamas","BT":"Bhutan",
"BW":"Botswana","BY":"Belarus","BZ":"Belize","CA":"Canada","CD":"DR Congo",
"CF":"C. African Rep.","CG":"Congo","CH":"Switzerland","CI":"Ivory Coast",
"CL":"Chile","CM":"Cameroon","CN":"China","CO":"Colombia","CR":"Costa Rica",
"CU":"Cuba","CV":"Cape Verde","CY":"Cyprus","CZ":"Czechia","DE":"Germany",
"DJ":"Djibouti","DK":"Denmark","DO":"Dominican Rep.","DZ":"Algeria","EC":"Ecuador",
"EE":"Estonia","EG":"Egypt","ER":"Eritrea","ES":"Spain","ET":"Ethiopia",
"FI":"Finland","FJ":"Fiji","FR":"France","GA":"Gabon","GB":"United Kingdom",
"GE":"Georgia","GH":"Ghana","GM":"Gambia","GN":"Guinea","GQ":"Eq. Guinea",
"GR":"Greece","GT":"Guatemala","GW":"Guinea-Bissau","GY":"Guyana","HK":"Hong Kong",
"HN":"Honduras","HR":"Croatia","HT":"Haiti","HU":"Hungary","ID":"Indonesia",
"IE":"Ireland","IL":"Israel","IN":"India","IQ":"Iraq","IR":"Iran","IS":"Iceland",
"IT":"Italy","JM":"Jamaica","JO":"Jordan","JP":"Japan","KE":"Kenya",
"KG":"Kyrgyzstan","KH":"Cambodia","KM":"Comoros","KP":"North Korea",
"KR":"South Korea","KW":"Kuwait","KZ":"Kazakhstan","LA":"Laos","LB":"Lebanon",
"LK":"Sri Lanka","LR":"Liberia","LS":"Lesotho","LT":"Lithuania","LU":"Luxembourg",
"LV":"Latvia","LY":"Libya","MA":"Morocco","MC":"Monaco","MD":"Moldova",
"ME":"Montenegro","MG":"Madagascar","MK":"N. Macedonia","ML":"Mali","MM":"Myanmar",
"MN":"Mongolia","MO":"Macau","MR":"Mauritania","MT":"Malta","MU":"Mauritius",
"MV":"Maldives","MW":"Malawi","MX":"Mexico","MY":"Malaysia","MZ":"Mozambique",
"NA":"Namibia","NE":"Niger","NG":"Nigeria","NI":"Nicaragua","NL":"Netherlands",
"NO":"Norway","NP":"Nepal","NZ":"New Zealand","OM":"Oman","PA":"Panama",
"PE":"Peru","PG":"Papua New Guinea","PH":"Philippines","PK":"Pakistan",
"PL":"Poland","PR":"Puerto Rico","PS":"Palestine","PT":"Portugal","PY":"Paraguay",
"QA":"Qatar","RO":"Romania","RS":"Serbia","RU":"Russia","RW":"Rwanda",
"SA":"Saudi Arabia","SC":"Seychelles","SD":"Sudan","SE":"Sweden","SG":"Singapore",
"SI":"Slovenia","SK":"Slovakia","SL":"Sierra Leone","SN":"Senegal","SO":"Somalia",
"SR":"Suriname","SS":"South Sudan","SV":"El Salvador","SY":"Syria","SZ":"Eswatini",
"TD":"Chad","TG":"Togo","TH":"Thailand","TJ":"Tajikistan","TM":"Turkmenistan",
"TN":"Tunisia","TO":"Tonga","TR":"Turkey","TT":"Trinidad","TW":"Taiwan",
"TZ":"Tanzania","UA":"Ukraine","UG":"Uganda","US":"United States","UY":"Uruguay",
"UZ":"Uzbekistan","VE":"Venezuela","VN":"Vietnam","YE":"Yemen","ZA":"South Africa",
"ZM":"Zambia","ZW":"Zimbabwe","XK":"Kosovo"
};
// Anonymity color mapping (matches CSS variables)
var ANON_COLORS = {
elite: {fill: '#50c878', stroke: '#2d8a4e'},
anonymous: {fill: '#38bdf8', stroke: '#1d8acf'},
transparent: {fill: '#f97316', stroke: '#c2410c'},
unknown: {fill: '#6b7280', stroke: '#4b5563'}
};
// Heatmap gradient
var HEAT_GRADIENT = {
0.0: '#181f2a',
0.3: '#1d4e89',
0.5: '#38bdf8',
0.7: '#50c878',
1.0: '#7ee787'
};
// Map configuration
var MAP_CONFIG = {
center: [30, 10],
zoom: 2,
minZoom: 2,
maxZoom: 8,
tileUrl: 'https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png',
tileAttribution: '&copy; OSM &copy; CARTO'
};
// Cluster configuration
var CLUSTER_CONFIG = {
showCoverageOnHover: false,
spiderfyOnMaxZoom: true,
disableClusteringAtZoom: 7,
maxClusterRadius: 50
};
// DOM element references
var $countryCount, $proxyCount, map, clusterGroup;
/**
* Initialize the map
*/
function init() {
$countryCount = document.getElementById('countryCount');
$proxyCount = document.getElementById('proxyCount');
// Set loading state
$countryCount.classList.add('loading');
$proxyCount.classList.add('loading');
// Create map
map = L.map('map', {
center: MAP_CONFIG.center,
zoom: MAP_CONFIG.zoom,
minZoom: MAP_CONFIG.minZoom,
maxZoom: MAP_CONFIG.maxZoom,
zoomControl: true,
worldCopyJump: true
});
// Add tile layer
L.tileLayer(MAP_CONFIG.tileUrl, {
attribution: MAP_CONFIG.tileAttribution,
subdomains: 'abcd',
maxZoom: 19
}).addTo(map);
// Create cluster group
clusterGroup = L.markerClusterGroup({
showCoverageOnHover: CLUSTER_CONFIG.showCoverageOnHover,
spiderfyOnMaxZoom: CLUSTER_CONFIG.spiderfyOnMaxZoom,
disableClusteringAtZoom: CLUSTER_CONFIG.disableClusteringAtZoom,
maxClusterRadius: CLUSTER_CONFIG.maxClusterRadius,
iconCreateFunction: createClusterIcon
});
// Load data
loadData();
}
/**
* Create cluster icon
*/
function createClusterIcon(cluster) {
var count = cluster.getChildCount();
var size = count >= 100 ? 'lg' : count >= 10 ? 'md' : 'sm';
var sizeMap = {sm: 32, md: 40, lg: 50};
return L.divIcon({
html: '<div class="cluster-icon cluster-' + size + '">' + count + '</div>',
className: 'cluster-wrapper',
iconSize: L.point(sizeMap[size], sizeMap[size])
});
}
/**
* Calculate brightness based on count (logarithmic scale)
*/
function calcBrightness(count, maxCount, minBrightness, maxBrightness) {
minBrightness = minBrightness || 0.3;
maxBrightness = maxBrightness || 1.0;
var range = maxBrightness - minBrightness;
return minBrightness + range * (Math.log(count + 1) / Math.log(maxCount + 1));
}
/**
* Calculate radius based on count
*/
function calcRadius(count, baseRadius, maxExtra, divisor) {
baseRadius = baseRadius || 3;
maxExtra = maxExtra || 7;
divisor = divisor || 5;
return baseRadius + Math.min(maxExtra, Math.sqrt(count / divisor));
}
/**
* Create popup content
*/
function createPopup(code, count, anon, lat, lon, isApprox) {
var name = NAMES[code] || code;
var anonLabel = anon ? anon.charAt(0).toUpperCase() + anon.slice(1) : '';
var coords = isApprox ? 'approx. location' :
(anonLabel ? anonLabel + ' &bull; ' : '') + lat.toFixed(1) + ', ' + lon.toFixed(1);
return '<div class="popup-header">' +
'<span class="popup-code">' + code + '</span>' +
'<span class="popup-name">' + name + '</span>' +
'</div>' +
'<div class="popup-count">' + count.toLocaleString() + ' proxies</div>' +
'<div class="popup-coords">' + coords + '</div>';
}
/**
* Load and render map data
*/
function loadData() {
Promise.all([
fetch('/api/locations').then(function(r) { return r.json(); }).catch(function() { return {locations: []}; }),
fetch('/api/countries').then(function(r) { return r.json(); })
]).then(function(results) {
var locations = results[0].locations || [];
var countries = results[1].countries || {};
renderData(locations, countries);
}).catch(function() {
$proxyCount.textContent = 'Error';
$proxyCount.style.color = '#ef4444';
});
}
/**
* Render map data
*/
function renderData(locations, countries) {
var entries = Object.entries(countries).sort(function(a, b) { return b[1] - a[1]; });
var total = entries.reduce(function(s, e) { return s + e[1]; }, 0);
// Update stats
$countryCount.textContent = entries.length;
$proxyCount.textContent = total.toLocaleString();
$countryCount.classList.remove('loading');
$proxyCount.classList.remove('loading');
// Track countries with precise locations
var countriesWithPrecise = {};
locations.forEach(function(l) {
countriesWithPrecise[l.country] = (countriesWithPrecise[l.country] || 0) + l.count;
});
// Add heatmap layer
renderHeatmap(locations);
// Add precise location markers
renderPreciseLocations(locations);
// Add country centroid markers
renderCountryCentroids(entries, countriesWithPrecise);
}
/**
* Render heatmap layer
*/
function renderHeatmap(locations) {
if (locations.length === 0) return;
var heatData = locations.map(function(l) {
var intensity = Math.min(l.count / 50, 1.0);
return [l.lat, l.lon, intensity];
});
L.heatLayer(heatData, {
radius: 25,
blur: 20,
maxZoom: 6,
max: 1.0,
minOpacity: 0.3,
gradient: HEAT_GRADIENT
}).addTo(map);
}
/**
* Render precise location markers
*/
function renderPreciseLocations(locations) {
if (locations.length === 0) return;
var maxCount = Math.max.apply(null, locations.map(function(l) { return l.count; })) || 1;
locations.forEach(function(l) {
var brightness = calcBrightness(l.count, maxCount);
var radius = calcRadius(l.count);
var colors = ANON_COLORS[l.anon] || ANON_COLORS.unknown;
var marker = L.circleMarker([l.lat, l.lon], {
radius: radius,
fillColor: colors.fill,
color: colors.stroke,
weight: 1,
opacity: brightness,
fillOpacity: brightness * 0.85
});
marker.bindPopup(createPopup(l.country, l.count, l.anon, l.lat, l.lon, false));
// Hover effects
marker.on('mouseover', function() {
this.setStyle({fillOpacity: 0.95, opacity: 1, weight: 2});
});
marker.on('mouseout', function() {
this.setStyle({fillOpacity: brightness * 0.85, opacity: brightness, weight: 1});
});
clusterGroup.addLayer(marker);
});
map.addLayer(clusterGroup);
}
/**
* Render country centroid markers (for proxies without precise coords)
*/
function renderCountryCentroids(entries, countriesWithPrecise) {
// Find max remaining for normalization
var maxRemaining = 1;
entries.forEach(function(e) {
var remaining = e[1] - (countriesWithPrecise[e[0]] || 0);
if (remaining > maxRemaining) maxRemaining = remaining;
});
entries.forEach(function(e) {
var code = e[0], count = e[1], coords = COORDS[code];
if (!coords) return;
var preciseInCountry = countriesWithPrecise[code] || 0;
var remaining = count - preciseInCountry;
if (remaining <= 0) return;
var brightness = calcBrightness(remaining, maxRemaining, 0.25, 0.9);
var radius = calcRadius(remaining, 4, 10, 10);
var circle = L.circleMarker([coords[0], coords[1]], {
radius: radius,
fillColor: '#38bdf8',
color: '#1d8acf',
weight: 1,
opacity: brightness,
fillOpacity: brightness * 0.7
}).addTo(map);
circle.bindPopup(createPopup(code, remaining, null, coords[0], coords[1], true));
// Hover effects
circle.on('mouseover', function() {
this.setStyle({fillOpacity: 0.9, opacity: 1});
});
circle.on('mouseout', function() {
this.setStyle({fillOpacity: brightness * 0.7, opacity: brightness});
});
});
}
// Initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

117
static/mitm.html Normal file
View File

@@ -0,0 +1,117 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MITM Certificate Search</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/static/style.css">
</head>
<body>
<div class="container search-container">
<!-- Header -->
<div class="search-header">
<h1>MITM Certificate Search</h1>
<div style="display:flex;align-items:center;gap:12px">
<a href="/dashboard" class="back-link">&larr; Dashboard</a>
<button class="theme-toggle" id="themeToggle" title="Toggle theme">
<span class="theme-toggle-icon">&#9790;</span>
</button>
</div>
</div>
<!-- Stats Summary -->
<div class="stats-summary" id="statsSummary">
<div class="stat-card">
<div class="lbl">Total Detections</div>
<div class="val red" id="totalDetections">-</div>
</div>
<div class="stat-card">
<div class="lbl">Unique Certificates</div>
<div class="val yel" id="uniqueCerts">-</div>
</div>
<div class="stat-card">
<div class="lbl">Unique Organizations</div>
<div class="val cyn" id="uniqueOrgs">-</div>
</div>
<div class="stat-card">
<div class="lbl">Affected Proxies</div>
<div class="val blu" id="uniqueProxies">-</div>
</div>
</div>
<!-- Search Box -->
<div class="search-box">
<span class="search-icon">&#128269;</span>
<input type="text" class="search-input" id="searchInput"
placeholder="Search certificates, organizations, proxies..."
autocomplete="off" spellcheck="false">
<button class="search-clear" id="searchClear" title="Clear search">&times;</button>
<div class="suggestions" id="suggestions"></div>
</div>
<!-- Help Box -->
<div class="help-box" id="helpBox">
<div class="help-header" onclick="toggleHelp()">
<div class="help-title">Search Syntax Help</div>
<span class="help-toggle">&#9660;</span>
</div>
<div class="help-content">
<div class="help-section">
<div class="help-section-title">Field Filters</div>
<div class="help-examples">
<div class="help-example"><code>org:Cloudflare</code> <span>organization name</span></div>
<div class="help-example"><code>issuer:DigiCert</code> <span>certificate issuer</span></div>
<div class="help-example"><code>cn:*.example.com</code> <span>common name</span></div>
<div class="help-example"><code>proxy:192.168.1</code> <span>proxy IP address</span></div>
<div class="help-example"><code>fp:a1b2c3</code> <span>fingerprint prefix</span></div>
<div class="help-example"><code>serial:12345</code> <span>serial number</span></div>
</div>
</div>
<div class="help-section">
<div class="help-section-title">Date Filters</div>
<div class="help-examples">
<div class="help-example"><code>expires:2024</code> <span>expiration year</span></div>
<div class="help-example"><code>expired:yes</code> <span>show expired certs</span></div>
</div>
</div>
<div class="help-section">
<div class="help-section-title">General Search</div>
<div class="help-examples">
<div class="help-example"><code>cloudflare</code> <span>search all fields</span></div>
<div class="help-example"><code>"security proxy"</code> <span>exact phrase</span></div>
</div>
</div>
</div>
</div>
<!-- Results Area -->
<div class="results-container" id="resultsContainer">
<div class="results-empty" id="resultsEmpty">
<div class="results-empty-icon">&#128274;</div>
<div class="results-empty-text">Start typing to search MITM certificates</div>
<div class="results-empty-hint">Use field filters like <code>org:</code> or <code>proxy:</code> for precise results</div>
</div>
<div class="results-loading" id="resultsLoading" style="display:none">Searching</div>
<div id="resultsList" style="display:none">
<div class="results-header">
<div class="results-count"><strong id="resultsCount">0</strong> results found</div>
</div>
<div id="resultsContent"></div>
</div>
</div>
<!-- No Data State (shown when no MITM data exists) -->
<div class="no-data" id="noDataState" style="display:none">
<div class="no-data-icon">&#128737;</div>
<div class="no-data-title">No MITM Certificates Detected</div>
<div class="no-data-text">
MITM certificates are captured when proxies attempt SSL interception.
As the system validates proxies with SSL enabled, any detected MITM
certificates will appear here.
</div>
</div>
</div>
<script src="/static/mitm.js"></script>
</body>
</html>

703
static/mitm.js Normal file
View File

@@ -0,0 +1,703 @@
/**
* MITM Certificate Search Interface
* Provides search functionality for SSL MITM certificate data
*/
(function() {
'use strict';
// Theme toggle (shared with dashboard)
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() {
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');
}
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);
// DOM elements
var searchInput = document.getElementById('searchInput');
var searchClear = document.getElementById('searchClear');
var suggestions = document.getElementById('suggestions');
var resultsContainer = document.getElementById('resultsContainer');
var resultsEmpty = document.getElementById('resultsEmpty');
var resultsLoading = document.getElementById('resultsLoading');
var resultsList = document.getElementById('resultsList');
var resultsContent = document.getElementById('resultsContent');
var resultsCount = document.getElementById('resultsCount');
var noDataState = document.getElementById('noDataState');
// Stats elements
var totalDetections = document.getElementById('totalDetections');
var uniqueCerts = document.getElementById('uniqueCerts');
var uniqueOrgs = document.getElementById('uniqueOrgs');
var uniqueProxies = document.getElementById('uniqueProxies');
// State
var searchTimeout = null;
var currentData = null;
var suggestionIndex = -1;
// Search field definitions for autocomplete
var searchFields = [
{ prefix: 'org:', desc: 'organization name', icon: '&#127970;' },
{ prefix: 'issuer:', desc: 'certificate issuer', icon: '&#128196;' },
{ prefix: 'cn:', desc: 'common name', icon: '&#128279;' },
{ prefix: 'proxy:', desc: 'proxy IP address', icon: '&#128421;' },
{ prefix: 'fp:', desc: 'fingerprint', icon: '&#128273;' },
{ prefix: 'serial:', desc: 'serial number', icon: '&#128290;' },
{ prefix: 'expires:', desc: 'expiration year', icon: '&#128197;' },
{ prefix: 'expired:', desc: 'yes/no', icon: '&#9888;' }
];
/**
* Escape HTML to prevent XSS
*/
function escapeHtml(str) {
if (str === null || str === undefined) return '';
var div = document.createElement('div');
div.textContent = String(str);
return div.innerHTML;
}
/**
* Sanitize search query - remove potentially dangerous characters
*/
function sanitizeQuery(query) {
if (!query) return '';
// Allow alphanumeric, spaces, common punctuation for searches
// Remove control characters and dangerous patterns
return String(query)
.replace(/[\x00-\x1f\x7f]/g, '') // Control characters
.replace(/<[^>]*>/g, '') // HTML tags
.trim()
.substring(0, 200); // Limit length
}
/**
* Format number with comma separators
*/
function formatNumber(n) {
if (n === null || n === undefined || n === '-') return '-';
return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
/**
* Format timestamp to readable date
*/
function formatDate(ts) {
if (!ts) return '-';
// Handle ASN.1 time format (YYYYMMDDhhmmssZ)
if (typeof ts === 'string' && ts.length >= 14) {
var y = ts.substring(0, 4);
var m = ts.substring(4, 6);
var d = ts.substring(6, 8);
return y + '-' + m + '-' + d;
}
// Handle Unix timestamp
if (typeof ts === 'number') {
var date = new Date(ts * 1000);
return date.toISOString().split('T')[0];
}
return String(ts);
}
/**
* Check if certificate is expired
*/
function isExpired(notAfter) {
if (!notAfter || notAfter.length < 14) return false;
var expDate = new Date(
parseInt(notAfter.substring(0, 4)),
parseInt(notAfter.substring(4, 6)) - 1,
parseInt(notAfter.substring(6, 8))
);
return expDate < new Date();
}
/**
* Load initial stats
*/
function loadStats() {
fetch('/api/mitm')
.then(function(r) { return r.json(); })
.then(function(data) {
currentData = data;
// Update summary stats
totalDetections.textContent = formatNumber(data.total_detections || 0);
uniqueCerts.textContent = formatNumber(data.unique_certs || 0);
uniqueProxies.textContent = formatNumber(data.unique_proxies || 0);
// Count unique orgs from top_organizations
var orgCount = (data.top_organizations || []).length;
uniqueOrgs.textContent = formatNumber(orgCount);
// Show no data state if empty
if (!data.certificates || data.certificates.length === 0) {
document.getElementById('statsSummary').style.display = 'none';
document.querySelector('.search-box').style.display = 'none';
document.getElementById('helpBox').style.display = 'none';
resultsContainer.style.display = 'none';
noDataState.style.display = 'block';
}
})
.catch(function(err) {
console.error('Failed to load MITM stats:', err);
});
}
/**
* Parse search query into structured filters
*/
function parseQuery(query) {
var filters = {
org: null,
issuer: null,
cn: null,
proxy: null,
fp: null,
serial: null,
expires: null,
expired: null,
text: []
};
if (!query) return filters;
// Match field:value patterns and quoted strings
var parts = query.match(/(\w+:"[^"]+"|"[^"]+"|[\w:.]+)/g) || [];
for (var i = 0; i < parts.length; i++) {
var part = parts[i];
var colonIdx = part.indexOf(':');
if (colonIdx > 0 && colonIdx < part.length - 1) {
var field = part.substring(0, colonIdx).toLowerCase();
var value = part.substring(colonIdx + 1).replace(/^"|"$/g, '');
if (filters.hasOwnProperty(field)) {
filters[field] = value;
} else {
filters.text.push(part);
}
} else {
// Plain text search
filters.text.push(part.replace(/^"|"$/g, ''));
}
}
return filters;
}
/**
* Check if certificate matches filters
*/
function matchesCert(cert, filters) {
// Organization filter
if (filters.org) {
var org = (cert.subject_o || '').toLowerCase();
if (org.indexOf(filters.org.toLowerCase()) === -1) return false;
}
// Issuer filter
if (filters.issuer) {
var issuer = (cert.issuer_cn || cert.issuer_o || '').toLowerCase();
if (issuer.indexOf(filters.issuer.toLowerCase()) === -1) return false;
}
// Common name filter
if (filters.cn) {
var cn = (cert.subject_cn || '').toLowerCase();
if (cn.indexOf(filters.cn.toLowerCase()) === -1) return false;
}
// Proxy filter
if (filters.proxy) {
var proxies = cert.proxies || [];
var found = false;
for (var i = 0; i < proxies.length; i++) {
if (proxies[i].indexOf(filters.proxy) !== -1) {
found = true;
break;
}
}
if (!found) return false;
}
// Fingerprint filter
if (filters.fp) {
var fp = (cert.fingerprint || cert.fingerprint_full || '').toLowerCase();
if (fp.indexOf(filters.fp.toLowerCase()) === -1) return false;
}
// Serial filter
if (filters.serial) {
var serial = (cert.serial || '').toLowerCase();
if (serial.indexOf(filters.serial.toLowerCase()) === -1) return false;
}
// Expiration year filter
if (filters.expires) {
var notAfter = cert.not_after || '';
if (notAfter.indexOf(filters.expires) === -1) return false;
}
// Expired filter
if (filters.expired) {
var wantExpired = filters.expired.toLowerCase() === 'yes';
var certExpired = isExpired(cert.not_after);
if (wantExpired !== certExpired) return false;
}
// Text search (searches all fields)
if (filters.text.length > 0) {
var searchable = [
cert.subject_cn || '',
cert.subject_o || '',
cert.issuer_cn || '',
cert.issuer_o || '',
cert.fingerprint || '',
cert.serial || ''
].join(' ').toLowerCase();
for (var j = 0; j < filters.text.length; j++) {
if (searchable.indexOf(filters.text[j].toLowerCase()) === -1) {
return false;
}
}
}
return true;
}
/**
* Perform search on current data
*/
function performSearch(query) {
query = sanitizeQuery(query);
if (!query) {
showEmpty();
return;
}
if (!currentData || !currentData.certificates) {
showEmpty();
return;
}
showLoading();
// Use setTimeout to not block UI
setTimeout(function() {
var filters = parseQuery(query);
var results = [];
var certs = currentData.certificates || [];
for (var i = 0; i < certs.length; i++) {
if (matchesCert(certs[i], filters)) {
results.push(certs[i]);
}
}
showResults(results, query);
}, 50);
}
/**
* Show empty state
*/
function showEmpty() {
resultsEmpty.style.display = 'block';
resultsLoading.style.display = 'none';
resultsList.style.display = 'none';
}
/**
* Show loading state
*/
function showLoading() {
resultsEmpty.style.display = 'none';
resultsLoading.style.display = 'block';
resultsList.style.display = 'none';
}
/**
* Render search results
*/
function showResults(results, query) {
resultsEmpty.style.display = 'none';
resultsLoading.style.display = 'none';
resultsList.style.display = 'block';
resultsCount.textContent = formatNumber(results.length);
if (results.length === 0) {
resultsContent.innerHTML =
'<div class="results-empty">' +
'<div class="results-empty-icon">&#128269;</div>' +
'<div class="results-empty-text">No certificates match your search</div>' +
'<div class="results-empty-hint">Try adjusting your filters or search terms</div>' +
'</div>';
return;
}
var html = '';
for (var i = 0; i < results.length; i++) {
html += renderCertCard(results[i]);
}
resultsContent.innerHTML = html;
}
/**
* Render a single certificate card
*/
function renderCertCard(cert) {
var expired = isExpired(cert.not_after);
var proxies = cert.proxies || [];
var html = '<div class="result-card">';
html += '<div class="result-header">';
html += '<div class="result-title">';
html += '<span class="result-badge cert">Certificate</span>';
html += escapeHtml(cert.subject_cn || cert.subject_o || 'Unknown');
html += '</div>';
html += '<div class="result-meta">';
if (expired) {
html += '<span class="red">Expired</span> &middot; ';
}
html += 'Seen ' + escapeHtml(String(cert.count || 1)) + ' time(s)';
html += '</div>';
html += '</div>';
html += '<div class="result-body">';
// Subject
html += '<div class="result-field">';
html += '<div class="result-field-label">Subject (CN)</div>';
html += '<div class="result-field-value">' + escapeHtml(cert.subject_cn || '-') + '</div>';
html += '</div>';
// Organization
html += '<div class="result-field">';
html += '<div class="result-field-label">Organization</div>';
html += '<div class="result-field-value">' + escapeHtml(cert.subject_o || '-') + '</div>';
html += '</div>';
// Issuer
html += '<div class="result-field">';
html += '<div class="result-field-label">Issuer (CN)</div>';
html += '<div class="result-field-value">' + escapeHtml(cert.issuer_cn || '-') + '</div>';
html += '</div>';
// Issuer Org
html += '<div class="result-field">';
html += '<div class="result-field-label">Issuer Org</div>';
html += '<div class="result-field-value">' + escapeHtml(cert.issuer_o || '-') + '</div>';
html += '</div>';
// Fingerprint
html += '<div class="result-field">';
html += '<div class="result-field-label">Fingerprint</div>';
html += '<div class="result-field-value highlight">' + escapeHtml(cert.fingerprint || cert.fingerprint_full || '-') + '</div>';
html += '</div>';
// Serial
html += '<div class="result-field">';
html += '<div class="result-field-label">Serial</div>';
html += '<div class="result-field-value">' + escapeHtml(cert.serial || '-') + '</div>';
html += '</div>';
// Valid From
html += '<div class="result-field">';
html += '<div class="result-field-label">Valid From</div>';
html += '<div class="result-field-value">' + escapeHtml(formatDate(cert.not_before)) + '</div>';
html += '</div>';
// Valid Until
html += '<div class="result-field">';
html += '<div class="result-field-label">Valid Until</div>';
html += '<div class="result-field-value' + (expired ? ' red' : '') + '">' + escapeHtml(formatDate(cert.not_after)) + '</div>';
html += '</div>';
html += '</div>';
// Proxies list
if (proxies.length > 0) {
html += '<div class="result-proxies">';
html += '<div class="result-proxies-title">Proxies using this certificate (' + proxies.length + ')</div>';
html += '<div class="proxy-tags">';
for (var i = 0; i < Math.min(proxies.length, 10); i++) {
html += '<span class="proxy-tag">' + escapeHtml(proxies[i]) + '</span>';
}
if (proxies.length > 10) {
html += '<span class="proxy-tag">+' + (proxies.length - 10) + ' more</span>';
}
html += '</div>';
html += '</div>';
}
html += '</div>';
return html;
}
/**
* Show autocomplete suggestions
*/
function showSuggestions(query) {
query = sanitizeQuery(query);
if (!query) {
hideSuggestions();
return;
}
var html = '';
// Check if user is typing a field prefix
var lastWord = query.split(/\s+/).pop() || '';
var colonIdx = lastWord.indexOf(':');
if (colonIdx === -1) {
// Show matching field suggestions
var matchingFields = [];
for (var i = 0; i < searchFields.length; i++) {
if (searchFields[i].prefix.toLowerCase().indexOf(lastWord.toLowerCase()) === 0) {
matchingFields.push(searchFields[i]);
}
}
if (matchingFields.length > 0) {
html += '<div class="suggestion-group">';
html += '<div class="suggestion-header">Field Filters</div>';
for (var j = 0; j < matchingFields.length; j++) {
var field = matchingFields[j];
html += '<div class="suggestion-item" data-value="' + escapeHtml(field.prefix) + '">';
html += '<span class="suggestion-icon">' + field.icon + '</span>';
html += '<span class="suggestion-text"><code>' + escapeHtml(field.prefix) + '</code></span>';
html += '<span class="suggestion-desc">' + escapeHtml(field.desc) + '</span>';
html += '</div>';
}
html += '</div>';
}
}
// Add quick value suggestions from data
if (currentData && colonIdx > 0) {
var fieldName = lastWord.substring(0, colonIdx);
var valuePart = lastWord.substring(colonIdx + 1).toLowerCase();
var valueSuggestions = getValueSuggestions(fieldName, valuePart);
if (valueSuggestions.length > 0) {
html += '<div class="suggestion-group">';
html += '<div class="suggestion-header">Matching Values</div>';
for (var k = 0; k < Math.min(valueSuggestions.length, 5); k++) {
var val = valueSuggestions[k];
html += '<div class="suggestion-item" data-value="' + escapeHtml(fieldName + ':' + val) + '">';
html += '<span class="suggestion-icon">&#8594;</span>';
html += '<span class="suggestion-text">' + escapeHtml(val) + '</span>';
html += '</div>';
}
html += '</div>';
}
}
if (html) {
suggestions.innerHTML = html;
suggestions.classList.add('visible');
suggestionIndex = -1;
} else {
hideSuggestions();
}
}
/**
* Get value suggestions for a field
*/
function getValueSuggestions(field, partial) {
if (!currentData) return [];
var values = [];
field = field.toLowerCase();
if (field === 'org') {
var orgs = currentData.top_organizations || [];
for (var i = 0; i < orgs.length; i++) {
var orgName = orgs[i].name || '';
if (partial === '' || orgName.toLowerCase().indexOf(partial) !== -1) {
values.push(orgName);
}
}
} else if (field === 'issuer') {
var issuers = currentData.top_issuers || [];
for (var j = 0; j < issuers.length; j++) {
var issuerName = issuers[j].name || '';
if (partial === '' || issuerName.toLowerCase().indexOf(partial) !== -1) {
values.push(issuerName);
}
}
} else if (field === 'expired') {
values = ['yes', 'no'];
}
return values;
}
/**
* Hide suggestions dropdown
*/
function hideSuggestions() {
suggestions.classList.remove('visible');
suggestionIndex = -1;
}
/**
* Apply suggestion to search input
*/
function applySuggestion(value) {
var query = searchInput.value;
var parts = query.split(/\s+/);
parts[parts.length - 1] = value;
searchInput.value = parts.join(' ');
searchInput.focus();
hideSuggestions();
// Trigger search if value is complete
if (value.indexOf(':') !== -1) {
handleSearch();
}
}
/**
* Handle search input
*/
function handleSearch() {
var query = searchInput.value;
// Show/hide clear button
if (query) {
searchClear.classList.add('visible');
} else {
searchClear.classList.remove('visible');
}
// Debounce search
if (searchTimeout) {
clearTimeout(searchTimeout);
}
searchTimeout = setTimeout(function() {
performSearch(query);
}, 200);
// Show suggestions immediately
showSuggestions(query);
}
/**
* Clear search
*/
function clearSearch() {
searchInput.value = '';
searchClear.classList.remove('visible');
hideSuggestions();
showEmpty();
searchInput.focus();
}
/**
* Toggle help box
*/
window.toggleHelp = function() {
var helpBox = document.getElementById('helpBox');
helpBox.classList.toggle('collapsed');
};
/**
* Keyboard navigation for suggestions
*/
function handleKeydown(e) {
var items = suggestions.querySelectorAll('.suggestion-item');
if (!items.length) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
suggestionIndex = Math.min(suggestionIndex + 1, items.length - 1);
updateSuggestionHighlight(items);
} else if (e.key === 'ArrowUp') {
e.preventDefault();
suggestionIndex = Math.max(suggestionIndex - 1, 0);
updateSuggestionHighlight(items);
} else if (e.key === 'Enter' && suggestionIndex >= 0) {
e.preventDefault();
var value = items[suggestionIndex].getAttribute('data-value');
if (value) applySuggestion(value);
} else if (e.key === 'Escape') {
hideSuggestions();
}
}
/**
* Update suggestion highlight
*/
function updateSuggestionHighlight(items) {
for (var i = 0; i < items.length; i++) {
if (i === suggestionIndex) {
items[i].classList.add('active');
} else {
items[i].classList.remove('active');
}
}
}
// Event listeners
searchInput.addEventListener('input', handleSearch);
searchInput.addEventListener('keydown', handleKeydown);
searchInput.addEventListener('focus', function() {
if (searchInput.value) showSuggestions(searchInput.value);
});
searchClear.addEventListener('click', clearSearch);
// Click on suggestion
suggestions.addEventListener('click', function(e) {
var item = e.target.closest('.suggestion-item');
if (item) {
var value = item.getAttribute('data-value');
if (value) applySuggestion(value);
}
});
// Click outside to hide suggestions
document.addEventListener('click', function(e) {
if (!e.target.closest('.search-box')) {
hideSuggestions();
}
});
// Initialize
loadStats();
})();

815
static/style.css Normal file
View File

@@ -0,0 +1,815 @@
/* PPF Dashboard Styles - Electric Cyan Theme */
/* Theme variables are substituted at runtime by httpd.py */
:root {
--bg: {bg}; --card: {card}; --card-alt: {card_alt}; --border: {border};
--text: {text}; --dim: {dim}; --green: {green}; --red: {red};
--yellow: {yellow}; --blue: {blue}; --purple: {purple};
--cyan: {cyan}; --orange: {orange}; --pink: {pink}; --map-bg: {map_bg};
--shadow-sm: 0 1px 2px rgba(0,0,0,0.2);
--shadow-md: 0 4px 12px rgba(0,0,0,0.25);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.3);
--glow-green: 0 0 12px rgba(63,185,80,0.4);
--glow-blue: 0 0 12px rgba(88,166,255,0.4);
--glow-red: 0 0 12px rgba(248,81,73,0.4);
--transition: 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
/* Muted dark theme - desaturated/softer dark colors */
html.muted-dark {
--bg: #1a1c20;
--card: #22252a;
--card-alt: #282c32;
--border: #3a3f47;
--text: #c8ccd4;
--dim: #7a7f88;
--green: #5a9a66;
--red: #c06060;
--yellow: #b39540;
--blue: #6090b8;
--purple: #8a6aa8;
--cyan: #5a9098;
--orange: #b87850;
--pink: #a86080;
--map-bg: #1a1c20;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.25);
--shadow-md: 0 4px 12px rgba(0,0,0,0.3);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.35);
--glow-green: 0 0 10px rgba(90,154,102,0.3);
--glow-blue: 0 0 10px rgba(96,144,184,0.3);
--glow-red: 0 0 10px rgba(192,96,96,0.3);
}
/* Light theme - muted/broken colors (not pure white) */
html.light {
--bg: #e8e4df;
--card: #f5f2ed;
--card-alt: #ebe7e2;
--border: #c5c0b8;
--text: #2c2a26;
--dim: #6b6860;
--green: #2a7d3f;
--red: #c53030;
--yellow: #a67c00;
--blue: #2563a8;
--purple: #7c3aad;
--cyan: #1a7a7f;
--orange: #c05621;
--pink: #b5366b;
--map-bg: #e8e4df;
--shadow-sm: 0 1px 3px rgba(0,0,0,0.08);
--shadow-md: 0 4px 12px rgba(0,0,0,0.1);
--shadow-lg: 0 8px 24px rgba(0,0,0,0.12);
--glow-green: 0 0 8px rgba(42,125,63,0.25);
--glow-blue: 0 0 8px rgba(37,99,168,0.25);
--glow-red: 0 0 8px rgba(197,48,48,0.25);
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
font-size: 13px; background: var(--bg); color: var(--text);
padding: 16px; min-height: 100vh; line-height: 1.5;
-webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;
}
a { color: var(--blue); text-decoration: none; transition: color var(--transition); }
a:hover { color: var(--cyan); }
h1 { font-size: 18px; font-weight: 600; color: var(--text); margin-bottom: 16px; }
h2 { font-size: 15px; font-weight: 600; color: var(--text); margin-bottom: 12px; }
h3 { font-size: 13px; font-weight: 600; color: var(--dim); margin-bottom: 8px; }
.container { max-width: 1400px; margin: 0 auto; }
/* Header */
.hdr { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; padding-bottom: 12px; border-bottom: 1px solid var(--border); }
.hdr h1 { font-size: 18px; font-weight: 600; display: flex; align-items: center; gap: 10px; }
.hdr h1::before { content: ""; width: 10px; height: 10px; background: var(--green); border-radius: 50%; box-shadow: 0 0 8px var(--green); }
.status { display: flex; align-items: center; gap: 12px; font-size: 12px; color: var(--dim); }
.status-item { display: flex; align-items: center; gap: 4px; }
.dot { width: 6px; height: 6px; border-radius: 50%; background: var(--green); animation: pulse 2s infinite; }
.dot.err { background: var(--red); animation: none; }
@keyframes pulse { 50% { opacity: 0.5; } }
.mode-badge { padding: 4px 10px; border-radius: 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.mode-ssl { background: rgba(63,185,80,0.2); color: var(--green); border: 1px solid var(--green); }
.mode-profile { background: rgba(255,165,0,0.2); color: #ffa500; border: 1px solid #ffa500; margin-left: 6px; }
.mode-judges { background: rgba(88,166,255,0.2); color: var(--blue); border: 1px solid var(--blue); }
.mode-head { background: rgba(210,153,34,0.2); color: var(--yellow); border: 1px solid var(--yellow); }
.mode-irc { background: rgba(163,113,247,0.2); color: var(--purple); border: 1px solid var(--purple); }
/* System monitor bar */
.sysbar { display: flex; gap: 16px; padding: 8px 12px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; margin-bottom: 16px; font-size: 11px; }
.sysbar-item { display: flex; align-items: center; gap: 6px; }
.sysbar-lbl { color: var(--dim); }
.sysbar-val { font-weight: 600; font-feature-settings: "tnum"; }
.sysbar-bar { width: 50px; height: 4px; background: var(--border); border-radius: 2px; overflow: hidden; }
.sysbar-fill { height: 100%; border-radius: 2px; transition: width 0.3s; }
.sysbar-net .net-tx::before { content: '↑'; opacity: 0.5; margin-right: 1px; }
.sysbar-net .net-rx::before { content: '↓'; opacity: 0.5; margin-right: 1px; }
.sysbar-net .sysbar-val { min-width: 42px; }
/* Grid */
.g { display: grid; gap: 12px; margin-bottom: 16px; }
.g2 { grid-template-columns: repeat(2, 1fr); }
.g3 { grid-template-columns: repeat(3, 1fr); }
.g4 { grid-template-columns: repeat(4, 1fr); }
.g5 { grid-template-columns: repeat(5, 1fr); }
.g6 { grid-template-columns: repeat(6, 1fr); }
@media (max-width: 1200px) { .g5, .g6 { grid-template-columns: repeat(4, 1fr); } }
@media (max-width: 900px) { .g3, .g4, .g5, .g6 { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 600px) { .g2, .g3, .g4, .g5, .g6 { grid-template-columns: 1fr; } }
/* Cards */
.c {
background: var(--card); border: 1px solid var(--border); border-radius: 10px; padding: 14px;
box-shadow: var(--shadow-sm); transition: transform var(--transition), box-shadow var(--transition);
}
.c:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.c-lg { padding: 16px 18px; }
.c-sm { padding: 10px 12px; }
.c-glow { box-shadow: var(--shadow-md), var(--glow-blue); }
.lbl { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; font-weight: 500; }
.val { font-size: 26px; font-weight: 700; font-feature-settings: "tnum"; letter-spacing: -0.5px; line-height: 1.2; }
.val-md { font-size: 20px; font-weight: 600; }
.val-sm { font-size: 16px; font-weight: 600; }
.sub { font-size: 11px; color: var(--dim); margin-top: 4px; }
.grn { color: var(--green); } .red { color: var(--red); } .yel { color: var(--yellow); }
.blu { color: var(--blue); } .pur { color: var(--purple); } .cyn { color: var(--cyan); }
.org { color: var(--orange); } .pnk { color: var(--pink); }
/* Section headers */
.sec { margin-bottom: 16px; }
.sec-hdr { font-size: 11px; font-weight: 600; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 10px; display: flex; align-items: center; gap: 8px; }
.sec-hdr::before { content: ""; width: 3px; height: 12px; background: var(--blue); border-radius: 2px; }
/* Progress bars */
.bar-wrap { height: 6px; background: var(--border); border-radius: 3px; margin-top: 8px; overflow: hidden; position: relative; }
.bar { height: 100%; border-radius: 3px; transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); position: relative; }
.bar::after { content: ''; position: absolute; inset: 0; background: linear-gradient(90deg, transparent, rgba(255,255,255,0.1), transparent); }
.bar.grn { background: linear-gradient(90deg, #238636, #3fb950); box-shadow: 0 0 8px rgba(63,185,80,0.3); }
.bar.red { background: linear-gradient(90deg, #da3633, #f85149); box-shadow: 0 0 8px rgba(248,81,73,0.3); }
.bar.yel { background: linear-gradient(90deg, #9e6a03, #d29922); box-shadow: 0 0 8px rgba(210,153,34,0.3); }
.bar.blu { background: linear-gradient(90deg, #1f6feb, #58a6ff); box-shadow: 0 0 8px rgba(88,166,255,0.3); }
/* Charts */
.chart-wrap {
background: linear-gradient(145deg, rgba(24,31,42,0.65), rgba(18,24,34,0.75));
border: 1px solid rgba(56,189,248,0.35); border-radius: 10px; padding: 12px 14px; margin-top: 8px;
box-shadow: 0 0 28px rgba(56,189,248,0.12), 0 0 1px rgba(56,189,248,0.4), var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.08);
position: relative; overflow: hidden; backdrop-filter: blur(8px);
}
.chart-wrap::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.06) 0%, transparent 40%);
pointer-events: none;
}
.chart { width: 100%; height: 80px; position: relative; }
.chart-lg { height: 120px; }
.chart svg { width: 100%; height: 100%; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.2)); }
.chart-line { fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; filter: drop-shadow(0 0 4px currentColor); }
.chart-area { opacity: 0.25; }
.chart-grid { stroke: var(--border); stroke-width: 0.5; stroke-dasharray: 2 4; }
.chart-label { font-size: 9px; fill: var(--dim); }
/* Histogram bars */
.histo-wrap {
background: linear-gradient(145deg, rgba(24,31,42,0.65), rgba(18,24,34,0.75));
border: 1px solid rgba(56,189,248,0.35); border-radius: 10px; padding: 14px 16px 26px;
box-shadow: 0 0 28px rgba(56,189,248,0.12), 0 0 1px rgba(56,189,248,0.4), var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.08);
position: relative; overflow: hidden; backdrop-filter: blur(8px);
}
.histo-wrap::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.06) 0%, transparent 40%);
pointer-events: none;
}
.histo { display: flex; align-items: flex-end; gap: 3px; height: 70px; padding: 0 2px; position: relative; z-index: 1; }
.histo-bar {
flex: 1; border-radius: 4px 4px 0 0; min-height: 3px; position: relative;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
background: linear-gradient(180deg, var(--cyan), rgba(88,166,255,0.6));
box-shadow: 0 0 10px rgba(56,189,248,0.4), inset 0 1px 0 rgba(255,255,255,0.15);
}
.histo-bar:hover { transform: scaleY(1.1) translateY(-1px); filter: brightness(1.35); box-shadow: 0 0 16px rgba(56,189,248,0.6); }
.histo-bar::after { content: attr(data-label); position: absolute; bottom: -16px; left: 50%; transform: translateX(-50%); font-size: 8px; color: var(--dim); white-space: nowrap; opacity: 0.8; }
.histo-labels { display: flex; justify-content: space-between; margin-top: 20px; font-size: 9px; color: var(--dim); position: relative; z-index: 1; }
/* Stat rows */
.stats-wrap {
background: linear-gradient(145deg, rgba(24,31,42,0.7), rgba(18,24,34,0.8));
border: 1px solid rgba(56,189,248,0.28); border-radius: 10px; padding: 14px 16px;
box-shadow: 0 0 24px rgba(56,189,248,0.1), 0 0 1px rgba(56,189,248,0.35), var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.06);
position: relative; overflow: hidden; backdrop-filter: blur(6px);
}
.stats-wrap::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.05) 0%, transparent 45%);
pointer-events: none;
}
.stat-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; font-size: 12px; }
.stat-row + .stat-row { border-top: 1px solid rgba(58,68,80,0.4); }
.stat-lbl { color: var(--dim); display: flex; align-items: center; gap: 6px; }
.stat-val { font-weight: 600; font-feature-settings: "tnum"; }
.stat-bar { width: 60px; height: 4px; background: var(--border); border-radius: 2px; margin-left: 8px; overflow: hidden; }
.stat-bar-fill { height: 100%; border-radius: 2px; }
/* Leaderboard */
.lb-wrap {
background: linear-gradient(145deg, rgba(24,31,42,0.7), rgba(18,24,34,0.8));
border: 1px solid rgba(56,189,248,0.28); border-radius: 10px; padding: 14px 16px;
box-shadow: 0 0 24px rgba(56,189,248,0.1), 0 0 1px rgba(56,189,248,0.35), var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.06);
position: relative; overflow: hidden; backdrop-filter: blur(6px);
}
.lb-wrap::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.05) 0%, transparent 45%);
pointer-events: none;
}
.lb { font-size: 12px; }
.lb-item { display: flex; align-items: center; gap: 8px; padding: 5px 0; }
.lb-item + .lb-item { border-top: 1px solid rgba(58,68,80,0.3); }
.lb-rank { width: 18px; height: 18px; border-radius: 4px; background: var(--card-alt); display: flex; align-items: center; justify-content: center; font-size: 10px; font-weight: 600; color: var(--dim); }
.lb-rank.top { background: var(--yellow); color: var(--bg); }
.lb-name { flex: 1; font-family: ui-monospace, monospace; color: var(--text); }
.lb-val { font-weight: 600; font-feature-settings: "tnum"; }
/* Tags */
.tag {
display: inline-flex; align-items: center; gap: 4px; padding: 3px 8px;
border-radius: 4px; font-size: 10px; font-weight: 600;
transition: all var(--transition); backdrop-filter: blur(4px);
}
.tag-ok { background: rgba(63,185,80,0.15); color: var(--green); border: 1px solid rgba(63,185,80,0.3); }
.tag-ok:hover { background: rgba(63,185,80,0.25); box-shadow: 0 0 8px rgba(63,185,80,0.2); }
.tag-err { background: rgba(248,81,73,0.15); color: var(--red); border: 1px solid rgba(248,81,73,0.3); }
.tag-err:hover { background: rgba(248,81,73,0.25); box-shadow: 0 0 8px rgba(248,81,73,0.2); }
.tag-warn { background: rgba(210,153,34,0.15); color: var(--yellow); border: 1px solid rgba(210,153,34,0.3); }
.tag-warn:hover { background: rgba(210,153,34,0.25); box-shadow: 0 0 8px rgba(210,153,34,0.2); }
.tag-info { background: rgba(88,166,255,0.15); color: var(--blue); border: 1px solid rgba(88,166,255,0.3); }
.tag-info:hover { background: rgba(88,166,255,0.25); box-shadow: 0 0 8px rgba(88,166,255,0.2); }
/* Mini stats */
.mini { display: flex; gap: 16px; flex-wrap: wrap; margin-top: 8px; }
.mini-item { display: flex; flex-direction: column; align-items: center; gap: 2px; }
.mini-val { font-size: 14px; font-weight: 600; font-feature-settings: "tnum"; }
.mini-lbl { font-size: 10px; color: var(--dim); }
/* Proto cards */
.proto-card { text-align: center; transition: transform var(--transition); }
.proto-card:hover { transform: translateY(-2px); }
.proto-icon { font-size: 24px; margin-bottom: 6px; filter: drop-shadow(0 2px 4px rgba(0,0,0,0.3)); }
.proto-name { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; }
.proto-val { font-size: 20px; font-weight: 700; margin: 6px 0; }
.proto-rate { font-size: 11px; padding: 3px 8px; border-radius: 4px; display: inline-block; }
/* Pie charts */
.pie-wrap {
display: flex; gap: 20px; align-items: center;
background: linear-gradient(145deg, rgba(24,31,42,0.65), rgba(18,24,34,0.75));
border: 1px solid rgba(56,189,248,0.35); border-radius: 10px; padding: 16px;
box-shadow: 0 0 28px rgba(56,189,248,0.12), 0 0 1px rgba(56,189,248,0.4), var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.08);
position: relative; overflow: hidden; backdrop-filter: blur(8px);
}
.pie-wrap::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.06) 0%, transparent 40%);
pointer-events: none;
}
.pie {
width: 100px; height: 100px; border-radius: 50%; flex-shrink: 0;
box-shadow: var(--shadow-md), inset 0 0 20px rgba(0,0,0,0.3);
position: relative; transition: transform var(--transition);
}
.pie:hover { transform: scale(1.02); }
.legend { flex: 1; }
.legend-item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; transition: opacity var(--transition); }
.legend-item:hover { opacity: 0.8; }
.legend-dot { width: 10px; height: 10px; border-radius: 3px; flex-shrink: 0; box-shadow: 0 0 4px currentColor; }
.legend-name { flex: 1; color: var(--dim); }
.legend-val { font-weight: 600; font-feature-settings: "tnum"; }
/* Tor/Host cards */
.tor-card {
background: linear-gradient(145deg, rgba(24,31,42,0.7), rgba(18,24,34,0.8));
border: 1px solid rgba(56,189,248,0.28); border-radius: 10px; padding: 12px 14px;
box-shadow: 0 0 24px rgba(56,189,248,0.1), 0 0 1px rgba(56,189,248,0.35), var(--shadow-md), inset 0 1px 0 rgba(255,255,255,0.06);
position: relative; overflow: hidden; transition: all 0.2s ease; backdrop-filter: blur(6px);
}
.tor-card::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.05) 0%, transparent 45%);
pointer-events: none;
}
.tor-card:hover { transform: translateY(-2px); box-shadow: 0 0 36px rgba(56,189,248,0.15), 0 0 2px rgba(56,189,248,0.5), var(--shadow-lg); }
.host-card { display: flex; justify-content: space-between; align-items: center; position: relative; z-index: 1; }
.host-addr { font-family: ui-monospace, monospace; font-size: 12px; font-weight: 500; }
.host-stats { font-size: 11px; color: var(--dim); margin-top: 6px; position: relative; z-index: 1; }
.judge-item { display: flex; justify-content: space-between; align-items: center; padding: 4px 0; font-size: 11px; }
.judge-item + .judge-item { border-top: 1px solid rgba(58,68,80,0.3); }
.judge-name { font-family: ui-monospace, monospace; color: var(--dim); flex: 1; }
.judge-stats { display: flex; gap: 12px; }
/* Percentile badges */
.pct-badges {
display: flex; gap: 8px; margin-top: 10px;
background: linear-gradient(135deg, rgba(28,35,45,0.85), rgba(18,25,33,0.95));
border: 1px solid var(--border); border-radius: 8px; padding: 10px;
box-shadow: var(--shadow-sm), inset 0 1px 0 rgba(255,255,255,0.04);
}
.pct-badge {
flex: 1; text-align: center; padding: 8px 6px;
background: linear-gradient(180deg, rgba(36,44,56,0.6), rgba(28,35,45,0.8));
border: 1px solid rgba(58,68,80,0.5); border-radius: 6px;
transition: all var(--transition);
}
.pct-badge:hover { background: linear-gradient(180deg, rgba(42,52,64,0.7), rgba(32,40,50,0.9)); transform: translateY(-1px); }
.pct-label { font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.3px; }
.pct-value { font-size: 16px; font-weight: 700; margin-top: 3px; }
/* Map page */
.nav { margin-bottom: 16px; font-size: 12px; }
.map-stats { margin-bottom: 16px; color: var(--dim); font-size: 12px; padding: 8px 12px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; }
.country-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); gap: 8px; }
.country { padding: 12px 8px; border-radius: 6px; text-align: center; background: var(--card); border: 1px solid var(--border); transition: transform 0.15s, box-shadow 0.15s; }
.country:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.3); }
.country .code { font-weight: bold; font-size: 1.1em; letter-spacing: 0.5px; }
.country .count { font-size: 0.85em; color: var(--dim); margin-top: 4px; font-feature-settings: "tnum"; }
.country.t1 { background: #0d4429; border-color: #238636; }
.country.t1 .code { color: #7ee787; }
.country.t2 { background: #1a3d2e; border-color: #2ea043; }
.country.t2 .code { color: #7ee787; }
.country.t3 { background: #1f3d2a; border-color: #3fb950; }
.country.t3 .code { color: #56d364; }
.country.t4 { background: #2a4a35; border-color: #56d364; }
.country.t4 .code { color: #3fb950; }
.country.t5 { background: #35573f; border-color: #7ee787; }
.country.t5 .code { color: #3fb950; }
.map-legend { display: flex; gap: 16px; margin-top: 20px; flex-wrap: wrap; padding: 12px; background: var(--card); border: 1px solid var(--border); border-radius: 6px; }
.map-legend .legend-item { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--dim); }
.map-legend .legend-dot { width: 12px; height: 12px; border-radius: 3px; }
/* Footer */
.ftr { text-align: center; font-size: 11px; color: var(--dim); padding: 16px 0; margin-top: 8px; border-top: 1px solid var(--border); }
/* Tabs */
.tabs { margin-bottom: 16px; }
.tab-nav {
display: flex; gap: 4px; padding: 4px; background: var(--card);
border: 1px solid var(--border); border-radius: 10px;
overflow-x: auto; scrollbar-width: none; -ms-overflow-style: none;
}
.tab-nav::-webkit-scrollbar { display: none; }
.tab-btn {
padding: 8px 16px; border: none; background: transparent; color: var(--dim);
font-size: 12px; font-weight: 500; border-radius: 6px; cursor: pointer;
transition: all var(--transition); white-space: nowrap;
font-family: inherit;
}
.tab-btn:hover { color: var(--text); background: rgba(255,255,255,0.05); }
.tab-btn.active {
background: var(--blue); color: #fff; box-shadow: var(--shadow-sm), var(--glow-blue);
}
.tab-content { display: none; animation: fadeIn 0.3s ease; }
.tab-content.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
/* Responsive tabs */
@media (max-width: 768px) {
.tab-nav { padding: 3px; gap: 2px; }
.tab-btn { padding: 6px 12px; font-size: 11px; }
}
/* ==========================================================================
Map Page Styles
========================================================================== */
/* Glass panel effect - reusable */
.glass {
background: linear-gradient(145deg, rgba(24,31,42,0.65), rgba(18,24,34,0.75));
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(56,189,248,0.35); border-radius: 10px;
box-shadow: 0 0 28px rgba(56,189,248,0.12), 0 0 1px rgba(56,189,248,0.4),
var(--shadow-lg), inset 0 1px 0 rgba(255,255,255,0.08);
position: relative; overflow: hidden;
}
.glass::before {
content: ''; position: absolute; inset: 0; border-radius: 10px;
background: linear-gradient(180deg, rgba(56,189,248,0.06) 0%, transparent 40%);
pointer-events: none;
}
/* Map container */
.map-page { padding: 0; overflow: hidden; background: var(--map-bg); }
.map-page #map { width: 100%; height: 100vh; }
/* Map navigation */
.map-nav { position: fixed; top: 20px; left: 20px; z-index: 1000; padding: 12px 20px; }
.map-nav a {
color: var(--text); text-decoration: none; font-size: 13px;
font-weight: 500; letter-spacing: 0.3px; transition: color var(--transition);
position: relative; z-index: 1;
}
.map-nav a:hover { color: var(--cyan); }
/* Map stats panel */
.map-stats-panel {
position: fixed; top: 20px; right: 20px; z-index: 1000;
padding: 16px 24px; display: flex; gap: 28px;
}
.map-stat { display: flex; flex-direction: column; align-items: flex-end; position: relative; z-index: 1; }
.map-stat-val {
font-size: 26px; font-weight: 600; color: var(--cyan); line-height: 1.1;
font-feature-settings: "tnum"; letter-spacing: -0.5px;
text-shadow: 0 0 20px rgba(56,189,248,0.4);
}
.map-stat-lbl {
font-size: 10px; color: rgba(56,189,248,0.6); text-transform: uppercase;
letter-spacing: 1px; margin-top: 4px;
}
/* Map legend panel */
.map-legend-panel { position: fixed; bottom: 24px; left: 20px; z-index: 1000; padding: 16px 20px; }
.map-legend-title {
font-size: 10px; text-transform: uppercase; letter-spacing: 1px;
color: rgba(56,189,248,0.5); margin-bottom: 12px; position: relative; z-index: 1;
}
.map-legend-row {
display: flex; align-items: center; gap: 12px; margin: 8px 0;
font-size: 12px; color: rgba(230,237,243,0.7); position: relative; z-index: 1;
}
.map-legend-dot { width: 12px; height: 12px; border-radius: 50%; }
.map-legend-dot.elite { background: var(--green); box-shadow: 0 0 12px rgba(80,200,120,0.5); }
.map-legend-dot.anonymous { background: var(--cyan); box-shadow: 0 0 12px rgba(56,189,248,0.5); }
.map-legend-dot.transparent { background: var(--orange); box-shadow: 0 0 12px rgba(249,115,22,0.5); }
.map-legend-dot.unknown { background: var(--dim); box-shadow: 0 0 12px rgba(107,114,128,0.5); }
/* Map footer */
.map-footer {
position: fixed; bottom: 20px; right: 20px; z-index: 1000;
font-size: 11px; color: rgba(56,189,248,0.4);
}
.map-footer a { color: rgba(56,189,248,0.5); text-decoration: none; transition: color var(--transition); }
.map-footer a:hover { color: var(--cyan); }
/* Leaflet overrides for map page */
.map-page .leaflet-container { background: var(--map-bg); font-family: inherit; }
.map-page .leaflet-control-zoom { border: none !important; margin: 20px !important; }
.map-page .leaflet-control-zoom a {
background: linear-gradient(145deg, rgba(24,31,42,0.85), rgba(18,24,34,0.9)) !important;
backdrop-filter: blur(12px); color: rgba(56,189,248,0.8) !important;
border: 1px solid rgba(56,189,248,0.25) !important;
width: 36px; height: 36px; line-height: 36px !important;
font-size: 18px; font-weight: 300; transition: all var(--transition);
}
.map-page .leaflet-control-zoom a:first-child { border-radius: 8px 8px 0 0 !important; }
.map-page .leaflet-control-zoom a:last-child { border-radius: 0 0 8px 8px !important; border-top: none !important; }
.map-page .leaflet-control-zoom a:hover {
background: rgba(56,189,248,0.15) !important; color: var(--cyan) !important;
box-shadow: 0 0 16px rgba(56,189,248,0.3);
}
.map-page .leaflet-control-attribution {
background: transparent !important; color: rgba(56,189,248,0.3) !important;
font-size: 10px; padding: 4px 8px !important;
}
.map-page .leaflet-control-attribution a { color: rgba(56,189,248,0.4) !important; }
/* Map popups */
.map-page .leaflet-popup-content-wrapper {
background: linear-gradient(145deg, rgba(24,31,42,0.95), rgba(18,24,34,0.97));
backdrop-filter: blur(20px); color: var(--text); border-radius: 10px;
border: 1px solid rgba(56,189,248,0.3);
box-shadow: 0 0 32px rgba(56,189,248,0.15), 0 8px 32px rgba(0,0,0,0.5);
}
.map-page .leaflet-popup-tip { background: rgba(24,31,42,0.95); border: 1px solid rgba(56,189,248,0.2); }
.map-page .leaflet-popup-content { margin: 16px 20px; }
.popup-header { display: flex; align-items: baseline; gap: 10px; margin-bottom: 8px; }
.popup-code { font-size: 24px; font-weight: 600; letter-spacing: -0.5px; color: var(--cyan); text-shadow: 0 0 16px rgba(56,189,248,0.4); }
.popup-name { font-size: 12px; color: rgba(230,237,243,0.5); }
.popup-count { font-size: 16px; font-weight: 500; color: var(--green); }
.popup-coords { font-size: 11px; color: rgba(56,189,248,0.5); margin-top: 6px; font-family: ui-monospace, monospace; }
.map-page .leaflet-popup-close-button {
color: rgba(56,189,248,0.5) !important; font-size: 22px !important;
padding: 8px 12px !important; font-weight: 300;
}
.map-page .leaflet-popup-close-button:hover { color: var(--cyan) !important; }
/* Map cluster styles */
.cluster-wrapper { background: transparent !important; border: none !important; }
.cluster-icon {
display: flex; align-items: center; justify-content: center;
border-radius: 50%; font-weight: 600; font-size: 12px;
background: linear-gradient(145deg, rgba(80,200,120,0.9), rgba(56,189,248,0.9));
border: 2px solid rgba(56,189,248,0.6); color: #fff;
box-shadow: 0 0 16px rgba(56,189,248,0.5), var(--shadow-md);
text-shadow: 0 1px 2px rgba(0,0,0,0.3);
}
.cluster-sm { width: 32px; height: 32px; font-size: 11px; }
.cluster-md { width: 40px; height: 40px; font-size: 13px; }
.cluster-lg { width: 50px; height: 50px; font-size: 15px; }
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large { background: transparent !important; }
.marker-cluster-small div, .marker-cluster-medium div, .marker-cluster-large div { background: transparent !important; }
/* Loading animation */
@keyframes loading-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.5; } }
.loading { animation: loading-pulse 1.8s ease-in-out infinite; }
/* ==========================================================================
MITM Search Page Styles
========================================================================== */
.search-container { max-width: 900px; margin: 0 auto; }
/* Search Header */
.search-header {
display: flex; justify-content: space-between; align-items: center;
margin-bottom: 24px; padding-bottom: 12px; border-bottom: 1px solid var(--border);
}
.search-header h1 {
font-size: 18px; font-weight: 600;
display: flex; align-items: center; gap: 10px;
}
.search-header h1::before {
content: ""; width: 10px; height: 10px;
background: var(--red); border-radius: 50%; box-shadow: 0 0 8px var(--red);
}
.back-link { font-size: 12px; color: var(--dim); }
.back-link:hover { color: var(--cyan); }
/* Search Box */
.search-box { position: relative; margin-bottom: 20px; }
.search-input {
width: 100%; padding: 14px 16px 14px 44px; font-size: 15px; font-family: inherit;
background: var(--card); border: 2px solid var(--border); border-radius: 10px;
color: var(--text); outline: none; transition: border-color 0.2s, box-shadow 0.2s;
}
.search-input:focus { border-color: var(--cyan); box-shadow: 0 0 0 3px rgba(88, 166, 255, 0.15); }
.search-input::placeholder { color: var(--dim); }
.search-icon {
position: absolute; left: 16px; top: 50%; transform: translateY(-50%);
color: var(--dim); font-size: 16px; pointer-events: none;
}
.search-clear {
position: absolute; right: 14px; top: 50%; transform: translateY(-50%);
background: var(--border); border: none; border-radius: 50%;
width: 22px; height: 22px; cursor: pointer; color: var(--dim); font-size: 14px;
display: none; align-items: center; justify-content: center; transition: background 0.2s, color 0.2s;
}
.search-clear:hover { background: var(--red); color: var(--text); }
.search-clear.visible { display: flex; }
/* Suggestions Dropdown */
.suggestions {
position: absolute; top: 100%; left: 0; right: 0;
background: var(--card); border: 1px solid var(--border); border-radius: 8px;
margin-top: 4px; box-shadow: var(--shadow-lg); z-index: 100;
display: none; max-height: 300px; overflow-y: auto;
}
.suggestions.visible { display: block; }
.suggestion-group { padding: 8px 0; border-bottom: 1px solid var(--border); }
.suggestion-group:last-child { border-bottom: none; }
.suggestion-header {
font-size: 10px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.5px; color: var(--dim); padding: 4px 14px;
}
.suggestion-item {
padding: 8px 14px; cursor: pointer;
display: flex; align-items: center; gap: 10px; transition: background 0.15s;
}
.suggestion-item:hover, .suggestion-item.active { background: rgba(88, 166, 255, 0.1); }
.suggestion-icon { font-size: 12px; color: var(--dim); width: 20px; text-align: center; }
.suggestion-text { flex: 1; font-size: 13px; }
.suggestion-text code {
background: var(--card-alt); padding: 2px 6px; border-radius: 4px;
font-family: "SFMono-Regular", Consolas, monospace; font-size: 12px; color: var(--cyan);
}
.suggestion-desc { font-size: 11px; color: var(--dim); }
/* Help Box */
.help-box {
background: var(--card); border: 1px solid var(--border);
border-radius: 10px; padding: 16px; margin-bottom: 20px;
}
.help-box.collapsed .help-content { display: none; }
.help-header { display: flex; justify-content: space-between; align-items: center; cursor: pointer; }
.help-title {
font-size: 13px; font-weight: 600; color: var(--text);
display: flex; align-items: center; gap: 8px;
}
.help-title::before {
content: "?"; display: flex; align-items: center; justify-content: center;
width: 18px; height: 18px; background: var(--blue); color: var(--bg);
border-radius: 50%; font-size: 11px; font-weight: 700;
}
.help-toggle { font-size: 12px; color: var(--dim); transition: transform 0.2s; }
.help-box.collapsed .help-toggle { transform: rotate(-90deg); }
.help-content { margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border); }
.help-section { margin-bottom: 14px; }
.help-section:last-child { margin-bottom: 0; }
.help-section-title {
font-size: 11px; font-weight: 600; text-transform: uppercase;
letter-spacing: 0.5px; color: var(--dim); margin-bottom: 8px;
}
.help-examples { display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; }
@media (max-width: 600px) { .help-examples { grid-template-columns: 1fr; } }
.help-example {
background: var(--card-alt); padding: 8px 12px; border-radius: 6px; font-size: 12px;
}
.help-example code { color: var(--cyan); font-family: "SFMono-Regular", Consolas, monospace; }
.help-example span { color: var(--dim); margin-left: 6px; }
/* Stats Summary */
.stats-summary { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px; }
@media (max-width: 700px) { .stats-summary { grid-template-columns: repeat(2, 1fr); } }
.stat-card {
background: var(--card); border: 1px solid var(--border);
border-radius: 8px; padding: 12px; text-align: center;
}
.stat-card .lbl { font-size: 10px; margin-bottom: 4px; }
.stat-card .val { font-size: 22px; font-weight: 700; }
/* Results Area */
.results-container { min-height: 200px; }
.results-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.results-count { font-size: 12px; color: var(--dim); }
.results-count strong { color: var(--text); }
.results-empty { text-align: center; padding: 60px 20px; color: var(--dim); }
.results-empty-icon { font-size: 48px; margin-bottom: 16px; opacity: 0.5; }
.results-empty-text { font-size: 14px; margin-bottom: 8px; }
.results-empty-hint { font-size: 12px; }
.results-empty-hint code {
background: var(--card-alt); padding: 2px 6px; border-radius: 4px;
font-family: "SFMono-Regular", Consolas, monospace; color: var(--cyan);
}
/* Result Cards */
.result-card {
background: var(--card); border: 1px solid var(--border); border-radius: 10px;
padding: 16px; margin-bottom: 12px; transition: transform 0.2s, box-shadow 0.2s;
}
.result-card:hover { transform: translateY(-1px); box-shadow: var(--shadow-md); }
.result-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 12px; }
.result-title {
font-size: 14px; font-weight: 600; color: var(--text);
display: flex; align-items: center; gap: 8px;
}
.result-badge {
font-size: 10px; padding: 3px 8px; border-radius: 4px;
font-weight: 600; text-transform: uppercase;
}
.result-badge.cert { background: rgba(248, 81, 73, 0.2); color: var(--red); }
.result-badge.proxy { background: rgba(88, 166, 255, 0.2); color: var(--blue); }
.result-badge.org { background: rgba(163, 113, 247, 0.2); color: var(--purple); }
.result-meta { font-size: 11px; color: var(--dim); }
.result-body { display: grid; grid-template-columns: repeat(2, 1fr); gap: 12px; }
@media (max-width: 600px) { .result-body { grid-template-columns: 1fr; } }
.result-field { display: flex; flex-direction: column; gap: 2px; }
.result-field-label {
font-size: 10px; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px;
}
.result-field-value {
font-size: 13px; font-family: "SFMono-Regular", Consolas, monospace;
color: var(--text); word-break: break-all;
}
.result-field-value.highlight { color: var(--cyan); }
.result-proxies { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.result-proxies-title {
font-size: 10px; color: var(--dim); text-transform: uppercase;
letter-spacing: 0.5px; margin-bottom: 8px;
}
.proxy-tags { display: flex; flex-wrap: wrap; gap: 6px; }
.proxy-tag {
font-size: 11px; font-family: "SFMono-Regular", Consolas, monospace;
background: var(--card-alt); padding: 4px 8px; border-radius: 4px; color: var(--blue);
}
/* Loading State */
.results-loading { text-align: center; padding: 40px; color: var(--dim); }
.results-loading::after {
content: ""; display: inline-block; width: 20px; height: 20px;
border: 2px solid var(--border); border-top-color: var(--cyan); border-radius: 50%;
animation: spin 0.8s linear infinite; margin-left: 10px; vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* No Data State */
.no-data { text-align: center; padding: 80px 20px; }
.no-data-icon { font-size: 64px; margin-bottom: 20px; opacity: 0.3; }
.no-data-title { font-size: 16px; font-weight: 600; margin-bottom: 8px; }
.no-data-text { font-size: 13px; color: var(--dim); max-width: 400px; margin: 0 auto; }
/* ==========================================================================
Theme Toggle
========================================================================== */
.theme-toggle {
background: var(--card-alt);
border: 1px solid var(--border);
border-radius: 20px;
padding: 4px 10px;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: var(--dim);
transition: all var(--transition);
margin-left: 12px;
}
.theme-toggle:hover {
background: var(--border);
color: var(--text);
}
.theme-toggle-icon {
font-size: 14px;
transition: transform 0.3s ease;
}
html.muted-dark .theme-toggle-icon {
transform: rotate(90deg);
}
html.light .theme-toggle-icon {
transform: rotate(180deg);
}
/* Muted dark theme specific overrides */
html.muted-dark .chart-wrap,
html.muted-dark .histo-wrap,
html.muted-dark .stats-wrap,
html.muted-dark .lb-wrap,
html.muted-dark .tor-card,
html.muted-dark .pie-wrap {
background: linear-gradient(145deg, rgba(34,37,42,0.85), rgba(26,28,32,0.9));
border-color: var(--border);
box-shadow: var(--shadow-md);
}
html.muted-dark .chart-wrap::before,
html.muted-dark .histo-wrap::before,
html.muted-dark .stats-wrap::before,
html.muted-dark .lb-wrap::before,
html.muted-dark .tor-card::before,
html.muted-dark .pie-wrap::before {
background: linear-gradient(180deg, rgba(90,144,152,0.04) 0%, transparent 40%);
}
html.muted-dark .histo-bar {
background: linear-gradient(180deg, var(--cyan), rgba(90,144,152,0.5));
box-shadow: 0 0 8px rgba(90,144,152,0.35);
}
html.muted-dark .glass {
background: linear-gradient(145deg, rgba(34,37,42,0.85), rgba(26,28,32,0.9));
border-color: var(--border);
}
/* Light theme specific overrides */
html.light .chart-wrap,
html.light .histo-wrap,
html.light .stats-wrap,
html.light .lb-wrap,
html.light .tor-card,
html.light .pie-wrap {
background: linear-gradient(145deg, rgba(245,242,237,0.9), rgba(235,231,226,0.95));
border-color: var(--border);
box-shadow: var(--shadow-md);
}
html.light .chart-wrap::before,
html.light .histo-wrap::before,
html.light .stats-wrap::before,
html.light .lb-wrap::before,
html.light .tor-card::before,
html.light .pie-wrap::before {
background: linear-gradient(180deg, rgba(200,195,188,0.1) 0%, transparent 40%);
}
html.light .histo-bar {
background: linear-gradient(180deg, var(--cyan), rgba(26,122,127,0.6));
box-shadow: 0 0 8px rgba(26,122,127,0.3);
}
html.light .glass {
background: linear-gradient(145deg, rgba(245,242,237,0.9), rgba(235,231,226,0.95));
border-color: var(--border);
}
html.light .pct-badges {
background: linear-gradient(135deg, rgba(245,242,237,0.95), rgba(235,231,226,0.98));
}
html.light .pct-badge {
background: linear-gradient(180deg, rgba(255,255,255,0.6), rgba(245,242,237,0.8));
border-color: var(--border);
}
html.light .map-stat-val {
color: var(--cyan);
text-shadow: none;
}
html.light .map-stat-lbl {
color: var(--dim);
}
html.light .map-legend-title {
color: var(--dim);
}
html.light .map-legend-row {
color: var(--text);
}
html.light .leaflet-popup-content-wrapper {
background: rgba(245,242,237,0.98);
border-color: var(--border);
}
html.light .leaflet-popup-tip {
background: rgba(245,242,237,0.98);
border-color: var(--border);
}
html.light .popup-code {
color: var(--cyan);
text-shadow: none;
}