Files
Ivan RazinandClaude Fable 5 de1d95e6e0 Replace speed test with LibreSpeed engine: fix 4x-slow upload
Single 512 MB POST over HTTP/2 was throttled by the per-stream h2
flow-control window, reading ~4x low. LibreSpeed uses parallel upload
streams measured via XHR progress events, which amortizes the window
limit (h2 must stay on for trojan-gRPC).

- Vendor speedtest.js + speedtest_worker.js (LGPL) into assets
- mtr-backend.py: ThreadingHTTPServer, /api/st/up upload sink,
  /api/st/getip; parallel streams need concurrent handling
- nginx: speedtest locations without limit_req (limit_conn instead),
  ping answered by nginx directly, h2 preread/body buffer tuning
- No telemetry, no database
- Drop 512 MB test file; patch script now also substitutes
  __SERVER_DOMAIN__/__SERVER_IP__ placeholders

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 14:15:22 +03:00

462 lines
17 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Network Diagnostics</title>
<meta name="robots" content="noindex, nofollow">
<style>
:root {
--bg: #0f1117;
--surface: #1a1d27;
--border: #2d3148;
--accent: #4f86f7;
--accent2: #2ecc71;
--warn: #e67e22;
--error: #e74c3c;
--text: #cdd6f4;
--text-dim: #6c7086;
--radius: 8px;
--mono: 'SFMono-Regular', 'Consolas', 'Liberation Mono', monospace;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
min-height: 100vh;
padding: 24px 16px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { font-size: 1.6rem; font-weight: 700; margin-bottom: 6px; color: #fff; }
.subtitle { color: var(--text-dim); font-size: 0.9rem; margin-bottom: 32px; }
.section {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 24px;
margin-bottom: 24px;
}
.section-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
color: #fff;
}
.section-title::before {
content: '';
display: block;
width: 4px; height: 18px;
background: var(--accent);
border-radius: 2px;
}
/* Speed test */
.speed-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 16px;
margin-bottom: 20px;
}
.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 { display: flex; gap: 12px; align-items: flex-end; flex-wrap: wrap; }
.form-group { display: flex; flex-direction: column; gap: 6px; }
.form-group label { font-size: 0.82rem; color: var(--text-dim); }
.form-group input[type="number"] {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text);
padding: 8px 12px;
font-size: 0.95rem;
width: 120px;
}
.form-group input:focus { outline: none; border-color: var(--accent); }
.btn {
background: var(--accent);
border: none;
border-radius: 6px;
color: #fff;
cursor: pointer;
font-size: 0.95rem;
font-weight: 600;
padding: 10px 20px;
transition: opacity 0.15s;
}
.btn:hover { opacity: 0.85; }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
#mtr-result {
margin-top: 16px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
font-family: var(--mono);
font-size: 0.82rem;
line-height: 1.5;
padding: 14px 16px;
white-space: pre;
overflow-x: auto;
min-height: 48px;
color: var(--text);
display: none;
}
.status-tag {
display: inline-block;
border-radius: 4px;
font-size: 0.78rem;
font-weight: 600;
padding: 2px 8px;
}
.tag-info { background: rgba(79,134,247,0.2); color: var(--accent); }
.tag-ok { background: rgba(46,204,113,0.2); color: var(--accent2); }
.tag-warn { background: rgba(230,126,34,0.2); color: var(--warn); }
.tag-error { background: rgba(231,76,60,0.2); color: var(--error); }
#mtr-status { margin-top: 10px; font-size: 0.85rem; }
#client-ip { font-family: var(--mono); font-weight: 600; color: var(--accent2); }
/* Download test files */
.file-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 12px; }
.file-card {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 14px 16px;
text-decoration: none;
color: var(--text);
transition: border-color 0.15s;
display: flex;
flex-direction: column;
gap: 4px;
}
.file-card:hover { border-color: var(--accent); }
.file-size { font-size: 1.3rem; font-weight: 700; color: #fff; }
.file-name { font-size: 0.8rem; color: var(--text-dim); font-family: var(--mono); }
/* WinMTR instructions */
.instruction-steps { list-style: none; counter-reset: steps; }
.instruction-steps li {
counter-increment: steps;
padding: 8px 0 8px 40px;
position: relative;
border-bottom: 1px solid var(--border);
font-size: 0.9rem;
}
.instruction-steps li:last-child { border-bottom: none; }
.instruction-steps li::before {
content: counter(steps);
position: absolute;
left: 0;
background: var(--accent);
border-radius: 50%;
width: 24px; height: 24px;
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 700;
color: #fff;
top: 8px;
}
.code-inline {
background: var(--bg);
border: 1px solid var(--border);
border-radius: 4px;
font-family: var(--mono);
font-size: 0.85rem;
padding: 2px 6px;
}
#server-ip { font-family: var(--mono); color: var(--accent); }
a.link { color: var(--accent); text-decoration: none; }
a.link:hover { text-decoration: underline; }
.spinner { display: none; }
.spinner.active {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid var(--border);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 0.8s linear infinite;
vertical-align: middle;
margin-left: 8px;
}
@keyframes spin { to { transform: rotate(360deg); } }
</style>
</head>
<body>
<div class="container">
<h1>Network Diagnostics</h1>
<p class="subtitle">Server diagnostics panel — <span class="status-tag tag-info">Private</span></p>
<!-- ── Speed Test ──────────────────────────────────────────────────── -->
<div class="section">
<div class="section-title">Speed Test</div>
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:20px;">
Runs directly between your browser and this server — no external connections.
Powered by <a class="link" href="https://github.com/librespeed/speedtest" target="_blank" rel="noopener noreferrer">LibreSpeed</a>:
parallel streams, 15 s per direction.
</p>
<div class="speed-grid">
<div class="speed-card">
<div class="speed-card-label">↓ Download</div>
<div class="speed-card-value" id="dl-speed"></div>
<div class="speed-card-unit">Mbps</div>
<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 class="speed-card">
<div class="speed-card-label">Ping</div>
<div class="speed-card-value" id="ping-val"></div>
<div class="speed-card-unit">ms</div>
<div class="speed-card-note"></div>
</div>
<div class="speed-card">
<div class="speed-card-label">Jitter</div>
<div class="speed-card-value" id="jitter-val"></div>
<div class="speed-card-unit">ms</div>
<div class="speed-card-note"></div>
</div>
</div>
<button class="btn" id="speed-btn" onclick="toggleSpeedTest()">Start Speed Test</button>
<div id="speed-status" style="margin-top:14px;font-size:0.85rem;min-height:1.4em;"></div>
</div>
<!-- ── MTR Test ───────────────────────────────────────────────────── -->
<div class="section">
<div class="section-title">MTR Network Path Test</div>
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:16px;">
Runs <code class="code-inline">mtr</code> from this server to your IP address
(<span id="client-ip">detecting…</span>). Shows hop-by-hop latency and packet loss.
</p>
<div class="mtr-form">
<div class="form-group">
<label for="pkt-count">Packet count (1100)</label>
<input type="number" id="pkt-count" value="5" min="1" max="100">
</div>
<button class="btn" id="mtr-btn" onclick="runMtr()">Run MTR</button>
<span class="spinner" id="mtr-spinner"></span>
</div>
<div id="mtr-status"></div>
<pre id="mtr-result"></pre>
</div>
<!-- ── Download Test Files ────────────────────────────────────────── -->
<div class="section">
<div class="section-title">Download Test Files</div>
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:16px;">
Static binary files for bandwidth testing. Generated from <code class="code-inline">/dev/zero</code>.
</p>
<div class="file-grid">
<a class="file-card" href="__DIAG_PATH__testfiles/test-15k.bin" download>
<span class="file-size">15 KB</span>
<span class="file-name">test-15k.bin</span>
</a>
<a class="file-card" href="__DIAG_PATH__testfiles/test-17k.bin" download>
<span class="file-size">17 KB</span>
<span class="file-name">test-17k.bin</span>
</a>
<a class="file-card" href="__DIAG_PATH__testfiles/test-100m.bin" download>
<span class="file-size">100 MB</span>
<span class="file-name">test-100m.bin</span>
</a>
<a class="file-card" href="__DIAG_PATH__testfiles/test-1g.bin" download>
<span class="file-size">1 GB</span>
<span class="file-name">test-1g.bin</span>
</a>
</div>
</div>
<!-- ── WinMTR Instructions ────────────────────────────────────────── -->
<div class="section">
<div class="section-title">Run MTR from Windows (WinMTR)</div>
<p style="font-size:0.85rem;color:var(--text-dim);margin-bottom:16px;">
Test the network path from <em>your PC</em> to this server at
<code class="code-inline" id="server-display">__SERVER_DOMAIN__</code>
(IP: <span id="server-ip">__SERVER_IP__</span>).
</p>
<ol class="instruction-steps">
<li>
Download WinMTR from the official site:
<a class="link" href="https://github.com/White-Tiger/WinMTR/releases" target="_blank" rel="noopener noreferrer">
github.com/White-Tiger/WinMTR</a>
— extract the ZIP and run <code class="code-inline">WinMTR.exe</code>.
</li>
<li>
In the <strong>Host</strong> field, enter either the server IP
(<code class="code-inline" id="server-ip-step">__SERVER_IP__</code>)
or the domain
(<code class="code-inline">__SERVER_DOMAIN__</code>).
</li>
<li>
Set <strong>Max hosts</strong> to <code class="code-inline">30</code> and
<strong>Interval</strong> to <code class="code-inline">1000 ms</code>.
</li>
<li>
Click <strong>Start</strong> and let it run for at least <strong>300 packets</strong> (about 5 minutes).
</li>
<li>
Click <strong>Export TEXT</strong> to save the results, then share them with your support team.
</li>
</ol>
</div>
</div>
<script src="speedtest.js"></script>
<script>
// Detect client IP from server response header or API
(async function detectIP() {
try {
const r = await fetch('__DIAG_PATH__api/mtr', {
method: 'POST',
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
body: 'count=1&detect_only=1'
});
const d = await r.json();
if (d.target) {
document.getElementById('client-ip').textContent = d.target;
}
} catch { /* non-critical */ }
})();
async function runMtr() {
const btn = document.getElementById('mtr-btn');
const spinner = document.getElementById('mtr-spinner');
const resultEl = document.getElementById('mtr-result');
const statusEl = document.getElementById('mtr-status');
// Validate input
const countInput = document.getElementById('pkt-count');
const count = parseInt(countInput.value, 10);
if (isNaN(count) || count < 1 || count > 100) {
statusEl.innerHTML = '<span class="status-tag tag-error">Packet count must be 1100</span>';
return;
}
btn.disabled = true;
spinner.classList.add('active');
resultEl.style.display = 'none';
statusEl.innerHTML = '<span class="status-tag tag-info">Running MTR… this may take up to ' +
(count * 3) + ' seconds</span>';
try {
const resp = await fetch('__DIAG_PATH__api/mtr', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({count})
});
const data = await resp.json();
spinner.classList.remove('active');
btn.disabled = false;
if (resp.status === 429) {
statusEl.innerHTML = '<span class="status-tag tag-warn">Rate limit reached — please wait 60 seconds</span>';
return;
}
if (data.success) {
document.getElementById('client-ip').textContent = data.target || '';
statusEl.innerHTML = '<span class="status-tag tag-ok">MTR complete — ' +
count + ' packets to ' + (data.target || 'your IP') + '</span>';
resultEl.textContent = data.output || '(no output)';
} else {
statusEl.innerHTML = '<span class="status-tag tag-error">Error: ' +
escHtml(data.error || 'unknown') + '</span>';
resultEl.textContent = data.output || '';
}
resultEl.style.display = 'block';
} catch (err) {
spinner.classList.remove('active');
btn.disabled = false;
statusEl.innerHTML = '<span class="status-tag tag-error">Request failed: ' + escHtml(String(err)) + '</span>';
}
}
function escHtml(s) {
return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
// ── Speed Test (LibreSpeed engine) ──────────────────────────────────────────
// Upload accuracy over HTTP/2 requires parallel streams + XHR progress events;
// a single big POST is throttled by the per-stream h2 flow-control window.
let st = null;
function speedUI(running) {
document.getElementById('speed-btn').textContent =
running ? 'Abort Test' : 'Start Speed Test';
}
function toggleSpeedTest() {
const status = document.getElementById('speed-status');
if (st && st.getState() === 3) {
st.abort();
st = null;
speedUI(false);
status.innerHTML = '<span class="status-tag tag-warn">Aborted</span>';
return;
}
['dl-speed','ul-speed','ping-val','jitter-val'].forEach(id => {
document.getElementById(id).textContent = '—';
});
['dl-note','ul-note'].forEach(id => { document.getElementById(id).textContent = ''; });
st = new Speedtest();
// All URLs are relative to speedtest_worker.js (served from the diag root)
st.setParameter('url_dl', 'testfiles/test-100m.bin');
st.setParameter('url_ul', 'api/st/up');
st.setParameter('url_ping', 'api/st/ping');
st.setParameter('url_getIp','api/st/getip');
st.setParameter('getIp_ispInfo', false);
st.setParameter('telemetry_level', 0); // no telemetry, no database
st.onupdate = function (d) {
if (d.dlStatus) document.getElementById('dl-speed').textContent = d.dlStatus;
if (d.ulStatus) document.getElementById('ul-speed').textContent = d.ulStatus;
if (d.pingStatus) document.getElementById('ping-val').textContent = d.pingStatus;
if (d.jitterStatus) document.getElementById('jitter-val').textContent = d.jitterStatus;
document.getElementById('dl-note').textContent =
d.testState === 1 ? Math.round(d.dlProgress * 100) + '%' : '';
document.getElementById('ul-note').textContent =
d.testState === 3 ? Math.round(d.ulProgress * 100) + '%' : '';
const phase = {0:'Starting…',1:'Testing download…',2:'Testing ping…',3:'Testing upload…'}[d.testState];
if (phase)
status.innerHTML = '<span class="status-tag tag-info">' + phase + '</span>';
};
st.onend = function (aborted) {
speedUI(false);
document.getElementById('dl-note').textContent = '';
document.getElementById('ul-note').textContent = '';
if (!aborted)
status.innerHTML = '<span class="status-tag tag-ok">Done</span>';
st = null;
};
speedUI(true);
st.start();
}
</script>
</body>
</html>