Replace OpenSpeedTest with custom speed test widget
Download uses fetch() with streaming progress on 100MB test file. Upload sends 25MB to /api/upload on mtr-backend which reads the full body before responding, giving accurate client-side timing. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
6bac6aa84c
commit
222d748f4e
@@ -54,14 +54,24 @@
|
|||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Speed test embed */
|
/* Speed test */
|
||||||
#speedtest-frame {
|
.speed-grid {
|
||||||
width: 100%;
|
display: grid;
|
||||||
height: 520px;
|
grid-template-columns: 1fr 1fr;
|
||||||
border: none;
|
gap: 16px;
|
||||||
border-radius: var(--radius);
|
margin-bottom: 20px;
|
||||||
background: #000;
|
|
||||||
}
|
}
|
||||||
|
.speed-card {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 24px 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.speed-card-label { font-size: 0.85rem; color: var(--text-dim); margin-bottom: 10px; }
|
||||||
|
.speed-card-value { font-size: 2.8rem; font-weight: 700; color: #fff; line-height: 1; font-variant-numeric: tabular-nums; }
|
||||||
|
.speed-card-unit { font-size: 0.8rem; color: var(--text-dim); margin-top: 4px; }
|
||||||
|
.speed-card-note { font-size: 0.75rem; color: var(--text-dim); margin-top: 8px; min-height: 1em; }
|
||||||
|
|
||||||
/* MTR form */
|
/* MTR form */
|
||||||
.mtr-form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
|
.mtr-form { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
|
||||||
@@ -195,15 +205,26 @@
|
|||||||
<!-- ── Speed Test ──────────────────────────────────────────────────── -->
|
<!-- ── Speed Test ──────────────────────────────────────────────────── -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<div class="section-title">Speed Test</div>
|
<div class="section-title">Speed Test</div>
|
||||||
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:12px;">
|
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:20px;">
|
||||||
Runs directly between your browser and this server. Served locally — no external connections.
|
Runs directly between your browser and this server — no external connections.
|
||||||
|
Download: 100 MB test file. Upload: 25 MB payload.
|
||||||
</p>
|
</p>
|
||||||
<iframe id="speedtest-frame"
|
<div class="speed-grid">
|
||||||
src="__DIAG_PATH__speedtest/"
|
<div class="speed-card">
|
||||||
allow="fullscreen"
|
<div class="speed-card-label">↓ Download</div>
|
||||||
loading="lazy"
|
<div class="speed-card-value" id="dl-speed">—</div>
|
||||||
title="OpenSpeedTest">
|
<div class="speed-card-unit">Mbps</div>
|
||||||
</iframe>
|
<div class="speed-card-note" id="dl-note"></div>
|
||||||
|
</div>
|
||||||
|
<div class="speed-card">
|
||||||
|
<div class="speed-card-label">↑ Upload</div>
|
||||||
|
<div class="speed-card-value" id="ul-speed">—</div>
|
||||||
|
<div class="speed-card-unit">Mbps</div>
|
||||||
|
<div class="speed-card-note" id="ul-note"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button class="btn" id="speed-btn" onclick="runSpeedTest()">Start Speed Test</button>
|
||||||
|
<div id="speed-status" style="margin-top:14px;font-size:0.85rem;min-height:1.4em;"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ── MTR Test ───────────────────────────────────────────────────── -->
|
<!-- ── MTR Test ───────────────────────────────────────────────────── -->
|
||||||
@@ -359,6 +380,69 @@ async function runMtr() {
|
|||||||
function escHtml(s) {
|
function escHtml(s) {
|
||||||
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
return s.replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Speed Test ──────────────────────────────────────────────────────────────
|
||||||
|
async function runSpeedTest() {
|
||||||
|
const btn = document.getElementById('speed-btn');
|
||||||
|
const status = document.getElementById('speed-status');
|
||||||
|
btn.disabled = true;
|
||||||
|
|
||||||
|
['dl-speed','ul-speed'].forEach(id => { document.getElementById(id).textContent = '—'; });
|
||||||
|
['dl-note','ul-note'].forEach(id => { document.getElementById(id).textContent = ''; });
|
||||||
|
|
||||||
|
// Download
|
||||||
|
try {
|
||||||
|
status.innerHTML = '<span class="status-tag tag-info">Testing download…</span>';
|
||||||
|
const mbps = await measureDownload();
|
||||||
|
document.getElementById('dl-speed').textContent = mbps.toFixed(1);
|
||||||
|
document.getElementById('dl-note').textContent = '100 MB file';
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('dl-speed').textContent = 'ERR';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Upload
|
||||||
|
try {
|
||||||
|
status.innerHTML = '<span class="status-tag tag-info">Testing upload…</span>';
|
||||||
|
const mbps = await measureUpload();
|
||||||
|
document.getElementById('ul-speed').textContent = mbps.toFixed(1);
|
||||||
|
document.getElementById('ul-note').textContent = '25 MB payload';
|
||||||
|
} catch(e) {
|
||||||
|
document.getElementById('ul-speed').textContent = 'ERR';
|
||||||
|
}
|
||||||
|
|
||||||
|
status.innerHTML = '<span class="status-tag tag-ok">Done</span>';
|
||||||
|
btn.disabled = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureDownload() {
|
||||||
|
const url = '__DIAG_PATH__testfiles/test-100m.bin?t=' + Date.now();
|
||||||
|
const start = performance.now();
|
||||||
|
const resp = await fetch(url, { cache: 'no-store' });
|
||||||
|
const reader = resp.body.getReader();
|
||||||
|
let bytes = 0;
|
||||||
|
while (true) {
|
||||||
|
const { done, value } = await reader.read();
|
||||||
|
if (done) break;
|
||||||
|
bytes += value.length;
|
||||||
|
const secs = (performance.now() - start) / 1000;
|
||||||
|
if (secs > 0.3)
|
||||||
|
document.getElementById('dl-speed').textContent = ((bytes * 8) / secs / 1e6).toFixed(1);
|
||||||
|
}
|
||||||
|
return (bytes * 8) / ((performance.now() - start) / 1000) / 1e6;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function measureUpload() {
|
||||||
|
const size = 25 * 1024 * 1024;
|
||||||
|
const data = new Uint8Array(size);
|
||||||
|
const start = performance.now();
|
||||||
|
const resp = await fetch('__DIAG_PATH__api/upload', {
|
||||||
|
method: 'POST',
|
||||||
|
body: data,
|
||||||
|
headers: { 'Content-Type': 'application/octet-stream' }
|
||||||
|
});
|
||||||
|
await resp.json();
|
||||||
|
return (size * 8) / ((performance.now() - start) / 1000) / 1e6;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -187,6 +187,22 @@ class Handler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def do_POST(self):
|
def do_POST(self):
|
||||||
parsed = urlparse(self.path)
|
parsed = urlparse(self.path)
|
||||||
|
|
||||||
|
# ── Upload speed test receiver ─────────────────────────────────────
|
||||||
|
# Reads the entire body before responding so the client timer is accurate
|
||||||
|
if parsed.path == "/api/upload" or parsed.path.endswith("/api/upload"):
|
||||||
|
content_length = int(self.headers.get("Content-Length", "0"))
|
||||||
|
to_read = min(content_length, 128 * 1024 * 1024) # cap at 128 MB
|
||||||
|
received = 0
|
||||||
|
buf = 65536
|
||||||
|
while received < to_read:
|
||||||
|
chunk = self.rfile.read(min(buf, to_read - received))
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
received += len(chunk)
|
||||||
|
self._send_json(200, {"received": received, "ok": True})
|
||||||
|
return
|
||||||
|
|
||||||
if not (parsed.path in ("/mtr", "/api/mtr") or parsed.path.endswith("/api/mtr")):
|
if not (parsed.path in ("/mtr", "/api/mtr") or parsed.path.endswith("/api/mtr")):
|
||||||
self._send_json(404, {"error": "not found"})
|
self._send_json(404, {"error": "not found"})
|
||||||
return
|
return
|
||||||
|
|||||||
+14
-22
@@ -141,7 +141,7 @@ uninstall_xui() {
|
|||||||
$Pak -y purge nginx nginx-common nginx-core nginx-full python3-certbot-nginx
|
$Pak -y purge nginx nginx-common nginx-core nginx-full python3-certbot-nginx
|
||||||
$Pak -y autoremove
|
$Pak -y autoremove
|
||||||
$Pak -y autoclean
|
$Pak -y autoclean
|
||||||
rm -rf /var/www/html/ /var/www/diagnostics/ /var/www/openspeedtest/ /etc/nginx/ /usr/share/nginx/
|
rm -rf /var/www/html/ /var/www/diagnostics/ /etc/nginx/ /usr/share/nginx/
|
||||||
systemctl stop mtr-backend 2>/dev/null || true
|
systemctl stop mtr-backend 2>/dev/null || true
|
||||||
systemctl disable mtr-backend 2>/dev/null || true
|
systemctl disable mtr-backend 2>/dev/null || true
|
||||||
rm -f /etc/systemd/system/mtr-backend.service
|
rm -f /etc/systemd/system/mtr-backend.service
|
||||||
@@ -470,15 +470,19 @@ server {
|
|||||||
proxy_send_timeout 120s;
|
proxy_send_timeout 120s;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── OpenSpeedTest (served locally, no external connections) ─────────────
|
# ── Speed test upload receiver ───────────────────────────────────────────
|
||||||
location ^~ ${diag_path}speedtest/ {
|
# proxy_request_buffering off = nginx streams body to backend in real time;
|
||||||
limit_req zone=diag_page burst=30 nodelay;
|
# backend responds only after reading all bytes → accurate timing on client
|
||||||
alias /var/www/openspeedtest/;
|
location ^~ ${diag_path}api/upload {
|
||||||
index index.html;
|
limit_req zone=diag_page burst=5 nodelay;
|
||||||
try_files \$uri \$uri/ =404;
|
proxy_pass http://127.0.0.1:${mtr_backend_port}/api/upload;
|
||||||
client_max_body_size 35m;
|
proxy_http_version 1.1;
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
proxy_set_header X-Real-IP \$remote_addr;
|
||||||
access_log off;
|
proxy_request_buffering off;
|
||||||
|
client_max_body_size 128m;
|
||||||
|
proxy_read_timeout 120s;
|
||||||
|
proxy_send_timeout 120s;
|
||||||
|
add_header Cache-Control "no-store" always;
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Download test files ──────────────────────────────────────────────────
|
# ── Download test files ──────────────────────────────────────────────────
|
||||||
@@ -872,18 +876,6 @@ install_fake_site() {
|
|||||||
install_diagnostics() {
|
install_diagnostics() {
|
||||||
local diag_webroot="/var/www/diagnostics"
|
local diag_webroot="/var/www/diagnostics"
|
||||||
local backend_script="/usr/local/lib/3x-ui-pro/mtr-backend.py"
|
local backend_script="/usr/local/lib/3x-ui-pro/mtr-backend.py"
|
||||||
local openspeedtest_webroot="/var/www/openspeedtest"
|
|
||||||
|
|
||||||
# OpenSpeedTest static files
|
|
||||||
if [[ ! -f "${openspeedtest_webroot}/index.html" ]]; then
|
|
||||||
mkdir -p "${openspeedtest_webroot}"
|
|
||||||
curl -fsSL --retry 3 \
|
|
||||||
"https://github.com/openspeedtest/Speed-Test/archive/refs/heads/main.tar.gz" \
|
|
||||||
-o /tmp/openspeedtest.tar.gz
|
|
||||||
tar -xzf /tmp/openspeedtest.tar.gz -C "${openspeedtest_webroot}" --strip-components=1
|
|
||||||
rm -f /tmp/openspeedtest.tar.gz
|
|
||||||
chown -R www-data:www-data "${openspeedtest_webroot}" 2>/dev/null || true
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Diagnostics HTML page
|
# Diagnostics HTML page
|
||||||
mkdir -p "${diag_webroot}"
|
mkdir -p "${diag_webroot}"
|
||||||
|
|||||||
Reference in New Issue
Block a user