Skip to content

add internet stability test#805

Open
sstidl wants to merge 22 commits into
masterfrom
internet-stability
Open

add internet stability test#805
sstidl wants to merge 22 commits into
masterfrom
internet-stability

Conversation

@sstidl
Copy link
Copy Markdown
Collaborator

@sstidl sstidl commented May 16, 2026

add internet stability test

tomrhudson and others added 5 commits March 15, 2026 18:07
Add a prolonged ping-based stability test with real-time canvas chart,
stats (avg/min/max/jitter/packet loss), stability rating, external ping
targets, CSV export, and Docker support. Link from main page to stability test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Load server list dynamically from servers.json for Docker frontend/dual modes
- Copy servers.json to web root in entrypoint.sh for frontend/dual modes
- Change back link from href="/" to href="./" for subdirectory installs
- Use binary search for visible chart data range (O(log n) vs O(n))
- Add 200ms minimum interval between pings to limit sample rate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The current Dockerfile.alpine pulls FROM php:8-alpine and then `apk
add php-apache2`. The result is two PHP installs side by side: the
docker-library PHP at /usr/local/bin/php (~30 MB, never used by
Apache) and the apk-installed PHP at /usr/bin/php (which mod_php
actually loads).

Pinning a fresh Alpine release and installing the apk packages
directly drops the dead /usr/local/bin/php install entirely.
Expected wins:
- Smaller image (~30 MB less; 2024 baseline was ~120 MB)
- One PHP binary, no $PATH ambiguity
- Cleaner story for any follow-up that touches PHP config
ca-certificates-bundle (which provides /etc/ssl/certs/ca-certificates.crt)
is already pulled in transitively by apache2 / php-apache2 on alpine:3.23,
so live HTTPS calls from PHP work today (verified with file_get_contents
against ipinfo.io). But the master image (FROM php:8-alpine) installs
the full ca-certificates package explicitly, and operators with
IPINFO_APIKEY configured rely on outbound HTTPS. Make the dependency
explicit to:

* Match master's package set rather than relying on a transitive pull
  that some future apk dep change could drop.
* Document the runtime TLS requirement at the Dockerfile level instead
  of leaving it implicit.

~50 KB image-size cost; the umbrella ca-certificates package adds the
update-ca-certificates CLI on top of the bundle. Per Qodo's review on
PR #800.
@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Add internet stability test with real-time charting and metrics

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add comprehensive internet stability test feature with real-time ping monitoring
• Implement canvas-based chart visualization with configurable alert thresholds
• Support local server and external ping targets (Google, Cloudflare, Apple)
• Include stability metrics: current/average/min/max ping, jitter, packet loss, and rating
• Add CSV export functionality and Docker integration for stability test page
Diagram
flowchart LR
  A["User Interface<br/>stability.html"] -->|"Worker Messages"| B["Web Worker<br/>stability_worker.js"]
  B -->|"Ping Requests"| C["Local Server<br/>or External Target"]
  C -->|"Response Times"| B
  B -->|"Status Updates"| A
  A -->|"Render Chart"| D["Canvas Chart<br/>Real-time Visualization"]
  A -->|"Export"| E["CSV Download"]
  F["Navigation Links<br/>index-classic.html<br/>index-modern.html"] -->|"Link to"| A
Loading

Grey Divider

File Changes

1. stability_worker.js ✨ Enhancement +227/-0

Web Worker for background ping testing

• New Web Worker for background ping operations with configurable intervals
• Implements ping recording with statistics: average, min, max, jitter, packet loss
• Supports both local server pings (XMLHttpRequest) and external targets (fetch no-cors)
• Tracks elapsed time, progress, and maintains delta delivery of ping data points
• Handles test state management (idle, starting, running, finished, aborted)

stability_worker.js


2. stability.html ✨ Enhancement +905/-0

Complete stability test UI with charting

• New 905-line HTML page for internet stability testing interface
• Real-time canvas chart rendering with grid lines, threshold markers, and lost packet indicators
• Test controls: duration selection (60s-5m), target selection (local/Google/Cloudflare/Apple)
• Statistics display: current/average/min/max ping, jitter, packet loss, elapsed time
• Stability rating system (Great/Good/Poor/Bad) based on ping, jitter, and loss metrics
• Alert threshold slider with audio beep notification when exceeded
• CSV export of all ping data with timestamps
• Dark mode support with theme-aware colors
• Dynamic server list loading from server-list.json or servers.json

