httpd: consolidate map page with shared css
This commit is contained in:
112
httpd.py
112
httpd.py
@@ -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">← 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 →</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)
|
||||
|
||||
Reference in New Issue
Block a user