Jelajahi Sumber

Speedtest: redesign UI in fast.com style

Layout:
- Remove SVG gauge and three small cards entirely
- Left-aligned phase label ("Your internet speed:") above the number
- Huge left-aligned download number (font-size 88px, weight 800) with
  unit and phase sublabel to its right — matches fast.com's hero display
- Number pulses (opacity keyframe) while each test phase is active,
  locking solid when the result lands
- Slim full-width progress bar beneath the hero
- Secondary metrics row (Latency | Upload) fades in with a translateY
  slide once the download result is ready; each metric pulses while its
  test is running and locks on completion
- Minimal dark button ("Go" / "Go again"), inverts to light in dark mode
- Clean black-on-white / white-on-black palette instead of blue-grey cards

Window resized 460×430 → 440×400 to match the more compact layout.

https://claude.ai/code/session_01QP5ab5t2ZekwBFCNB57WEq
Claude 3 minggu lalu
induk
melakukan
9262f852e9
2 mengubah file dengan 244 tambahan dan 335 penghapusan
  1. 243 334
      src/web/Speedtest/index.html
  2. 1 1
      src/web/Speedtest/init.agi

+ 243 - 334
src/web/Speedtest/index.html

@@ -4,44 +4,37 @@
     <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">
+    <meta name="theme-color" content="#141414">
     <script src="../script/jquery.min.js"></script>
     <script src="../script/ao_module.js"></script>
     <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;
+            --bg:           #ffffff;
+            --text:         #141414;
+            --text-muted:   #777;
+            --divider:      #e8e8e8;
+            --progress-bg:  #e8e8e8;
+            --progress-fg:  #141414;
+            --btn-bg:       #141414;
+            --btn-hover:    #333;
+            --btn-text:     #ffffff;
+            --btn-disabled: rgba(20, 20, 20, 0.35);
         }
 
         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;