stability.html


3. index-classic.html ✨ Enhancement +1/-1

Add stability test navigation link

• Add navigation link to stability test page in footer
• Link text: "Stability Test" with pipe separator before source code link

index-classic.html


View more (5)
4. index-modern.html ✨ Enhancement +1/-1

Add stability test navigation link

• Add navigation link to stability test page in footer
• Link text: "stability test" with pipe separator before source code link

index-modern.html


5. docker/entrypoint.sh ⚙️ Configuration changes +7/-4

Docker entrypoint configuration for stability page

• Copy stability.html to web root for frontend/dual/standalone modes
• Copy server-list.json to servers.json for stability page server list access
• Ensure stability page has access to same server configuration as main UI
• Minor whitespace cleanup and formatting adjustments

docker/entrypoint.sh


6. Dockerfile ⚙️ Configuration changes +1/-0

Add stability.html to Docker build

• Add COPY directive for stability.html to Docker image
• Ensures stability page is included in container build

Dockerfile


7. Dockerfile.alpine ⚙️ Configuration changes +1/-0

Add stability.html to Alpine Docker build

• Add COPY directive for stability.html to Alpine Docker image
• Ensures stability page is included in lightweight container build

Dockerfile.alpine


8. package.json ⚙️ Configuration changes +4/-2

Update package.json for stability test files

• Add stability_worker.js to ESLint configuration for code quality checks
• Add stability_worker.js and stability.html to npm package files list
• Update lint and lint:fix scripts to include new worker file

package.json


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects
Copy link
Copy Markdown
Contributor

qodo-free-for-open-source-projects Bot commented May 16, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Stop/status ping-pong loop ✓ Resolved 🐞 Bug ☼ Reliability
Description
In stability.html, worker.onmessage calls stopTest() when testState>=4, and stopTest() posts another
"status" request; since the worker immediately replies to "status", each final status response
triggers another stopTest(), generating repeated status messages/timeouts until termination. This
can spike CPU/message traffic and schedule multiple terminate timers on every completion/abort.
Code

stability.html[R518-566]

