Sfoglia il codice sorgente

Speedtest: modernize UI and fix measurement accuracy

UI (mirrors system_settings design language):
- Replace Semantic UI statistics / progress widget with custom CSS
- Add speedometer-style SVG gauge (270° arc, fills as test runs)
- Three coloured result cards: blue download, green upload, amber ping
- Slim 3-px animated progress bar beneath the cards
- Modern typography: Segoe UI / system-ui stack, matching weights/spacing
- Full light + dark theme via CSS custom properties and
  ao_module_getSystemThemeColor(), consistent with system_settings tokens
- Drop unused semantic.min.css / semantic.min.js imports
- Responsive layout (shrinks on screens ≤ 360 px)

Accuracy fixes:
- Download progress: was using increment with TIME_DIFF (current batch
  duration), causing the bar to overshoot 100%; now uses SET capped at
  96% so the final TTL_BANDWIDTH message lands cleanly at 100%
- Download live speed: client now counts bytes from every DATA: frame
  and shows a live Mbps reading inside the gauge while the test runs
- Upload progress: was computing '100 - bufferedAmount/1MB', which only
  works if the buffer is exactly 100 MB; now snapshots initialBuf after
  the fill loop and uses (1 - remaining/initialBuf)*100 as the reference
- Upload live speed: computes bytes-on-the-wire / elapsed each 100 ms
  tick and displays it in the gauge centre during the upload phase
- Ping timing: switched from new Date() (1 ms resolution) to
  performance.now() (sub-ms) for both send and receive timestamps
- Ping array alignment: was calling shift() twice on only 2 of 3 arrays,
  leaving clientRecv mis-aligned; now uses splice(0,2) uniformly on both
  receive arrays so index i in clientSend/clientRecv/serverResp always
  maps to the same round-trip

https://claude.ai/code/session_01QP5ab5t2ZekwBFCNB57WEq
Claude 3 settimane fa
parent
commit
4be14b4851
1 ha cambiato i file con 558 aggiunte e 174 eliminazioni
  1. 558 174
      src/web/Speedtest/index.html

+ 558 - 174
src/web/Speedtest/index.html

@@ -1,250 +1,634 @@
 <!DOCTYPE html>
 <html>
-
 <head>
     <meta name="apple-mobile-web-app-capable" content="yes" />
     <meta name="viewport" content="user-scalable=no, width=device-width, initial-scale=1, maximum-scale=1" />
     <meta charset="UTF-8">
     <meta name="theme-color" content="#4b75ff">
-    <link rel="stylesheet" href="../script/semantic/semantic.min.css">
     <script src="../script/jquery.min.js"></script>
     <script src="../script/ao_module.js"></script>
