diff --git a/assets/diagnostics/index.html b/assets/diagnostics/index.html
index d135002..65d8d54 100644
--- a/assets/diagnostics/index.html
+++ b/assets/diagnostics/index.html
@@ -54,14 +54,24 @@
border-radius: 2px;
}
- /* Speed test embed */
- #speedtest-frame {
- width: 100%;
- height: 520px;
- border: none;
- border-radius: var(--radius);
- background: #000;
+ /* Speed test */
+ .speed-grid {
+ display: grid;
+ grid-template-columns: 1fr 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; }
@@ -195,15 +205,26 @@
Speed Test
-
- 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.
-
+
+
+
↓ Download
+
—
+
Mbps
+
+
+
+
↑ Upload
+
—
+
Mbps
+
+
+
+
+
@@ -359,6 +380,69 @@ async function runMtr() {
function escHtml(s) {
return s.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 = 'Testing download…';
+ 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 = 'Testing upload…';
+ 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 = 'Done';
+ 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;
+}