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...
+
+
+
PPF Python Proxy Finder
+
+
+
+
+'''
+
DASHBOARD_HTML = '''
@@ -716,7 +791,7 @@ DASHBOARD_HTML = '''
-
PPF Dashboard
+
PPF Dashboard Map →
-
PROFILING
@@ -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)