| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165 |
- <!DOCTYPE html><html><head>
- <meta charset='UTF-8'>
- <meta name='viewport' content='width=device-width,initial-scale=1'>
- <title>WiFi Speedtest</title>
- <style>
- *{box-sizing:border-box;margin:0;padding:0}
- body{font-family:-apple-system,system-ui,sans-serif;background:linear-gradient(135deg,#1a1a2e,#16213e);min-height:100vh;padding:20px;color:#fff}
- .wrap{max-width:560px;margin:0 auto}
- h1{font-size:24px;margin-bottom:8px}
- .sub{color:#888;font-size:13px;margin-bottom:20px}
- .card{background:rgba(255,255,255,0.05);border-radius:12px;padding:18px;margin-bottom:14px;border:1px solid rgba(255,255,255,0.1)}
- .card h2{font-size:15px;color:#e94560;margin-bottom:12px}
- .row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;font-size:13px}
- .row .lbl{color:#888}
- .row .val{font-weight:600;font-family:ui-monospace,monospace}
- .btn{padding:10px 16px;border:none;border-radius:8px;font-size:14px;font-weight:500;cursor:pointer;background:#e94560;color:#fff;width:100%;margin-top:10px}
- .btn:disabled{opacity:0.5;cursor:not-allowed}
- .btn.sec{background:rgba(255,255,255,0.1)}
- select,input{width:100%;padding:10px;background:rgba(0,0,0,0.3);border:1px solid rgba(255,255,255,0.1);border-radius:6px;color:#fff;font-size:13px;margin-bottom:8px}
- .bar{height:6px;background:rgba(0,0,0,0.3);border-radius:3px;overflow:hidden;margin-top:8px}
- .bar>div{height:100%;background:#e94560;width:0%;transition:width 0.1s}
- .big{font-size:28px;font-weight:700;color:#e94560}
- .unit{font-size:14px;color:#888;margin-left:4px}
- .log{font-family:ui-monospace,monospace;font-size:11px;color:#888;max-height:140px;overflow-y:auto;padding:8px;background:rgba(0,0,0,0.3);border-radius:6px;white-space:pre-wrap}
- a{color:#e94560}
- </style></head><body>
- <div class='wrap'>
- <h1>WiFi Speedtest</h1>
- <p class='sub'>Measure latency and throughput between this browser and the device.</p>
- <div class='card'>
- <h2>Ping (RTT)</h2>
- <div class='row'><span class='lbl'>Samples</span><span class='val' id='p-n'>0</span></div>
- <div class='row'><span class='lbl'>Min</span><span class='val' id='p-min'>—</span></div>
- <div class='row'><span class='lbl'>Avg</span><span class='val' id='p-avg'>—</span></div>
- <div class='row'><span class='lbl'>Max</span><span class='val' id='p-max'>—</span></div>
- <div class='row'><span class='lbl'>Jitter (stdev)</span><span class='val' id='p-jit'>—</span></div>
- <button class='btn' id='b-ping' onclick='runPing()'>Run 20 Pings</button>
- </div>
- <div class='card'>
- <h2>Download (device → browser)</h2>
- <div style='text-align:center;padding:10px 0'><span class='big' id='dl-mbps'>0.00</span><span class='unit'>Mbit/s</span></div>
- <div class='row'><span class='lbl'>Bytes</span><span class='val' id='dl-bytes'>—</span></div>
- <div class='row'><span class='lbl'>Elapsed</span><span class='val' id='dl-time'>—</span></div>
- <div class='bar'><div id='dl-bar'></div></div>
- <label style='font-size:12px;color:#888;margin-top:10px;display:block'>Payload size</label>
- <select id='dl-size'>
- <option value='262144'>256 KB</option>
- <option value='1048576' selected>1 MB</option>
- <option value='4194304'>4 MB</option>
- <option value='8388608'>8 MB</option>
- </select>
- <button class='btn' id='b-dl' onclick='runDownload()'>Start Download Test</button>
- </div>
- <div class='card'>
- <h2>Upload (browser → device)</h2>
- <div style='text-align:center;padding:10px 0'><span class='big' id='ul-mbps'>0.00</span><span class='unit'>Mbit/s</span></div>
- <div class='row'><span class='lbl'>Bytes</span><span class='val' id='ul-bytes'>—</span></div>
- <div class='row'><span class='lbl'>Elapsed</span><span class='val' id='ul-time'>—</span></div>
- <div class='bar'><div id='ul-bar'></div></div>
- <label style='font-size:12px;color:#888;margin-top:10px;display:block'>Payload size</label>
- <select id='ul-size'>
- <option value='262144'>256 KB</option>
- <option value='1048576' selected>1 MB</option>
- <option value='4194304'>4 MB</option>
- </select>
- <button class='btn' id='b-ul' onclick='runUpload()'>Start Upload Test</button>
- </div>
- <div class='card'>
- <h2>Log</h2>
- <div class='log' id='log'></div>
- </div>
- <p style='text-align:center;margin-top:10px'><a href='/'>← Back to main</a></p>
- </div>
- <script>
- const $ = id => document.getElementById(id);
- function log(s){const l=$('log');l.textContent+=s+'\n';l.scrollTop=l.scrollHeight;}
- function fmtBytes(n){if(n>=1048576)return (n/1048576).toFixed(2)+' MB';if(n>=1024)return (n/1024).toFixed(1)+' KB';return n+' B';}
- function fmtMs(ms){return ms.toFixed(2)+' ms';}
- async function runPing(){
- const btn=$('b-ping');btn.disabled=true;
- const N=20;const samples=[];
- $('p-n').textContent='0';
- for(let i=0;i<N;i++){
- const t0=performance.now();
- try{
- await fetch('/api/speedtest/ping?'+Math.random(),{cache:'no-store'});
- const dt=performance.now()-t0;
- samples.push(dt);
- $('p-n').textContent=samples.length;
- }catch(e){log('ping err: '+e);break;}
- await new Promise(r=>setTimeout(r,50));
- }
- if(samples.length){
- const mn=Math.min(...samples),mx=Math.max(...samples);
- const avg=samples.reduce((a,b)=>a+b,0)/samples.length;
- const v=samples.reduce((a,b)=>a+(b-avg)*(b-avg),0)/samples.length;
- const sd=Math.sqrt(v);
- $('p-min').textContent=fmtMs(mn);
- $('p-max').textContent=fmtMs(mx);
- $('p-avg').textContent=fmtMs(avg);
- $('p-jit').textContent=fmtMs(sd);
- log(`ping n=${samples.length} min=${mn.toFixed(2)} avg=${avg.toFixed(2)} max=${mx.toFixed(2)} sd=${sd.toFixed(2)} ms`);
- }
- btn.disabled=false;
- }
- async function runDownload(){
- const btn=$('b-dl');btn.disabled=true;
- const bytes=parseInt($('dl-size').value);
- $('dl-bytes').textContent=fmtBytes(bytes);
- $('dl-mbps').textContent='0.00';
- $('dl-bar').style.width='0%';
- const t0=performance.now();
- try{
- const resp=await fetch('/api/speedtest/download?bytes='+bytes+'&_='+Math.random(),{cache:'no-store'});
- if(!resp.ok){throw new Error('HTTP '+resp.status);}
- const reader=resp.body.getReader();
- let got=0;
- while(true){
- const {done,value}=await reader.read();
- if(done)break;
- got+=value.length;
- $('dl-bar').style.width=(100*got/bytes).toFixed(1)+'%';
- const dt=(performance.now()-t0)/1000;
- if(dt>0)$('dl-mbps').textContent=((got*8/1e6)/dt).toFixed(2);
- }
- const dt=(performance.now()-t0)/1000;
- $('dl-time').textContent=(dt*1000).toFixed(0)+' ms';
- $('dl-mbps').textContent=((got*8/1e6)/dt).toFixed(2);
- log(`download ${fmtBytes(got)} in ${(dt*1000).toFixed(0)} ms = ${((got*8/1e6)/dt).toFixed(2)} Mbit/s`);
- }catch(e){log('dl err: '+e.message);}
- btn.disabled=false;
- }
- async function runUpload(){
- const btn=$('b-ul');btn.disabled=true;
- const bytes=parseInt($('ul-size').value);
- $('ul-bytes').textContent=fmtBytes(bytes);
- $('ul-mbps').textContent='0.00';
- $('ul-bar').style.width='0%';
- // Pre-build payload (random data, in 64KB chunks to limit memory spike)
- const payload=new Uint8Array(bytes);
- for(let i=0;i<bytes;i+=4){payload[i]=(i*73)&0xff;}
- const t0=performance.now();
- try{
- const resp=await fetch('/api/speedtest/upload',{method:'POST',body:payload,headers:{'Content-Type':'application/octet-stream'}});
- if(!resp.ok){throw new Error('HTTP '+resp.status);}
- const dt=(performance.now()-t0)/1000;
- const txt=await resp.text();
- $('ul-bar').style.width='100%';
- $('ul-time').textContent=(dt*1000).toFixed(0)+' ms';
- $('ul-mbps').textContent=((bytes*8/1e6)/dt).toFixed(2);
- log(`upload ${fmtBytes(bytes)} in ${(dt*1000).toFixed(0)} ms = ${((bytes*8/1e6)/dt).toFixed(2)} Mbit/s (server: ${txt.trim()})`);
- }catch(e){log('ul err: '+e.message);}
- btn.disabled=false;
- }
- </script>
- </body></html>
|