-    <script src="../script/semantic/semantic.min.js"></script>
-    <title>WebSocket Test</title>
+    <title>Speed Test</title>
+    <style>
+        /* ── Reset & theme tokens (mirrors system_settings design language) ── */
+        *, *::before, *::after { box-sizing: border-box; }
+
+        :root {
+            --bg:              #f3f3f3;
+            --text:            #202020;
+            --text-dim:        #555;
+            --text-muted:      #888;
+            --card-bg:         #ffffff;
+            --card-border:     #e5e5e5;
+            --progress-bg:     #e0e0e0;
+            --gauge-track:     #e0e0e0;
+            --btn-bg:          #4b75ff;
+            --btn-hover:       #3b65ef;
+            --btn-text:        #ffffff;
+            --btn-disabled:    rgba(75, 117, 255, 0.45);
+            --scrollbar-thumb: #c8c8c8;
+        }
+
+        body.dark {
+            --bg:              #1f1f1f;
+            --text:            #e3e3e3;
+            --text-dim:        #999;
+            --text-muted:      #666;
+            --card-bg:         #2d2d2d;
+            --card-border:     #3a3a3a;
+            --progress-bg:     #383838;
+            --gauge-track:     #383838;
+            --btn-bg:          #5b85ff;
+            --btn-hover:       #4b75ef;
+            --btn-text:        #ffffff;
+            --btn-disabled:    rgba(91, 133, 255, 0.40);
+            --scrollbar-thumb: #555555;
+        }
+
+        html, body {
+            height: 100%;
+            margin: 0;
+            background: var(--bg);
+            font-family: 'Segoe UI', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
+            font-size: 14px;
+            color: var(--text);
+            transition: background 0.15s, color 0.15s;
+            overflow: hidden;
+        }
+
+        /* ── Outer shell ── */
+        .st-wrap {
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            height: 100vh;
+            padding: 20px 16px;
+            gap: 18px;
+        }
+
+        /* ── Gauge ── */
+        .gauge-wrap {
+            position: relative;
+            width: 172px;
+            height: 172px;
+            flex-shrink: 0;
+        }
+
+        .gauge-wrap svg {
+            width: 100%;
+            height: 100%;
+            overflow: visible;
+        }
+
+        .gauge-center {
+            position: absolute;
+            inset: 0;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            justify-content: center;
+            pointer-events: none;
+        }
+
+        .gauge-val {
+            font-size: 32px;
+            font-weight: 700;
+            letter-spacing: -0.5px;
+            color: var(--text);
+            line-height: 1;
+            transition: color 0.2s;
+        }
+
+        .gauge-unit {
+            font-size: 11px;
+            color: var(--text-muted);
+            margin-top: 4px;
+            letter-spacing: 0.5px;
+            min-height: 14px;
+        }
+
+        .gauge-phase {
+            font-size: 11px;
+            font-weight: 500;
+            color: var(--text-muted);
+            margin-top: 7px;
+            letter-spacing: 0.7px;
+            text-transform: uppercase;
+        }
+
+        /* ── Result cards ── */
+        .result-row {
+            display: flex;
+            gap: 10px;
+            width: 100%;
+            max-width: 400px;
+        }
+
+        .rc {
+            flex: 1;
+            background: var(--card-bg);
+            border: 1px solid var(--card-border);
+            border-radius: 10px;
+            padding: 12px 8px;
+            display: flex;
+            flex-direction: column;
+            align-items: center;
+            gap: 3px;
+            transition: background 0.15s, border-color 0.15s;
+        }
+
+        .rc-icon {
+            width: 28px;
+            height: 28px;
+            border-radius: 7px;
+            display: flex;
+            align-items: center;
+            justify-content: center;
+            margin-bottom: 3px;
+        }
+
+        .rc-icon svg { width: 14px; height: 14px; }
+
+        .rc-dl   .rc-icon { background: rgba(75,  117, 255, 0.12); color: #4b75ff; }
+        .rc-ul   .rc-icon { background: rgba(34,  197,  94, 0.12); color: #22c55e; }
+        .rc-ping .rc-icon { background: rgba(245, 158,  11, 0.12); color: #f59e0b; }
+
+        .rc-value {
+            font-size: 17px;
+            font-weight: 700;
+            color: var(--text);
+            letter-spacing: -0.2px;
+            line-height: 1;
+        }
+
+        .rc-label {
+            font-size: 10.5px;
+            color: var(--text-muted);
+            letter-spacing: 0.2px;
+            text-align: center;
+        }
+
+        /* ── Slim progress bar ── */
+        .progress-track {
+            width: 100%;
+            max-width: 400px;
+            height: 3px;
+            background: var(--progress-bg);
+            border-radius: 2px;
+            overflow: hidden;
+            opacity: 0;
+            transition: opacity 0.2s;
+        }
+
+        .progress-track.visible { opacity: 1; }
+
+        .progress-fill {
+            height: 100%;
+            width: 0%;
+            border-radius: 2px;
+            background: #4b75ff;
+            transition: width 0.25s ease, background 0.2s;
+        }
+
+        /* ── Button ── */
+        .st-btn {
+            width: 100%;
+            max-width: 400px;
+            padding: 10px 0;
+            background: var(--btn-bg);
+            color: var(--btn-text);
+            border: none;
+            border-radius: 8px;
+            font-size: 14px;
+            font-weight: 600;
+            font-family: inherit;
+            cursor: pointer;
+            letter-spacing: 0.1px;
+            transition: background 0.1s, transform 0.08s;
+            flex-shrink: 0;
+        }
+
+        .st-btn:hover:not(:disabled)  { background: var(--btn-hover); }
+        .st-btn:active:not(:disabled) { transform: scale(0.98); }
+        .st-btn:disabled {
+            background: var(--btn-disabled);
+            cursor: not-allowed;
+            transform: none;
+        }
+
+        /* ── Small screens ── */
+        @media (max-width: 360px) {
+            .gauge-wrap  { width: 144px; height: 144px; }
+            .gauge-val   { font-size: 26px; }
+            .rc-value    { font-size: 14px; }
+        }
+    </style>
 </head>
 
 <body>
-    <br><br>
-    <div class="ui container">
-        <div class="ui three statistics" id="stat">
-            <div class="statistic">
-                <div class="value" id="dl" style="padding: 5px;">
-                    -
-                </div>
-                <div class="label" id="dll">
-                    Download
-                </div>
-            </div>
-            <div class="statistic">
-                <div class="value" id="ul" style="padding: 5px;">
-                    -
-                </div>
-                <div class="label" id="ull">
-                    Upload
-                </div>
+<div class="st-wrap">
+
+    <!-- ── Central speedometer gauge ── -->
+    <!--
+        SVG geometry:
+          viewBox 0 0 160 160, circle cx=80 cy=80 r=64
+          C = 2π×64 ≈ 402.12
+          270° arc (3/4 of C) ≈ 301.59  |  90° gap ≈ 100.53
+          rotate(225, 80, 80) places the arc start at the lower-left (≈7 o'clock),
+          going clockwise through top, ending at lower-right (≈5 o'clock).
+    -->
+    <div class="gauge-wrap">
+        <svg viewBox="0 0 160 160">
+            <circle id="gaugeTrack"
+                cx="80" cy="80" r="64"
+                fill="none"
+                stroke="var(--gauge-track)"
+                stroke-width="9"
+                stroke-linecap="round"
+                stroke-dasharray="301.59 100.53"
+                transform="rotate(225, 80, 80)"
+                style="transition: stroke 0.15s;"
+            />
+            <circle id="gaugeFill"
+                cx="80" cy="80" r="64"
+                fill="none"
+                stroke="#4b75ff"
+                stroke-width="9"
+                stroke-linecap="round"
+                stroke-dasharray="0 402.12"
+                transform="rotate(225, 80, 80)"
+                style="transition: stroke-dasharray 0.25s ease, stroke 0.2s ease;"
+            />
+        </svg>
+        <div class="gauge-center">
+            <div class="gauge-val"   id="gaugeVal">—</div>
+            <div class="gauge-unit"  id="gaugeUnit"></div>
+            <div class="gauge-phase" id="gaugePhase">Ready</div>
+        </div>
+    </div>
+
+    <!-- ── Result cards ── -->
+    <div class="result-row">
+
+        <div class="rc rc-dl">
+            <div class="rc-icon">
+                <!-- download arrow -->
+                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                     stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
+                    <line x1="12" y1="3"  x2="12" y2="15"/>
+                    <polyline points="7 10 12 15 17 10"/>
+                    <line x1="5"  y1="21" x2="19" y2="21"/>
+                </svg>
             </div>
-            <div class="statistic">
-                <div class="value" id="ping" style="padding: 5px;">
-                    -
-                </div>
-                <div class="label" id="pingl">
-                    Ping
-                </div>
+            <div class="rc-value" id="dl">—</div>
+            <div class="rc-label" id="dll">Download</div>
+        </div>
+
+        <div class="rc rc-ul">
+            <div class="rc-icon">
+                <!-- upload arrow -->
+                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                     stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
+                    <line x1="12" y1="21" x2="12" y2="9"/>
+                    <polyline points="7 14 12 9 17 14"/>
+                    <line x1="5"  y1="3"  x2="19" y2="3"/>
+                </svg>
             </div>
+            <div class="rc-value" id="ul">—</div>
+            <div class="rc-label" id="ull">Upload</div>
         </div>
-        <br>
-        <div>
-            <div class="ui top attached teal progress" id="progressbar" style="display: none">
-                <div class="bar">
-                    <div class="progress"></div>
-                </div>
+
+        <div class="rc rc-ping">
+            <div class="rc-icon">
+                <!-- wifi / signal icon -->
+                <svg viewBox="0 0 24 24" fill="none" stroke="currentColor"
+                     stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round">
+                    <path d="M5 12.55a11 11 0 0 1 14.08 0"/>
+                    <path d="M1.42 9a16 16 0 0 1 21.16 0"/>
+                    <path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
+                    <circle cx="12" cy="20" r="1" fill="currentColor" stroke="none"/>
+                </svg>
             </div>
-            <button class="ui fluid button" onclick="startBench();" id="startbtn">Test</button>
+            <div class="rc-value" id="ping">—</div>
+            <div class="rc-label" id="pingl">Ping</div>
         </div>
+
     </div>
 
-</body>
+    <!-- ── Slim progress bar ── -->
+    <div class="progress-track" id="progressTrack">
+        <div class="progress-fill" id="progressFill"></div>
+    </div>
+
+    <!-- ── Action button ── -->
+    <button class="st-btn" id="startbtn" onclick="startBench()">Start Test</button>
+
+</div><!-- /.st-wrap -->
+
 <script>
     ao_module_setFixedWindowSize();
-    autoResize();
-    $('#progressbar').progress({
-        label: 'ratio',
-        text: {
-            ratio: ''
-        },
-        total: 100,
-        percent: 0
-    });
 
-    $(window).on('resize', function() {
-        autoResize();
+    /* ── Theme ── */
+    ao_module_getSystemThemeColor(function(color) {
+        document.body.classList.toggle('dark', color !== 'whiteTheme');
     });
 
-    function autoResize() {
-        if ($(window).width() < 550) {
-            $("#stat").attr("class", "ui three horizontal statistics");
-        } else {
-            $("#stat").attr("class", "ui three statistics");
-        }
+    /* ── Gauge constants ──────────────────────────────────────────────────
+       Circle r=64, circumference C = 2π×64 ≈ 402.12
+       270° arc = 0.75 × C ≈ 301.59   90° gap = 0.25 × C ≈ 100.53
+       ──────────────────────────────────────────────────────────────────── */
+    var GAUGE_C   = 2 * Math.PI * 64;   // ≈ 402.12
+    var GAUGE_ARC = GAUGE_C * 0.75;     // ≈ 301.59  (the visible 270° track)
+
+    var _gaugePct = 0;
+
+    /** Set gauge fill (0–100) and the slim progress bar simultaneously. */
+    function setProgress(pct) {
+        _gaugePct = Math.max(0, Math.min(100, pct));
+        var fill = (GAUGE_ARC * _gaugePct / 100).toFixed(2);
+        var rest = (GAUGE_C - GAUGE_ARC * _gaugePct / 100).toFixed(2);
+        document.getElementById('gaugeFill').setAttribute('stroke-dasharray', fill + ' ' + rest);
+        document.getElementById('progressFill').style.width = _gaugePct + '%';
+    }
+
+    /** Tint the gauge fill and the progress bar. */
+    function setColor(hex) {
+        document.getElementById('gaugeFill').style.stroke = hex;
+        document.getElementById('progressFill').style.background = hex;
     }
 
+    /** Update the three text lines inside the gauge. */
+    function setLabel(val, unit, phase) {
+        document.getElementById('gaugeVal').textContent   = val   === undefined ? '—' : val;
+        document.getElementById('gaugeUnit').textContent  = unit  || '';
+        document.getElementById('gaugePhase').textContent = phase || '';
+    }
+
+    function showBar() { document.getElementById('progressTrack').classList.add('visible'); }
+    function hideBar() { document.getElementById('progressTrack').classList.remove('visible'); }
+
+    /* ── Test orchestration ───────────────────────────────────────────── */
+
     function startBench() {
-        $('#progressbar').removeAttr('style');
-        $('#progressbar').progress('set progress', 0);
-        $("#startbtn").attr('disabled', 'disabled');
-        $("#startbtn").html('Testing...');
+        // Reset display
+        ['dl', 'ul', 'ping'].forEach(function(id) {
+            document.getElementById(id).textContent = '—';
+        });
+        document.getElementById('dll').textContent  = 'Download';
+        document.getElementById('ull').textContent  = 'Upload';
+        document.getElementById('pingl').textContent = 'Ping';
+
+        setColor('#4b75ff');
+        setProgress(0);
+        setLabel('—', '', 'Starting…');
+        showBar();
+
+        var btn = document.getElementById('startbtn');
+        btn.disabled    = true;
+        btn.textContent = 'Testing…';
+
         downloadBench();
     }
 
+    /* ── Download ─────────────────────────────────────────────────────── */
+
     function downloadBench() {
-        $("#dl").text("⏱️");
-        let socket = new WebSocket(getWSEndpoint() + "/system/ajgi/interface?script=Speedtest/special/wspeedtest.js");
+        setColor('#4b75ff');
+        setLabel('—', '', 'Download');
+        setProgress(0);
 
-        socket.onopen = function(e) {
-            $("#dl").text("⌛");
-            socket.send("DWL");
-            $('#progressbar').progress('set progress', 0);
-        };
+        var dlBytes = 0;        // bytes received from DATA: frames
+        var dlStart = null;     // set on first DATA: frame for live-speed accuracy
 
-        socket.onmessage = function(event) {
-            if (event.data.indexOf("DATA:") == -1) {
-                if (event.data.indexOf("TTL_BANDWIDTH") != -1) {
-                    let bandwidth = event.data.split("=")[1].split(" ");
-                    $("#dl").text(bandwidth[0]);
-                    $("#dll").text("Download (" + bandwidth[1] + ")");
-                    uploadBench();
-                }
-                if (event.data.indexOf("TIME_DIFF") != -1) {
-                    let timediff = parseInt(event.data.split("=")[1]);
-                    $('#progressbar').progress('increment', timediff / 5 * 100);
+        var socket = new WebSocket(wsEndpoint() +
+            '/system/ajgi/interface?script=Speedtest/special/wspeedtest.js');
+
+        socket.onopen = function() { socket.send('DWL'); };
+
+        socket.onmessage = function(ev) {
+            /* ── Live speed: count raw bytes from each DATA frame ── */
+            if (ev.data.indexOf('DATA:') === 0) {
+                if (dlStart === null) dlStart = performance.now();
+                dlBytes += ev.data.length;   // ASCII payload ≈ byte count
+                var elapsed = (performance.now() - dlStart) / 1000;
+                if (elapsed > 0.3) {
+                    var sp = bytesToSpeed(dlBytes / elapsed).split(' ');
+                    setLabel(sp[0], sp[1], 'Download');
                 }
+                return; // skip further processing for data frames
             }
-        };
 
-        socket.onerror = function(error) {
-            if ($("#dl").text() != "⌛") {
-                $("#dl").text("❌");
+            /* ── TIME_DIFF: server reports elapsed time for the last batch.
+               We SET (not increment) the gauge so it never overshoots.    ── */
+            if (ev.data.indexOf('TIME_DIFF=') === 0) {
+                var td = parseFloat(ev.data.split('=')[1]);
+                // The test runs until a batch takes ≥5 s; cap live progress at 96%.
+                setProgress(Math.min(td / 5 * 100, 96));
+                return;
             }
+
+            /* ── Final result ── */
+            if (ev.data.indexOf('TTL_BANDWIDTH=') === 0) {
+                var parts = ev.data.split('=')[1].split(' ');
+                document.getElementById('dl').textContent  = parts[0];
+                document.getElementById('dll').textContent = 'Download (' + parts[1] + ')';
+                setLabel(parts[0], parts[1], 'Download');
+                setProgress(100);
+                setTimeout(uploadBench, 350);
+            }
+        };
+
+        socket.onerror = function() {
+            setLabel('Error', '', 'Download');
+            document.getElementById('dl').textContent = 'Error';
         };
     }
 
+    /* ── Upload ───────────────────────────────────────────────────────── */
+
     function uploadBench() {
-        $("#ul").text("⏱️");
-        let socket = new WebSocket(getWSEndpoint() + "/system/ajgi/interface?script=Speedtest/special/wspeedtest.js");
+        setColor('#22c55e');
+        setLabel('—', '', 'Upload');
+        setProgress(0);
 
-        socket.onopen = function(e) {
-            $("#ul").text("⌛");
-            socket.send("UPL");
-            setTimeout(executeULTest, 1000, socket);
+        var socket = new WebSocket(wsEndpoint() +
+            '/system/ajgi/interface?script=Speedtest/special/wspeedtest.js');
+
+        socket.onopen = function() {
+            socket.send('UPL');
+            setTimeout(function() { runUpload(socket); }, 800);
         };
 
-        socket.onerror = function(error) {
-            if ($("#ul").text() != "⌛") {
-                $("#ul").text("❌");
-            }
+        socket.onerror = function() {
+            setLabel('Error', '', 'Upload');
+            document.getElementById('ul').textContent = 'Error';
         };
     }
 
-    function executeULTest(socket) {
-        $('#progressbar').progress('set progress', 0);
-        let CurrentMB = 0;
-        let start = new Date();
-        while (socket.bufferedAmount < 10485760 * 10) {
-            socket.send(new ArrayBuffer(32768));
-            CurrentMB += 32768;
-        }
-        let checker = setInterval(function() {
-            if (socket.bufferedAmount == 0) {
-                let end = new Date();
-                CurrentDif = (end.getTime() - start.getTime()) / 1000;
-                socket.send("stop");
-                let bandwidth = bytesToSpeed(CurrentMB / CurrentDif).split(" ");
-                $("#ul").text(bandwidth[0]);
-                $("#ull").text("Upload (" + bandwidth[1] + ")");
-                $('#progressbar').progress('set progress', 0);
+    function runUpload(socket) {
+        var CHUNK   = 32768;
+        var MAX_BUF = 10485760 * 10; // 100 MB ceiling (matches server limit)
+        var bytesSent = 0;
+        var start  = performance.now();
+
+        /* Fill the send buffer up to MAX_BUF */
+        while (socket.bufferedAmount < MAX_BUF) {
+            socket.send(new ArrayBuffer(CHUNK));
+            bytesSent += CHUNK;
+        }
+
+        /* Snapshot the initial buffer size as the 100%-reference for progress.
+           This is accurate regardless of how full the buffer actually got.  */
+        var initialBuf = socket.bufferedAmount;
+
+        var checker = setInterval(function() {
+            var remaining = socket.bufferedAmount;
+
+            if (remaining === 0) {
                 clearInterval(checker);
-                pingTest();
+                socket.send('stop');
+
+                var elapsed = (performance.now() - start) / 1000;
+                var sp = bytesToSpeed(bytesSent / elapsed).split(' ');
+                document.getElementById('ul').textContent  = sp[0];
+                document.getElementById('ull').textContent = 'Upload (' + sp[1] + ')';
+                setLabel(sp[0], sp[1], 'Upload');
+                setProgress(100);
+                setTimeout(pingTest, 350);
+
             } else {
-                //(1 - (socket.bufferedAmount / (10485760 * 10))) * 100
-                $('#progressbar').progress('set progress', 100 - (socket.bufferedAmount / 1048576));
+                /* Accurate progress: fraction of the initial buffer that has drained */
+                setProgress((1 - remaining / initialBuf) * 100);
+
+                /* Live speed: bytes actually on the wire ÷ elapsed time */
+                var sent    = bytesSent - remaining;
+                var elapsed = (performance.now() - start) / 1000;
+                if (elapsed > 0.2 && sent > 0) {
+                    var sp = bytesToSpeed(sent / elapsed).split(' ');
+                    setLabel(sp[0], sp[1], 'Upload');
+                }
             }
         }, 100);
     }
 
+    /* ── Ping ─────────────────────────────────────────────────────────── */
+
     function pingTest() {
-        $('#progressbar').progress('set progress', 0);
-        let clientSendTimeStamp = [];
-        let serverResponseTimeStamp = [];
-        let clientReceiveTimeStamp = [];
-        $("#ping").text("⏱️");
-        let socket = new WebSocket(getWSEndpoint() + "/system/ajgi/interface?script=Speedtest/special/wspeedtest.js");
-
-        socket.onopen = function(e) {
-            $("#ping").text("⌛");
-            socket.send("PING");
-            setTimeout(executePingTest, 500, socket, clientSendTimeStamp, serverResponseTimeStamp, clientReceiveTimeStamp, 0);
+        setColor('#f59e0b');
+        setLabel('—', '', 'Ping');
+        setProgress(0);
+
+        /*
+         * Timeline of WebSocket messages (5 total received by client):
+         *   [0] "DWL/UPL?"  – server handshake on open
+         *   [1] "UPL"       – sent by server's pingTest() before the loop
+         *   [2-4] "rcv,snd" – one per client ping (3 × server echo)
+         *
+         * clientSend[0..2] aligns with serverResp[2..4] after discarding
+         * the two handshake messages.
+         */
+        var clientSend = [];   // performance.now() when each ping was sent
+        var serverResp = [];   // raw server echo strings
+        var clientRecv = [];   // performance.now() when each message arrived
+
+        var socket = new WebSocket(wsEndpoint() +
+            '/system/ajgi/interface?script=Speedtest/special/wspeedtest.js');
+
+        socket.onopen = function() {
+            socket.send('PING');
+            // Stagger pings by 500 ms after the open to let the handshake settle
+            setTimeout(function() { doPing(socket, clientSend, 0); }, 500);
         };
 
-        socket.onmessage = function(event) {
-            clientReceiveTimeStamp.push(new Date().getTime());
-            serverResponseTimeStamp.push(event.data);
-            if (clientReceiveTimeStamp.length == 5) {
-                setTimeout(showPingResult, 2000, clientSendTimeStamp, serverResponseTimeStamp, clientReceiveTimeStamp);
+        socket.onmessage = function(ev) {
+            clientRecv.push(performance.now());
+            serverResp.push(ev.data);
+
+            var n = clientRecv.length;
+            // Messages 1-2 are handshake; show progress only for ping replies (3-5)
+            if (n >= 3) {
+                setProgress(Math.min((n - 2) / 3 * 100, 96));
+            }
+            if (n === 5) {
+                setTimeout(function() {
+                    showPingResult(clientSend, serverResp, clientRecv);
+                }, 600);
             }
-            $('#progressbar').progress('increment', 16.67);
         };
 
-        socket.onerror = function(error) {
-            if ($("#ping").text() != "⌛") {
-                $("#ping").text("❌");
-            }
+        socket.onerror = function() {
+            setLabel('Error', '', 'Ping');
+            document.getElementById('ping').textContent = 'Error';
         };
     }
 
-    function executePingTest(socket, clientSendTimeStamp, serverResponseTimeStamp, clientReceiveTimeStamp, count) {
-        let clientTime = new Date();
-        socket.send(clientTime);
-        clientSendTimeStamp.push(clientTime.getTime());
+    function doPing(socket, clientSend, count) {
+        clientSend.push(performance.now());
+        socket.send(String(Date.now()));
         count++;
-        $('#progressbar').progress('increment', 16.67);
         if (count < 3) {
-            setTimeout(executePingTest, 1000, socket, clientSendTimeStamp, serverResponseTimeStamp, clientReceiveTimeStamp, count);
+            setTimeout(function() { doPing(socket, clientSend, count); }, 1000);
         }
     }
 
-    function showPingResult(clientSendTimeStamp, serverResponseTimeStamp, clientReceiveTimeStamp) {
-        serverResponseTimeStamp.shift();
-        serverResponseTimeStamp.shift();
-        clientReceiveTimeStamp.shift();
-        clientReceiveTimeStamp.shift();
-        let diff = 0;
-        for (let i = 0; i < clientSendTimeStamp.length; i++) {
-            diff += (clientReceiveTimeStamp[i] - clientSendTimeStamp[i]) - (serverResponseTimeStamp[i].split(",")[1] - serverResponseTimeStamp[i].split(",")[0]);
-        }
-        $("#ping").text(Math.round((diff / clientSendTimeStamp.length) * 100) / 100);
-        $("#pingl").text("Ping (ms)");
-        //restart the test
-        $('#progressbar').attr('style', 'display: none');
-        $("#startbtn").html('Re-Test');
-        $("#startbtn").attr('onclick', 'location.reload();');
-        $("#startbtn").removeAttr('disabled');
-    }
+    function showPingResult(clientSend, serverResp, clientRecv) {
+        /*
+         * Discard the 2 handshake entries from the receive arrays so that
+         * index i in clientSend, clientRecv, serverResp all refer to the
+         * same ping round-trip.
+         *
+         * RTT formula (per sample):
+         *   rtt             = clientRecv[i] – clientSend[i]       (ms, high-res)
+         *   srvProcessTime  = srvSendTime  – srvRecvTime           (ms, approx 0)
+         *   networkRTT      = rtt – srvProcessTime
+         */
+        serverResp.splice(0, 2);
+        clientRecv.splice(0, 2);
 
-    function getWSEndpoint() {
-        //Open opeartion in websocket
-        let protocol = "wss://";
-        if (location.protocol !== 'https:') {
-            protocol = "ws://";
+        var total = 0;
+        var n = Math.min(clientSend.length, clientRecv.length, serverResp.length);
+
+        for (var i = 0; i < n; i++) {
+            var rtt      = clientRecv[i] - clientSend[i];
+            var parts    = (serverResp[i] || '').split(',');
+            var srvProc  = (parts.length === 2)
+                ? (parseFloat(parts[1]) - parseFloat(parts[0]))
+                : 0;
+            total += Math.max(0, rtt - srvProc);
         }
-        wsControlEndpoint = (protocol + window.location.hostname + ":" + window.location.port);
-        return wsControlEndpoint;
+
+        var avgMs = n > 0 ? (Math.round(total / n * 100) / 100) : 0;
+
+        document.getElementById('ping').textContent  = avgMs.toFixed(2);
+        document.getElementById('pingl').textContent = 'Ping (ms)';
+        setLabel(avgMs.toFixed(2), 'ms', 'Done');
+        setProgress(100);
+
+        setTimeout(function() {
+            hideBar();
+            setColor('#22c55e');   // green = complete
+            var btn = document.getElementById('startbtn');
+            btn.textContent = 'Re-Test';
+            btn.disabled    = false;
+            btn.onclick     = function() { location.reload(); };
+        }, 500);
     }
 
-    //https://stackoverflow.com/questions/15900485/correct-way-to-convert-size-in-bytes-to-kb-mb-gb-in-javascript
-    function bytesToSize(bytes) {
-        let sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
-        if (bytes == 0) return '0 Byte';
-        let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024)));
-        return Math.round((bytes / Math.pow(1024, i)) * 100, 3) / 100 + ' ' + sizes[i];
+    /* ── Shared helpers ───────────────────────────────────────────────── */
+
+    function wsEndpoint() {
+        var proto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
+        return proto + location.hostname + ':' + location.port;
     }
 
-    function bytesToSpeed(bytes) {
-        bytes = bytes * 8;
-        let sizes = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
-        if (bytes == 0) return '0 Byte';
-        let i = parseInt(Math.floor(Math.log(bytes) / Math.log(1000)));
-        return Math.round((bytes / Math.pow(1000, i)) * 100, 3) / 100 + ' ' + sizes[i];
+    function bytesToSpeed(bytesPerSec) {
+        var bps   = bytesPerSec * 8;
+        var units = ['bps', 'Kbps', 'Mbps', 'Gbps', 'Tbps'];
+        if (bps === 0) return '0 bps';
+        var i = Math.floor(Math.log(bps) / Math.log(1000));
+        return (Math.round(bps / Math.pow(1000, i) * 100) / 100) + ' ' + units[i];
     }
 </script>
 
-</html>
+</body>
+</html>