mirror of
https://github.com/R0GGER/iperf3-map.git
synced 2026-11-11 01:20:06 +01:00
Add files via upload
This commit is contained in:
1 parent
23f71e8716
commit
db8af1f016
13 files changed
+1377
-2
No files matched your search
+17
@@ -0,0 +1,17 @@
|
|||||||
|
FROM python:3.9-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y iperf3 ca-certificates && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY requirements.txt .
|
||||||
|
RUN pip install --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
|
||||||
|
CMD ["python", "app.py"]
|
||||||
|
|
||||||
@@ -1,2 +1,37 @@
|
|||||||
# iPERF3-MAP
|
# IPERF3 MAP
|
||||||
isualizes iPerf3 servers on a global map using Leaflet.js, allowing you to initiate speed tests directly from the map markers.
|
|
||||||
|
This project provides a web-based interactive map interface for visualizing and testing public iPerf3 servers. It automatically fetches a list of [public iperf3 servers](https://github.com/R0GGER/public-iperf3-servers), determines their geographic locations, and allows you to run iPerf3 speed tests directly from your browser.
|
||||||
|
|
||||||
|
<img src="screenshots/1.png" alt="iPerf3 Server Map" width="400" /> <img src="screenshots/2.png" alt="iPerf3 Server Map" width="400" />
|
||||||
|
|
||||||
|
|
||||||
|
- **Interactive Map & Testing**: Visualizes iPerf3 servers on a global map using Leaflet.js, allowing you to initiate speed tests directly from the map markers.
|
||||||
|
- **Automatic Geocoding**: resolving server locations (City/Country) to coordinates using `geonamescache` (offline) and OpenStreetMap's Nominatim (online fallback).
|
||||||
|
- **Smart Caching**: Caches geocoded locations locally to improve performance and respect API rate limits.
|
||||||
|
- **Integrated Speed Tests**: Run `iperf3` commands directly from the web interface against any server on the map.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- Docker Compose
|
||||||
|
|
||||||
|
## Installation & Usage
|
||||||
|
|
||||||
|
1. **Clone the repository:**
|
||||||
|
```bash
|
||||||
|
git clone <repository-url>
|
||||||
|
cd iperf3-map
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Build and run the container:**
|
||||||
|
```bash
|
||||||
|
docker-compose up -d --build
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Access the application:**
|
||||||
|
Open your browser and navigate to `http://localhost:5000`.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
[MIT License](LICENSE)
|
||||||
|
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
|
import requests
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from flask import Flask, jsonify, request, send_from_directory
|
||||||
|
from geopy.geocoders import Nominatim
|
||||||
|
from geopy.exc import GeocoderTimedOut
|
||||||
|
import geonamescache
|
||||||
|
from generate_locations import generate_locations
|
||||||
|
|
||||||
|
app = Flask(__name__, static_url_path='')
|
||||||
|
|
||||||
|
LOCATION_CACHE_FILE = 'location_cache.json'
|
||||||
|
location_cache = {}
|
||||||
|
failed_lookups = set()
|
||||||
|
|
||||||
|
processed_servers = []
|
||||||
|
processing_lock = threading.Lock()
|
||||||
|
|
||||||
|
gc = geonamescache.GeonamesCache()
|
||||||
|
cities_by_name = {}
|
||||||
|
|
||||||
|
CITY_ALIASES = {
|
||||||
|
"new york": "new york city",
|
||||||
|
"frankfurt": "frankfurt am main",
|
||||||
|
"st. louis": "saint louis",
|
||||||
|
"montreal": "montréal",
|
||||||
|
"reykjavik": "reykjavík",
|
||||||
|
"sao paulo": "são paulo",
|
||||||
|
"zurich": "zürich",
|
||||||
|
"quebec": "québec"
|
||||||
|
}
|
||||||
|
|
||||||
|
COUNTRY_FIXES = {
|
||||||
|
"UK": "GB",
|
||||||
|
"USA": "US"
|
||||||
|
}
|
||||||
|
|
||||||
|
def setup_offline_geocoding():
|
||||||
|
print("Loading offline city database...", flush=True)
|
||||||
|
cities = gc.get_cities()
|
||||||
|
for city_id, city_data in cities.items():
|
||||||
|
name = city_data['name'].lower()
|
||||||
|
country = city_data['countrycode']
|
||||||
|
key = f"{name}, {country}"
|
||||||
|
cities_by_name[key] = {
|
||||||
|
"lat": float(city_data['latitude']),
|
||||||
|
"lon": float(city_data['longitude'])
|
||||||
|
}
|
||||||
|
print(f"Offline database loaded ({len(cities_by_name)} cities).", flush=True)
|
||||||
|
|
||||||
|
geolocator = Nominatim(user_agent="iperf3-map-app", timeout=10)
|
||||||
|
|
||||||
|
def load_cache():
|
||||||
|
global location_cache
|
||||||
|
if os.path.exists(LOCATION_CACHE_FILE):
|
||||||
|
try:
|
||||||
|
with open(LOCATION_CACHE_FILE, 'r') as f:
|
||||||
|
location_cache = json.load(f)
|
||||||
|
except:
|
||||||
|
location_cache = {}
|
||||||
|
|
||||||
|
if os.path.exists('prefilled_locations.json'):
|
||||||
|
try:
|
||||||
|
with open('prefilled_locations.json', 'r') as f:
|
||||||
|
static_cache = json.load(f)
|
||||||
|
for k, v in static_cache.items():
|
||||||
|
if k not in location_cache:
|
||||||
|
location_cache[k] = v
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error loading prefilled locations: {e}")
|
||||||
|
|
||||||
|
def save_cache():
|
||||||
|
with open(LOCATION_CACHE_FILE, 'w') as f:
|
||||||
|
json.dump(location_cache, f)
|
||||||
|
|
||||||
|
def get_coordinates(city, country):
|
||||||
|
|
||||||
|
if country in COUNTRY_FIXES:
|
||||||
|
country = COUNTRY_FIXES[country]
|
||||||
|
|
||||||
|
key = f"{city}, {country}"
|
||||||
|
|
||||||
|
if key in location_cache:
|
||||||
|
return location_cache[key]
|
||||||
|
|
||||||
|
if key in failed_lookups:
|
||||||
|
return None
|
||||||
|
|
||||||
|
city_lower = city.lower()
|
||||||
|
if city_lower in CITY_ALIASES:
|
||||||
|
search_city = CITY_ALIASES[city_lower]
|
||||||
|
else:
|
||||||
|
search_city = city_lower
|
||||||
|
|
||||||
|
offline_key = f"{search_city}, {country}"
|
||||||
|
if offline_key in cities_by_name:
|
||||||
|
print(f"Found offline: {key} (via {offline_key})", flush=True)
|
||||||
|
coords = cities_by_name[offline_key]
|
||||||
|
location_cache[key] = coords
|
||||||
|
save_cache()
|
||||||
|
return coords
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Geocoding online: {key}...", flush=True)
|
||||||
|
search_key = f"{search_city}, {country}"
|
||||||
|
|
||||||
|
location = geolocator.geocode(search_key)
|
||||||
|
if location:
|
||||||
|
coords = {"lat": location.latitude, "lon": location.longitude}
|
||||||
|
location_cache[key] = coords
|
||||||
|
save_cache()
|
||||||
|
time.sleep(1.1)
|
||||||
|
return coords
|
||||||
|
else:
|
||||||
|
print(f"Could not find location for: {key}", flush=True)
|
||||||
|
failed_lookups.add(key)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error geocoding {key}: {e}", flush=True)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
def update_servers_background():
|
||||||
|
global processed_servers
|
||||||
|
|
||||||
|
if not cities_by_name:
|
||||||
|
setup_offline_geocoding()
|
||||||
|
|
||||||
|
print("Starting background server update...", flush=True)
|
||||||
|
url = "https://export.iperf3serverlist.net/listed_iperf3_servers.json"
|
||||||
|
|
||||||
|
try:
|
||||||
|
print(f"Fetching {url}...", flush=True)
|
||||||
|
response = requests.get(url, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
servers = response.json()
|
||||||
|
print(f"Fetched {len(servers)} servers.", flush=True)
|
||||||
|
|
||||||
|
load_cache()
|
||||||
|
|
||||||
|
temp_processed = []
|
||||||
|
|
||||||
|
for server in servers:
|
||||||
|
city = server.get('SITE')
|
||||||
|
country = server.get('COUNTRY')
|
||||||
|
if not city or not country:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = f"{city}, {country}"
|
||||||
|
|
||||||
|
if key in location_cache:
|
||||||
|
server['lat'] = location_cache[key]['lat']
|
||||||
|
server['lon'] = location_cache[key]['lon']
|
||||||
|
temp_processed.append(server)
|
||||||
|
|
||||||
|
with processing_lock:
|
||||||
|
processed_servers = list(temp_processed)
|
||||||
|
|
||||||
|
print(f"Initial load: {len(processed_servers)} servers from cache.", flush=True)
|
||||||
|
|
||||||
|
changes_made = False
|
||||||
|
for server in servers:
|
||||||
|
city = server.get('SITE')
|
||||||
|
country = server.get('COUNTRY')
|
||||||
|
|
||||||
|
if not city or not country:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if 'lat' in server:
|
||||||
|
continue
|
||||||
|
|
||||||
|
key = f"{city}, {country}"
|
||||||
|
if key in location_cache:
|
||||||
|
server['lat'] = location_cache[key]['lat']
|
||||||
|
server['lon'] = location_cache[key]['lon']
|
||||||
|
with processing_lock:
|
||||||
|
processed_servers.append(server)
|
||||||
|
continue
|
||||||
|
|
||||||
|
coords = get_coordinates(city, country)
|
||||||
|
|
||||||
|
if coords:
|
||||||
|
server['lat'] = coords['lat']
|
||||||
|
server['lon'] = coords['lon']
|
||||||
|
|
||||||
|
with processing_lock:
|
||||||
|
processed_servers.append(server)
|
||||||
|
changes_made = True
|
||||||
|
|
||||||
|
print("Background update finished.", flush=True)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error in background update: {e}")
|
||||||
|
|
||||||
|
update_thread = threading.Thread(target=update_servers_background, daemon=True)
|
||||||
|
update_thread.start()
|
||||||
|
|
||||||
|
@app.route('/')
|
||||||
|
def index():
|
||||||
|
return send_from_directory('static', 'index.html')
|
||||||
|
|
||||||
|
@app.route('/api/servers')
|
||||||
|
def get_servers():
|
||||||
|
with processing_lock:
|
||||||
|
return jsonify(processed_servers)
|
||||||
|
|
||||||
|
@app.route('/api/update-locations', methods=['POST'])
|
||||||
|
def update_locations_endpoint():
|
||||||
|
try:
|
||||||
|
generate_locations()
|
||||||
|
|
||||||
|
load_cache()
|
||||||
|
|
||||||
|
threading.Thread(target=update_servers_background, daemon=True).start()
|
||||||
|
|
||||||
|
return jsonify({"message": "Locations updated successfully! Server list is refreshing."})
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
@app.route('/api/test', methods=['POST'])
|
||||||
|
def run_test():
|
||||||
|
data = request.json
|
||||||
|
host = data.get('host')
|
||||||
|
port = data.get('port')
|
||||||
|
flags = data.get('flags', '')
|
||||||
|
|
||||||
|
if not host:
|
||||||
|
return jsonify({"error": "Host is required"}), 400
|
||||||
|
|
||||||
|
if any(c in host for c in [';', '&', '|', ' ']):
|
||||||
|
return jsonify({"error": "Invalid host format"}), 400
|
||||||
|
|
||||||
|
cmd = ["iperf3", "-c", host, "-p", str(port) if port else "5201"]
|
||||||
|
|
||||||
|
if flags:
|
||||||
|
if any(c in flags for c in [';', '&', '|']):
|
||||||
|
return jsonify({"error": "Invalid flags"}), 400
|
||||||
|
|
||||||
|
for flag in flags.split():
|
||||||
|
cmd.append(flag)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=20)
|
||||||
|
output = result.stdout
|
||||||
|
if result.stderr:
|
||||||
|
output += "\n" + result.stderr
|
||||||
|
|
||||||
|
return jsonify({"output": output})
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
return jsonify({"error": "Test timed out"}), 504
|
||||||
|
except Exception as e:
|
||||||
|
return jsonify({"error": str(e)}), 500
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
load_cache()
|
||||||
|
app.run(host='0.0.0.0', port=5000, debug=True)
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
services:
|
||||||
|
iperf3-map:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- "5000:5000"
|
||||||
|
restart: unless-stopped
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import json
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
import geonamescache
|
||||||
|
import sys
|
||||||
|
from geopy.geocoders import Nominatim
|
||||||
|
|
||||||
|
CITY_ALIASES = {
|
||||||
|
"new york": "new york city",
|
||||||
|
"frankfurt": "frankfurt am main",
|
||||||
|
"st. louis": "saint louis",
|
||||||
|
"montreal": "montréal",
|
||||||
|
"reykjavik": "reykjavík",
|
||||||
|
"sao paulo": "são paulo",
|
||||||
|
"zurich": "zürich",
|
||||||
|
"quebec": "québec",
|
||||||
|
"washington": "washington, d.c."
|
||||||
|
}
|
||||||
|
|
||||||
|
COUNTRY_FIXES = {
|
||||||
|
"UK": "GB",
|
||||||
|
"USA": "US"
|
||||||
|
}
|
||||||
|
|
||||||
|
def generate_locations():
|
||||||
|
print("Fetching server list...")
|
||||||
|
url = "https://export.iperf3serverlist.net/listed_iperf3_servers.json"
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=30)
|
||||||
|
response.raise_for_status()
|
||||||
|
servers = response.json()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to fetch server list: {e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"Found {len(servers)} servers.")
|
||||||
|
|
||||||
|
gc = geonamescache.GeonamesCache()
|
||||||
|
cities = gc.get_cities()
|
||||||
|
cities_by_name = {}
|
||||||
|
|
||||||
|
print("Loading offline database...")
|
||||||
|
for city_id, city_data in cities.items():
|
||||||
|
name = city_data['name'].lower()
|
||||||
|
country = city_data['countrycode']
|
||||||
|
key = f"{name}, {country}"
|
||||||
|
cities_by_name[key] = {
|
||||||
|
"lat": float(city_data['latitude']),
|
||||||
|
"lon": float(city_data['longitude'])
|
||||||
|
}
|
||||||
|
|
||||||
|
geolocator = Nominatim(user_agent="iperf3-map-generator-v2", timeout=10)
|
||||||
|
|
||||||
|
unique_keys = set()
|
||||||
|
for server in servers:
|
||||||
|
city = server.get('SITE')
|
||||||
|
country = server.get('COUNTRY')
|
||||||
|
if city and country:
|
||||||
|
if country in COUNTRY_FIXES:
|
||||||
|
country = COUNTRY_FIXES[country]
|
||||||
|
unique_keys.add((city, country))
|
||||||
|
|
||||||
|
print(f"Found {len(unique_keys)} unique locations to process.")
|
||||||
|
|
||||||
|
results = {}
|
||||||
|
|
||||||
|
for city, country in unique_keys:
|
||||||
|
key = f"{city}, {country}"
|
||||||
|
|
||||||
|
city_lower = city.lower()
|
||||||
|
search_city = CITY_ALIASES.get(city_lower, city_lower)
|
||||||
|
|
||||||
|
offline_key = f"{search_city}, {country}"
|
||||||
|
if offline_key in cities_by_name:
|
||||||
|
results[key] = cities_by_name[offline_key]
|
||||||
|
continue
|
||||||
|
|
||||||
|
try:
|
||||||
|
search_query = f"{search_city}, {country}"
|
||||||
|
print(f"Geocoding online: {search_query}...")
|
||||||
|
location = geolocator.geocode(search_query)
|
||||||
|
if location:
|
||||||
|
results[key] = {
|
||||||
|
"lat": location.latitude,
|
||||||
|
"lon": location.longitude
|
||||||
|
}
|
||||||
|
print(f"Found: {location.latitude}, {location.longitude}")
|
||||||
|
time.sleep(1.1)
|
||||||
|
else:
|
||||||
|
print(f"Not found online: {search_query}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error geocoding {key}: {e}")
|
||||||
|
|
||||||
|
print(f"Resolved {len(results)} locations.")
|
||||||
|
|
||||||
|
sorted_results = dict(sorted(results.items()))
|
||||||
|
|
||||||
|
with open('prefilled_locations.json', 'w', encoding='utf-8') as f:
|
||||||
|
json.dump(sorted_results, f, indent=2)
|
||||||
|
|
||||||
|
print("Saved to prefilled_locations.json")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.stdout = open('gen_log.txt', 'w', encoding='utf-8')
|
||||||
|
sys.stderr = sys.stdout
|
||||||
|
generate_locations()
|
||||||
@@ -0,0 +1,374 @@
|
|||||||
|
{
|
||||||
|
"Accra, GH": {
|
||||||
|
"lat": 5.55602,
|
||||||
|
"lon": -0.1969
|
||||||
|
},
|
||||||
|
"Algiers, DZ": {
|
||||||
|
"lat": 36.73225,
|
||||||
|
"lon": 3.08746
|
||||||
|
},
|
||||||
|
"Amsterdam, NL": {
|
||||||
|
"lat": 52.37403,
|
||||||
|
"lon": 4.88969
|
||||||
|
},
|
||||||
|
"Arezzo, IT": {
|
||||||
|
"lat": 43.46276,
|
||||||
|
"lon": 11.88068
|
||||||
|
},
|
||||||
|
"Ashburn, US": {
|
||||||
|
"lat": 41.74753,
|
||||||
|
"lon": -87.71116
|
||||||
|
},
|
||||||
|
"Athens, GR": {
|
||||||
|
"lat": 37.98376,
|
||||||
|
"lon": 23.72784
|
||||||
|
},
|
||||||
|
"Atlanta, US": {
|
||||||
|
"lat": 33.749,
|
||||||
|
"lon": -84.38798
|
||||||
|
},
|
||||||
|
"Auckland, NZ": {
|
||||||
|
"lat": -36.84853,
|
||||||
|
"lon": 174.76349
|
||||||
|
},
|
||||||
|
"Banjul, GM": {
|
||||||
|
"lat": 13.45274,
|
||||||
|
"lon": -16.57803
|
||||||
|
},
|
||||||
|
"Bata, GQ": {
|
||||||
|
"lat": 1.86391,
|
||||||
|
"lon": 9.76582
|
||||||
|
},
|
||||||
|
"Belgrade, RS": {
|
||||||
|
"lat": 44.80401,
|
||||||
|
"lon": 20.46513
|
||||||
|
},
|
||||||
|
"Berlin, DE": {
|
||||||
|
"lat": 52.52437,
|
||||||
|
"lon": 13.41053
|
||||||
|
},
|
||||||
|
"Bissen, LU": {
|
||||||
|
"lat": 49.7891516,
|
||||||
|
"lon": 6.0662981
|
||||||
|
},
|
||||||
|
"Bogot\u00e1, CO": {
|
||||||
|
"lat": 4.60971,
|
||||||
|
"lon": -74.08175
|
||||||
|
},
|
||||||
|
"Boston, US": {
|
||||||
|
"lat": 42.35843,
|
||||||
|
"lon": -71.05977
|
||||||
|
},
|
||||||
|
"Bratislava, SK": {
|
||||||
|
"lat": 48.14816,
|
||||||
|
"lon": 17.10674
|
||||||
|
},
|
||||||
|
"Brussels, BE": {
|
||||||
|
"lat": 50.85045,
|
||||||
|
"lon": 4.34878
|
||||||
|
},
|
||||||
|
"Bucharest, RO": {
|
||||||
|
"lat": 44.43225,
|
||||||
|
"lon": 26.10626
|
||||||
|
},
|
||||||
|
"Cairo, EG": {
|
||||||
|
"lat": 30.06263,
|
||||||
|
"lon": 31.24967
|
||||||
|
},
|
||||||
|
"Cartago, CO": {
|
||||||
|
"lat": 4.74639,
|
||||||
|
"lon": -75.91167
|
||||||
|
},
|
||||||
|
"Charleston, US": {
|
||||||
|
"lat": 38.34982,
|
||||||
|
"lon": -81.63262
|
||||||
|
},
|
||||||
|
"Chicago, US": {
|
||||||
|
"lat": 41.85003,
|
||||||
|
"lon": -87.65005
|
||||||
|
},
|
||||||
|
"Christchurch, NZ": {
|
||||||
|
"lat": -43.53333,
|
||||||
|
"lon": 172.63333
|
||||||
|
},
|
||||||
|
"Col\u00f3n, PA": {
|
||||||
|
"lat": 9.36025,
|
||||||
|
"lon": -79.90019
|
||||||
|
},
|
||||||
|
"Copenhagen, DK": {
|
||||||
|
"lat": 55.67594,
|
||||||
|
"lon": 12.56553
|
||||||
|
},
|
||||||
|
"Curug, ID": {
|
||||||
|
"lat": -6.26583,
|
||||||
|
"lon": 106.55639
|
||||||
|
},
|
||||||
|
"Dallas, US": {
|
||||||
|
"lat": 44.91928,
|
||||||
|
"lon": -123.31705
|
||||||
|
},
|
||||||
|
"Dubai, AE": {
|
||||||
|
"lat": 25.07725,
|
||||||
|
"lon": 55.30927
|
||||||
|
},
|
||||||
|
"Dublin, IE": {
|
||||||
|
"lat": 53.33306,
|
||||||
|
"lon": -6.24889
|
||||||
|
},
|
||||||
|
"D\u00fcsseldorf, DE": {
|
||||||
|
"lat": 51.22172,
|
||||||
|
"lon": 6.77616
|
||||||
|
},
|
||||||
|
"Floreal, MU": {
|
||||||
|
"lat": -20.3110617,
|
||||||
|
"lon": 57.5134651
|
||||||
|
},
|
||||||
|
"Frankfurt, DE": {
|
||||||
|
"lat": 50.11552,
|
||||||
|
"lon": 8.68417
|
||||||
|
},
|
||||||
|
"Helsinki, FI": {
|
||||||
|
"lat": 60.16952,
|
||||||
|
"lon": 24.93545
|
||||||
|
},
|
||||||
|
"Hong Kong, HK": {
|
||||||
|
"lat": 22.27832,
|
||||||
|
"lon": 114.17469
|
||||||
|
},
|
||||||
|
"Houston, US": {
|
||||||
|
"lat": 29.76328,
|
||||||
|
"lon": -95.36327
|
||||||
|
},
|
||||||
|
"Hrdlo\u0159ezy, CZ": {
|
||||||
|
"lat": 50.4513933,
|
||||||
|
"lon": 14.8753972
|
||||||
|
},
|
||||||
|
"Istanbul, TR": {
|
||||||
|
"lat": 41.01384,
|
||||||
|
"lon": 28.94966
|
||||||
|
},
|
||||||
|
"Jakarta, ID": {
|
||||||
|
"lat": -6.21462,
|
||||||
|
"lon": 106.84513
|
||||||
|
},
|
||||||
|
"Jerusalem, IL": {
|
||||||
|
"lat": 31.76904,
|
||||||
|
"lon": 35.21633
|
||||||
|
},
|
||||||
|
"Johannesburg, ZA": {
|
||||||
|
"lat": -26.20227,
|
||||||
|
"lon": 28.04363
|
||||||
|
},
|
||||||
|
"Kampala, UG": {
|
||||||
|
"lat": 0.31628,
|
||||||
|
"lon": 32.58219
|
||||||
|
},
|
||||||
|
"Kista, SE": {
|
||||||
|
"lat": 59.4031236,
|
||||||
|
"lon": 17.9424221
|
||||||
|
},
|
||||||
|
"Kyiv, UA": {
|
||||||
|
"lat": 50.45466,
|
||||||
|
"lon": 30.5238
|
||||||
|
},
|
||||||
|
"Lisbon, PT": {
|
||||||
|
"lat": 38.72509,
|
||||||
|
"lon": -9.1498
|
||||||
|
},
|
||||||
|
"London, GB": {
|
||||||
|
"lat": 51.50853,
|
||||||
|
"lon": -0.12574
|
||||||
|
},
|
||||||
|
"Los Angeles, US": {
|
||||||
|
"lat": 34.05223,
|
||||||
|
"lon": -118.24368
|
||||||
|
},
|
||||||
|
"Luanda, AO": {
|
||||||
|
"lat": -8.83682,
|
||||||
|
"lon": 13.23432
|
||||||
|
},
|
||||||
|
"Madrid, ES": {
|
||||||
|
"lat": 40.4165,
|
||||||
|
"lon": -3.70256
|
||||||
|
},
|
||||||
|
"Marseille, FR": {
|
||||||
|
"lat": 43.29695,
|
||||||
|
"lon": 5.38107
|
||||||
|
},
|
||||||
|
"Melbourne, AU": {
|
||||||
|
"lat": -37.814,
|
||||||
|
"lon": 144.96332
|
||||||
|
},
|
||||||
|
"Miami, US": {
|
||||||
|
"lat": 25.77427,
|
||||||
|
"lon": -80.19366
|
||||||
|
},
|
||||||
|
"Milan, IT": {
|
||||||
|
"lat": 45.46427,
|
||||||
|
"lon": 9.18951
|
||||||
|
},
|
||||||
|
"Montreal, CA": {
|
||||||
|
"lat": 45.50884,
|
||||||
|
"lon": -73.58781
|
||||||
|
},
|
||||||
|
"Montr\u00e9al, CA": {
|
||||||
|
"lat": 45.50884,
|
||||||
|
"lon": -73.58781
|
||||||
|
},
|
||||||
|
"New York City, US": {
|
||||||
|
"lat": 40.71427,
|
||||||
|
"lon": -74.00597
|
||||||
|
},
|
||||||
|
"New York, US": {
|
||||||
|
"lat": 40.71427,
|
||||||
|
"lon": -74.00597
|
||||||
|
},
|
||||||
|
"Norderstedt, DE": {
|
||||||
|
"lat": 53.70177,
|
||||||
|
"lon": 9.99328
|
||||||
|
},
|
||||||
|
"Panam\u00e1, PA": {
|
||||||
|
"lat": 8.9936,
|
||||||
|
"lon": -79.51973
|
||||||
|
},
|
||||||
|
"Paris, FR": {
|
||||||
|
"lat": 48.85341,
|
||||||
|
"lon": 2.3488
|
||||||
|
},
|
||||||
|
"Phoenix, US": {
|
||||||
|
"lat": 33.44838,
|
||||||
|
"lon": -112.07404
|
||||||
|
},
|
||||||
|
"Ponte San Pietro, IT": {
|
||||||
|
"lat": 45.6977541,
|
||||||
|
"lon": 9.5872254
|
||||||
|
},
|
||||||
|
"Port Louis, MU": {
|
||||||
|
"lat": -20.16194,
|
||||||
|
"lon": 57.49889
|
||||||
|
},
|
||||||
|
"Port Moresby, PG": {
|
||||||
|
"lat": -9.47723,
|
||||||
|
"lon": 147.15089
|
||||||
|
},
|
||||||
|
"Portmore, JM": {
|
||||||
|
"lat": 17.97102,
|
||||||
|
"lon": -76.88691
|
||||||
|
},
|
||||||
|
"Porto, PT": {
|
||||||
|
"lat": 41.14961,
|
||||||
|
"lon": -8.61099
|
||||||
|
},
|
||||||
|
"Prague, CZ": {
|
||||||
|
"lat": 50.08804,
|
||||||
|
"lon": 14.42076
|
||||||
|
},
|
||||||
|
"Quer\u00e9taro, MX": {
|
||||||
|
"lat": 20.8052225,
|
||||||
|
"lon": -99.8837376
|
||||||
|
},
|
||||||
|
"Reykjavik, IS": {
|
||||||
|
"lat": 64.13548,
|
||||||
|
"lon": -21.89541
|
||||||
|
},
|
||||||
|
"San Francisco, US": {
|
||||||
|
"lat": 37.77493,
|
||||||
|
"lon": -122.41942
|
||||||
|
},
|
||||||
|
"San Jose, CR": {
|
||||||
|
"lat": 9.9327707,
|
||||||
|
"lon": -84.0796144
|
||||||
|
},
|
||||||
|
"Sandefjord, NO": {
|
||||||
|
"lat": 59.13118,
|
||||||
|
"lon": 10.21665
|
||||||
|
},
|
||||||
|
"Santa Ana, EC": {
|
||||||
|
"lat": -1.1944701,
|
||||||
|
"lon": -80.2631268
|
||||||
|
},
|
||||||
|
"Santiago, CL": {
|
||||||
|
"lat": -33.45694,
|
||||||
|
"lon": -70.64827
|
||||||
|
},
|
||||||
|
"Schaffhausen, CH": {
|
||||||
|
"lat": 47.69732,
|
||||||
|
"lon": 8.63493
|
||||||
|
},
|
||||||
|
"Seattle, US": {
|
||||||
|
"lat": 47.60621,
|
||||||
|
"lon": -122.33207
|
||||||
|
},
|
||||||
|
"Singapore, SG": {
|
||||||
|
"lat": 1.28967,
|
||||||
|
"lon": 103.85007
|
||||||
|
},
|
||||||
|
"Sofia, BG": {
|
||||||
|
"lat": 42.69751,
|
||||||
|
"lon": 23.32415
|
||||||
|
},
|
||||||
|
"Stockholm, SE": {
|
||||||
|
"lat": 59.32938,
|
||||||
|
"lon": 18.06871
|
||||||
|
},
|
||||||
|
"Sydney, AU": {
|
||||||
|
"lat": -33.86785,
|
||||||
|
"lon": 151.20732
|
||||||
|
},
|
||||||
|
"S\u00e3o Paulo, BR": {
|
||||||
|
"lat": -23.5475,
|
||||||
|
"lon": -46.63611
|
||||||
|
},
|
||||||
|
"Tashkent, UZ": {
|
||||||
|
"lat": 41.26465,
|
||||||
|
"lon": 69.21627
|
||||||
|
},
|
||||||
|
"Tel Aviv, IL": {
|
||||||
|
"lat": 32.08088,
|
||||||
|
"lon": 34.78057
|
||||||
|
},
|
||||||
|
"Tokyo, JP": {
|
||||||
|
"lat": 35.6895,
|
||||||
|
"lon": 139.69171
|
||||||
|
},
|
||||||
|
"Toronto, CA": {
|
||||||
|
"lat": 43.70643,
|
||||||
|
"lon": -79.39864
|
||||||
|
},
|
||||||
|
"Tunis, TN": {
|
||||||
|
"lat": 36.81897,
|
||||||
|
"lon": 10.16579
|
||||||
|
},
|
||||||
|
"Vienna, AT": {
|
||||||
|
"lat": 48.20849,
|
||||||
|
"lon": 16.37208
|
||||||
|
},
|
||||||
|
"Vitry-sur-Seine, FR": {
|
||||||
|
"lat": 48.78716,
|
||||||
|
"lon": 2.40332
|
||||||
|
},
|
||||||
|
"Warsaw, PL": {
|
||||||
|
"lat": 52.22977,
|
||||||
|
"lon": 21.01178
|
||||||
|
},
|
||||||
|
"Washington, US": {
|
||||||
|
"lat": 38.8950368,
|
||||||
|
"lon": -77.0365427
|
||||||
|
},
|
||||||
|
"Winterthur, CH": {
|
||||||
|
"lat": 47.50564,
|
||||||
|
"lon": 8.72413
|
||||||
|
},
|
||||||
|
"Wolfsburg, DE": {
|
||||||
|
"lat": 52.42452,
|
||||||
|
"lon": 10.7815
|
||||||
|
},
|
||||||
|
"Zagreb, HR": {
|
||||||
|
"lat": 45.81444,
|
||||||
|
"lon": 15.97798
|
||||||
|
},
|
||||||
|
"Z\u00fcrich, CH": {
|
||||||
|
"lat": 47.36667,
|
||||||
|
"lon": 8.55
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
flask
|
||||||
|
requests
|
||||||
|
geopy
|
||||||
|
geonamescache
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 793 KiB |
@@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>IPERF3 MAP - iperf3serverlist.net</title>
|
||||||
|
<link rel="icon" type="image/png" href="iperf3serverlist_256.png">
|
||||||
|
<link rel="apple-touch-icon" href="iperf3serverlist_256.png">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||||
|
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||||
|
crossorigin=""/>
|
||||||
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
||||||
|
<link rel="stylesheet" href="style.css?v=2">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
|
||||||
|
<div id="map-logo">
|
||||||
|
<img src="iperf3serverlist_256.png" alt="iPerf3 Server List Logo">
|
||||||
|
<div class="logo-text">
|
||||||
|
<div class="logo-title">iPERF3 MAP</div>
|
||||||
|
<div class="logo-subtitle">by iPERF3 SERVERLIST</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="map"></div>
|
||||||
|
|
||||||
|
<div class="bottom-controls">
|
||||||
|
<button id="update-locations-btn" class="btn-floating" title="Update Locations">
|
||||||
|
<i class="fas fa-sync"></i>
|
||||||
|
</button>
|
||||||
|
<div class="footer-links">
|
||||||
|
<a href="https://github.com/R0GGER/public-iperf3-servers" target="_blank" class="footer-link">
|
||||||
|
<i class="fab fa-github"></i> GitHub
|
||||||
|
</a>
|
||||||
|
<a href="https://iperf3serverlist.net/" target="_blank" class="footer-link">
|
||||||
|
<i class="fas fa-globe"></i> iperf3serverlist.net
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="loading-status" style="position: absolute; top: 10px; right: 10px; z-index: 1000; background: white; padding: 10px; border-radius: 5px; box-shadow: 0 0 5px rgba(0,0,0,0.3); display: none;">
|
||||||
|
Loading servers... <span id="server-count">0</span> found
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div id="testModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<span class="close">×</span>
|
||||||
|
<h2 id="modal-title">Run Speedtest</h2>
|
||||||
|
<div id="modal-body">
|
||||||
|
<!-- Content injected by JS -->
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||||
|
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||||
|
crossorigin=""></script>
|
||||||
|
<script src="script.js?v=2"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1,289 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const map = L.map('map', { zoomControl: false }).setView([52.13, 5.29], 5);
|
||||||
|
L.control.zoom({ position: 'topright' }).addTo(map);
|
||||||
|
|
||||||
|
const openStreetMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
|
});
|
||||||
|
|
||||||
|
openStreetMap.addTo(map);
|
||||||
|
|
||||||
|
const modal = document.getElementById('testModal');
|
||||||
|
const span = document.getElementsByClassName("close")[0];
|
||||||
|
const modalTitle = document.getElementById('modal-title');
|
||||||
|
const modalBody = document.getElementById('modal-body');
|
||||||
|
|
||||||
|
span.onclick = function() {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
window.onclick = function(event) {
|
||||||
|
if (event.target == modal) {
|
||||||
|
modal.style.display = "none";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const serversByLocation = {};
|
||||||
|
let markersLayer = L.layerGroup().addTo(map);
|
||||||
|
const loadingStatus = document.getElementById('loading-status');
|
||||||
|
const serverCountSpan = document.getElementById('server-count');
|
||||||
|
|
||||||
|
if ("geolocation" in navigator) {
|
||||||
|
navigator.geolocation.getCurrentPosition(position => {
|
||||||
|
const lat = position.coords.latitude;
|
||||||
|
const lon = position.coords.longitude;
|
||||||
|
|
||||||
|
const userIcon = L.divIcon({
|
||||||
|
className: 'custom-div-icon',
|
||||||
|
html: "<div class='user-marker'>📍</div>",
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 30]
|
||||||
|
});
|
||||||
|
|
||||||
|
L.marker([lat, lon], { icon: userIcon })
|
||||||
|
.addTo(map)
|
||||||
|
.openPopup();
|
||||||
|
|
||||||
|
}, error => {
|
||||||
|
console.log("Geolocation access denied or failed.", error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadServers() {
|
||||||
|
fetch('/api/servers')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(servers => {
|
||||||
|
if (servers.length > 0) {
|
||||||
|
loadingStatus.style.display = 'block';
|
||||||
|
serverCountSpan.textContent = servers.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (servers.length === 0) {
|
||||||
|
console.log("No servers yet, retrying...");
|
||||||
|
setTimeout(loadServers, 3000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
markersLayer.clearLayers();
|
||||||
|
|
||||||
|
for (const key in serversByLocation) delete serversByLocation[key];
|
||||||
|
|
||||||
|
servers.forEach(server => {
|
||||||
|
if (server.lat && server.lon) {
|
||||||
|
const key = `${server.lat},${server.lon}`;
|
||||||
|
if (!serversByLocation[key]) {
|
||||||
|
serversByLocation[key] = {
|
||||||
|
lat: server.lat,
|
||||||
|
lon: server.lon,
|
||||||
|
city: server.SITE,
|
||||||
|
country: server.COUNTRY,
|
||||||
|
servers: []
|
||||||
|
};
|
||||||
|
}
|
||||||
|
serversByLocation[key].servers.push(server);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(serversByLocation).forEach(loc => {
|
||||||
|
const starIcon = L.divIcon({
|
||||||
|
className: 'custom-div-icon',
|
||||||
|
html: "<div class='star-marker'>★</div>",
|
||||||
|
iconSize: [30, 30],
|
||||||
|
iconAnchor: [15, 15]
|
||||||
|
});
|
||||||
|
|
||||||
|
const marker = L.marker([loc.lat, loc.lon], { icon: starIcon });
|
||||||
|
|
||||||
|
const container = document.createElement('div');
|
||||||
|
const title = document.createElement('h3');
|
||||||
|
title.textContent = `${loc.city}, ${loc.country}`;
|
||||||
|
container.appendChild(title);
|
||||||
|
|
||||||
|
const list = document.createElement('div');
|
||||||
|
loc.servers.forEach(server => {
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'server-list-item';
|
||||||
|
|
||||||
|
const info = document.createElement('span');
|
||||||
|
info.textContent = `${server.PROVIDER} (${server['IP/HOST']})`;
|
||||||
|
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.className = 'btn';
|
||||||
|
btn.textContent = 'Select';
|
||||||
|
btn.onclick = () => openTestModal(server);
|
||||||
|
|
||||||
|
item.appendChild(info);
|
||||||
|
item.appendChild(btn);
|
||||||
|
list.appendChild(item);
|
||||||
|
});
|
||||||
|
container.appendChild(list);
|
||||||
|
|
||||||
|
marker.bindPopup(container);
|
||||||
|
markersLayer.addLayer(marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (servers.length < 100) {
|
||||||
|
setTimeout(loadServers, 5000);
|
||||||
|
} else {
|
||||||
|
loadingStatus.textContent = `Loaded ${servers.length} servers.`;
|
||||||
|
setTimeout(() => { loadingStatus.style.display = 'none'; }, 3000);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.error('Error loading servers:', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
loadServers();
|
||||||
|
|
||||||
|
const updateBtn = document.getElementById('update-locations-btn');
|
||||||
|
if (updateBtn) {
|
||||||
|
updateBtn.onclick = () => {
|
||||||
|
if (!confirm("Update locations? This will fetch the latest server list and regenerate the location database. It may take a while.")) return;
|
||||||
|
|
||||||
|
const icon = updateBtn.querySelector('i');
|
||||||
|
icon.classList.add('fa-spin');
|
||||||
|
updateBtn.disabled = true;
|
||||||
|
|
||||||
|
fetch('/api/update-locations', { method: 'POST' })
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.error) {
|
||||||
|
alert("Error: " + data.error);
|
||||||
|
} else {
|
||||||
|
alert(data.message);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => alert("Error: " + err))
|
||||||
|
.finally(() => {
|
||||||
|
icon.classList.remove('fa-spin');
|
||||||
|
updateBtn.disabled = false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function openTestModal(server) {
|
||||||
|
modal.style.display = "block";
|
||||||
|
modalTitle.textContent = `Test to ${server.SITE} (${server['IP/HOST']})`;
|
||||||
|
|
||||||
|
let ports = [];
|
||||||
|
if (server.PORT) {
|
||||||
|
if (server.PORT.includes('-')) {
|
||||||
|
const parts = server.PORT.split('-');
|
||||||
|
const start = parseInt(parts[0]);
|
||||||
|
const end = parseInt(parts[1]);
|
||||||
|
for (let p = start; p <= end; p++) {
|
||||||
|
ports.push(p);
|
||||||
|
if (ports.length > 20) break;
|
||||||
|
}
|
||||||
|
} else if (server.PORT.includes(',')) {
|
||||||
|
ports = server.PORT.split(',').map(p => parseInt(p.trim()));
|
||||||
|
} else {
|
||||||
|
ports = [parseInt(server.PORT)];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ports = [5201];
|
||||||
|
}
|
||||||
|
|
||||||
|
let portOptions = ports.map(p => `<option value="${p}">${p}</option>`).join('');
|
||||||
|
|
||||||
|
modalBody.innerHTML = `
|
||||||
|
<div style="margin-bottom: 15px;">
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<strong>Provider:</strong> ${server.PROVIDER}
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom: 5px;">
|
||||||
|
<strong>Host:</strong> ${server['IP/HOST']}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px; margin-top: 10px;">
|
||||||
|
<label for="port-select"><strong>Port:</strong></label>
|
||||||
|
<select id="port-select" class="form-control" style="padding: 5px;">
|
||||||
|
${portOptions}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom: 10px;">
|
||||||
|
<label><strong>Options:</strong></label><br>
|
||||||
|
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<input type="checkbox" id="opt-reverse" checked>
|
||||||
|
<label for="opt-reverse">Reverse Mode (-R) <span style="color:#666; font-size:0.9em;">(Download test)</span></label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<input type="checkbox" id="opt-ipv6">
|
||||||
|
<label for="opt-ipv6">IPv6 (-6)</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 5px;">
|
||||||
|
<label for="opt-custom">Custom flags:</label>
|
||||||
|
<input type="text" id="opt-custom" placeholder="e.g. -u -b 10M" style="padding: 4px; width: 60%;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 5px; font-size: 0.85em; color: #666;">
|
||||||
|
Server suggested options: ${server.OPTIONS || 'None'}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<button id="start-test-btn" class="btn" style="font-size: 1.1em; padding: 8px 16px;">Run iperf3 Test</button>
|
||||||
|
<div id="test-status" style="margin-top: 15px;"></div>
|
||||||
|
<div id="test-output" style="display:none;"></div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const startBtn = document.getElementById('start-test-btn');
|
||||||
|
const statusDiv = document.getElementById('test-status');
|
||||||
|
const outputDiv = document.getElementById('test-output');
|
||||||
|
const portSelect = document.getElementById('port-select');
|
||||||
|
const optReverse = document.getElementById('opt-reverse');
|
||||||
|
const optIPv6 = document.getElementById('opt-ipv6');
|
||||||
|
const optCustom = document.getElementById('opt-custom');
|
||||||
|
|
||||||
|
startBtn.onclick = () => {
|
||||||
|
startBtn.disabled = true;
|
||||||
|
statusDiv.innerHTML = '<span class="loader"></span> Running test... (this may take 10-20 seconds)';
|
||||||
|
outputDiv.style.display = 'none';
|
||||||
|
outputDiv.textContent = '';
|
||||||
|
|
||||||
|
const selectedPort = parseInt(portSelect.value);
|
||||||
|
let flags = "";
|
||||||
|
|
||||||
|
if (optReverse.checked) flags += " -R";
|
||||||
|
if (optIPv6.checked) flags += " -6";
|
||||||
|
if (optCustom.value) flags += " " + optCustom.value;
|
||||||
|
|
||||||
|
fetch('/api/test', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
host: server['IP/HOST'],
|
||||||
|
port: selectedPort,
|
||||||
|
flags: flags.trim()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
startBtn.disabled = false;
|
||||||
|
statusDiv.innerHTML = '';
|
||||||
|
outputDiv.style.display = 'block';
|
||||||
|
|
||||||
|
if (data.error) {
|
||||||
|
outputDiv.textContent = "Error: " + data.error;
|
||||||
|
outputDiv.style.color = 'red';
|
||||||
|
} else {
|
||||||
|
outputDiv.textContent = data.output;
|
||||||
|
outputDiv.style.color = '#00ff00';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
startBtn.disabled = false;
|
||||||
|
statusDiv.innerHTML = '';
|
||||||
|
outputDiv.style.display = 'block';
|
||||||
|
outputDiv.textContent = "Network/Server Error: " + err;
|
||||||
|
outputDiv.style.color = 'red';
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,224 @@
|
|||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
height: 100vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map-logo {
|
||||||
|
position: absolute;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#map-logo img {
|
||||||
|
width: 40px;
|
||||||
|
height: auto;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-title {
|
||||||
|
font-weight: 900;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #333;
|
||||||
|
line-height: 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo-subtitle {
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.star-marker {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #FFD700; /* Gold */
|
||||||
|
text-shadow: 0 0 3px #000;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-marker {
|
||||||
|
font-size: 30px;
|
||||||
|
color: #ff0000; /* Red for user */
|
||||||
|
text-shadow: 0 0 3px #fff;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 30px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal styles */
|
||||||
|
.modal {
|
||||||
|
display: none;
|
||||||
|
position: fixed;
|
||||||
|
z-index: 2000;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: rgba(0,0,0,0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background-color: #fefefe;
|
||||||
|
margin: 10% auto;
|
||||||
|
padding: 20px;
|
||||||
|
border: 1px solid #888;
|
||||||
|
width: 80%;
|
||||||
|
max-width: 800px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close {
|
||||||
|
color: #aaa;
|
||||||
|
float: right;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close:hover,
|
||||||
|
.close:focus {
|
||||||
|
color: black;
|
||||||
|
text-decoration: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-item {
|
||||||
|
padding: 10px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.server-list-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
#test-output {
|
||||||
|
background-color: #1e1e1e;
|
||||||
|
color: #00ff00;
|
||||||
|
padding: 15px;
|
||||||
|
font-family: monospace;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
margin-top: 20px;
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loader {
|
||||||
|
border: 4px solid #f3f3f3;
|
||||||
|
border-top: 4px solid #3498db;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
animation: spin 2s linear infinite;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Bottom Controls */
|
||||||
|
.bottom-controls {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 1000;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Floating Action Button */
|
||||||
|
.btn-floating {
|
||||||
|
/* Removed fixed positioning as it's now handled by .bottom-controls */
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #007bff;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-floating:hover {
|
||||||
|
background-color: #0056b3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-floating:disabled {
|
||||||
|
background-color: #ccc;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-links {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 10px 15px;
|
||||||
|
border-radius: 25px;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #333;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 14px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-link:hover {
|
||||||
|
color: #007bff;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user