+        worker = new Worker("stability_worker.js?r=" + Math.random());
+        worker.onmessage = function (e) {
+          var data = JSON.parse(e.data);
+          latestData = data;
+
+          // accumulate ping data
+          if (data.pingData && data.pingData.length > 0) {
+            for (var i = 0; i < data.pingData.length; i++) {
+              allPingData.push(data.pingData[i]);
+            }
+          }
+
+          // check alert threshold
+          if (alertThresholdMs > 0 && data.currentPing > alertThresholdMs) {
+            playBeep();
+          }
+
+          if (data.testState >= 4) {
+            stopTest();
+          }
+        };
+        updater = setInterval(function () {
+          if (worker) worker.postMessage("status");
+        }, 200);
+        worker.postMessage("start " + JSON.stringify(workerSettings));
+      }
+
+      function stopTest() {
+        running = false;
+        I("startBtn").className = "";
+        I("durationSelect").disabled = false;
+        I("targetSelect").disabled = false;
+        I("server").disabled = false;
+        if (updater) {
+          clearInterval(updater);
+          updater = null;
+        }
+        // request final status
+        if (worker) {
+          worker.postMessage("status");
+          setTimeout(function () {
+            if (worker) {
+              try {
+                worker.terminate();
+              } catch (e) {}
+              worker = null;
+            }
+          }, 500);
+        }
Evidence
The UI triggers stopTest() on any status with testState>=4, while stopTest() posts another status
request; the worker replies to every status request, so the UI will receive another testState>=4
message and call stopTest() again until termination occurs.

stability.html[518-567]
stability_worker.js[42-65]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`stability.html` triggers a repeated async stop/status cycle after the test finishes or aborts.
- `worker.onmessage` calls `stopTest()` when `data.testState >= 4`.
- `stopTest()` itself calls `worker.postMessage("status")`.
- The worker replies synchronously to each `status` request.
This creates a ping-pong where each final `status` response triggers another `stopTest()`, which triggers another `status`, until the worker is terminated.
## Issue Context
The worker always responds to `status` by `postMessage(...)`, so the loop can iterate many times within the 500ms termination delay.
## Fix Focus Areas
- stability.html[519-567]
- stability_worker.js[42-65]
## Suggested fix
Implement a one-way shutdown sequence, e.g.:
- Add a `stopping` boolean. In `stopTest()`, if `stopping` is true, return immediately; otherwise set `stopping=true`.
- In `worker.onmessage`, only call `stopTest()` if `running === true` (or `!stopping`).
- Alternatively, remove the extra `worker.postMessage("status")` from `stopTest()` and rely on the last received status (or request final status once and temporarily disable the `testState>=4` handler).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. SERVER_LIST_URL ignored stability ✓ Resolved 🐞 Bug ≡ Correctness
Description
Docker supports SERVER_LIST_URL by rewriting the main UI to load a remote server list, but
stability.html always loads local server-list.json/servers.json. With SERVER_LIST_URL set, the
stability test will use a different server list than the main UI, producing inconsistent/incorrect
targeting.
Code

stability.html[R359-426]

+      // Server configuration (same pattern as index.html)
+      var SPEEDTEST_SERVERS = [];
+
+      // State
+      var worker = null;
+      var updater = null;
+      var running = false;
+      var allPingData = []; // full dataset for chart and CSV
+      var latestData = null;
+      var alertThresholdMs = 0;
+      var lastBeepTime = 0;
+      var audioCtx = null;
+      var selectedServer = null;
+
+      // Dark mode detection for canvas colors
+      var isDark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches;
+      try {
+        window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", function (e) {
+          isDark = e.matches;
+        });
+      } catch (e) {}
+
+      // Chart constants
+      var VISIBLE_SECONDS = 60;
+      var CHART_PADDING_LEFT = 50;
+      var CHART_PADDING_RIGHT = 15;
+      var CHART_PADDING_TOP = 15;
+      var CHART_PADDING_BOTTOM = 30;
+
+      function joinServerUrl(server, path) {
+        if (!server) return path;
+        if (!path) return server;
+        if (server.charAt(server.length - 1) === "/" || path.charAt(0) === "/") return server + path;
+        return server + "/" + path;
+      }
+
+      // Load server list dynamically (Docker exposes server-list.json; older setups may expose servers.json)
+      function loadServers(callback) {
+        var xhr = new XMLHttpRequest();
+        xhr.onload = function () {
+          try {
+            var servers = JSON.parse(xhr.responseText);
+            if (Array.isArray(servers) && servers.length > 0) {
+              SPEEDTEST_SERVERS = servers;
+            }
+          } catch (e) {}
+          if (SPEEDTEST_SERVERS.length > 0 || xhr._fallbackTried) callback();
+          else {
+            xhr._fallbackTried = true;
+            xhr.open("GET", "servers.json?r=" + Math.random());
+            xhr.send();
+          }
+        };
+        xhr.onerror = function () {
+          if (xhr._fallbackTried) callback();
+          else {
+            xhr._fallbackTried = true;
+            xhr.open("GET", "servers.json?r=" + Math.random());
+            xhr.send();
+          }
+        };
+        xhr.open("GET", "server-list.json?r=" + Math.random());
+        try {
+          xhr.timeout = 2000;
+          xhr.ontimeout = xhr.onerror;
+        } catch (e) {}
+        xhr.send();
+      }
Evidence
Docs and entrypoint show SERVER_LIST_URL is intended to redirect server list loading for the
frontend; the modern frontend code reads SPEEDTEST_SERVERS as either an array or a URL.
Stability.html instead always fetches server-list.json/servers.json, so it cannot follow
SERVER_LIST_URL and will diverge from the main UI configuration.

doc_docker.md[60-64]
docker/entrypoint.sh[94-104]
frontend/javascript/index.js[141-149]
stability.html[359-426]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The stability page does not respect the repository's existing server-list indirection.
- Docker entrypoint supports `SERVER_LIST_URL` by patching `index-modern.html` and `index-classic.html` to load servers from a remote URL.
- Modern frontend code also supports `globalThis.SPEEDTEST_SERVERS` being a URL string.
- `stability.html` hardcodes loading from `server-list.json` and then `servers.json` only.
Result: when `SERVER_LIST_URL` is set (documented behavior), the stability test points at the local generated/mounted list instead of the configured remote list used by the main UI.
## Issue Context
This is especially problematic for MPOT deployments where the desired server list is centralized remotely.
## Fix Focus Areas
- stability.html[359-426]
- docker/entrypoint.sh[94-104]
- doc_docker.md[60-64]
- frontend/javascript/index.js[141-149]
## Suggested fix
Update `stability.html` to follow the same convention as the main UI:
- Determine the server list source as:
- if `globalThis.SPEEDTEST_SERVERS` is a string => fetch that URL
- else if it is an array => use it directly
- else => fetch `server-list.json` (and optionally fallback).
Optionally, have `docker/entrypoint.sh` patch `stability.html` similarly to how it patches index pages when `SERVER_LIST_URL` is set.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Server selection start race ✓ Resolved 🐞 Bug ☼ Reliability
Description
initServers() picks selectedServer only after asynchronously pinging all servers, but the Start
button is enabled immediately; if the user starts quickly on "Local Server", selectedServer may
still be null and the worker will fall back to its default backend/empty.php rather than the best
server from the list. This produces unstable/incorrect targeting depending on timing.
Code

