speedtest.html 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. <!DOCTYPE html><html><head>
  2. <meta charset='UTF-8'>
  3. <meta name='viewport' content='width=device-width,initial-scale=1'>
  4. <title>WiFi Speedtest</title>
  5. <style>
  6. *{box-sizing:border-box;margin:0;padding:0}
  7. body{font-family:-apple-system,system-ui,sans-serif;background:linear-gradient(135deg,#1a1a2e,#16213e);min-height:100vh;padding:20px;color:#fff}
  8. .wrap{max-width:560px;margin:0 auto}
  9. h1{font-size:24px;margin-bottom:8px}
  10. .sub{color:#888;font-size:13px;margin-bottom:20px}
  11. .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)}
  12. .card h2{font-size:15px;color:#e94560;margin-bottom:12px}
  13. .row{display:flex;justify-content:space-between;align-items:center;padding:6px 0;font-size:13px}
  14. .row .lbl{color:#888}
  15. .row .val{font-weight:600;font-family:ui-monospace,monospace}
  16. .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}
  17. .btn:disabled{opacity:0.5;cursor:not-allowed}
  18. .btn.sec{background:rgba(255,255,255,0.1)}
  19. 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}
  20. .bar{height:6px;background:rgba(0,0,0,0.3);border-radius:3px;overflow:hidden;margin-top:8px}
  21. .bar>div{height:100%;background:#e94560;width:0%;transition:width 0.1s}
  22. .big{font-size:28px;font-weight:700;color:#e94560}
  23. .unit{font-size:14px;color:#888;margin-left:4px}
  24. .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}
  25. a{color:#e94560}
  26. </style></head><body>
  27. <div class='wrap'>
  28. <h1>WiFi Speedtest</h1>
  29. <p class='sub'>Measure latency and throughput between this browser and the device.</p>
  30. <div class='card'>
  31. <h2>Ping (RTT)</h2>
  32. <div class='row'><span class='lbl'>Samples</span><span class='val' id='p-n'>0</span></div>
  33. <div class='row'><span class='lbl'>Min</span><span class='val' id='p-min'>—</span></div>
  34. <div class='row'><span class='lbl'>Avg</span><span class='val' id='p-avg'>—</span></div>
  35. <div class='row'><span class='lbl'>Max</span><span class='val' id='p-max'>—</span></div>
  36. <div class='row'><span class='lbl'>Jitter (stdev)</span><span class='val' id='p-jit'>—</span></div>
  37. <button class='btn' id='b-ping' onclick='runPing()'>Run 20 Pings</button>
  38. </div>
  39. <div class='card'>
  40. <h2>Download (device → browser)</h2>
  41. <div style='text-align:center;padding:10px 0'><span class='big' id='dl-mbps'>0.00</span><span class='unit'>Mbit/s</span></div>
  42. <div class='row'><span class='lbl'>Bytes</span><span class='val' id='dl-bytes'>—</span></div>
  43. <div class='row'><span class='lbl'>Elapsed</span><span class='val' id='dl-time'>—</span></div>
  44. <div class='bar'><div id='dl-bar'></div></div>
  45. <label style='font-size:12px;color:#888;margin-top:10px;display:block'>Payload size</label>
  46. <select id='dl-size'>
  47. <option value='262144'>256 KB</option>
  48. <option value='1048576' selected>1 MB</option>
  49. <option value='4194304'>4 MB</option>
  50. <option value='8388608'>8 MB</option>
  51. </select>
  52. <button class='btn' id='b-dl' onclick='runDownload()'>Start Download Test</button>
  53. </div>
  54. <div class='card'>
  55. <h2>Upload (browser → device)</h2>
  56. <div style='text-align:center;padding:10px 0'><span class='big' id='ul-mbps'>0.00</span><span class='unit'>Mbit/s</span></div>
  57. <div class='row'><span class='lbl'>Bytes</span><span class='val' id='ul-bytes'>—</span></div>
  58. <div class='row'><span class='lbl'>Elapsed</span><span class='val' id='ul-time'>—</span></div>
  59. <div class='bar'><div id='ul-bar'></div></div>
  60. <label style='font-size:12px;color:#888;margin-top:10px;display:block'>Payload size</label>
  61. <select id='ul-size'>
  62. <option value='262144'>256 KB</option>
  63. <option value='1048576' selected>1 MB</option>
  64. <option value='4194304'>4 MB</option>
  65. </select>
  66. <button class='btn' id='b-ul' onclick='runUpload()'>Start Upload Test</button>
  67. </div>
  68. <div class='card'>
  69. <h2>Log</h2>
  70. <div class='log' id='log'></div>
  71. </div>
  72. <p style='text-align:center;margin-top:10px'><a href='/'>← Back to main</a></p>
  73. </div>
  74. <script>
  75. const $ = id => document.getElementById(id);
  76. function log(s){const l=$('log');l.textContent+=s+'\n';l.scrollTop=l.scrollHeight;}
  77. function fmtBytes(n){if(n>=1048576)return (n/1048576).toFixed(2)+' MB';if(n>=1024)return (n/1024).toFixed(1)+' KB';return n+' B';}
  78. function fmtMs(ms){return ms.toFixed(2)+' ms';}
  79. async function runPing(){
  80. const btn=$('b-ping');btn.disabled=true;
  81. const N=20;const samples=[];
  82. $('p-n').textContent='0';
  83. for(let i=0;i<N;i++){
  84. const t0=performance.now();
  85. try{
  86. await fetch('/api/speedtest/ping?'+Math.random(),{cache:'no-store'});
  87. const dt=performance.now()-t0;
  88. samples.push(dt);
  89. $('p-n').textContent=samples.length;
  90. }catch(e){log('ping err: '+e);break;}
  91. await new Promise(r=>setTimeout(r,50));
  92. }
  93. if(samples.length){
  94. const mn=Math.min(...samples),mx=Math.max(...samples);
  95. const avg=samples.reduce((a,b)=>a+b,0)/samples.length;
  96. const v=samples.reduce((a,b)=>a+(b-avg)*(b-avg),0)/samples.length;
  97. const sd=Math.sqrt(v);
  98. $('p-min').textContent=fmtMs(mn);
  99. $('p-max').textContent=fmtMs(mx);
  100. $('p-avg').textContent=fmtMs(avg);
  101. $('p-jit').textContent=fmtMs(sd);
  102. log(`ping n=${samples.length} min=${mn.toFixed(2)} avg=${avg.toFixed(2)} max=${mx.toFixed(2)} sd=${sd.toFixed(2)} ms`);
  103. }
  104. btn.disabled=false;
  105. }
  106. async function runDownload(){
  107. const btn=$('b-dl');btn.disabled=true;
  108. const bytes=parseInt($('dl-size').value);
  109. $('dl-bytes').textContent=fmtBytes(bytes);
  110. $('dl-mbps').textContent='0.00';
  111. $('dl-bar').style.width='0%';
  112. const t0=performance.now();
  113. try{
  114. const resp=await fetch('/api/speedtest/download?bytes='+bytes+'&_='+Math.random(),{cache:'no-store'});
  115. if(!resp.ok){throw new Error('HTTP '+resp.status);}
  116. const reader=resp.body.getReader();
  117. let got=0;
  118. while(true){
  119. const {done,value}=await reader.read();
  120. if(done)break;
  121. got+=value.length;
  122. $('dl-bar').style.width=(100*got/bytes).toFixed(1)+'%';
  123. const dt=(performance.now()-t0)/1000;
  124. if(dt>0)$('dl-mbps').textContent=((got*8/1e6)/dt).toFixed(2);
  125. }
  126. const dt=(performance.now()-t0)/1000;
  127. $('dl-time').textContent=(dt*1000).toFixed(0)+' ms';
  128. $('dl-mbps').textContent=((got*8/1e6)/dt).toFixed(2);
  129. log(`download ${fmtBytes(got)} in ${(dt*1000).toFixed(0)} ms = ${((got*8/1e6)/dt).toFixed(2)} Mbit/s`);
  130. }catch(e){log('dl err: '+e.message);}
  131. btn.disabled=false;
  132. }
  133. async function runUpload(){
  134. const btn=$('b-ul');btn.disabled=true;
  135. const bytes=parseInt($('ul-size').value);
  136. $('ul-bytes').textContent=fmtBytes(bytes);
  137. $('ul-mbps').textContent='0.00';
  138. $('ul-bar').style.width='0%';
  139. // Pre-build payload (random data, in 64KB chunks to limit memory spike)
  140. const payload=new Uint8Array(bytes);
  141. for(let i=0;i<bytes;i+=4){payload[i]=(i*73)&0xff;}
  142. const t0=performance.now();
  143. try{
  144. const resp=await fetch('/api/speedtest/upload',{method:'POST',body:payload,headers:{'Content-Type':'application/octet-stream'}});
  145. if(!resp.ok){throw new Error('HTTP '+resp.status);}
  146. const dt=(performance.now()-t0)/1000;
  147. const txt=await resp.text();
  148. $('ul-bar').style.width='100%';
  149. $('ul-time').textContent=(dt*1000).toFixed(0)+' ms';
  150. $('ul-mbps').textContent=((bytes*8/1e6)/dt).toFixed(2);
  151. log(`upload ${fmtBytes(bytes)} in ${(dt*1000).toFixed(0)} ms = ${((bytes*8/1e6)/dt).toFixed(2)} Mbit/s (server: ${txt.trim()})`);
  152. }catch(e){log('ul err: '+e.message);}
  153. btn.disabled=false;
  154. }
  155. </script>
  156. </body></html>