From 9738cc9664a58c311c66674b38575d0f7a1a374b Mon Sep 17 00:00:00 2001 From: Username Date: Wed, 24 Dec 2025 01:32:25 +0100 Subject: [PATCH] httpd: consolidate map page with shared css --- httpd.py | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 2 deletions(-) diff --git a/httpd.py b/httpd.py index 18d90ba..89c1de7 100644 --- a/httpd.py +++ b/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 = ''' + + + + PPF Proxy Map + + + + +
+ +

Proxy Distribution by Country

+
Loading...
+
+
+
1000+
+
500-999
+
100-499
+
10-99
+
1-9
+
+
PPF Python Proxy Finder
+
+ + + +''' + DASHBOARD_HTML = ''' @@ -716,7 +791,7 @@ DASHBOARD_HTML = '''
-

PPF Dashboard

+

PPF Dashboard Map →

- @@ -960,7 +1035,7 @@ DASHBOARD_HTML = '''
-
PPF Proxy Fetcher
+
PPF Python Proxy Finder
@@ -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)