httpd: consolidate map page with shared css
All checks were successful
CI / syntax-check (push) Successful in 6s
CI / memory-leak-check (push) Successful in 14s

This commit is contained in:
Username
2025-12-24 01:32:25 +01:00
parent 38fb16a439
commit 9738cc9664

112
httpd.py
View File

@@ -219,6 +219,11 @@ body {
font-size: 13px; background: var(--bg); color: var(--text);
padding: 16px; min-height: 100vh; line-height: 1.4;
}
a { color: var(--blue); text-decoration: none; }
a:hover { text-decoration: underline; }
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 */
@@ -360,6 +365,28 @@ body {
.pct-label { font-size: 10px; color: var(--dim); text-transform: uppercase; }
.pct-value { font-size: 16px; font-weight: 700; margin-top: 2px; }
/* 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); }
'''
@@ -705,6 +732,54 @@ fetchStats();
setInterval(fetchStats, 3000);
'''
MAP_HTML = '''<!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">
</head>
<body>
<div class="container">
<div class="nav"><a href="/dashboard">&larr; Dashboard</a></div>
<h1>Proxy Distribution by Country</h1>
<div class="map-stats" id="stats">Loading...</div>
<div class="country-grid" id="grid"></div>
<div class="map-legend">
<div class="legend-item"><div class="legend-dot" style="background:#0d4429"></div> 1000+</div>
<div class="legend-item"><div class="legend-dot" style="background:#1a3d2e"></div> 500-999</div>
<div class="legend-item"><div class="legend-dot" style="background:#1f3d2a"></div> 100-499</div>
<div class="legend-item"><div class="legend-dot" style="background:#2a4a35"></div> 10-99</div>
<div class="legend-item"><div class="legend-dot" style="background:#35573f"></div> 1-9</div>
</div>
<div class="ftr">PPF Python Proxy Finder</div>
</div>
<script>
fetch('/api/countries')
.then(r => r.json())
.then(data => {
var countries = data.countries || {};
var entries = Object.entries(countries).sort((a,b) => b[1] - a[1]);
var total = entries.reduce((s, e) => s + e[1], 0);
document.getElementById('stats').textContent = entries.length + ' countries, ' + total.toLocaleString() + ' working proxies';
var html = '';
entries.forEach(function(e) {
var code = e[0], count = e[1];
var tier = count >= 1000 ? 't1' : count >= 500 ? 't2' : count >= 100 ? 't3' : count >= 10 ? 't4' : 't5';
html += '<div class="country ' + tier + '"><div class="code">' + code + '</div><div class="count">' + count.toLocaleString() + '</div></div>';
});
document.getElementById('grid').innerHTML = html;
})
.catch(function(e) {
document.getElementById('stats').textContent = 'Error loading data';
document.getElementById('stats').style.color = 'var(--red)';
});
</script>
</body>
</html>
'''
DASHBOARD_HTML = '''<!DOCTYPE html>
<html lang="en">
<head>
@@ -716,7 +791,7 @@ DASHBOARD_HTML = '''<!DOCTYPE html>
<body>
<div class="container">
<div class="hdr">
<h1>PPF Dashboard</h1>
<h1>PPF Dashboard <a href="/map" style="font-size:12px;font-weight:normal;color:var(--dim);margin-left:12px">Map &rarr;</a></h1>
<div class="status">
<span class="mode-badge mode-ssl" id="checktypeBadge">-</span>
<span class="mode-badge mode-profile" id="profileBadge" style="display:none">PROFILING</span>
@@ -960,7 +1035,7 @@ DASHBOARD_HTML = '''<!DOCTYPE html>
</div>
</div>
<div class="ftr">PPF Proxy Fetcher</div>
<div class="ftr">PPF Python Proxy Finder</div>
</div>
<script src="/static/dashboard.js"></script>
</body>
@@ -1006,9 +1081,11 @@ class ProxyAPIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
routes = {
'/': self.handle_index,
'/dashboard': self.handle_dashboard,
'/map': self.handle_map,
'/static/style.css': self.handle_css,
'/static/dashboard.js': self.handle_js,
'/api/stats': self.handle_stats,
'/api/countries': self.handle_countries,
'/proxies': self.handle_proxies,
'/proxies/count': self.handle_count,
'/health': self.handle_health,
@@ -1033,6 +1110,22 @@ class ProxyAPIHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def handle_dashboard(self):
self.send_html(DASHBOARD_HTML)
def handle_map(self):
self.send_html(MAP_HTML)
def handle_countries(self):
"""Return all countries with proxy counts."""
try:
db = mysqlite.mysqlite(self.database, str)
rows = db.execute(
'SELECT country, COUNT(*) as c FROM proxylist WHERE failed=0 AND country IS NOT NULL '
'GROUP BY country ORDER BY c DESC'
).fetchall()
countries = {r[0]: r[1] for r in rows}
self.send_json({'countries': countries})
except Exception as e:
self.send_json({'error': str(e)}, 500)
def handle_css(self):
self.send_css(DASHBOARD_CSS)
@@ -1208,7 +1301,9 @@ class ProxyAPIServer(threading.Thread):
body = json.dumps({
'endpoints': {
'/dashboard': 'web dashboard (HTML)',
'/map': 'proxy distribution by country (HTML)',
'/api/stats': 'runtime statistics (JSON)',
'/api/countries': 'proxy counts by country (JSON)',
'/proxies': 'list working proxies (params: limit, proto, country, asn)',
'/proxies/count': 'count working proxies',
'/health': 'health check',
@@ -1217,6 +1312,8 @@ class ProxyAPIServer(threading.Thread):
return body, 'application/json', 200
elif path == '/dashboard':
return DASHBOARD_HTML, 'text/html; charset=utf-8', 200
elif path == '/map':
return MAP_HTML, 'text/html; charset=utf-8', 200
elif path == '/static/style.css':
# Use str.format() instead of % to avoid issues with % escaping
css = DASHBOARD_CSS
@@ -1239,6 +1336,17 @@ class ProxyAPIServer(threading.Thread):
except Exception:
pass
return json.dumps(stats, indent=2), 'application/json', 200
elif path == '/api/countries':
try:
db = mysqlite.mysqlite(self.database, str)
rows = db.execute(
'SELECT country, COUNT(*) as c FROM proxylist WHERE failed=0 AND country IS NOT NULL '
'GROUP BY country ORDER BY c DESC'
).fetchall()
countries = {r[0]: r[1] for r in rows}
return json.dumps({'countries': countries}, indent=2), 'application/json', 200
except Exception as e:
return json.dumps({'error': str(e)}), 'application/json', 500
elif path == '/proxies':
try:
db = mysqlite.mysqlite(self.database, str)