stability.html[R495-516]

+      function startTest() {
+        allPingData = [];
+        latestData = null;
+        resetUI();
+
+        running = true;
+        I("startBtn").className = "running";
+        I("durationSelect").disabled = true;
+        I("targetSelect").disabled = true;
+        I("server").disabled = true;
+
+        var externalTarget = I("targetSelect").value;
+        var workerSettings = {
+          duration: parseInt(I("durationSelect").value),
+          ping_allowPerformanceApi: true,
+          url_ping_external: externalTarget
+        };
+
+        if (!externalTarget && selectedServer) {
+          workerSettings.url_ping = joinServerUrl(selectedServer.server, selectedServer.pingURL);
+          workerSettings.mpot = true;
+        }
Evidence
selectedServer is assigned only after the async ping loop completes, but startTest checks `if
(!externalTarget && selectedServer)` and otherwise does not set url_ping, meaning starting early
can’t use the intended best server from the list.

stability.html[428-455]
stability.html[495-516]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`stability.html` has a race between server discovery/selection and test start.
- `initServers()` pings all servers and sets `selectedServer = best` only once all pings complete.
- `startTest()` uses `selectedServer` only if it is already set.
A user can click Start before `selectedServer` is assigned, causing the run to use the worker's default `url_ping` instead of the intended chosen/best test point.
## Issue Context
This only impacts the "Local Server" mode (no externalTarget selected) and is timing-dependent.
## Fix Focus Areas
- stability.html[428-455]
- stability.html[495-516]
## Suggested fix
Introduce a simple readiness gate:
- Disable the Start button (or treat it as no-op) until either:
- server list is empty (local backend mode), or
- `selectedServer` has been chosen (or the dropdown has at least one reachable option).
- Optionally show a "Finding best server…" state while pings run, and/or set `selectedServer` to the first reachable server as soon as one ping succeeds.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

@sstidl
Copy link
Copy Markdown
Collaborator Author

sstidl commented May 16, 2026

#766 was the base for this

@sstidl sstidl mentioned this pull request May 16, 2026
11 tasks
Comment thread stability.html Outdated
@sstidl sstidl changed the title Merge origin/master into internet-stability add internet stability test May 16, 2026
@sstidl sstidl linked an issue May 16, 2026 that may be closed by this pull request
sstidl and others added 15 commits May 16, 2026 17:23
Drop FROM php:8-alpine in favor of FROM alpine:3.23 (~55% smaller image)
Sort the server dropdown by country first, then by city within the same
country. This makes it easier to find servers in a specific country when
the list is long. Applies to both modern and classic UIs.
Address code review findings:
- Sort a shallow copy instead of mutating the caller's array
- Add null guard on server.name to handle malformed entries
- Use original SPEEDTEST_SERVERS index for classic UI option values
Parse server names more robustly for sorting:
- "City, Country, Provider" → use second part as country
- "City, Country (1) (Hetzner)" → strip parentheticals from country
- "Frankfurt, Germany (FRA01)" → country is "Germany" not "Germany (FRA01)"
Build an array of {idx, server} pairs before sorting so the original
SPEEDTEST_SERVERS index is carried through, eliminating the per-option
indexOf call.
Add a prolonged ping-based stability test with real-time canvas chart,
stats (avg/min/max/jitter/packet loss), stability rating, external ping
targets, CSV export, and Docker support. Link from main page to stability test.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Load server list dynamically from servers.json for Docker frontend/dual modes
- Copy servers.json to web root in entrypoint.sh for frontend/dual modes
- Change back link from href="/" to href="./" for subdirectory installs
- Use binary search for visible chart data range (O(log n) vs O(n))
- Add 200ms minimum interval between pings to limit sample rate

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@sstidl sstidl self-assigned this May 18, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

add internet stability test

4 participants