+            --bg:           #141414;
+            --text:         #f0f0f0;
+            --text-muted:   #666;
+            --divider:      #2a2a2a;
+            --progress-bg:  #2a2a2a;
+            --progress-fg:  #f0f0f0;
+            --btn-bg:       #f0f0f0;
+            --btn-hover:    #d8d8d8;
+            --btn-text:     #141414;
+            --btn-disabled: rgba(240, 240, 240, 0.30);
         }
 
         html, body {
@@ -49,165 +42,174 @@
             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 {
+        /* ── Shell ── */
+        .wrap {
             display: flex;
             flex-direction: column;
             align-items: center;
             justify-content: center;
             height: 100vh;
-            padding: 20px 16px;
-            gap: 18px;
+            padding: 20px 28px;
+            gap: 0;
         }
 
-        /* ── Gauge ── */
-        .gauge-wrap {
-            position: relative;
-            width: 172px;
-            height: 172px;
-            flex-shrink: 0;
+        /* ── Subtitle above the number ── */
+        .phase-label {
+            font-size: 13px;
+            color: var(--text-muted);
+            letter-spacing: 0.2px;
+            margin-bottom: 4px;
+            align-self: flex-start;
         }
 
-        .gauge-wrap svg {
-            width: 100%;
-            height: 100%;
-            overflow: visible;
+        /* ── The big number ── */
+        .hero {
+            display: flex;
+            align-items: flex-end;
+            gap: 10px;
+            margin-bottom: 14px;
+            align-self: flex-start;
         }
 
-        .gauge-center {
-            position: absolute;
-            inset: 0;
+        .hero-num {
+            font-size: 88px;
+            font-weight: 800;
+            line-height: 1;
+            letter-spacing: -4px;
+            color: var(--text);
+            transition: color 0.2s;
+            min-width: 3ch;   /* prevent layout shift */
+        }
+
+        .hero-right {
             display: flex;
             flex-direction: column;
-            align-items: center;
-            justify-content: center;
-            pointer-events: none;
+            padding-bottom: 10px;
+            gap: 2px;
         }
 
-        .gauge-val {
-            font-size: 32px;
+        .hero-unit {
+            font-size: 26px;
             font-weight: 700;
-            letter-spacing: -0.5px;
             color: var(--text);
+            letter-spacing: -0.5px;
             line-height: 1;
-            transition: color 0.2s;
+            min-height: 26px;
         }
 
-        .gauge-unit {
-            font-size: 11px;
+        .hero-sublabel {
+            font-size: 10px;
             color: var(--text-muted);
-            margin-top: 4px;
-            letter-spacing: 0.5px;
-            min-height: 14px;
+            letter-spacing: 0.4px;
+            text-transform: uppercase;
         }
 
-        .gauge-phase {
-            font-size: 11px;
-            font-weight: 500;
-            color: var(--text-muted);
-            margin-top: 7px;
-            letter-spacing: 0.7px;
-            text-transform: uppercase;
+        /* Pulse animation during active testing */
+        @keyframes testing-pulse {
+            0%, 100% { opacity: 1; }
+            50%       { opacity: 0.45; }
         }
+        .hero-num.pulsing { animation: testing-pulse 1.3s ease-in-out infinite; }
 
-        /* ── Result cards ── */
-        .result-row {
-            display: flex;
-            gap: 10px;
+        /* ── Slim progress bar ── */
+        .progress-track {
+            width: 100%;
+            height: 3px;
+            background: var(--progress-bg);
+            border-radius: 2px;
+            overflow: hidden;
+            margin-bottom: 18px;
+            opacity: 0;
+            transition: opacity 0.2s;
+        }
+        .progress-track.visible { opacity: 1; }
+
+        .progress-fill {
+            height: 100%;
+            width: 0%;
+            border-radius: 2px;
+            background: var(--progress-fg);
+            transition: width 0.25s ease;
+        }
+
+        /* ── Secondary metrics (ping + upload) ── */
+        .metrics {
             width: 100%;
-            max-width: 400px;
+            border-top: 1px solid var(--divider);
+            padding-top: 14px;
+            margin-bottom: 18px;
+            display: flex;
+            gap: 0;
+            opacity: 0;
+            transform: translateY(6px);
+            transition: opacity 0.3s ease, transform 0.3s ease;
+            pointer-events: none;
+        }
+        .metrics.visible {
+            opacity: 1;
+            transform: translateY(0);
+            pointer-events: auto;
         }
 
-        .rc {
+        .metric-col {
             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;
+        .metric-col + .metric-col {
+            border-left: 1px solid var(--divider);
+            padding-left: 20px;
         }
 
-        .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; }
+        .metric-title {
+            font-size: 10px;
+            font-weight: 700;
+            color: var(--text-muted);
+            letter-spacing: 1px;
+            text-transform: uppercase;
+            margin-bottom: 2px;
+        }
 
-        .rc-value {
-            font-size: 17px;
+        .metric-val {
+            font-size: 28px;
             font-weight: 700;
             color: var(--text);
-            letter-spacing: -0.2px;
+            letter-spacing: -0.5px;
             line-height: 1;
         }
 
-        .rc-label {
-            font-size: 10.5px;
+        .metric-unit {
+            font-size: 11px;
             color: var(--text-muted);
+            margin-top: 1px;
             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;
-        }
+        /* Pulse on the metric while its phase is active */
+        .metric-val.pulsing { animation: testing-pulse 1.3s ease-in-out infinite; }
 
         /* ── Button ── */
         .st-btn {
             width: 100%;
-            max-width: 400px;
-            padding: 10px 0;
+            padding: 11px 0;
             background: var(--btn-bg);
             color: var(--btn-text);
             border: none;
-            border-radius: 8px;
+            border-radius: 6px;
             font-size: 14px;
             font-weight: 600;
             font-family: inherit;
             cursor: pointer;
-            letter-spacing: 0.1px;
+            letter-spacing: 0.2px;
             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 {
@@ -216,114 +218,55 @@
             transform: none;
         }
 
-        /* ── Small screens ── */
-        @media (max-width: 360px) {
-            .gauge-wrap  { width: 144px; height: 144px; }
-            .gauge-val   { font-size: 26px; }
-            .rc-value    { font-size: 14px; }
+        /* ── Narrow screens ── */
+        @media (max-width: 340px) {
+            .hero-num  { font-size: 68px; }
+            .hero-unit { font-size: 20px; }
+            .metric-val { font-size: 22px; }
         }
     </style>
 </head>
-
 <body>
-<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 class="wrap">
+
+    <!-- Phase label -->
+    <div class="phase-label" id="phaseLabel">Your internet speed:</div>
+
+    <!-- Big download number -->
+    <div class="hero">
+        <div class="hero-num" id="heroNum">—</div>
+        <div class="hero-right">
+            <div class="hero-unit"     id="heroUnit"></div>
+            <div class="hero-sublabel" id="heroSub">Download</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="rc-value" id="dl">—</div>
-            <div class="rc-label" id="dll">Download</div>
-        </div>
+    <!-- Slim progress bar -->
+    <div class="progress-track" id="progressTrack">
+        <div class="progress-fill" id="progressFill"></div>
+    </div>
+
+    <!-- Secondary metrics: Latency + Upload -->
+    <div class="metrics" id="metrics">
 
-        <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 class="metric-col">
+            <div class="metric-title">Latency</div>
+            <div class="metric-val" id="ping">—</div>
+            <div class="metric-unit" id="pingl">ms</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>
-            <div class="rc-value" id="ping">—</div>
-            <div class="rc-label" id="pingl">Ping</div>
+        <div class="metric-col">
+            <div class="metric-title">Upload</div>
+            <div class="metric-val" id="ul">—</div>
+            <div class="metric-unit" id="ull"></div>
         </div>
 
     </div>
 
-    <!-- ── 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>
+    <!-- Action button -->
+    <button class="st-btn" id="startbtn" onclick="startBench()">Go</button>
 
-</div><!-- /.st-wrap -->
+</div><!-- /.wrap -->
 
 <script>
     ao_module_setFixedWindowSize();
@@ -333,56 +276,55 @@
         document.body.classList.toggle('dark', color !== 'whiteTheme');
     });
 
-    /* ── 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;
+    /* ── UI helpers ─────────────────────────────────────────────────────── */
 
-    /** 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 + '%';
+        document.getElementById('progressFill').style.width =
+            Math.max(0, Math.min(100, pct)) + '%';
     }
 
-    /** Tint the gauge fill and the progress bar. */
-    function setColor(hex) {
-        document.getElementById('gaugeFill').style.stroke = hex;
-        document.getElementById('progressFill').style.background = hex;
+    function showBar() { document.getElementById('progressTrack').classList.add('visible'); }
+    function hideBar() { document.getElementById('progressTrack').classList.remove('visible'); }
+
+    /** Update the big hero number. phase = small sublabel below the unit. */
+    function setHero(val, unit, phase) {
+        document.getElementById('heroNum').textContent  = (val  === null || val  === undefined) ? '—' : val;
+        document.getElementById('heroUnit').textContent = unit  || '';
+        document.getElementById('heroSub').textContent  = phase || '';
     }
 
-    /** 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 || '';
+    /** Pulse the hero number (true = animate, false = stop). */
+    function setHeroPulse(on) {
+        document.getElementById('heroNum').classList.toggle('pulsing', on);
     }
 
-    function showBar() { document.getElementById('progressTrack').classList.add('visible'); }
-    function hideBar() { document.getElementById('progressTrack').classList.remove('visible'); }
+    /** Reveal the secondary metrics section. */
+    function showMetrics() {
+        document.getElementById('metrics').classList.add('visible');
+    }
 
-    /* ── Test orchestration ───────────────────────────────────────────── */
+    /** Pulse a secondary metric value. */
+    function setMetricPulse(id, on) {
+        document.getElementById(id).classList.toggle('pulsing', on);
+    }
+
+    /* ── Test orchestration ─────────────────────────────────────────────── */
 
     function startBench() {
-        // 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');
+        // Reset secondaries
+        document.getElementById('ping').textContent  = '—';
+        document.getElementById('pingl').textContent = 'ms';
+        document.getElementById('ul').textContent    = '—';
+        document.getElementById('ull').textContent   = '';
+        document.getElementById('metrics').classList.remove('visible');
+
+        setHero('—', '', 'Download');
+        setHeroPulse(false);
         setProgress(0);
-        setLabel('—', '', 'Starting…');
         showBar();
 
+        document.getElementById('phaseLabel').textContent = 'Testing your internet speed…';
+
         var btn = document.getElementById('startbtn');
         btn.disabled    = true;
         btn.textContent = 'Testing…';
@@ -390,15 +332,17 @@
         downloadBench();
     }
 
-    /* ── Download ─────────────────────────────────────────────────────── */
-
+    /* ── Download ─────────────────────────────────────────────────────────
+       Live speed is read from counting DATA: frame bytes so the big number
+       animates upward in real time, just like fast.com.
+       ──────────────────────────────────────────────────────────────────── */
     function downloadBench() {
-        setColor('#4b75ff');
-        setLabel('—', '', 'Download');
+        setHero('—', '', 'Download');
+        setHeroPulse(true);
         setProgress(0);
 
-        var dlBytes = 0;        // bytes received from DATA: frames
-        var dlStart = null;     // set on first DATA: frame for live-speed accuracy
+        var dlBytes = 0;
+        var dlStart = null;
 
         var socket = new WebSocket(wsEndpoint() +
             '/system/ajgi/interface?script=Speedtest/special/wspeedtest.js');
@@ -406,49 +350,42 @@
         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
+                dlBytes += ev.data.length;
                 var elapsed = (performance.now() - dlStart) / 1000;
                 if (elapsed > 0.3) {
                     var sp = bytesToSpeed(dlBytes / elapsed).split(' ');
-                    setLabel(sp[0], sp[1], 'Download');
+                    setHero(sp[0], sp[1], 'Download');
                 }
-                return; // skip further processing for data frames
+                return;
             }
 
-            /* ── 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');
+                setHero(parts[0], parts[1], 'Download');
+                setHeroPulse(false);
                 setProgress(100);
                 setTimeout(uploadBench, 350);
             }
         };
 
         socket.onerror = function() {
-            setLabel('Error', '', 'Download');
-            document.getElementById('dl').textContent = 'Error';
+            setHero('Error', '', 'Download');
+            setHeroPulse(false);
         };
     }
 
-    /* ── Upload ───────────────────────────────────────────────────────── */
-
+    /* ── Upload ─────────────────────────────────────────────────────────── */
     function uploadBench() {
-        setColor('#22c55e');
-        setLabel('—', '', 'Upload');
+        showMetrics();
+        setMetricPulse('ul', true);
         setProgress(0);
 
         var socket = new WebSocket(wsEndpoint() +
@@ -460,25 +397,22 @@
         };
 
         socket.onerror = function() {
-            setLabel('Error', '', 'Upload');
+            setMetricPulse('ul', false);
             document.getElementById('ul').textContent = 'Error';
         };
     }
 
     function runUpload(socket) {
-        var CHUNK   = 32768;
-        var MAX_BUF = 10485760 * 10; // 100 MB ceiling (matches server limit)
+        var CHUNK    = 32768;
+        var MAX_BUF  = 10485760 * 10;
         var bytesSent = 0;
-        var start  = performance.now();
+        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() {
@@ -491,64 +425,53 @@
                 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');
+                document.getElementById('ull').textContent = sp[1];
+                setMetricPulse('ul', false);
                 setProgress(100);
                 setTimeout(pingTest, 350);
 
             } else {
-                /* 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');
+                    document.getElementById('ul').textContent  = sp[0];
+                    document.getElementById('ull').textContent = sp[1];
                 }
             }
         }, 100);
     }
 
-    /* ── Ping ─────────────────────────────────────────────────────────── */
-
+    /* ── Ping ────────────────────────────────────────────────────────────
+       Message timeline (5 total):
+         [0] "DWL/UPL?"  – server handshake
+         [1] "UPL"       – pingTest() preamble
+         [2-4]           – 3 × "rcvTime,sndTime" echoes
+       clientSend[0..2] aligns with serverResp[2..4] after splicing.
+       ──────────────────────────────────────────────────────────────────── */
     function pingTest() {
-        setColor('#f59e0b');
-        setLabel('—', '', 'Ping');
+        setMetricPulse('ping', true);
         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 clientSend = [];
+        var serverResp = [];
+        var clientRecv = [];
 
         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(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 >= 3) setProgress(Math.min((n - 2) / 3 * 100, 96));
             if (n === 5) {
                 setTimeout(function() {
                     showPingResult(clientSend, serverResp, clientRecv);
@@ -557,7 +480,7 @@
         };
 
         socket.onerror = function() {
-            setLabel('Error', '', 'Ping');
+            setMetricPulse('ping', false);
             document.getElementById('ping').textContent = 'Error';
         };
     }
@@ -572,49 +495,36 @@
     }
 
     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);
 
         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;
+            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);
         }
 
         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');
+        document.getElementById('pingl').textContent = 'ms';
+        setMetricPulse('ping', false);
         setProgress(100);
 
         setTimeout(function() {
             hideBar();
-            setColor('#22c55e');   // green = complete
+            document.getElementById('phaseLabel').textContent = 'Your internet speed:';
             var btn = document.getElementById('startbtn');
-            btn.textContent = 'Re-Test';
+            btn.textContent = 'Go again';
             btn.disabled    = false;
             btn.onclick     = function() { location.reload(); };
-        }, 500);
+        }, 400);
     }
 
-    /* ── Shared helpers ───────────────────────────────────────────────── */
+    /* ── Shared helpers ─────────────────────────────────────────────────── */
 
     function wsEndpoint() {
         var proto = (location.protocol === 'https:') ? 'wss://' : 'ws://';
@@ -629,6 +539,5 @@
         return (Math.round(bps / Math.pow(1000, i) * 100) / 100) + ' ' + units[i];
     }
 </script>
-
 </body>
 </html>

+ 1 - 1
src/web/Speedtest/init.agi

@@ -9,7 +9,7 @@ var moduleLaunchInfo = {
 	SupportFW: true,
 	LaunchFWDir: "Speedtest/index.html",
 	SupportEmb: false,
-	InitFWSize: [460, 430]
+	InitFWSize: [440, 400]
 }
 
 //Register the module