|
|
@@ -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>
|